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





Дешифрование не удается, поскольку для получения ключа во время шифрования и дешифрования используются разные соли и, следовательно, используются разные ключи.
Хотя соль извлекается с помощью функции 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...');
}
})();Теперь расшифровка прошла успешно (для шифрования и дешифрования в одном или разных вызовах).
Спасибо, это была абсолютная проблема. Я просмотрел старый код, который написал для его тестирования, и причина, по которой это сработало, заключалась в том, что я лениво повторно использовал одну и ту же соль, не принимая во внимание ее важность. Кроме того, вы отлично разобрали изменения, очень легко отслеживать, что вы делали и где были ошибки.