Ошибка: OperationError (в обещании) при попытке расшифровать контент

Я играл с WebCrypto, создавая небольшой сайт Astro с закодированным блоком, который декодируется с его помощью (я знаю, что это не будет особенно безопасно, и его не будут использовать для чего-то серьезного). У меня есть версия, работающая на одной странице, но у меня возникает вышеуказанная ошибка при попытке сделать это через интерфейс и промежуточное программное обеспечение (nodejs). У меня есть следующий файл, который обрабатывает большую часть криптографических данных как во фронтальном, так и в промежуточном программном обеспечении.

/**
 * Prepared some variables and does a very basic sanity check.
 */

var Crypto, Subtle;
if (typeof window === "undefined") {
  Crypto = globalThis.crypto;
  Subtle = Crypto.subtle;
} else {
  Crypto = window.crypto;
  Subtle = Crypto.subtle || Crypto.webkitSubtle;
}
if (!Subtle) {
  throw new Error(`Web Crypto API not supported, you're outta luck pal.`);
}

/**
 * Return a base64 string from the provided buffer array. From https://stackoverflow.com/a/68161336
 * @param {Uint8Array} arrayBuffer
 * @returns {String}
 */
function ab2b64(arrayBuffer) {
  return btoa(String.fromCharCode.apply(null, new Uint8Array(arrayBuffer)));
}
/**
 * Return an array from the provided buffer bae64 string. From https://stackoverflow.com/a/68161336
 * @param {String} base64string
 * @returns {Uint8Array}
 */
function b642ab(base64string) {
  return Uint8Array.from(atob(base64string), (c) => c.charCodeAt(0));
}

/**
 * Returns a initialization vector for the AES encryption
 * @returns {Uint8Array}
 */
export const getInitVect = () => {
  return Crypto.getRandomValues(new Uint8Array(12));
};
/**
 * Returns a salt to spice up encryption
 * @returns {Uint8Array}
 */
export const getSalt = () => {
  return Crypto.getRandomValues(new Uint8Array(16));
};

/**
 * Returns a key derived from provided password with the encryption usage
 * @param {string} password
 * @returns {CryptoKey}
 */
export const getEncryptionKey = async (password) =>
  getEncryptionOrDecryptionKey(password, ["encrypt"]);
/**
 * Returns a key derived from provided password with the decryption usage
 * @param {string} password
 * @returns {CryptoKey}
 */
export const getDecryptionKey = async (password) =>
  getEncryptionOrDecryptionKey(password, ["decrypt"]);

/**
 * Returns an encryptionKey derived from the provided password with the requested usages
 * @param {string} password
 * @returns {CryptoKey}
 */
const getEncryptionOrDecryptionKey = async (password, usages) => {
  // Failure is not only an option, it's a probability.
  try {
    // Check that a password was even provided.
    if (password == null || password == undefined || password.trim() == "") {
      throw new Error("No password was provided");
    }

    // Turn a regular boring password and turn it into a key
    const importedKey = await Subtle.importKey(
      "raw",
      new TextEncoder().encode("password"),
      "PBKDF2",
      false,
      ["deriveKey"]
    );
    const encryptionKey = await Subtle.deriveKey(
      {
        name: "PBKDF2",
        salt: getSalt(),
        iterations: 250000,
        hash: "SHA-256",
      },
      importedKey,
      { name: "AES-GCM", length: 256 },
      false,
      usages
    );

    return encryptionKey;
    // Handle those errors by simply throwing them away.
  } catch (err) {
    throw err;
  }
};

/**
 * Encrypts the provided content with the provided password and returns a base64 string for use.
 * @param {string} content
 * @param {string} password
 * @returns {string}
 */
export const encrypt = async (content, password) => {
  if (password == undefined || password.trim() == "") {
    throw new Error("No password was provided.");
  }

  // Get the bits we need to start encryption
  const encryptionKey = await getEncryptionKey(password);
  const initVector = await getInitVect();
  const salt = await getSalt();

  // Encrypt the data
  const encryptedContent = new Uint8Array(
    await Subtle.encrypt(
      {
        name: "AES-GCM",
        iv: initVector,
        tagLength: 128,
      },
      encryptionKey,
      new TextEncoder().encode(content)
    )
  );
  console.info("salt", salt);
  console.info("initVector", initVector);
  console.info("encryptedContent", encryptedContent);

  // Return the encoded string
  return ab2b64([...salt, ...initVector, ...encryptedContent]);
};

/**
 * Decrypts a provided base64 string created by encode with the provided password.
 * @param {string} encryptedContent
 * @param {string} password
 * @returns {string}
 */
export const decrypt = async (content, password) => {
  if (password == undefined || password.trim() == "") {
    throw new Error("No password was provided.");
  }

  const encryptedData = b642ab(content);
  const Salt = encryptedData.slice(0, 16);
  const InitVect = encryptedData.slice(16, 16 + 12);
  const encryptedContent = encryptedData.slice(16 + 12);

  const encryptionKey = await getDecryptionKey(password);

  console.info("Salt", Salt);
  console.info("InitVect", InitVect);
  console.info("encryptedContent", encryptedContent);

  // Decrypt
  const decryptedContent = await Subtle.decrypt(
    {
      name: "AES-GCM",
      iv: InitVect,
      tagLength: 128,
    },
    encryptionKey,
    encryptedContent
  );

  // Return decrypted content
  return decryptedContent;
};

А код в компоненте выглядит так:

  document
    .querySelector("form.password-box")
    .addEventListener("submit", async (e) => {
      e.preventDefault();

      const providedPassword =
        document.querySelector('input[name = "password"]').value + "password";
      const encryptedData = document.querySelector(
        "section[data-encryptedContent]"
      ).innerHTML;

      const decrypted = await decrypt(encryptedData, providedPassword);
    });

Используя console.infos в первом блоке кода, я подтвердил, что данные, поступающие из шифрования, и данные, поступающие в расшифровку, одинаковы, и код работал на одной странице.

Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать 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
0
55
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

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

Дешифрование не удается, поскольку для получения ключа во время шифрования и дешифрования используются разные соли и, следовательно, используются разные ключи.

Хотя соль извлекается с помощью функции decrypt() (const Salt = encryptedData.slice(0, 16)), она не используется для получения ключа.
Вместо этого генерируется новый случайный IV, поскольку getSalt() (который возвращает случайный IV) также вызывается в getEncryptionOrDecryptionKey() для расшифровки.

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

(async () => {

/**
 * Prepared some variables and does a very basic sanity check.
 */

var Crypto, Subtle;
if (typeof window === "undefined") {
  Crypto = globalThis.crypto;
  Subtle = Crypto.subtle;
} else {
  Crypto = window.crypto;
  Subtle = Crypto.subtle || Crypto.webkitSubtle;
}
if (!Subtle) {
  throw new Error(`Web Crypto API not supported, you're outta luck pal.`);
}

/**
 * Return a base64 string from the provided buffer array. From https://stackoverflow.com/a/68161336
 * @param {Uint8Array} arrayBuffer
 * @returns {String}
 */
function ab2b64(arrayBuffer) {
  return btoa(String.fromCharCode.apply(null, new Uint8Array(arrayBuffer)));
}
/**
 * Return an array from the provided buffer bae64 string. From https://stackoverflow.com/a/68161336
 * @param {String} base64string
 * @returns {Uint8Array}
 */
function b642ab(base64string) {
  return Uint8Array.from(atob(base64string), (c) => c.charCodeAt(0));
}

/**
 * Returns a initialization vector for the AES encryption
 * @returns {Uint8Array}
 */
/*export*/ const getInitVect = () => {
  return Crypto.getRandomValues(new Uint8Array(12));
};
/**
 * Returns a salt to spice up encryption
 * @returns {Uint8Array}
 */
/*export*/ const getSalt = () => {
  return Crypto.getRandomValues(new Uint8Array(16));
};

/**
 * Returns a key derived from provided password with the encryption usage
 * @param {string} password
 * @returns {CryptoKey}
 */
/*export*/ const getEncryptionKey = async (password) =>
  getEncryptionOrDecryptionKey(password, ["encrypt"]);
/**
 * Returns a key derived from provided password with the decryption usage
 * @param {string} password
 * @returns {CryptoKey}
 */
/*export*/ const getDecryptionKey = async (password) =>
  getEncryptionOrDecryptionKey(password, ["decrypt"]);

/**
 * Returns an encryptionKey derived from the provided password with the requested usages
 * @param {string} password
 * @returns {CryptoKey}
 */
const getEncryptionOrDecryptionKey = async (password, usages) => {
  // Failure is not only an option, it's a probability.
  try {
    // Check that a password was even provided.
    if (password == null || password == undefined || password.trim() == "") {
      throw new Error("No password was provided");
    }

    // Turn a regular boring password and turn it into a key
    const importedKey = await Subtle.importKey(
      "raw",
      new TextEncoder().encode("password"),
      "PBKDF2",
      false,
      ["deriveKey"]
    );
    const encryptionKey = await Subtle.deriveKey(
      {
        name: "PBKDF2",
        salt: getSalt(),
        iterations: 250000,
        hash: "SHA-256",
      },
      importedKey,
      { name: "AES-GCM", length: 256 },
      true, // false,  // for testing
      usages
    );

    return encryptionKey;
    // Handle those errors by simply throwing them away.
  } catch (err) {
    throw err;
  }
};

/**
 * Encrypts the provided content with the provided password and returns a base64 string for use.
 * @param {string} content
 * @param {string} password
 * @returns {string}
 */
/*export*/ const encrypt = async (content, password) => {
  if (password == undefined || password.trim() == "") {
    throw new Error("No password was provided.");
  }

  // Get the bits we need to start encryption
  const encryptionKey = await getEncryptionKey(password);
  console.info("Key", ab2b64(await Subtle.exportKey('raw', encryptionKey)));
  const initVector = await getInitVect();
  const salt = await getSalt();

  // Encrypt the data
  const encryptedContent = new Uint8Array(
    await Subtle.encrypt(
      {
        name: "AES-GCM",
        iv: initVector,
        tagLength: 128,
      },
      encryptionKey,
      new TextEncoder().encode(content)
    )
  );
  //console.info("salt", salt);
  //console.info("initVector", initVector);
  //console.info("encryptedContent", encryptedContent);

  // Return the encoded string
  return ab2b64([...salt, ...initVector, ...encryptedContent]);
};

/**
 * Decrypts a provided base64 string created by encode with the provided password.
 * @param {string} encryptedContent
 * @param {string} password
 * @returns {string}
 */
/*export*/ const decrypt = async (content, password) => {
  if (password == undefined || password.trim() == "") {
    throw new Error("No password was provided.");
  }

  const encryptedData = b642ab(content);
  const Salt = encryptedData.slice(0, 16);
  const InitVect = encryptedData.slice(16, 16 + 12);
  const encryptedContent = encryptedData.slice(16 + 12);

  const encryptionKey = await getDecryptionKey(password);
  console.info("Key", ab2b64(await Subtle.exportKey('raw', encryptionKey)));

  //console.info("Salt", Salt);
  //console.info("InitVect", InitVect);
  //console.info("encryptedContent", encryptedContent);

  // Decrypt
  const decryptedContent = await Subtle.decrypt(
    {
      name: "AES-GCM",
      iv: InitVect,
      tagLength: 128,
    },
    encryptionKey,
    encryptedContent
  );

  // Return decrypted content
  return decryptedContent;
};

var ct = await encrypt('The quick brown fox jumps over the lazy dog', 'my password');
console.info('Ciphertext:', ct);
try {
var dt = await decrypt(ct, 'my password');
console.info('Decrypted:', new TextDecoder().decode(dt));
} catch (err) {
  console.info(err.name, 'Decryption failed...');
}

})();

Таким образом, исправление состоит в том, чтобы использовать соль, сгенерированную во время шифрования, при расшифровке, например. следующее:

(async () => {

/**
 * Prepared some variables and does a very basic sanity check.
 */

var Crypto, Subtle;
if (typeof window === "undefined") {
  Crypto = globalThis.crypto;
  Subtle = Crypto.subtle;
} else {
  Crypto = window.crypto;
  Subtle = Crypto.subtle || Crypto.webkitSubtle;
}
if (!Subtle) {
  throw new Error(`Web Crypto API not supported, you're outta luck pal.`);
}

/**
 * Return a base64 string from the provided buffer array. From https://stackoverflow.com/a/68161336
 * @param {Uint8Array} arrayBuffer
 * @returns {String}
 */
function ab2b64(arrayBuffer) {
  return btoa(String.fromCharCode.apply(null, new Uint8Array(arrayBuffer)));
}
/**
 * Return an array from the provided buffer bae64 string. From https://stackoverflow.com/a/68161336
 * @param {String} base64string
 * @returns {Uint8Array}
 */
function b642ab(base64string) {
  return Uint8Array.from(atob(base64string), (c) => c.charCodeAt(0));
}

/**
 * Returns a initialization vector for the AES encryption
 * @returns {Uint8Array}
 */
/*export*/ const getInitVect = () => {
  return Crypto.getRandomValues(new Uint8Array(12));
};
/**
 * Returns a salt to spice up encryption
 * @returns {Uint8Array}
 */
/*export*/ const getSalt = () => {
  return Crypto.getRandomValues(new Uint8Array(16));
};

/**
 * Returns a key derived from provided password with the encryption usage
 * @param {string} password
 * @returns {CryptoKey}
 */
/*export*/ const getEncryptionKey = async (password, salt) =>   // Fix 1/7: pass salt as 2nd parameter
  getEncryptionOrDecryptionKey(password, salt, ["encrypt"]);
/**
 * Returns a key derived from provided password with the decryption usage
 * @param {string} password
 * @returns {CryptoKey}
 */
/*export*/ const getDecryptionKey = async (password, salt) =>  // Fix 2/7: pass salt as 2nd parameter
  getEncryptionOrDecryptionKey(password, salt, ["decrypt"]);

/**
 * Returns an encryptionKey derived from the provided password with the requested usages
 * @param {string} password
 * @returns {CryptoKey}
 */
const getEncryptionOrDecryptionKey = async (password, salt, usages) => {  // Fix 3/7: pass salt as 2nd parameter
  // Failure is not only an option, it's a probability.
  try {
    // Check that a password was even provided.
    if (password == null || password == undefined || password.trim() == "") {
      throw new Error("No password was provided");
    }

    // Turn a regular boring password and turn it into a key
    const importedKey = await Subtle.importKey(
      "raw",
      new TextEncoder().encode("password"),
      "PBKDF2",
      false,
      ["deriveKey"]
    );
    const encryptionKey = await Subtle.deriveKey(
      {
        name: "PBKDF2",
        salt: salt,  // Fix 4/7: apply passed salt
        iterations: 250000,
        hash: "SHA-256",
      },
      importedKey,
      { name: "AES-GCM", length: 256 },
      true, // false,  // for testing
      usages
    );

    return encryptionKey;
    // Handle those errors by simply throwing them away.
  } catch (err) {
    throw err;
  }
};

/**
 * Encrypts the provided content with the provided password and returns a base64 string for use.
 * @param {string} content
 * @param {string} password
 * @returns {string}
 */
/*export*/ const encrypt = async (content, password) => {
  if (password == undefined || password.trim() == "") {
    throw new Error("No password was provided.");
  }

  // Get the bits we need to start encryption
  const salt = await getSalt(); // Fix 5/7: generate salt
  const encryptionKey = await getEncryptionKey(password, salt); // Fix 6/7: apply generated salt for key derivation
  console.info("Key", ab2b64(await Subtle.exportKey('raw', encryptionKey)));
  const initVector = await getInitVect();

  // Encrypt the data
  const encryptedContent = new Uint8Array(
    await Subtle.encrypt(
      {
        name: "AES-GCM",
        iv: initVector,
        tagLength: 128,
      },
      encryptionKey,
      new TextEncoder().encode(content)
    )
  );
  //console.info("salt", salt);
  //console.info("initVector", initVector);
  //console.info("encryptedContent", encryptedContent);

  // Return the encoded string
  return ab2b64([...salt, ...initVector, ...encryptedContent]);
};

/**
 * Decrypts a provided base64 string created by encode with the provided password.
 * @param {string} encryptedContent
 * @param {string} password
 * @returns {string}
 */
/*export*/ const decrypt = async (content, password) => {
  if (password == undefined || password.trim() == "") {
    throw new Error("No password was provided.");
  }

  const encryptedData = b642ab(content);
  const salt = encryptedData.slice(0, 16);
  const InitVect = encryptedData.slice(16, 16 + 12);
  const encryptedContent = encryptedData.slice(16 + 12);

  const encryptionKey = await getDecryptionKey(password, salt); // Fix 7/7: apply separated salt for key derivation
  console.info("Key", ab2b64(await Subtle.exportKey('raw', encryptionKey)));

  //console.info("Salt", Salt);
  //console.info("InitVect", InitVect);
  //console.info("encryptedContent", encryptedContent);

  // Decrypt
  const decryptedContent = await Subtle.decrypt(
    {
      name: "AES-GCM",
      iv: InitVect,
      tagLength: 128,
    },
    encryptionKey,
    encryptedContent
  );

  // Return decrypted content
  return decryptedContent;
};

var ct = await encrypt('The quick brown fox jumps over the lazy dog', 'my password');
console.info('Ciphertext:', ct);
try {
  var dt = await decrypt(ct, 'my password');
  console.info('Decrypted:', new TextDecoder().decode(dt));
} catch (err) {
  console.info(err.name, 'Decryption failed...');
}

})();

Теперь расшифровка прошла успешно (для шифрования и дешифрования в одном или разных вызовах).

Спасибо, это была абсолютная проблема. Я просмотрел старый код, который написал для его тестирования, и причина, по которой это сработало, заключалась в том, что я лениво повторно использовал одну и ту же соль, не принимая во внимание ее важность. Кроме того, вы отлично разобрали изменения, очень легко отслеживать, что вы делали и где были ошибки.

Phillip Gooch 15.06.2024 23:29

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

Как очистить данные из тега ul li, включая ссылки страниц с помощью селенового питона?
Получение тега аутентификации шифра AES-GCM в Web Crypto API
Web Crypto Api не может проверить подпись после преобразования из строки
Расшифровать AES-256-CBC с помощью openssl_decrypt в PHP из полезной нагрузки Subtle Crypto Javascript
Возможно ли получение 128-битного ключа AES с PBKDF2 с использованием SHA-256
Работает ли WebCryptoAPI в браузере или на удаленном сервере?
Как импортировать Base64 PublicKey через WebCrypto «importKey» в скрипте ReactJS?
Почему функция шифрования WebCryptoAPI RSA-OAEP не может использовать ожидаемый максимальный размер фрагмента для данного размера ключа?
Используйте WebCrypto API для шифрования/дешифрования данных с помощью ключа шифрования, сгенерированного из строки
Как установить начальное значение, или энтропию, или строку по умолчанию для ключей, сгенерированных Web Crypto, пожалуйста, что-нибудь, чтобы сделать его детерминированным

Похожие вопросы

Реализация шифрования/дешифрования в Typescript с использованием Web Crypto API
Почему шифрование выдает «ERR_CRYPTO_UNKNOWN_CIPHER» при использовании AES-KW в качестве алгоритма шифрования в основном процессе Electron?
Невозможно проверить подпись EDSCSA SHA-256
Подтвердите подпись PHP ECDSA, созданную с помощью API Web Crypto
Использование веб-криптографии для расшифровки AES-CBC: неперехваченная (обещанная) ошибка
Открытый показатель не совпадает с рассчитанным на основе JWK (d, p, q) из ExportKey('jwk')
При использовании Crypto Subtle в Javascript для подписи сообщения нам нужно подписывать хэш закодированного сообщения или само закодированное сообщение?
Как расшифровать криптографию NodeJS на стороне клиента с помощью известного ключа шифрования?
Pycryptodome неверная расшифровка
Используйте Web Crypto API для расшифровки зашифрованного текста, который был зашифрован с помощью библиотеки Python pycrytodome в Django