Как расшифровать криптографию NodeJS на стороне клиента с помощью известного ключа шифрования?

Я пытаюсь использовать шифрование AES на стороне сервера и расшифровку на стороне клиента. Я следовал примеру, где CryptoJS используется на стороне клиента для шифрования, а SubtleCrypto на стороне клиента также для расшифровки, но в моем случае шифрование и расшифровка разделены.

Предположим, у меня есть следующая функция шифрования в React Native:

const encrypt = (str: string) => {
  const iv = crypto.randomBytes(12);
  const myHexToken = "0x...."
  const cipher = crypto.createCipheriv('aes-256-gcm', myHexToken.slice(0,32), iv)
  let encrypted = cipher.update(str, 'utf8', 'hex')
  encrypted += cipher.final('hex');
  const tag = cipher.getAuthTag();

  return {
    message: encrypted,
    tag: tag.toString('hex'),
    iv: iv.toString('hex'),
  };
};

Затем этот json отправляется клиенту через postMessage веб-просмотра.

На стороне клиента вставлен следующий javascript:

var myHexToken = "0x....";

window.addEventListener("message", async function (event) {
  var responseData = JSON.parse(event.data);
  try {
  var decryptedData = await decrypt(responseData.iv, responseData.message, responseData.tag);
  } catch (e) {
    alert(e);
  } 
  // ...

Как я могу расшифровать responseData.message в WebView через SubtleCrypto API Web Crypto?

Я пробовал разные вещи с помощью следующих методов, но я продолжаю получать «OperationalError»:

function fromHex(hexString) { 
  return new Uint8Array(hexString.match(/.{1,2}/g).map(byte => parseInt(byte, 16)));
}

function str2ab(str) {
  const buf = new ArrayBuffer(str.length);
  const bufView = new Uint8Array(buf);
  for (let i = 0, strLen = str.length; i < strLen; i++) {
    bufView[i] = str.charCodeAt(i);
  }
  return buf;
}

function fromBase64(base64String) {
 return Uint8Array.from(window.atob(base64String), c => c.charCodeAt(0));
}

async function importKey(rawKey) {
  var key = await crypto.subtle.importKey(
    "raw",
    rawKey,                                                 
    "AES-GCM",
    true,
    ["encrypt", "decrypt"]
  );
  return key;
}

async function decrypt(iv, data, tag) {
  var rawKey = fromHex(myHexToken.slice(0,32));
  var iv = fromHex(iv);
  var ciphertext = str2ab(data + tag);
  
  var cryptoKey = await importKey(rawKey)

  var decryptedData = await window.crypto.subtle.decrypt(
    {
      name: "AES-GCM",
      iv: iv
    },
    cryptoKey,
    ciphertext
  )
  
   var decoder = new TextDecoder();
   var plaintext = decoder.decode(decryptedData);

  return plaintext;
}

ОБНОВЛЕНИЕ 1: добавлена ​​серверная часть реализации getAuthTag. Изменен IV, чтобы иметь длину 12 байтов. Попытка конкатенации зашифрованного текста и тега на стороне клиента.

Я проверил, что «myHexToken» одинаков как на стороне клиента, так и на стороне сервера. Кроме того, возвращаемые значения метода encrypt() на стороне сервера правильно отправляются клиенту.

В качестве отправной точки ваш код шифрования — «aes-256-gcm», а код дешифрования — «AES-CBC». Переключение режимов никогда не будет работать. Я бы рекомендовал использовать GCM с обеих сторон, но они абсолютно должны совпадать. Каждая часть кода шифрования должна совпадать с кодом дешифрования.

Rob Napier 13.02.2023 01:44

В коде NodeJS отсутствует определение тега GCM через getAuthTag(). Кроме того, для GCM рекомендуемая длина IV/nonce составляет 12 байт (а не 16). Обратите внимание, что для GCM, в отличие от криптомодуля, WebCrypto не обрабатывает зашифрованный текст и тег по отдельности, вместо этого шифрование возвращается, а для расшифровки требуются конкатенированные значения зашифрованного текста и тега.

Topaco 13.02.2023 08:46

Спасибо @RobNapier. Извините, это была ошибка копирования/вставки, так как я пробовал разные вещи. Я гарантировал, что на стороне клиента и сервера используется один и тот же алгоритм шифрования.

apfz 13.02.2023 10:29

@Topaco Спасибо за ваши идеи. Я обновил код в меру своих возможностей. Извините, относительно новичок в этом, любые дальнейшие указания приветствуются. На данный момент все еще получаю OperationalError. Есть идеи, что мне не хватает?

apfz 13.02.2023 10:31
Laravel с Turbo JS
Laravel с Turbo JS
Turbo - это библиотека JavaScript для упрощения создания быстрых и высокоинтерактивных веб-приложений. Она работает с помощью техники под названием...
Типы ввода HTML: Лучшие практики и советы
Типы ввода HTML: Лучшие практики и советы
HTML, или HyperText Markup Language , является стандартным языком разметки, используемым для создания веб-страниц. Типы ввода HTML - это различные...
Аутсорсинг разработки PHP для индивидуальных веб-решений
Аутсорсинг разработки PHP для индивидуальных веб-решений
Услуги PHP-разработки могут быть экономически эффективным решением для компаний, которые ищут высококачественные услуги веб-разработки по доступным...
Понимание Python и переход к SQL
Понимание Python и переход к SQL
Перед нами лабораторная работа по BloodOath:
Слишком много useState? Давайте useReducer!
Слишком много useState? Давайте useReducer!
Современный фронтенд похож на старую добрую веб-разработку, но с одной загвоздкой: страница в браузере так же сложна, как и бэкенд.
Узнайте, как использовать теги &lt;ul&gt; и &lt;li&gt; для создания неупорядоченных списков в HTML
Узнайте, как использовать теги <ul> и <li> для создания неупорядоченных списков в HTML
HTML предоставляет множество тегов для структурирования и организации содержимого веб-страницы. Одним из наиболее часто используемых тегов для...
0
4
78
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

В коде WebCrypto ключ не должен быть шестнадцатерично декодирован с помощью fromHex(), но должен быть преобразован в ArrayBuffer с помощью str2ab(). Кроме того, конкатенация зашифрованного текста и тега не должна преобразовываться в ArrayBuffer с помощью str2ab(), но должна быть шестнадцатерично декодирована с помощью fromHex().

С этими исправлениями расшифровка работает:

Тест:

Для теста на стороне NodeJS используются следующий шестнадцатеричный ключ и открытый текст:

const myHexToken = '000102030405060708090a0b0c0d0e0ff0f1f2f3f4f5f6f7f8f9fafbfcfdfeff';
const plaintext = "The quick brown fox jumps over the lazy dog";
const encryptedData = encrypt(plaintext);
console.info(encryptedData);

Это приводит, например. в следующем выводе:

{
    message: 'cc4beae785cda5c9413f49cf9449a6ae17fdc0f7435b9a8fd954602bdb4f4b825793f6b561c0d9a709007c',
    tag: '046c8e56bbd13db2faed82d1b19c665e',
    iv: '11f87b0eaf006373ae8bc94d'
} 

Созданный таким образом зашифрованный текст можно успешно расшифровать с помощью фиксированного кода JavaScript:

(async () => {

function fromHex(hexString) { 
    return new Uint8Array(hexString.match(/.{1,2}/g).map(byte => parseInt(byte, 16)));
}

function str2ab(str) {
    const buf = new ArrayBuffer(str.length);
    const bufView = new Uint8Array(buf);
    for (let i = 0, strLen = str.length; i < strLen; i++) {
        bufView[i] = str.charCodeAt(i);
    }
    return buf;
}

async function importKey(rawKey) {
    var key = await crypto.subtle.importKey(
        "raw",
        rawKey,                                                 
        "AES-GCM",
        true,
        ["encrypt", "decrypt"]
    );
    return key;
}

async function decrypt(iv, data, tag) {
    //var rawKey = fromHex(myHexToken.slice(0,32)); // Fix 1
    var rawKey = str2ab(myHexToken.slice(0,32));
  
    var iv = fromHex(iv);
  
    //var ciphertext = str2ab(data + tag); // Fix 2
    var ciphertext = fromHex(data + tag);
  
    var cryptoKey = await importKey(rawKey)

    var decryptedData = await window.crypto.subtle.decrypt(
        {
            name: "AES-GCM",
            iv: iv
        },
        cryptoKey,
        ciphertext
    );
  
     var decoder = new TextDecoder();
     var plaintext = decoder.decode(decryptedData);

    return plaintext;
}

var myHexToken = '000102030405060708090a0b0c0d0e0ff0f1f2f3f4f5f6f7f8f9fafbfcfdfeff'
var data = {
    message: 'cc4beae785cda5c9413f49cf9449a6ae17fdc0f7435b9a8fd954602bdb4f4b825793f6b561c0d9a709007c',
    tag: '046c8e56bbd13db2faed82d1b19c665e',
    iv: '11f87b0eaf006373ae8bc94d'
} 
 
var plaintext = await decrypt(data.iv, data.message, data.tag);
console.info(plaintext);

})();

Замечание по поводу ключа: в опубликованном коде NodeJS установлено const myHexToken = "0x....". Мне не ясно, должен ли префикс 0x просто символизировать строку в шестнадцатеричном коде или действительно содержится в строке. Если последнее, его следует удалить перед неявной кодировкой UTF-8 (по createCiperiv()). В случае шестнадцатеричного декодирования его все равно нужно удалить. В опубликованном примере используется действительный 32-байтовый ключ в шестнадцатеричном кодировании (т.е. без префикса 0x).


Что касается кодировки ключей, также обратите внимание на следующее:

  • Преобразование ключа из шестнадцатеричной строки в кодировку UTF-8 (или ASCII) приводит к тому, что рассматривается только половина ключа, в примере: 000102030405060708090a0b0c0d0e0f. Это снижает безопасность, потому что диапазон значений на байт уменьшен с 256 до 16 значений. Чтобы учитывался весь ключ, правильным преобразованием на стороне NodeJS будет: Buffer.from(myHexToken, 'hex'), а на стороне WebCrypto: var rawKey = fromHex(myHexToken).

  • Из-за своей неявной кодировки UTF8 crypto.createCipheriv(..., myHexToken.slice(0,32), ...) создает 32-байтовый ключ и функционально идентичен str2ab(myHexToken.slice(0,32)) только до тех пор, пока символы в подстроке myHexToken.slice(0,32) соответствуют символам ASCII (что верно для строки в шестнадцатеричном кодировании).

Это очень полезно, очень ценю это! Теперь это работает, еще раз спасибо за время, потраченное на это! Если возможно, мне было бы интересно узнать, почему нужно удалить 0x, разве это не считается допустимым шестнадцатеричным значением?

apfz 14.02.2023 11:06

@apfz — 0x — это просто префикс, который помечает следующие значения как шестнадцатеричные значения, но не принадлежит самому значению. Большинство реализаций шестнадцатеричного декодирования также не принимают префикс, например. в NodeJS Buffer.from('0x...', 'hex') не работает.

Topaco 14.02.2023 13:36

@apfz — с кодировкой UTF-8/ASCII (как в вашем коде) 0x будет закодировано (как последовательность байтов 0x30, 0x78), но не подходит для ключа, поскольку это постоянное значение. Однако кодирование UTF-8/ASCII шестнадцатеричной последовательности байтов, как правило, является неправильным подходом для ключа (как уже указано в ответе).

Topaco 14.02.2023 13:38

Другие вопросы по теме