使用CryptoJS进行AES加密会破坏unicode表情符号

时间:2016-02-19 23:02:19

标签: javascript unicode cryptojs

我正在编写一个系统,用户可以在其中编写内容(通过移动浏览器),并且“String”将使用用户选择的密码进行加密。由于经常使用unicode表情符号,因此也必须支持它们。

作为加密的lib,我选择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 + "";
};

有人可以告诉我我做错了吗?

2 个答案:

答案 0 :(得分:6)

  

警告:正确获取加密代码非常困难。在JavaScript中可能更难,在那里你经常缺乏对执行环境的控制,并且(如下所述)缺乏语言支持导致了不一致的约定。我没有对CryptoJS库进行足够的研究来了解它的设计或安全性,或者是否在这种情况下安全地使用它。

     

如果没有专业审核,请不要依赖此代码中的任何一项。

在JavaScript中使用加密代码时的一个常见问题是,没有内置的方法来表示二进制数据。这已在现代引擎中解决(浏览器中的类型为BlobsTypedArrays,Node.js中为Buffers),但仍有许多代码无法利用这是出于历史或兼容性原因。

如果没有这些内置类型,一个常见的约定(由内置的atobbtoa函数使用)是使用内置字符串类型来保存二进制数据。 JavaScript字符串实际上是一个双字节值列表(通常包含UCS-2 / UTF-16编码的Unicode字符)。想要存储二进制数据的用户通常只使用较低的字节,完全忽略较高的字节。

如果您只处理与ASCII兼容的数据,那么在使用这样的代码时可能会忽略这些细节(即事情会起作用 - 但可能会产生微妙的安全后果)。这是因为编码为ASCII的文本看起来与编码为UTF-16的文本相同,并且高字节被剥离。但是当你冒险超越它时,你需要做一些编码。

除了使用真正的二进制类型之外,最正确的做法是获取输入的字符串,将其编码为UTF-8,并将该数据放在输出字符串的低字节中。但是,JavaScript不提供内置函数来执行此操作。作为粗略但简单的替代方法,the encodeURIComponent function会将任何有效的unicode字符串编码为基于UTF-8的完全URL安全字符表示,这些字符都是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显然会为包含“未配对的代理字符”的字符串抛出错误。我不认为这些应该出现在普通的输入中,但有人可以制作它们。

我希望在CryptoJS中有一种更正确的方法来处理这样的事情,但我不知道它。如果您计划将此代码部署以供公众使用,请考虑进一步研究。

答案 1 :(得分:3)

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这样的攻击是不可能的。这可以通过GCM或EAX等经过身份验证的模式来完成,也可以使用具有强MAC的encrypt-then-MAC方案来完成,如CryptoJS提供的HMAC-SHA256。