如何使用CryptoJS用AES算法加密文件



我有使用RC4算法加密文件的代码。有人强烈建议我使用一种更可靠的算法:AES。从CryptoJS文档中,我了解到它的工作方式与RC4相同。也就是说,第一个参数是要加密的字符串,第二个参数是密码字符串。

但简单地用AES代替RC4方法并没有帮助,我不知道在哪里可以找到必要的信息。

谢谢!

这是我的工作(对于RC4(代码:

<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>
<button id="test">test</button>
</div>
</div>
<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();
});
};
const convertWordArrayToUint8Array = (wordArray) => {
const arrayOfWords = wordArray.hasOwnProperty('words') ? wordArray.words : [];
const length = wordArray.hasOwnProperty('sigBytes') ?
wordArray.sigBytes :
arrayOfWords.length * 4;
const uInt8Array = new Uint8Array(length);
let index = 0;
let word;
let 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;
};
// /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 wordArray = CryptoJS.lib.WordArray.create(e.target.result);
const encrypted = CryptoJS.RC4.encrypt(wordArray, pass).toString();
download(encrypted, `encrypted-${file.name}`, file.type);
};
reader.readAsArrayBuffer(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);
const typedArray = convertWordArrayToUint8Array(decrypted);
download(typedArray, `decrypted-${file.name}`, file.type);
} catch (error) {
console.log('wrong password!');
}
};
reader.readAsText(file);
});
});
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', app);
} else {
app();
}
</script>

从RC4转换为AES时,请考虑以下内容:

  • AES定义了16(AES-128(、24(AES-192(和32字节(AES-256(的密钥大小。密钥越大,就越安全(尽管现在所有的变体都被认为是安全的(。下面使用了一个32字节的密钥。

    您应用的SHA3 CryptoJS实现(实际上是Keccak,请参阅Hashing/SHA-3(默认情况下的输出大小为64字节,因此不能直接用作AES密钥。因此,为了简单起见,我应用了输出大小为32字节的SHA-256。您也可以使用SHA-3,并且只使用例如前32个字节。

    但是,通过摘要进行的密钥派生是一个漏洞。更安全的是使用像Argon2或PBKDF2这样的密钥派生。后者由CryptoJS支持,因此我建议切换到PBKDF2。由于我在这里的重点是将RC4转换为AES,所以我将把更改留给您。

  • AES在CBC模式下(默认情况下由CryptoJS使用;默认填充为PKCS#7 btw(需要一个随机IV(其长度等于块大小,因此AES需要16个字节(。出于安全原因,这不能是静态的,但必须为每次加密随机生成(使用CSPRNG(。

    IV不是秘密,需要解密。因此,它通常与密文连接:IV|密文。

    在解密之前,IV和密文根据IV的已知长度进行分离。

考虑到以上几点,从RC4到AES的可能切换是(有关更改的解释,请参阅代码中的注释(:

<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>
<button id="test">test</button>
</div>
</div>
<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();
});
};
const convertWordArrayToUint8Array = (wordArray) => {
const arrayOfWords = wordArray.hasOwnProperty('words') ? wordArray.words : [];
const length = wordArray.hasOwnProperty('sigBytes') ?
wordArray.sigBytes :
arrayOfWords.length * 4;
const uInt8Array = new Uint8Array(length);
let index = 0;
let word;
let 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;
};
// /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 key = CryptoJS.SHA256(passNode.value); // Fix 1: Derive 32 bytes key
pickAFile(false).then((file) => {
const reader = new FileReader();
reader.onload = (e) => {
const iv = CryptoJS.lib.WordArray.random(16); // Fix 2: Create random 16 bytes IV
const wordArray = CryptoJS.lib.WordArray.create(e.target.result);
const encrypted = CryptoJS.AES.encrypt(wordArray, key, {iv: iv}); // Fix 3: Encrypt with AES using the above key and IV
const ivCiphertext = iv.clone().concat(encrypted.ciphertext).toString(CryptoJS.enc.Base64); // Fix 4: Concatenate IV and ciphertext
download(ivCiphertext, `encrypted-${file.name}`, file.type);
};
reader.readAsArrayBuffer(file);
});
});
decryptNode.addEventListener('click', () => {
if (!passNode.value) return alert('Password input is empty! Aborting.');
const key = CryptoJS.SHA256(passNode.value); // Fix 5: Derive 32 bytes key
pickAFile(false).then((file) => {
const reader = new FileReader();
reader.onload = (e) => {
try {
const ivCiphertext = CryptoJS.enc.Base64.parse(e.target.result); // Fix 6: Separate IV and ciphertext
const iv = CryptoJS.lib.WordArray.create(ivCiphertext.words.slice(0, 4)); 
const ciphertext = CryptoJS.lib.WordArray.create(ivCiphertext.words.slice(4)); 
const decrypted = CryptoJS.AES.decrypt({ciphertext: ciphertext}, key, {iv: iv}); // Fix 7: Decrypt
const typedArray = convertWordArrayToUint8Array(decrypted);
download(typedArray, `decrypted-${file.name}`, file.type);
} catch (error) {
console.log('wrong password!');
}
};
reader.readAsText(file);
});
});
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', app);
} else {
app();
}
</script>

注意,代码仍然可以优化(这也适用于RC4变体(:在加密过程中,数据(即串联的IV和密文(是Base64编码的。Base64是一种二进制到文本编码,它将数据大小增加约33%。当任意二进制数据要表示为文本时,使用它
然而,由于数据存储在这里作为一个文件,因此这种转换并不是真正必要的(当然,可能有一些原因在帖子中并不明显(。相反,可以存储原始数据(即非Base64编码的数据(,这将相应地减小文件大小。

最新更新