使用 CryptoJS 破坏 unicode 表情符号的 AES 加密
AES encryption with CryptoJS corrupting unicode emoji
我正在编写一个系统,用户可以在其中写一些东西(通过移动浏览器),并且 "String" 将使用用户选择的密码进行加密。由于unicode表情符号经常使用,因此也必须支持它们。
作为加密的库,我选择 CryptoJs - 这样加密就可以在设备上本地完成。
目前,当我加密字符串并解密相同的字符串时,所有表情符号都会消失/被随机字符替换。
var key = "123";
var content = "secret text with an emoji, ";
var encrypted = aes_encrypt(key, content); //U2FsdGVkX19IOHIt+eRkaOcmNuZrc1rkU7JepL4iNdUknzhDaLOnSjYBCklTktSe
var decrypted = aes_decrypt(key, encrypted);//secret text with an emoji, Ø<ß®
我正在使用一对这样的辅助函数:
function aes_encrypt(key, content){
var key_string = key + "";
var content_string = ascii_to_hex(content) + "";
var key_sha3 = sha3(key_string);
var encrypted = CryptoJS.AES.encrypt(content_string, key_sha3, {
mode: CryptoJS.mode.CTR, padding: CryptoJS.pad.Iso10126});
return encrypted + "";
};
任何人都可以告诉我我做错了什么吗?
Warning: It is extremely difficult to get cryptographic code right. It can be even harder in JavaScript, where you often lack control over the execution environment and (as discussed below) a lack of language support has led to inconsistent conventions. I have not done enough research about the CryptoJS library to know about its design or security, or whether it is being used safely in this context.
Please do not rely on any of this code to be genuinely secure without a professional audit.
在 JavaScript 中使用加密代码时的一个常见问题是没有内置的方式来表示二进制数据。这在现代引擎中已经得到解决(浏览器中的类型为 Blobs
和 TypedArrays
,Node.js 中的类型为 Buffers
),但是仍然有很多代码没有采用出于历史或兼容性原因,利用这一点。
如果没有这些内置类型,一种常见的约定(由内置 atob
and btoa
函数使用)是使用内置字符串类型来保存二进制数据。 JavaScript 字符串实际上是一个双字节值列表(通常包含 UCS-2/UTF-16-encoded Unicode 字符)。想要存储二进制数据的用户通常只使用低字节,完全忽略高字节。
如果您只处理与 ASCII 兼容的数据,则在使用像这样的代码时可能会忽略这些细节(即一切正常,但可能会产生微妙的安全后果)。这是因为编码为 ASCII 的文本看起来与编码为 UTF-16 的文本相同,只是去掉了高字节。但是,当您尝试超出此范围时,您需要进行一些编码。
最正确的做法(除了使用真正的二进制类型之外)是获取输入字符串,将其编码为 UTF-8,并将该数据放入输出字符串的低位字节中。但是,JavaScript 不提供内置函数来执行此操作。作为一种粗略但简单的替代方法,the encodeURIComponent
function 会将任何有效的 unicode 字符串编码为完全 URL 安全字符的基于 UTF-8 的表示形式,这些字符都是 ASCII 兼容的。对于您的代码,这意味着这样的事情:
var key = "123";
var content = "secret text with an emoji, ";
var encrypted = aes_encrypt(key, encodeURIComponent(content));
var decrypted = decodeURIComponent(aes_decrypt(key, encrypted));
如果您有很多不 URL 安全的字符,这可能会导致编码数据比必要的大得多,但它应该是安全的。此外,encodeURIComponent
显然会对包含 "unpaired surrogate characters" 的字符串抛出错误。我不认为这些应该出现在普通输入中,但有人可以制作它们。
我希望在 CryptoJS 中有更正确的方法来处理这样的事情,但我不知道。如果您计划部署此代码以供 public 使用,请考虑进一步研究。
CryptoJS 能够将 UTF-8 编码的字符串转换为它自己的二进制数据格式 (WordArray
)。这可以通过 var binData = CryptoJS.enc.Utf8.parse(string);
:
来完成
var password = "123";
var content = "secret text with an emoji, ";
inContent.innerHTML = content;
var encrypted = aes_encrypt(password, content);
var decrypted = aes_decrypt(password, encrypted);
out.innerHTML = decrypted;
function aes_encrypt(password, content) {
return CryptoJS.AES.encrypt(content, password).toString();
}
function aes_decrypt(password, encrypted) {
return CryptoJS.AES.decrypt(encrypted, password).toString(CryptoJS.enc.Utf8);
}
#inContent { color: blue; }
#out { color: red; }
<script src="https://cdn.rawgit.com/CryptoStore/crypto-js/3.1.2/build/rollups/aes.js"></script>
<div>in: <span id="inContent"></span></div>
<div>out: <span id="out"></span></div>
这是可行的,因为如果将字符串作为内容传递给 CryptoJS.AES.encrypt
,那么它将自动解析为 UTF-8,但您需要在自己解密后将其转换回 UTF-8。这是通过 .toString(CryptoJS.enc.Utf8)
.
完成的
此代码仅说明 CryptoJS 已经很好地处理了 UTF-8。这不安全,因为
具有单次迭代的 MD5 用于从密码派生密钥。您需要使用 CryptoJS 提供的类似 PBKDF2 的东西。 (不要忘记每次都使用随机IV。它不必是秘密的,所以你可以将它与密文一起发送。)
密文未经验证,因此不太可能检测到对加密数据的(恶意)操作。最好对您的密文进行身份验证,以便像 padding oracle attack are not possible. This can be done with authenticated modes like GCM or EAX, or with an encrypt-then-MAC 方案这样的攻击具有强大的 MAC,例如 CryptoJS 提供的 HMAC-SHA256。
我正在编写一个系统,用户可以在其中写一些东西(通过移动浏览器),并且 "String" 将使用用户选择的密码进行加密。由于unicode表情符号经常使用,因此也必须支持它们。
作为加密的库,我选择 CryptoJs - 这样加密就可以在设备上本地完成。
目前,当我加密字符串并解密相同的字符串时,所有表情符号都会消失/被随机字符替换。
var key = "123";
var content = "secret text with an emoji, ";
var encrypted = aes_encrypt(key, content); //U2FsdGVkX19IOHIt+eRkaOcmNuZrc1rkU7JepL4iNdUknzhDaLOnSjYBCklTktSe
var decrypted = aes_decrypt(key, encrypted);//secret text with an emoji, Ø<ß®
我正在使用一对这样的辅助函数:
function aes_encrypt(key, content){
var key_string = key + "";
var content_string = ascii_to_hex(content) + "";
var key_sha3 = sha3(key_string);
var encrypted = CryptoJS.AES.encrypt(content_string, key_sha3, {
mode: CryptoJS.mode.CTR, padding: CryptoJS.pad.Iso10126});
return encrypted + "";
};
任何人都可以告诉我我做错了什么吗?
Warning: It is extremely difficult to get cryptographic code right. It can be even harder in JavaScript, where you often lack control over the execution environment and (as discussed below) a lack of language support has led to inconsistent conventions. I have not done enough research about the CryptoJS library to know about its design or security, or whether it is being used safely in this context.
Please do not rely on any of this code to be genuinely secure without a professional audit.
在 JavaScript 中使用加密代码时的一个常见问题是没有内置的方式来表示二进制数据。这在现代引擎中已经得到解决(浏览器中的类型为 Blobs
和 TypedArrays
,Node.js 中的类型为 Buffers
),但是仍然有很多代码没有采用出于历史或兼容性原因,利用这一点。
如果没有这些内置类型,一种常见的约定(由内置 atob
and btoa
函数使用)是使用内置字符串类型来保存二进制数据。 JavaScript 字符串实际上是一个双字节值列表(通常包含 UCS-2/UTF-16-encoded Unicode 字符)。想要存储二进制数据的用户通常只使用低字节,完全忽略高字节。
如果您只处理与 ASCII 兼容的数据,则在使用像这样的代码时可能会忽略这些细节(即一切正常,但可能会产生微妙的安全后果)。这是因为编码为 ASCII 的文本看起来与编码为 UTF-16 的文本相同,只是去掉了高字节。但是,当您尝试超出此范围时,您需要进行一些编码。
最正确的做法(除了使用真正的二进制类型之外)是获取输入字符串,将其编码为 UTF-8,并将该数据放入输出字符串的低位字节中。但是,JavaScript 不提供内置函数来执行此操作。作为一种粗略但简单的替代方法,the encodeURIComponent
function 会将任何有效的 unicode 字符串编码为完全 URL 安全字符的基于 UTF-8 的表示形式,这些字符都是 ASCII 兼容的。对于您的代码,这意味着这样的事情:
var key = "123";
var content = "secret text with an emoji, ";
var encrypted = aes_encrypt(key, encodeURIComponent(content));
var decrypted = decodeURIComponent(aes_decrypt(key, encrypted));
如果您有很多不 URL 安全的字符,这可能会导致编码数据比必要的大得多,但它应该是安全的。此外,encodeURIComponent
显然会对包含 "unpaired surrogate characters" 的字符串抛出错误。我不认为这些应该出现在普通输入中,但有人可以制作它们。
我希望在 CryptoJS 中有更正确的方法来处理这样的事情,但我不知道。如果您计划部署此代码以供 public 使用,请考虑进一步研究。
CryptoJS 能够将 UTF-8 编码的字符串转换为它自己的二进制数据格式 (WordArray
)。这可以通过 var binData = CryptoJS.enc.Utf8.parse(string);
:
var password = "123";
var content = "secret text with an emoji, ";
inContent.innerHTML = content;
var encrypted = aes_encrypt(password, content);
var decrypted = aes_decrypt(password, encrypted);
out.innerHTML = decrypted;
function aes_encrypt(password, content) {
return CryptoJS.AES.encrypt(content, password).toString();
}
function aes_decrypt(password, encrypted) {
return CryptoJS.AES.decrypt(encrypted, password).toString(CryptoJS.enc.Utf8);
}
#inContent { color: blue; }
#out { color: red; }
<script src="https://cdn.rawgit.com/CryptoStore/crypto-js/3.1.2/build/rollups/aes.js"></script>
<div>in: <span id="inContent"></span></div>
<div>out: <span id="out"></span></div>
这是可行的,因为如果将字符串作为内容传递给 CryptoJS.AES.encrypt
,那么它将自动解析为 UTF-8,但您需要在自己解密后将其转换回 UTF-8。这是通过 .toString(CryptoJS.enc.Utf8)
.
此代码仅说明 CryptoJS 已经很好地处理了 UTF-8。这不安全,因为
具有单次迭代的 MD5 用于从密码派生密钥。您需要使用 CryptoJS 提供的类似 PBKDF2 的东西。 (不要忘记每次都使用随机IV。它不必是秘密的,所以你可以将它与密文一起发送。)
密文未经验证,因此不太可能检测到对加密数据的(恶意)操作。最好对您的密文进行身份验证,以便像 padding oracle attack are not possible. This can be done with authenticated modes like GCM or EAX, or with an encrypt-then-MAC 方案这样的攻击具有强大的 MAC,例如 CryptoJS 提供的 HMAC-SHA256。