Nodejs AES-256-GCM 在客户端通过浏览器 webcrypto api 加密和解密

Posted

技术标签:

【中文标题】Nodejs AES-256-GCM 在客户端通过浏览器 webcrypto api 加密和解密【英文标题】:Nodejs AES-256-GCM encryption and decryption in client by browser webcrypto api 【发布时间】:2021-11-07 13:45:49 【问题描述】:

我在客户端生成一对公钥/私钥并将publicKey发送到服务器,后端将在其一侧生成sharedKey并回复我publicKey,这有助于我生成sharedKey在客户端上也用于加密/解密。所以我在 Nodejs 上通过 AES-256-GCM 加密了一条消息,并在 Client 上解密了这条消息。

后端:

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 错误,我不知道为什么?

【问题讨论】:

请创建一个minimal reproducible example。您甚至没有证明您尝试过 AES 密钥是否匹配(例如,通过比较 AES 密钥的十六进制字符串)。 你是对的,为了你的信息,当我在客户端加密消息并在后端解密它时,它可以工作,并且后端/客户端上的sharedKey 是真的并且没有问题.正如我在问题中所写,主要问题是后端的加密阶段和客户端的解密。 您在 NodeJS 代码中使用的不是 GCM,而是 CTR。你应该相应地改变它。此外,您应该发布复制所需的所有功能,例如getRandomValue()concatUint8Array()generateAesKey() 对不起,我的问题,我更新了我的问题 generateAesKey 函数生成的sharedKey 与后端生成的相同,没有问题。 【参考方案1】:

GCM 使用由 NodeJS/Crypto 单独处理的身份验证标签,而 WebCrypto 会自动将其与密文连接。 因此,在 NodeJS 代码中,标签必须明确确定并附加到密文中。当前的 NodeJS 代码中缺少这一点,可以按如下方式考虑。注意标记与cipher.getAuthTag() 及其连接的确定:

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

// https://***.com/a/21797381/9014097
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

【讨论】:

您好,我在这里与您建立了一个聊天室。你看见了吗?你能帮帮我吗? @AliTorki - 我没有足够的时间来完成这项任务,老实说,我不想提交。对不起。我建议您在 SO 上发布您的问题。通过这种方式,您将接触到一个大型社区,并且成功回答的机会将会增加。但谁知道呢,也许我会回答你关于 SO 的一两个问题。祝你好运。

以上是关于Nodejs AES-256-GCM 在客户端通过浏览器 webcrypto api 加密和解密的主要内容,如果未能解决你的问题,请参考以下文章

golang 在golang中使用AES256 GCM加密文本

golang 在golang中使用AES256 GCM解密文本

跨平台AES 256 GCM Javascript和Elixir

有没有办法过滤 aes 256 gcm 加密数据库中的数据?

使用 Java 的 AES-256-GCM 解密中的标签不匹配错误

使用 C# 与 PHP 的 AES GCM 加密