1

我想在 Python 3 中解密一个 AES 加密的字符串(CCM 模式)。

以下使用sjcl库的 JavaScript 代码工作正常:

const sjcl = require('sjcl');

const key = "ef530e1d82c154170296467bfe40cdb47b9ad77e685bbf8336b145dfa0e85640";
const keyArray = sjcl.codec.hex.toBits(key);
const iv = sjcl.codec.base64.fromBits(sjcl.codec.hex.toBits(key.substr(0,16))); 
const params = {
    "iv": iv,
    "v": 1,
    "iter": 1000,
    "ks": 256,
    "ts": 128,
    "mode": "ccm",
    "adata": "",
    "cipher": "aes",
    "salt": "",
};

function encrypt(data) {
    const ct = JSON.parse(sjcl.encrypt(keyArray, data, params)).ct;
    return sjcl.codec.hex.fromBits(sjcl.codec.base64.toBits(ct));
}

function decrypt(data) {
    const ct = sjcl.codec.base64.fromBits(sjcl.codec.hex.toBits(data));
    const paramsWithCt = JSON.stringify({ ...params, ...{ "ct": ct } });
    return sjcl.decrypt(keyArray, paramsWithCt);
}

let ct = encrypt("my secret string");
console.log("Cipher Text: " + ct);

let plain = decrypt(ct);
console.log("Plain Text: " + plain);

输出:

$ npm i sjcl
$ node index.js
Cipher Text: fa90bcdedbfe7ba89b69216e352a90fa57a63871fc4da7e69ab7f897f427f8e3
Plain Text: my secret string

我可以使用哪个库在 Python 中做同样的事情?

我尝试使用pycryptodome 库,但它接受一组不同的参数:

  • 密钥(字节)– 加密密钥
  • mode – 常量 Crypto.Cipher.<algorithm>.MODE_CCM
  • nonce (bytes) – 固定随机数的值。对于组合消息/密钥,它必须是唯一的。对于 AES,它的长度从 7 到 13 个字节不等。nonce 越长,允许的消息大小越小(nonce 为 13 字节,消息不能超过 64KB)。如果不存在,该库会创建一个 11 字节的随机 nonce(最大消息大小为 8GB)。
  • mac_len (integer) – MAC 标签的所需长度(如果不存在,则默认为:16 字节)。
  • msg_len (integer) – 预先声明要加密的消息长度。如果未指定,则 encrypt() 和 decrypt() 只能调用一次。
  • assoc_len (integer) -- 关联数据长度的预先声明。如果未指定,将在内部进行一些额外的缓冲。
4

1 回答 1

2

sjcl对 4 字节字的数组进行操作。用sjcl.codec.hex.toBits()十六进制编码的密钥被转换成这样的数组。密钥的前 8 个字节(16 个十六进制数字)用作随机数。
密钥大小、标签大小、算法和模式由params对象确定。该params对象还包含用于密钥派生的参数,例如iter,salt等),但这些在此处被忽略,因为密钥作为数组而不是字符串传递。
随机数和密文在对象中通过 Base64 编码params

密文是实际密文和标签按此顺序拼接而成,也必须以这种格式传递给解密。
虽然sjcl处理连接的密文和标签,但PyCryptodome分别处理两者。除此之外,Python 中的加密和解密使用PyCryptodome很简单:

from Crypto.Cipher import AES

data = b'my secret string'
key = bytes.fromhex('ef530e1d82c154170296467bfe40cdb47b9ad77e685bbf8336b145dfa0e85640')
nonce = bytes.fromhex('ef530e1d82c154170296467bfe40cdb47b9ad77e685bbf8336b145dfa0e85640')[:8]

# Encryption 
cipher = AES.new(key, AES.MODE_CCM, nonce)
ciphertext, tag = cipher.encrypt_and_digest(data)

ciphertextTagHex = ciphertext.hex() + tag.hex()
print(ciphertextTagHex) # fa90bcdedbfe7ba89b69216e352a90fa57a63871fc4da7e69ab7f897f427f8e3

# Decryption
ciphertextTag = bytes.fromhex(ciphertextTagHex)
ciphertext = ciphertextTag[:-16]
tag = ciphertextTag[-16:]

cipher = AES.new(key, AES.MODE_CCM, nonce)
try:
    decrypted = cipher.decrypt_and_verify(ciphertext, tag)
    print(decrypted.decode('utf-8')) # my secret string
except ValueError:
    print('Decryption failed')

请注意,从密钥中派生随机数是不安全的。这对于 CCM 尤其如此,seg RFC4309, p。3、最后一节

AES CCM 采用计数器模式进行加密。与任何流密码一样,使用相同的密钥重复使用相同的 IV 值是灾难性的。

相反,应该为每次加密随机生成随机数。随机数不是秘密的,通常在字节级别与密文连接,通常是nonce|ciphertext|tag.

于 2021-09-04T10:10:39.620 回答