CryptoJS AES CBC 256 解密在明文中间添加额外的字节

CryptoJS AES CBC 256 decrypt adds additional byte in the middle of plaintext

我正在编写一个 Chrome 扩展,它使用 CryptoJS 进行一些 Apache Thrift 工作。我目前正在尝试让 CryptoJS 工作。我在使用 CryptoJS 解密由 CryptoJS 加密的数据时遇到问题。我在下面附上一个测试用例,在问题描述之后。

发生的事情如下,我有一个 "bytes" 的数组:

var bArr = [11,0,1,0,0,0,6,100,105,103,101,115,116,11,0,2,0,0,0,152,67,119,65,66,65,65,65,65,69,109,78,111,99,109,57,116,90,83,49,48,90,88,78,48,76,87,78,115,97,87,86,117,100,65,103,65,65,103,65,65,49,68,69,75,65,65,77,65,65,65,65,65,86,75,102,66,85,103,115,65,66,65,65,65,65,67,81,49,90,68,99,119,77,71,73,120,78,67,48,121,78,84,90,107,76,84,81,119,77,109,81,116,79,84,65,48,90,105,48,52,79,84,86,105,78,68,73,50,89,109,78,108,78,84,99,76,65,65,85,65,65,65,65,85,89,50,104,121,98,50,49,108,76,87,78,115,97,87,86,117,100,67,49,122,90,87,78,121,90,88,81,65,11,0,3,0,0,0,36,52,51,52,55,54,56,98,53,45,50,48,102,102,45,52,99,100,102,45,56,53,97,50,45,57,49,49,56,50,98,55,98,51,102,57,53,0];
var stringToEncode = String.fromCharCode.apply(null, bArr);

我用CryptoJS加密,然后解密。加密前的前 25 个字节:

11,0,1,0,0,0,6,100,105,103,101,115,116,11,0,2,0,0,0,152,67,119,65,66,65

解密后:

11,0,1,0,0,0,6,100,105,103,101,115,116,11,0,2,0,0,0,194,152,67,119,65,66

唯一的区别是在位置 20 处多了一个 194。显然,除了填充之外,所有其他字节都完全相同。我想知道这是从哪里来的。

有关帽子的更多信息是这些字节。这是一个包含 3 个字段的 Thrift 结构,字段 2 包含另一个嵌入式 Thrift 结构的 Base64 表示。输入示例的前 20 个字节:

由于解密问题,Thrift 解析器错误地识别了 fid 2 值的长度。

我相信我在带有 PKCS7 填充的 OpenSSL 模式下使用带有 32 字节密钥 (SHA256) 和 16 字节 IV 的 AES256 CBC。

这是我的量子单元测试。

test("Decryption", function() {
  var bArr = [11,0,1,0,0,0,6,100,105,103,101,115,116,11,0,2,0,0,0,152,67,119,65,66,65,65,65,65,69,109,78,111,99,109,57,116,90,83,49,48,90,88,78,48,76,87,78,115,97,87,86,117,100,65,103,65,65,103,65,65,49,68,69,75,65,65,77,65,65,65,65,65,86,75,102,66,85,103,115,65,66,65,65,65,65,67,81,49,90,68,99,119,77,71,73,120,78,67,48,121,78,84,90,107,76,84,81,119,77,109,81,116,79,84,65,48,90,105,48,52,79,84,86,105,78,68,73,50,89,109,78,108,78,84,99,76,65,65,85,65,65,65,65,85,89,50,104,121,98,50,49,108,76,87,78,115,97,87,86,117,100,67,49,122,90,87,78,121,90,88,81,65,11,0,3,0,0,0,36,52,51,52,55,54,56,98,53,45,50,48,102,102,45,52,99,100,102,45,56,53,97,50,45,57,49,49,56,50,98,55,98,51,102,57,53,0];
  var stringToEncode = String.fromCharCode.apply(null, bArr);
  var symmetricKey = "v3JElaRswYgxOt4b";

  var key = CryptoJS.enc.Latin1.parse( CryptoJS.enc.Latin1.stringify( CryptoJS.SHA256( symmetricKey ) ) );
  var iv  = CryptoJS.lib.WordArray.random( 16 );

  var encrypted = CryptoJS.AES.encrypt( stringToEncode,
                                        key,
                                        { iv: iv, format: CryptoJS.format.OpenSSL }
                                      ).ciphertext.toString(CryptoJS.enc.Latin1);

  var decrypted = CryptoJS.AES.decrypt( { ciphertext: CryptoJS.enc.Latin1.parse(encrypted) },
                                          key,
                                          { iv: iv, padding: CryptoJS.pad.NoPadding }
                                      ).toString(CryptoJS.enc.Latin1);

  var buf = [];
  for (var i=0; i<decrypted.length; i++) {
    buf.push( decrypted.charCodeAt(i) );
  }

  var bstr1 = "";
  for (var i=0; i<bArr.length; i++) {
    bstr1 += (i>0) ? ","+bArr[i] : bArr[i]+"";
  }
  var bstr2 = "";
  for (var i=0; i<buf.length; i++) {
    bstr2 += (i>0) ? ","+buf[i] : buf[i]+"";
  }

  console.log("------------------------------------------");
  console.log(bstr1);
  console.log(bstr2);
  console.log("------------------------------------------");

  equal( stringToEncode.slice(0,200), decrypted.slice(0,200) );
});

我的测试 HTML 包装器加载这些:

<script src="../bower_components/jquery/dist/jquery.min.js"></script>
<script src="../bower_components/js-base64/base64.js"></script>
<script src="../bower_components/thrift/lib/js/src/thrift.js"></script>
<script src="../bower_components/underscore/underscore-min.js"></script>
<script src="../bower_components/qunit/qunit/qunit.js"></script>
<script src="../bower_components/browserify-cryptojs/components/core.js"></script>
<script src="../bower_components/browserify-cryptojs/components/sha256.js"></script>
<script src="../bower_components/browserify-cryptojs/components/enc-base64.js"></script>
<script src="../bower_components/browserify-cryptojs/components/cipher-core.js"></script>
<script src="../bower_components/browserify-cryptojs/components/format-hex.js"></script>
<script src="../bower_components/browserify-cryptojs/components/aes.js"></script>
<script src="../bower_components/browserify-cryptojs/components/pad-nopadding.js"></script>
<!-- the Test Suite-->
<script type="text/javascript" src="test-client.js" charset="utf-8"></script>
<!-- CSS-->
<link rel="stylesheet" href="../bower_components/qunit/qunit/qunit.css" type="text/css" media="screen" />

而我的 bower.json 是:

{
  "name": "gossiperl-client-chrome",
  "version": "0.1.0",
  "main": "manifest.json",
  "dependencies": {
    "jquery": "~1.11.0",
    "underscore": "~1.7.0",
    "thrift": "radekg/thrift#js-binary-protocol",
    "js-base64": "~2.1.5",
    "qunit": "~1.14.0",
    "browserify-cryptojs": "~0.3.1"
  },
  "authors": [
    "radekg <...@....com>"
  ],
  "description": "Gossiperl Chrome client with a sample application",
  "keywords": [
    "gossiperl",
    "client"
  ],
  "license": "MIT",
  "homepage": "http://....com",
  "private": true
}

问题是 CryptoJS 将输入视为 UTF-8 输入字符串,除非它已经是 WordArray。如果您的输入不是 UTF-8,这当然是个问题。您看到的是 0x80 (128) 以上的值被转换为两个字节以修复 UTF-8 编码。

只要 WordArray 不支持从数组到 WordArray 的直接转换,您就可以直接转换为十六进制,然后再转换为 WordArray。少了这个功能有点奇怪。

以下将具有无符号字节值的数组转换为十六进制(对无效字节值有一点保护):

function tohex(unsignedByteArray) {
    var hex = "";
    for (var i = 0; i < unsignedByteArray.length; i++) {
        var c = unsignedByteArray[i];
        if (c < 0 || c > 255) {
            throw "Value not an unsigned byte in array";
        }
        var h = c.toString(16);
        if (h.length == 1) {
            hex += "0" + h;
        } else {
            hex += h;
        }
    }
    return hex;
}

function fromhex(hex) {
    if (hex.length % 2 !== 0) {
        throw "Hex string should contain even number of hex digits, one per byte";
    }
    var unsignedByteArray = [];
    for (var i = 0; i < hex.length; i = i + 2) {
        var h = hex.substring(i, i + 2);
        if (!/^[0-9a-f]{2}$/i.test(h)) {
            throw "Invalid hexdigit at offset " + i;
        }
        var c = parseInt(h, 16);
        unsignedByteArray[unsignedByteArray.length] = c;
    }
    return unsignedByteArray;
}

因此您可以像这样使用这些函数:

var bArr = [11, 0, 1, 0, 0, 0, 6, 100, 105, 103, 101, 115, 116, 11, 0, 2, 0, 0, 0, 152, 67, 119, 65, 66, 65, 65, 65, 65, 69, 109, 78, 111, 99, 109, 57, 116, 90, 83, 49, 48, 90, 88, 78, 48, 76, 87, 78, 115, 97, 87, 86, 117, 100, 65, 103, 65, 65, 103, 65, 65, 49, 68, 69, 75, 65, 65, 77, 65, 65, 65, 65, 65, 86, 75, 102, 66, 85, 103, 115, 65, 66, 65, 65, 65, 65, 67, 81, 49, 90, 68, 99, 119, 77, 71, 73, 120, 78, 67, 48, 121, 78, 84, 90, 107, 76, 84, 81, 119, 77, 109, 81, 116, 79, 84, 65, 48, 90, 105, 48, 52, 79, 84, 86, 105, 78, 68, 73, 50, 89, 109, 78, 108, 78, 84, 99, 76, 65, 65, 85, 65, 65, 65, 65, 85, 89, 50, 104, 121, 98, 50, 49, 108, 76, 87, 78, 115, 97, 87, 86, 117, 100, 67, 49, 122, 90, 87, 78, 121, 90, 88, 81, 65, 11, 0, 3, 0, 0, 0, 36, 52, 51, 52, 55, 54, 56, 98, 53, 45, 50, 48, 102, 102, 45, 52, 99, 100, 102, 45, 56, 53, 97, 50, 45, 57, 49, 49, 56, 50, 98, 55, 98, 51, 102, 57, 53, 0];
var bArrHex = tohex(bArr);

var stringToEncode = CryptoJS.enc.Hex.parse(bArrHex);

var symmetricKey = "v3JElaRswYgxOt4b";

var key = CryptoJS.enc.Latin1.parse(CryptoJS.enc.Latin1.stringify(CryptoJS.SHA256(symmetricKey)));

var iv = CryptoJS.lib.WordArray.random(16);

var encrypted = CryptoJS.AES.encrypt(stringToEncode, key, { iv: iv, format: CryptoJS.format.OpenSSL });

var decrypted = CryptoJS.AES.decrypt(encrypted, key, { iv: iv, format: CryptoJS.format.OpenSSL });

var result = fromhex(decrypted.toString(CryptoJS.enc.Hex));

console.log(result);

if (bArr.toString() == result.toString()) {
    console.log("success");
}

请注意,encrypted 用作字符串时会自动编码为 base64。您不能对密文使用 Latin1 编码。请注意,您的密钥还应包含随机字节,而不仅仅是现在的可打印字符。

最后请注意,在没有 MAC 的情况下发送 AES 加密文本本质上是不安全的,例如因为填充 oracle 攻击以及任何人都可以更改传输中的数据这一事实。

作为补充参考。接受的答案包含问题的关键。

问题是 CryptoJS 将输入视为 UTF-8 输入字符串,除非它已经是 WordArray

确实,我已经将测试更改为以下内容:

test("Decryption", function() {
  var bArr = [11,0,1,0,0,0,6,100,105,103,101,115,116,11,0,2,0,0,0,152,67,119,65,66,65,65,65,65,69,109,78,111,99,109,57,116,90,83,49,48,90,88,78,48,76,87,78,115,97,87,86,117,100,65,103,65,65,103,65,65,49,68,69,75,65,65,77,65,65,65,65,65,86,75,102,66,85,103,115,65,66,65,65,65,65,67,81,49,90,68,99,119,77,71,73,120,78,67,48,121,78,84,90,107,76,84,81,119,77,109,81,116,79,84,65,48,90,105,48,52,79,84,86,105,78,68,73,50,89,109,78,108,78,84,99,76,65,65,85,65,65,65,65,85,89,50,104,121,98,50,49,108,76,87,78,115,97,87,86,117,100,67,49,122,90,87,78,121,90,88,81,65,11,0,3,0,0,0,36,52,51,52,55,54,56,98,53,45,50,48,102,102,45,52,99,100,102,45,56,53,97,50,45,57,49,49,56,50,98,55,98,51,102,57,53,0];
  var dataToEncrypt = toCryptoJSWordArray( bArr );

  var symmetricKey = "v3JElaRswYgxOt4b";

  var key = CryptoJS.enc.Latin1.parse( CryptoJS.enc.Latin1.stringify( CryptoJS.SHA256( symmetricKey ) ) );
  var iv  = CryptoJS.lib.WordArray.random( 16 );

  var encrypted = CryptoJS.AES.encrypt( dataToEncrypt,
                                        key,
                                        { iv: iv, format: CryptoJS.format.OpenSSL }
                                      ).ciphertext.toString(CryptoJS.enc.Latin1);

  var decrypted = toByteArray( CryptoJS.AES.decrypt( { ciphertext: CryptoJS.enc.Latin1.parse(encrypted) },
                                          key,
                                          { iv: iv, padding: CryptoJS.pad.NoPadding }
                                      ).toString(CryptoJS.enc.Latin1) );

  var bstr1 = "";
  for (var i=0; i<bArr.length; i++) {
    bstr1 += (i>0) ? ","+bArr[i] : bArr[i]+"";
  }
  var bstr2 = "";
  for (var i=0; i<decrypted.length; i++) {
    bstr2 += (i>0) ? ","+decrypted[i] : decrypted[i]+"";
  }

  console.log("------------------------------------------");
  console.log(bstr1);
  console.log(bstr2);
  console.log("------------------------------------------");

  deepEqual( bArr.slice(0,200), decrypted.slice(0,200) );
});

function toCryptoJSWordArray(bArr) {
  var latin1StrLength = bArr.length;
  // Convert
  var words = [];
  for (var i = 0; i < bArr.length; i++) {
    words[i >>> 2] |= (bArr[i] & 0xff) << (24 - (i % 4) * 8);
  }
  return new CryptoJS.lib.WordArray.init(words, bArr.length);
}

function toByteArray(str) {
  var bArr = [];
  for (var i=0; i<str.length; i++) {
    bArr.push( str.charCodeAt(i) );
  }
  return bArr;
}

这会从我的字节数组创建一个 WordArray,然后进行加密和解密。解密数据正确返回。