Nodejs AES-256-GCM 通过 webcrypto api 解密加密的客户端消息

Posted

技术标签:

【中文标题】Nodejs AES-256-GCM 通过 webcrypto api 解密加密的客户端消息【英文标题】:Nodejs AES-256-GCM decrypt the encrypted client message by webcrypto api 【发布时间】:2021-11-07 08:32:15 【问题描述】:

我已经通过 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¸Ê

【问题讨论】:

看来您已经从对称加密部分的其他(现已删除)问题中找到了错误。我在删除之前注意到,在 ECDH 部分中,在服务器端导入公钥时,密钥转换中存在另一个错误。可能你自己已经找到了问题所在。否则,您可以在新问题中发布 ECDH 部分以及共享密钥和 AES 密钥的计算。 【参考方案1】:

由于 GCM 是基于 CTR 的,因此原则上也可以使用 CTR 进行解密。但是,这在实践中通常不应该这样做,因为它跳过了密文的认证,这是 GCM 相对于 CTR 的附加值。 正确的方法是在 NodeJS 端使用 GCM 解密并适当考虑身份验证标签。 身份验证标签由 WebCrypto API 自动附加到密文中,而 NodeJS 的加密模块分别处理密文和标签。因此,NodeJS端不仅要分离nonce,还要分离authentication标签。

以下 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

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


// 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;
&lt;p style="font-family:'Courier New', monospace;" id="nonceCiphertextTag"&gt;&lt;/p&gt;

此代码与您的代码基本相同,但由于您未发布的方法(如 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

为了完整性:通过在 12 个字节的 nonce (0x00000002) 上附加 4 个字节,也可以使用 CTR 解密 GCM 密文。对于其他随机数大小,关系更复杂,请参见例如Relationship between AES GCM and AES CTR。然而,正如已经说过的,这在实践中不应该这样做,因为它绕过了密文的认证,因此是不安全的。

【讨论】:

哇,它成功了。您可以在后端的加密阶段帮助我吗?我应该为此创建另一个问题吗? 如果我在 Client 中加密一个 Text 并将它发送到 Backend,它可以解密它,但是如果我在 Backend 中加密一条消息,则 Client 无法解密它。 @AliTorki - 在 SO 上,在已经回答完问题后,像您所做的那样扩展问题并不常见。它经常使帖子混乱,使后续读者难以理解问题。因此,请将您的问题回滚到以前的状态,并将您的新问题放在新帖子中。如有必要,您可以链接到此帖子。谢谢。 你说得对,我又问了一个问题,感谢您的帮助,非常感谢:***.com/questions/69142812/… @AliTorki - 即使使用更长更复杂的明文,我也无法重现此问题。如果您可以提供数据来重建问题,那将会很有帮助:明文、密文和密钥。您还应该检查 unmodified 密文和 same 密钥是否应用于解密端。

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

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

跨平台AES 256 GCM Javascript和Elixir

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

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

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

使用 C# 与 PHP 的 AES GCM 加密