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