Nodejs AES-256-GCM在客户端通过浏览器webcrypto加密解密api
Nodejs AES-256-GCM encryption and decryption in client by browser webcrypto api
我在客户端生成一对 public/private 密钥并将 publicKey
发送到服务器,后端将在其端生成一个 sharedKey
并响应我一个 publicKey
这也帮助我在客户端上为 encryption/decryption 生成 sharedKey
。所以我在 Nodejs 上通过 AES-256-GCM 加密消息并在客户端解密消息。
后端:
export function encrypt(sharedKey: string, message: string) {
const firstIv = getRandomIV();
const cipher = crypto.createCipheriv(
'aes-256-gcm',
Buffer.from(sharedKey, 'base64'),
firstIv
);
const encrypted = cipher.update(message, 'utf8');
return Buffer.from(encrypted + cipher.final()).toString('base64');
}
function getRandomIV() {
return crypto.randomBytes(12);
}
客户端:
async function decrypt(encryptedData: Uint8Array) {
const aesKey = await generateAesKey();
const nonce = encryptedData.subarray(0, SERVER_ENCRYPTION_IV_LENGTH);
const data = encryptedData.subarray(SERVER_ENCRYPTION_IV_LENGTH);
const decrypted = await crypto.subtle.decrypt(
{
name: 'AES-GCM',
iv: nonce,
},
aesKey,
data
);
return {
decrypted: new Uint8Array(decrypted),
decryptedString: new TextDecoder().decode(decrypted),
};
}
async function generateAesKey() {
const publicKey = await getServerPublicKey();
const privateKey = await getPrivateKey();
const sharedSecret = await crypto.subtle.deriveBits(
{
name: 'ECDH',
public: publicKey!,
},
privateKey,
256
);
const aesSecret = await crypto.subtle.digest('SHA-256', sharedSecret);
return crypto.subtle.importKey('raw', aesSecret, 'AES-GCM', true, [
'encrypt',
'decrypt',
]);
}
现在,我无法在客户端解密服务器加密的响应,我遇到 DOMException
错误,我不知道为什么?
GCM 使用由 NodeJS/Crypto 单独处理的身份验证标签,而 WebCrypto 自动将其与密文连接。
因此,在NodeJS代码中,tag必须明确确定并附加到密文中。当前的 NodeJS 代码中缺少这一点,可以按如下方式考虑。注意带cipher.getAuthTag()
的tag的判断及其拼接:
var crypto = require('crypto');
function encrypt(key, plaintext) {
var nonce = getRandomIV();
var cipher = crypto.createCipheriv('aes-256-gcm', key, nonce);
var nonceCiphertextTag = Buffer.concat([
nonce,
cipher.update(plaintext),
cipher.final(),
cipher.getAuthTag() // Fix: Get tag with cipher.getAuthTag() and concatenate: nonce|ciphertext|tag
]);
return nonceCiphertextTag.toString('base64');
}
function getRandomIV() {
return crypto.randomBytes(12);
}
var message = Buffer.from('The quick brown fox jumps over the lazy dog', 'utf8');
var sharedKey = Buffer.from('MDEyMzQ1Njc4OTAxMjM0NTY3ODkwMTIzNDU2Nzg5MDE=', 'base64');
var ciphertext = encrypt(sharedKey, message);
console.log(ciphertext); // wRE5KM6FG81QSMNvG0xR+iaIeF77cyyeBceGS5NkcYaD17K9nL0/helnqRBOkD9pLVoWM/nRAcaKg/YdvfNJcO1Zn/7ZM0k=
可能的输出是
wRE5KM6FG81QSMNvG0xR+iaIeF77cyyeBceGS5NkcYaD17K9nL0/helnqRBOkD9pLVoWM/nRAcaKg/YdvfNJcO1Zn/7ZM0k=
以下WebCrypto端的解密代码基本上是基于你的代码(没有从共享密钥推导密钥,与当前问题无关):
(async () => {
var nonceCiphertextTag = base64ToArrayBuffer('wRE5KM6FG81QSMNvG0xR+iaIeF77cyyeBceGS5NkcYaD17K9nL0/helnqRBOkD9pLVoWM/nRAcaKg/YdvfNJcO1Zn/7ZM0k=');
var nonceCiphertextTag = new Uint8Array(nonceCiphertextTag);
var decrypted = await decrypt(nonceCiphertextTag);
console.log(decrypted); // The quick brown fox jumps over the lazy dog
})();
async function decrypt(nonceCiphertextTag) {
const SERVER_ENCRYPTION_IV_LENGTH = 12; // For GCM a nonce length of 12 bytes is recommended!
var nonce = nonceCiphertextTag.subarray(0, SERVER_ENCRYPTION_IV_LENGTH);
var ciphertextTag = nonceCiphertextTag.subarray(SERVER_ENCRYPTION_IV_LENGTH);
var aesKey = base64ToArrayBuffer('MDEyMzQ1Njc4OTAxMjM0NTY3ODkwMTIzNDU2Nzg5MDE=');
aesKey = await window.crypto.subtle.importKey('raw', aesKey, 'AES-GCM', true, ['encrypt', 'decrypt']);
var decrypted = await crypto.subtle.decrypt({name: 'AES-GCM', iv: nonce}, aesKey, ciphertextTag);
return new TextDecoder().decode(decrypted);
}
// Helper
//
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;
}
成功解密出NodeJS端的密文:
The quick brown fox jumps over the lazy dog
我在客户端生成一对 public/private 密钥并将 publicKey
发送到服务器,后端将在其端生成一个 sharedKey
并响应我一个 publicKey
这也帮助我在客户端上为 encryption/decryption 生成 sharedKey
。所以我在 Nodejs 上通过 AES-256-GCM 加密消息并在客户端解密消息。
后端:
export function encrypt(sharedKey: string, message: string) {
const firstIv = getRandomIV();
const cipher = crypto.createCipheriv(
'aes-256-gcm',
Buffer.from(sharedKey, 'base64'),
firstIv
);
const encrypted = cipher.update(message, 'utf8');
return Buffer.from(encrypted + cipher.final()).toString('base64');
}
function getRandomIV() {
return crypto.randomBytes(12);
}
客户端:
async function decrypt(encryptedData: Uint8Array) {
const aesKey = await generateAesKey();
const nonce = encryptedData.subarray(0, SERVER_ENCRYPTION_IV_LENGTH);
const data = encryptedData.subarray(SERVER_ENCRYPTION_IV_LENGTH);
const decrypted = await crypto.subtle.decrypt(
{
name: 'AES-GCM',
iv: nonce,
},
aesKey,
data
);
return {
decrypted: new Uint8Array(decrypted),
decryptedString: new TextDecoder().decode(decrypted),
};
}
async function generateAesKey() {
const publicKey = await getServerPublicKey();
const privateKey = await getPrivateKey();
const sharedSecret = await crypto.subtle.deriveBits(
{
name: 'ECDH',
public: publicKey!,
},
privateKey,
256
);
const aesSecret = await crypto.subtle.digest('SHA-256', sharedSecret);
return crypto.subtle.importKey('raw', aesSecret, 'AES-GCM', true, [
'encrypt',
'decrypt',
]);
}
现在,我无法在客户端解密服务器加密的响应,我遇到 DOMException
错误,我不知道为什么?
GCM 使用由 NodeJS/Crypto 单独处理的身份验证标签,而 WebCrypto 自动将其与密文连接。
因此,在NodeJS代码中,tag必须明确确定并附加到密文中。当前的 NodeJS 代码中缺少这一点,可以按如下方式考虑。注意带cipher.getAuthTag()
的tag的判断及其拼接:
var crypto = require('crypto');
function encrypt(key, plaintext) {
var nonce = getRandomIV();
var cipher = crypto.createCipheriv('aes-256-gcm', key, nonce);
var nonceCiphertextTag = Buffer.concat([
nonce,
cipher.update(plaintext),
cipher.final(),
cipher.getAuthTag() // Fix: Get tag with cipher.getAuthTag() and concatenate: nonce|ciphertext|tag
]);
return nonceCiphertextTag.toString('base64');
}
function getRandomIV() {
return crypto.randomBytes(12);
}
var message = Buffer.from('The quick brown fox jumps over the lazy dog', 'utf8');
var sharedKey = Buffer.from('MDEyMzQ1Njc4OTAxMjM0NTY3ODkwMTIzNDU2Nzg5MDE=', 'base64');
var ciphertext = encrypt(sharedKey, message);
console.log(ciphertext); // wRE5KM6FG81QSMNvG0xR+iaIeF77cyyeBceGS5NkcYaD17K9nL0/helnqRBOkD9pLVoWM/nRAcaKg/YdvfNJcO1Zn/7ZM0k=
可能的输出是
wRE5KM6FG81QSMNvG0xR+iaIeF77cyyeBceGS5NkcYaD17K9nL0/helnqRBOkD9pLVoWM/nRAcaKg/YdvfNJcO1Zn/7ZM0k=
以下WebCrypto端的解密代码基本上是基于你的代码(没有从共享密钥推导密钥,与当前问题无关):
(async () => {
var nonceCiphertextTag = base64ToArrayBuffer('wRE5KM6FG81QSMNvG0xR+iaIeF77cyyeBceGS5NkcYaD17K9nL0/helnqRBOkD9pLVoWM/nRAcaKg/YdvfNJcO1Zn/7ZM0k=');
var nonceCiphertextTag = new Uint8Array(nonceCiphertextTag);
var decrypted = await decrypt(nonceCiphertextTag);
console.log(decrypted); // The quick brown fox jumps over the lazy dog
})();
async function decrypt(nonceCiphertextTag) {
const SERVER_ENCRYPTION_IV_LENGTH = 12; // For GCM a nonce length of 12 bytes is recommended!
var nonce = nonceCiphertextTag.subarray(0, SERVER_ENCRYPTION_IV_LENGTH);
var ciphertextTag = nonceCiphertextTag.subarray(SERVER_ENCRYPTION_IV_LENGTH);
var aesKey = base64ToArrayBuffer('MDEyMzQ1Njc4OTAxMjM0NTY3ODkwMTIzNDU2Nzg5MDE=');
aesKey = await window.crypto.subtle.importKey('raw', aesKey, 'AES-GCM', true, ['encrypt', 'decrypt']);
var decrypted = await crypto.subtle.decrypt({name: 'AES-GCM', iv: nonce}, aesKey, ciphertextTag);
return new TextDecoder().decode(decrypted);
}
// Helper
//
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;
}
成功解密出NodeJS端的密文:
The quick brown fox jumps over the lazy dog