Nodejs AES-256-GCM 通过 webcrypto api 解密加密的客户端消息
Nodejs AES-256-GCM decrypt the encrypted client message by webcrypto api
我已经通过 AES-256-GCM 算法在客户端中用密钥加密了我的文本,我可以在客户端中解密它,但是当我将它发送到具有 SharedKey
(相同正如客户端所拥有的那样),它可以通过 AES-256-CTR 算法解密消息(我使用这个算法是因为 Nodejs 中的 AES-256-GCM 需要 authTag
我不在客户端中创建它并且 iv
是我唯一拥有的东西)。
当我在后端解密消息时,它没有错误,但结果不是我在客户端加密的结果
这是我写的:
客户:
async function encrypt(text: string) {
const encodedText = new TextEncoder().encode(text);
const aesKey = await generateAesKey();
const iv = window.crypto.getRandomValues(
new Uint8Array(SERVER_ENCRYPTION_IV_LENGTH)
);
const encrypted = await window.crypto.subtle.encrypt(
{
name: 'AES-GCM',
iv,
},
aesKey,
encodedText
);
const concatenatedData = new Uint8Array(
iv.byteLength + encrypted.byteLength
);
concatenatedData.set(iv);
concatenatedData.set(new Uint8Array(encrypted), iv.byteLength);
return arrayBufferToBase64(concatenatedData),
}
后端:
export function decrypt(sharedKey: string, message: string) {
const messageBuffer = new Uint8Array(base64ToArrayBuffer(message));
const iv = messageBuffer.subarray(0, 16);
const data = messageBuffer.subarray(16);
const decipher = crypto.createDecipheriv(
'aes-256-ctr',
Buffer.from(sharedKey, 'base64'),
iv
);
const decrypted =
decipher.update(data, 'binary', 'hex') + decipher.final('hex');
return Buffer.from(decrypted, 'hex').toString('base64');
}
示例用法:
const encrypted = encrypt("Hi Everybody");
// send the encrypted message to the server
// Response is: Ô\tp\x8F\x03$\f\x91m\x8B B\x1CkQPQ=\x85\x97\x8AêsÌG0¸Ê
由于GCM是基于CTR的,原则上用CTR解密也是可以的。然而,这在实践中通常不应该这样做,因为它跳过了密文的验证,这是 GCM 相对于 CTR 的附加值。
正确的做法是在NodeJS端用GCM解密,并适当考虑认证标签。
WebCrypto API 自动将认证标签附加到密文中,而NodeJS 的加密模块将密文和标签分开处理。因此,在NodeJS端不仅要分离nonce,还要分离认证标签。
下面的JavaScript/WebCrypto代码演示了加密:
(async () => {
var nonce = crypto.getRandomValues(new Uint8Array(12));
var plaintext = 'The quick brown fox jumps over the lazy dog';
var plaintextEncoded = new TextEncoder().encode(plaintext);
var aesKey = base64ToArrayBuffer('a068Sk+PXECrysAIN+fEGDzMQ3xlpWgE1bWXHVLb0AQ=');
var aesCryptoKey = await crypto.subtle.importKey('raw', aesKey, 'AES-GCM', true, ['encrypt', 'decrypt']);
var ciphertextTag = await crypto.subtle.encrypt({name: 'AES-GCM', iv: nonce}, aesCryptoKey, plaintextEncoded);
ciphertextTag = new Uint8Array(ciphertextTag);
var nonceCiphertextTag = new Uint8Array(nonce.length + ciphertextTag.length);
nonceCiphertextTag.set(nonce);
nonceCiphertextTag.set(ciphertextTag, nonce.length);
nonceCiphertextTag = arrayBufferToBase64(nonceCiphertextTag.buffer);
document.getElementById("nonceCiphertextTag").innerHTML = nonceCiphertextTag; // ihAdhr6595oyQ3koj52cnZp7VeB1fzWuY1v7vqFdSQGxK0VQxIXUegB1mVG4rC5Aymij7bQ9rmnFWbpo7C2znN4ROnnChB0=
})();
// Helper
//
function arrayBufferToBase64(buffer){
var binary = '';
var bytes = new Uint8Array(buffer);
var len = bytes.byteLength;
for (var i = 0; i < len; i++) {
binary += String.fromCharCode(bytes[i]);
}
return window.btoa(binary);
}
//
function base64ToArrayBuffer(base64) {
var binary_string = window.atob(base64);
var len = binary_string.length;
var bytes = new Uint8Array(len);
for (var i = 0; i < len; i++) {
bytes[i] = binary_string.charCodeAt(i);
}
return bytes.buffer;
}
<p style="font-family:'Courier New', monospace;" id="nonceCiphertextTag"></p>
此代码与您的代码基本相同,需要进行一些更改,因为您使用的方法 post 不像 generateAesKey()
或 arrayBufferToBase64()
。
示例输出:
ihAdhr6595oyQ3koj52cnZp7VeB1fzWuY1v7vqFdSQGxK0VQxIXUegB1mVG4rC5Aymij7bQ9rmnFWbpo7C2znN4ROnnChB0=
下面的NodeJS/crypto代码演示了解密。注意标签分隔和显式传递 setAuthTag()
:
var crypto = require('crypto');
function decrypt(key, nonceCiphertextTag) {
key = Buffer.from(key, 'base64');
nonceCiphertextTag = Buffer.from(nonceCiphertextTag, 'base64');
var nonce = nonceCiphertextTag.slice(0, 12);
var ciphertext = nonceCiphertextTag.slice(12, -16);
var tag = nonceCiphertextTag.slice(-16); // Separate tag!
var decipher = crypto.createDecipheriv('aes-256-gcm', key, nonce);
decipher.setAuthTag(tag); // Set tag!
var decrypted = decipher.update(ciphertext, '', 'utf8') + decipher.final('utf8');
return decrypted;
}
var nonceCiphertextTag = 'ihAdhr6595oyQ3koj52cnZp7VeB1fzWuY1v7vqFdSQGxK0VQxIXUegB1mVG4rC5Aymij7bQ9rmnFWbpo7C2znN4ROnnChB0=';
var key = 'a068Sk+PXECrysAIN+fEGDzMQ3xlpWgE1bWXHVLb0AQ=';
var decrypted = decrypt(key, nonceCiphertextTag);
console.log(decrypted);
输出:
The quick brown fox jumps over the lazy dog
为了完整性:通过将 4 个字节附加到 12 个字节的随机数 (0x00000002),也可以使用 CTR 解密 GCM 密文。对于其他随机数大小,关系更复杂,请参见例如Relationship between AES GCM and AES CTR。然而,正如已经说过的那样,在实践中不应该这样做,因为它绕过了密文的身份验证,因此是不安全的。
我已经通过 AES-256-GCM 算法在客户端中用密钥加密了我的文本,我可以在客户端中解密它,但是当我将它发送到具有 SharedKey
(相同正如客户端所拥有的那样),它可以通过 AES-256-CTR 算法解密消息(我使用这个算法是因为 Nodejs 中的 AES-256-GCM 需要 authTag
我不在客户端中创建它并且 iv
是我唯一拥有的东西)。
当我在后端解密消息时,它没有错误,但结果不是我在客户端加密的结果
这是我写的: 客户:
async function encrypt(text: string) {
const encodedText = new TextEncoder().encode(text);
const aesKey = await generateAesKey();
const iv = window.crypto.getRandomValues(
new Uint8Array(SERVER_ENCRYPTION_IV_LENGTH)
);
const encrypted = await window.crypto.subtle.encrypt(
{
name: 'AES-GCM',
iv,
},
aesKey,
encodedText
);
const concatenatedData = new Uint8Array(
iv.byteLength + encrypted.byteLength
);
concatenatedData.set(iv);
concatenatedData.set(new Uint8Array(encrypted), iv.byteLength);
return arrayBufferToBase64(concatenatedData),
}
后端:
export function decrypt(sharedKey: string, message: string) {
const messageBuffer = new Uint8Array(base64ToArrayBuffer(message));
const iv = messageBuffer.subarray(0, 16);
const data = messageBuffer.subarray(16);
const decipher = crypto.createDecipheriv(
'aes-256-ctr',
Buffer.from(sharedKey, 'base64'),
iv
);
const decrypted =
decipher.update(data, 'binary', 'hex') + decipher.final('hex');
return Buffer.from(decrypted, 'hex').toString('base64');
}
示例用法:
const encrypted = encrypt("Hi Everybody");
// send the encrypted message to the server
// Response is: Ô\tp\x8F\x03$\f\x91m\x8B B\x1CkQPQ=\x85\x97\x8AêsÌG0¸Ê
由于GCM是基于CTR的,原则上用CTR解密也是可以的。然而,这在实践中通常不应该这样做,因为它跳过了密文的验证,这是 GCM 相对于 CTR 的附加值。
正确的做法是在NodeJS端用GCM解密,并适当考虑认证标签。
WebCrypto API 自动将认证标签附加到密文中,而NodeJS 的加密模块将密文和标签分开处理。因此,在NodeJS端不仅要分离nonce,还要分离认证标签。
下面的JavaScript/WebCrypto代码演示了加密:
(async () => {
var nonce = crypto.getRandomValues(new Uint8Array(12));
var plaintext = 'The quick brown fox jumps over the lazy dog';
var plaintextEncoded = new TextEncoder().encode(plaintext);
var aesKey = base64ToArrayBuffer('a068Sk+PXECrysAIN+fEGDzMQ3xlpWgE1bWXHVLb0AQ=');
var aesCryptoKey = await crypto.subtle.importKey('raw', aesKey, 'AES-GCM', true, ['encrypt', 'decrypt']);
var ciphertextTag = await crypto.subtle.encrypt({name: 'AES-GCM', iv: nonce}, aesCryptoKey, plaintextEncoded);
ciphertextTag = new Uint8Array(ciphertextTag);
var nonceCiphertextTag = new Uint8Array(nonce.length + ciphertextTag.length);
nonceCiphertextTag.set(nonce);
nonceCiphertextTag.set(ciphertextTag, nonce.length);
nonceCiphertextTag = arrayBufferToBase64(nonceCiphertextTag.buffer);
document.getElementById("nonceCiphertextTag").innerHTML = nonceCiphertextTag; // ihAdhr6595oyQ3koj52cnZp7VeB1fzWuY1v7vqFdSQGxK0VQxIXUegB1mVG4rC5Aymij7bQ9rmnFWbpo7C2znN4ROnnChB0=
})();
// Helper
//
function arrayBufferToBase64(buffer){
var binary = '';
var bytes = new Uint8Array(buffer);
var len = bytes.byteLength;
for (var i = 0; i < len; i++) {
binary += String.fromCharCode(bytes[i]);
}
return window.btoa(binary);
}
//
function base64ToArrayBuffer(base64) {
var binary_string = window.atob(base64);
var len = binary_string.length;
var bytes = new Uint8Array(len);
for (var i = 0; i < len; i++) {
bytes[i] = binary_string.charCodeAt(i);
}
return bytes.buffer;
}
<p style="font-family:'Courier New', monospace;" id="nonceCiphertextTag"></p>
此代码与您的代码基本相同,需要进行一些更改,因为您使用的方法 post 不像 generateAesKey()
或 arrayBufferToBase64()
。
示例输出:
ihAdhr6595oyQ3koj52cnZp7VeB1fzWuY1v7vqFdSQGxK0VQxIXUegB1mVG4rC5Aymij7bQ9rmnFWbpo7C2znN4ROnnChB0=
下面的NodeJS/crypto代码演示了解密。注意标签分隔和显式传递 setAuthTag()
:
var crypto = require('crypto');
function decrypt(key, nonceCiphertextTag) {
key = Buffer.from(key, 'base64');
nonceCiphertextTag = Buffer.from(nonceCiphertextTag, 'base64');
var nonce = nonceCiphertextTag.slice(0, 12);
var ciphertext = nonceCiphertextTag.slice(12, -16);
var tag = nonceCiphertextTag.slice(-16); // Separate tag!
var decipher = crypto.createDecipheriv('aes-256-gcm', key, nonce);
decipher.setAuthTag(tag); // Set tag!
var decrypted = decipher.update(ciphertext, '', 'utf8') + decipher.final('utf8');
return decrypted;
}
var nonceCiphertextTag = 'ihAdhr6595oyQ3koj52cnZp7VeB1fzWuY1v7vqFdSQGxK0VQxIXUegB1mVG4rC5Aymij7bQ9rmnFWbpo7C2znN4ROnnChB0=';
var key = 'a068Sk+PXECrysAIN+fEGDzMQ3xlpWgE1bWXHVLb0AQ=';
var decrypted = decrypt(key, nonceCiphertextTag);
console.log(decrypted);
输出:
The quick brown fox jumps over the lazy dog
为了完整性:通过将 4 个字节附加到 12 个字节的随机数 (0x00000002),也可以使用 CTR 解密 GCM 密文。对于其他随机数大小,关系更复杂,请参见例如Relationship between AES GCM and AES CTR。然而,正如已经说过的那样,在实践中不应该这样做,因为它绕过了密文的身份验证,因此是不安全的。