I have a task: Implement a program that encrypts a file using a strong symmetric cipher. After researching the requirements and features, I chose the RC4 algorithm and its implementation in the CryptoJS library.
https://jsfiddle.net/alexander_js_developer/nuevwrp0/
HTML part:
<script src="https://cdnjs.cloudflare.com/ajax/libs/crypto-js/4.1.1/crypto-js.min.js" integrity="sha512-E8QSvWZ0eCLGk4km3hxSsNmGWbLtSCSUcewDQPQWZF6pEU8GlT8a5fF32wOl1i8ftdMhssTrF/OhyGWwonTcXA==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<div>
<h1>encrypt/decrypt file</h1>
<ol>
<li>Set password</li>
<li>Pick a file</li>
<li>Download decrypted/encrypted file</li>
</ol>
<div>
<input type="text" id="pass" placeholder="pass">
<button id="encrypt">encrypt file</button>
<button id="decrypt">decrypt file</button>
</div>
</div>
JavaScript part:
// support
const download = (data, filename, type) => {
const file = new Blob([data], { type: type });
const a = document.createElement('a');
const url = URL.createObjectURL(file);
a.href = url;
a.download = filename;
document.body.appendChild(a);
a.click();
setTimeout(function () {
document.body.removeChild(a);
window.URL.revokeObjectURL(url);
}, 0);
}
const pickAFile = (getText = true) => {
return new Promise((resolve, reject) => {
const input = document.createElement('input');
input.type = 'file';
input.onchange = (e) => {
const file = e.target.files[0];
const reader = new FileReader();
if (!getText) {
resolve(file);
} else {
reader.onload = (e) => resolve(e.target.result);
reader.onerror = (e) => reject(e);
reader.readAsText(file);
}
};
input.click();
});
};
// /support
function app () {
const passNode = document.querySelector('input#pass');
const encryptNode = document.querySelector('#encrypt');
const decryptNode = document.querySelector('#decrypt');
encryptNode.addEventListener('click', () => {
if (!passNode.value) return alert('Password input is empty! Aborting.');
const pass = CryptoJS.SHA3(passNode.value);
pickAFile(false).then((file) => {
const reader = new FileReader();
reader.onload = (e) => {
const encrypted = CryptoJS.RC4.encrypt(e.target.result, pass).toString();
download(encrypted, `encrypted-${file.name}`, file.type);
};
reader.readAsText(file);
});
});
decryptNode.addEventListener('click', () => {
if (!passNode.value) return alert('Password input is empty! Aborting.');
const pass = CryptoJS.SHA3(passNode.value);
pickAFile(false).then((file) => {
const reader = new FileReader();
reader.onload = (e) => {
try {
const decrypted = CryptoJS.RC4.decrypt(e.target.result, pass).toString(
CryptoJS.enc.Utf8
);
download(decrypted, `decrypted-${file.name}`, file.type);
} catch (error) {
console.log('wrong password!');
}
};
reader.readAsText(file);
});
});
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', app);
} else {
app();
}
This code quickly and stably encrypts and decrypts files with utf-8 content (.txt, .js). But binary files (pictures, .exe, etc.) break.
I suspect this is the place: reader.readAsText(file).
The program reads the file as text and already at this stage the binary files get corrupted. I think that I need to convert the file into a bit stream and encrypt them already.
But I still don't understand how to do it. I know about typed arrays, buffers and views, but I have very little experience with them.
How can I implement the following schema:
encrypt: file.ex => bytes => encrypt (CryptoJS) => bytes => file.ex
decrypt: file.ex => bytes => decrypt (CryptoJS) => bytes => file.ex
?
Metadata is important to keep. It would be nice if they were also encrypted, but this is not necessary. The speed of encryption / decryption and the weight of encrypted files are also critical.
Thank you!
Your guess is right.
reader.readAsText()applies a UTF8 encoding by default, which is correct for UTF8 encoded text files, but corrupts binary data files (images, .exe, etc.). Therefore more generalreader.readAsArrayBuffer()must be used. The data is loaded into anArrayBuffer, which must be converted to the CryptoJS internalWordArraytype withCryptoJS.lib.WordArray.create()before further processing by CryptoJS.During encryption the ciphertext was Base64 encoded, i.e. stored as text. Therefore, when decrypting, the ciphertext can be loaded with
reader.readAsText()as before. The decrypted data is again of the CryptoJS internalWordArraytype, which can be converted to a e.g.Uint8Array(see below, functionconvertWordArrayToUint8Array()), that can be processed directly by theBlobconstructor (see functiondownload()).With these changes, encryption and decryption of arbitrary binary files work.
Full code:
Keep in mind Peter's hint in the comments. Nowadays it is better to use AES instead of RC4