代码之家  ›  专栏  ›  技术社区  ›  Ali Torki

Nodejs AES-256-GCM通过浏览器WebCryptoAPI在客户端进行加密和解密

  •  -1
  • Ali Torki  · 技术社区  · 5 年前

    我在客户端生成一对公钥/私钥,并发送 publicKey 到服务器和后端将生成 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 错误,我不知道为什么?

    0 回复  |  直到 5 年前
        1
  •  2
  •   Topaco    5 年前

    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://stackoverflow.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
    
    推荐文章