Шифрование AES с помощью CryptoJS, искажающее эмодзи Unicode

Я пишу систему, в которой пользователь может что-то написать (через мобильный браузер), и эта «строка» будет зашифрована паролем, выбранным пользователем. Поскольку часто используются смайлики 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 + "";
};

Может ли кто-нибудь сказать мне, что я делаю неправильно?


person Bonar Scripta    schedule 19.02.2016    source источник
comment
Не могли бы вы предоставить ссылку на криптографическую библиотеку, которую вы используете? Основная проблема здесь в том, что криптоалгоритмы работают с двоичными данными, а не со строками JavaScript. Каждый символ в строке JavaScript занимает два байта. Код шифрования, который обрабатывает строки JavaScript как двоичные данные, обычно игнорирует старший байт и предполагает, что младшие байты используются для хранения данных. Emoji требует того старшего байта, данные которого теряются. Вам необходимо явно закодировать данные строковых символов в UTF-8 в той или иной форме. Хитрым решением было бы использовать encode/decodeURIComponent до/после декодирования.   -  person Jeremy    schedule 20.02.2016
comment
@JeremyBanks Я использую копию оригинальной библиотеки из кода Google (code.google .com/archive/p/crypto-js).   -  person Bonar Scripta    schedule 20.02.2016
comment
Вы написали aes_encrypt?   -  person alexandergs    schedule 20.02.2016
comment
да, но это всего лишь небольшая функция, поэтому мне не нужно писать все это каждый раз, когда я шифрую что-то. 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 + ""; };   -  person Bonar Scripta    schedule 20.02.2016
comment
Я не вижу, чтобы вы применяли кодировку — это правильные байты для ????, вы просто забыли сказать, какую кодировку использовать для отображения. ???? — это D83C + DFAE, которые, если их рассматривать как отдельные байты D8, 3C, DF, AE и рассматривать как ANSI, составляют четырехсимвольную строку Ø<ß®   -  person Mike 'Pomax' Kamermans    schedule 20.02.2016
comment
Эта функция вызывает ascii_to_hex, но содержимое несовместимо с ASCII, это полный юникод. Это может быть причиной или усилением вашей проблемы, в зависимости от того, как работает CryptoJS. Я снова предлагаю попробовать encodeURIComponent() и decodeURIComponent(), чтобы создать строку, совместимую с ASCII. (Поведение, которое наблюдает Майк, заставляет меня усомниться в безопасности модели CryptoJS — я надеюсь, что она не используется для чего-то, где требуется реальная подлинная безопасность.)   -  person Jeremy    schedule 20.02.2016
comment
@JeremyBanks спасибо!! он отлично работает с этими функциями :)   -  person Bonar Scripta    schedule 20.02.2016


Ответы (2)


Предупреждение. Очень сложно получить правильный криптографический код. Это может быть еще сложнее в JavaScript, где вам часто не хватает контроля над средой выполнения и (как обсуждается ниже) отсутствие языковой поддержки приводит к несогласованным соглашениям. Я недостаточно исследовал библиотеку CryptoJS, чтобы знать о ее конструкции или безопасности, а также о том, безопасно ли она используется в этом контексте.

Пожалуйста, не полагайтесь на какой-либо из этих кодов, чтобы быть действительно безопасным без профессионального аудита.

Распространенной проблемой при работе с криптографическим кодом в JavaScript было отсутствие встроенного способа представления двоичных данных. Это было решено в современных движках (с типами Blobs и TypedArrays в браузере и Buffers в Node.js), но все еще есть много кода, который не использует это преимущество по историческим причинам или по причинам совместимости.

Без этих встроенных типов одно общее соглашение (используемое встроенными atob и btoa) заключается в использовании встроенного строкового типа для хранения двоичных данных. Строка JavaScript на самом деле представляет собой список двухбайтовых значений (обычно содержащих символы Unicode в кодировке UCS-2/UTF-16). Пользователи, желающие хранить двоичные данные, часто просто используют младший байт, полностью игнорируя старший байт.

Если вы работаете только с данными, совместимыми с ASCII, вам может сойти с рук игнорирование этих деталей при использовании подобного кода (т. е. все будет работать, но могут быть тонкие последствия для безопасности). Это связано с тем, что текст, закодированный как ASCII, выглядит так же, как текст, закодированный как UTF-16, с удаленными старшими байтами. Но когда вы выходите за рамки этого, вам нужно выполнить кодирование.

Наиболее правильно (кроме использования реального двоичного типа) было бы взять входную строку символов, закодировать ее в UTF-8 и поместить эти данные в младшие байты выходной строки. Однако в JavaScript нет встроенной функции для этого. В качестве грубой, но простой альтернативы можно использовать функцию encodeURIComponent будет кодировать любую допустимую строку 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 есть более правильный способ обработки подобных вещей, но я о нем не знаю. Пожалуйста, подумайте об этом подробнее, если вы планируете развернуть этот код для общего пользования.

person Jeremy    schedule 20.02.2016
comment
Это быстрый, грубый ответ, но я подумал, что лучше иметь что-то здесь, чем оставлять информацию, разбросанную по комментариям. Я не эксперт по криптографии, это ненадежный совет по криптографии, «вставьте сюда еще дюжину оговорок, обратитесь к эксперту» и т. д. - person Jeremy; 20.02.2016
comment
a) На самом деле нет необходимости в encodeURIComponent, потому что CryptoJS может сам обрабатывать UTF-8. б) Увеличение размера незначительно, поскольку OP уже удваивает размер с помощью ascii_to_hex(). c) Вы правы, разобраться в криптовалюте сложно. В основном я даю текстовые описания (и ссылки) того, что нужно сделать, вместо того, чтобы показывать соответствующий код, потому что это увеличило бы длину поста. - person Artjom B.; 20.02.2016

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 с одной итерацией используется для получения ключа из пароля. Вам нужно будет использовать что-то вроде PBKDF2, который предоставляет CryptoJS. (Не забывайте каждый раз использовать случайный IV. Он не обязательно должен быть секретным, поэтому вы можете отправить его вместе с зашифрованным текстом.)

  • Зашифрованный текст не аутентифицируется, что делает маловероятным обнаружение (злонамеренных) манипуляций с зашифрованными данными. Лучше аутентифицировать ваши зашифрованные тексты, чтобы такие атаки, как атака оракула заполнения, были невозможны. Это можно сделать с помощью аутентифицированных режимов, таких как GCM или EAX, или с помощью схемы зашифровать-затем-MAC со строгим MAC, например HMAC-SHA256, который предоставляет CryptoJS.

person Artjom B.    schedule 20.02.2016
comment
Спасибо за более информативное объяснение! - person Jeremy; 20.02.2016