CryptoJS AES CBC 256 decrypt добавляет дополнительный байт в середину открытого текста

Я пишу расширение Chrome, которое использует CryptoJS для работы с Apache Thrift. В настоящее время я пытаюсь заставить CryptoJS работать. У меня проблема с расшифровкой CryptoJS данных, зашифрованных CryptoJS. Ниже, после описания проблемы, прилагаю тестовый пример.

Происходит следующее, у меня массив "байтов":

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

Единственная разница - это дополнительные 194 в позиции 20. Все остальные байты точно такие же, за исключением заполнения, очевидно. Я пытаюсь понять, откуда это.

Больше информации о том, что это байты. Это структура Thrift с 3 полями, поле 2 содержит представление Base64 другой встроенной структуры Thrift. Объяснение первых 20 байтов ввода:

  • 11,0,1: Бережливое поле типа string, fid 1
  • 0,0,0,6: int32 длина значения fid 1
  • 100,67,119,65,66,65: строка «дайджест»
  • 11,0,2: Бережливое поле типа string, fid 2
  • 0,0,0,152: int32 длина значения fid 2
  • 67 ... до следующего байта 11: представление встроенной структуры в формате Base64

Из-за проблемы с расшифровкой парсер Thrift неправильно распознает длину значения fid 2.

Я считаю, что использую AES256 CBC с 32-байтовым ключом (SHA256) и 16-байтовым IV в режиме OpenSSL с заполнением PKCS7.

Это мой тест qunit.

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 <[email protected]>"
  ],
  "description": "Gossiperl Chrome client with a sample application",
  "keywords": [
    "gossiperl",
    "client"
  ],
  "license": "MIT",
  "homepage": "http://....com",
  "private": true
}

person Community    schedule 03.01.2015    source источник
comment
C2 или 194 - это часть двухбайтового символа в кодировке UTF-8. Добро пожаловать в ад строк / двоичных кодов JavaScripts. Да, и 152 - это первый символ, не являющийся частью US-ASCII, требующий кодирования двух байтов в UTF-8.   -  person Maarten Bodewes    schedule 03.01.2015
comment
Ах, а другого выхода нет?   -  person    schedule 03.01.2015
comment
Ищу, но у CryptoJS нет прямого массива в WordArray преобразование. Ответ, вероятно, состоит в том, чтобы сначала преобразовать в шестнадцатеричные числа, а затем создать из него WordArray (что довольно неэффективно, но JavaScript обычно предназначен для операций с байтовым массивом / криптографическими операциями).   -  person Maarten Bodewes    schedule 03.01.2015
comment
Это немного грустно. Все остальное программное обеспечение на Erlang, java, mono, ruby ​​ожидает данные в этом формате - без кодирования внешнего дайджеста. Возможно, вы говорите, что создание массива слов прямо из моего массива было бы вариантом. Или посмотрите на NaCl: - /   -  person    schedule 03.01.2015
comment
Держите лошадей, небольшое кодирование / декодирование не должно вас останавливать: P   -  person Maarten Bodewes    schedule 03.01.2015


Ответы (2)


Проблема в том, что 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 для зашифрованного текста. Обратите внимание, что ваш ключ также должен содержать случайные байты, а не только печатные символы, как сейчас.

Наконец, обратите внимание, что отправка зашифрованного текста AES без MAC по своей сути небезопасна, например, из-за атак оракула с заполнением и того факта, что любой может изменить данные в пути.

person Maarten Bodewes    schedule 03.01.2015
comment
Прошу прощения, если мой JS-код выглядит ужасно как Java :) Имейте в виду, что приведенный выше код не является самым эффективным с точки зрения памяти и может быть недостаточным для очень больших входных данных. - person Maarten Bodewes; 03.01.2015
comment
Я не беспокоюсь об эффективности, поскольку эта штука не предназначена для больших объемов. Вскоре попробую написать код. Спасибо! - person ; 03.01.2015
comment
@radekg Сам parse не пробовал, но он должен работать. Я видел, что у вас был ограниченный ввод, главным образом, чтобы предупредить других разработчиков, использующих Google. - person Maarten Bodewes; 03.01.2015
comment
Пробуем код сейчас. Вся эта уловка больше для полноты решения на данный момент. У меня он работает на Java, Mono (.NET), Ruby, Erlang. Я знаю, что он будет работать в Python, Haskell с небольшим изгибом, поэтому я подумал, что было бы здорово, если бы он работал в Chrome. Пытаюсь туда попасть. Сообщу вам в ближайшее время. - person ; 04.01.2015
comment
Это отличный материал. Это определенно решает насущный вопрос и указывает мне правильное направление во всей этой хромированной штуке. Думаю о том, как я могу включить это в существующий процесс. Сервер получает данные в следующем формате: {iv: 1..16} {зашифрованные байты данных}, думаю, я мог бы вставить несколько байтов между IV и зашифрованными данными в этом случае, скажем, ^ * ^, на сервере Erlang Я могу сделать: ‹* IV: 16 / двоичный, шестнадцатеричный / двоичный, данные / двоичный ›› и вернуться к ‹< IV: 16 / двоичный, данные / двоичный ››, если не совпадают. - person ; 04.01.2015
comment
Получать проще, независимо от того, что я получаю в JS, я могу пройти расшифровку без каких-либо проблем? Это хороший вопрос! 194 добавлено при шифровании или расшифровке :) - person ; 04.01.2015

Просто как дополнительная ссылка. В принятом ответе содержится ключ к проблеме.

Проблема в том, что 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 из моего байтового массива, а затем шифрует и дешифрует. Расшифрованные данные возвращаются правильно.

person Community    schedule 04.01.2015
comment
Ах, интересное дополнение, меньше конверсии = лучше. - person Maarten Bodewes; 04.01.2015