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 general reader.readAsArrayBuffer()
must be used. The data is loaded into an ArrayBuffer
, which must be converted to the CryptoJS internal WordArray
type with CryptoJS.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 internal WordArray
type, which can be converted to a e.g. Uint8Array
(see below, function convertWordArrayToUint8Array()
), that can be processed directly by the Blob
constructor (see function download()
).
With these changes, encryption and decryption of arbitrary binary files work.
Full code:
<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>
<script>
// 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) => {
var wordArray = CryptoJS.lib.WordArray.create(e.target.result); // Fix 2a: Convert data to WordArray type
const encrypted = CryptoJS.RC4.encrypt(wordArray, pass).toString(); // Fix 2b: Pass the WordArray
download(encrypted, `encrypted-${file.name}`, file.type);
};
reader.readAsArrayBuffer(file); // Fix 1: replace readAsText() with readAsArrayBuffer()
});
});
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); // Fix 3a: Return the decrypted data as WordArray
var typedArray = convertWordArrayToUint8Array(decrypted); // Fix 3b: Convert the WordArray into a Uint8Array
download(typedArray, `decrypted-${file.name}`, file.type); // Fix 3c: Pass the typed array
} catch (error) {
console.log('wrong password!');
}
};
reader.readAsText(file);
});
});
// Fix 4: New function convertWordArrayToUint8Array
function convertWordArrayToUint8Array(wordArray) {
var arrayOfWords = wordArray.hasOwnProperty("words") ? wordArray.words : [];
var length = wordArray.hasOwnProperty("sigBytes") ? wordArray.sigBytes : arrayOfWords.length * 4;
var uInt8Array = new Uint8Array(length), index=0, word, i;
for (i=0; i<length; i++) {
word = arrayOfWords[i];
uInt8Array[index++] = word >> 24;
uInt8Array[index++] = (word >> 16) & 0xff;
uInt8Array[index++] = (word >> 8) & 0xff;
uInt8Array[index++] = word & 0xff;
}
return uInt8Array;
}
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', app);
} else {
app();
}
</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>
Keep in mind Peter's hint in the comments. Nowadays it is better to use AES instead of RC4