javascriptpythonencryptionaessjcl

What is the Python equivalent of this JS function to decrypt an AES-CCM-encrypted string?


I’d like to decrypt an AES-encrypted string (CCM mode) in Python 3.

The following JavaScript code which is using the sjcl library is working correctly:

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);

Output:

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

Which library can I use to do the same in Python?

I tried using the pycryptodome library, but it accepts a different set of parameters:


Solution

  • The sjcl operates on arrays of 4 byte words. With sjcl.codec.hex.toBits() the hex encoded key is converted into such an array. The first 8 bytes (16 hexdigits) of the key are used as nonce.
    Key size, tag size, algorithm and mode are determined from the params object. The params object further contains parameters for the key derivation, e.g. iter, salt, etc.), but these are ignored here since the key is passed as an array and not as a string.
    Nonce and ciphertext are passed Base64 encoded within the params object.

    The ciphertext is the concatenation of the actual ciphertext and the tag in this order, which must also be passed to the decryption in this format.
    While the sjcl processes ciphertext and tag concatenated, PyCryptodome handles both separately. Apart from that, encryption and decryption in Python is straightforward with 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')
    

    Note that it is insecure to derive the nonce from the key. This is especially true for CCM, s. e.g. RFC4309, p. 3, last section:

    AES CCM employs counter mode for encryption. As with any stream cipher, reuse of the same IV value with the same key is catastrophic.

    Instead, the nonce should be randomly generated for each encryption. The nonce is not secret and is usually concatenated with the ciphertext at byte level, typically nonce|ciphertext|tag.