Как расшифровать криптографию 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
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
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

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