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。然而,正如已经说过的那样,在实践中不应该这样做,因为它绕过了密文的身份验证,因此是不安全的。