使用 AES GCM 在 JS 前端加密并在 python 后端解密

Encrypt in JS front end and decrypt in python backend using AES GCM

我正在尝试使用 AES GCM 加密算法在 JS 前端加密并在 python 后端解密。我将 Web cryptography api for JS front end and python cryptography library 用于 python 后端作为加密库。我现在已经在两侧固定了 IV。我在双方都实现了加密解密代码,它们在每一方都有效。但我认为填充的方式不同,似乎无法弄清楚网络密码学中的填充是如何完成的 api。这是python后端的加密和解密:

def encrypt(derived_key, secret):
    IV = bytes("ddfbccae-b4c4-11", encoding="utf-8")
    aes = Cipher(algorithms.AES(derived_key), modes.GCM(IV))
    encryptor = aes.encryptor()
    padder = padding.PKCS7(128).padder()
    padded_data = padder.update(secret.encode()) + padder.finalize()
    return encryptor.update(padded_data) + encryptor.finalize()

def decrypt(derived_key, secret): 
    IV = bytes("ddfbccae-b4c4-11", encoding="utf-8")
    aes = Cipher(algorithms.AES(derived_key), modes.GCM(IV))
    decryptor = aes.decryptor()
    decrypted_data = decryptor.update(secret) 
    unpadder = padding.PKCS7(128).unpadder()
    return unpadder.update(decrypted_data) + unpadder.finalize()

加解密代码的JS代码如下:

async function encrypt(secretKey, message) {
  let iv = "ddfbccae-b4c4-11";
  iv = Uint8Array.from(iv, x => x.charCodeAt(0))
  let encoded = getMessageEncoding(message);
  ciphertext = await window.crypto.subtle.encrypt(
    {
      name: "AES-GCM",
      iv: iv
    },
    secretKey,
    encoded
  );
  return ciphertext;
}

async function decrypt(secretKey, cipherText) {
  iv = "ddfbccae-b4c4-11";
  iv = Uint8Array.from(iv, x => x.charCodeAt(0))
  try {
    let decrypted = await window.crypto.subtle.decrypt(
      {
        name: "AES-GCM",
        iv: iv
      },
      secretKey,
      cipherText
    );

    let dec = new TextDecoder();
    console.log("Decrypted message: ");
    console.log(dec.decode(decrypted));
   
  } catch (e) {
    console.log("error");
    
  }
}

我尝试在JS端加密,在python端解密。但我收到以下错误:

如果我尝试在两边加密同一个字符串,我会得到这些输出: 在python密文中:\x17O\xadn\x11*I\x94\x99\xc6\x90\x8a\xa9\x9cc=

JS中的密文:\x17O\xadn\x11*I\xdf\xe3F\x81(\x15\xcc\x8c^z\xdf+\x1d\x91K\xbc

如何解决这个填充问题?

GCM 是一种流密码模式,因此不需要填充。加密时会隐式生成一个鉴权标签,用于解密时的鉴权。此外,建议 GCM 使用 IV/nonce 12 字节。

张贴的 Python 代码不必要地填充并且没有考虑身份验证标签,这与 JavaScript 代码不同,这可能是不同密文的主要原因。这是否是唯一的原因以及 JavaScript 代码是否正确实现 GCM,很难说,因为 getMessageEncoding() 方法没有发布,所以无法测试它。

此外,两个代码都应用 16 字节 IV/nonce 而不是推荐的 12 字节 IV/nonce。


Cryptography 为 GCM 提供了两种可能的实现。一种实现使用非身份验证模式(如 CBC)的体系结构。发布的 Python 代码应用了这种设计,但没有考虑身份验证,因此未完全实现 GCM。可以找到此设计的正确示例 here
Cryptography 通常推荐 GCM 的其他方法(s. Danger note), namely the AESGCM class,它执行 implicit 身份验证,以便此不会被意外遗忘或错误实施。

以下实现使用 AESGCM class(并且还考虑了可选的 附加身份验证数据 ):

from cryptography.hazmat.primitives.ciphers.aead import AESGCM
import base64
#import os

#key = AESGCM.generate_key(bit_length=256)    
#nonce = os.urandom(12)
key = base64.b64decode('MDEyMzQ1Njc4OTAxMjM0NTY3ODkwMTIzNDU2Nzg5MDE=') # fix for testing, AES-256
nonce = base64.b64decode('MDEyMzQ1Njc4OTAx') # fix for testing, 12 bytes

plaintext = b'The quick brown fox jumps over the lazy dog'
aad = b'the aad' # aad = None without additional authenticated data

aesgcm = AESGCM(key)
ciphertext = aesgcm.encrypt(nonce, plaintext, aad)
print('Ciphertext (B64): ' + base64.b64encode(ciphertext).decode('utf8'))
decrypted = aesgcm.decrypt(nonce, ciphertext, aad)
print('Decrypted:        ' + decrypted.decode('utf8'))

输出:

Output
Ciphertext (B64): JOetStCANhPISvQ6G6IcRBauqbtC8fzRooblayHqkqSPKzLbidx/gBWfLNzBC+ZpcAGnGnHXaI7CB1U=
Decrypted:        The quick brown fox jumps over the lazy dog

认证标签附加到密文,所以(Base64解码)结果是明文的长度(43字节)加上标签的长度(16字节,默认),总共59字节.

为了测试,使用预定义键和 IV/nonce 与 JavaScript 代码的结果进行比较。请注意,出于安全原因,实际上 key/IV 对只能使用一次,这对于 GCM 模式尤其重要,例如here。因此,每次加密通常会生成一个随机 IV/nonce。


WebCrypto API 是密码学的低级别 API,不提供 Base64 encoding/decoding 的方法。在下文中,为了简单起见,使用 js-base64。就像Python代码一样,标签被附加到密文中。

使用 Python 代码的密钥和 IV/nonce 的 AES-GCM 的可能实现方式在功能上与已发布的 JavaScript 代码基本相同:

(async () => {      
    var key = Base64.toUint8Array('MDEyMzQ1Njc4OTAxMjM0NTY3ODkwMTIzNDU2Nzg5MDE='); // fix for testing, AES-256
    var nonce = Base64.toUint8Array('MDEyMzQ1Njc4OTAx'); // fix for testing, 12 bytes

    var plaintext = new TextEncoder().encode("The quick brown fox jumps over the lazy dog");
    var aad = new TextEncoder().encode('the aad');
                
    var keyImported = await await crypto.subtle.importKey(
        "raw",
        key,
        { name: "AES-GCM" },
        true,
        ["decrypt", "encrypt"]
    );
                
    var ciphertext = await await crypto.subtle.encrypt(
        { name: "AES-GCM", iv: nonce, additionalData: aad }, // { name: "AES-GCM", iv: nonce } without additional authenticated data
        keyImported,
        plaintext
    );
    console.log('Ciphertext (Base64):\n', Base64.fromUint8Array(new Uint8Array(ciphertext)).replace(/(.{48})/g,'\n'));
              
    var decrypted = await await crypto.subtle.decrypt(
        { name: "AES-GCM", iv: nonce, additionalData: aad }, // { name: "AES-GCM", iv: nonce } without additional authenticated data
        keyImported,
        ciphertext
    );
    console.log('Plaintext:\n', new TextDecoder().decode(decrypted).replace(/(.{48})/g,'\n'));
})();
<script src="https://cdn.jsdelivr.net/npm/js-base64@3.2.4/base64.min.js"></script>

输出:

Ciphertext (Base64):
 JOetStCANhPISvQ6G6IcRBauqbtC8fzRooblayHqkqSPKzLbidx/gBWfLNzBC+ZpcAGnGnHXaI7CB1U=
Plaintext:
 The quick brown fox jumps over the lazy dog

其中密文与Python密码相同