pythonnode.jscryptographyaespycrypto

Encrypt with Node.js AES CTR and decrypt with PyCrypto


Okay, so basically I am having issues decrypting with Python.

I've managed to encrypt/decrypt data with Node.js - using "aes-128-ctr", the same goes for PyCrypto, but when I try to encrypt with Node.js and decrypt with Python I get invalid deciphered text.

Node.js code:

var key = "1234567890123456";
var cipher = crypto.createCipher("aes-128-ctr",key)
var ctext = cipher.update('asasasa','utf8','hex') +  cipher.final('hex')
console.log(ctext) // outputs: "f2cf6ecd8f"

Python code:

counter = Counter.new(128)
cipher = AES.new("1234567890123456", AES.MODE_CTR, counter=counter)
cipher.decrypt("f2cf6ecd8f") // outputs: weird encoding characters

By the way, I don't care about the level of security of this encryption, I care about performance more.


Solution

  • crypto.createCipher takes a password and uses EVP_BytesToKey internally to derive a key and IV from that. Contrary to that, pycrypto directly expects a key and IV. You need to use exactly the same procedure on both sides.

    crypto.createCipher must never be used with CTR-mode, because the key and IV generation are not randomized. Since the CTR-mode is a streaming mode, it will always produce the same key stream which might enable an attacker who only observes multiple ciphertexts that are encrypted with the same password to deduce the plaintext. This is possible because of the resulting many-time pad issue.

    If you must use CTR-mode, then you have to use crypto.createCipheriv. If you use the same key, you have to use a different IV every time. This is why this is actually called a nonce for CTR-mode. For AES-CTR, a nonce of 96 bit is a good compromise between security and size of possibly encryptable plaintexts.

    var plaintext = 'asasasa'
    var key = "1234567890123456" # don't use this one!
    var nonce = crypto.randomBytes(12)
    var iv = Buffer.concat([nonce, Buffer.alloc(4, 0)])
    var cipher = crypto.createCipheriv("aes-128-ctr", key, iv)
    var ciphertext = nonce.toString('hex') + cipher.update(plaintext,'utf8','hex') +  cipher.final('hex')
    console.log(ciphertext)
    

    Example output:

    5b88aeb265712b6c8bfa8dbd63012d1e52eb42
    

    The IV is not secret and you have to use the exact same IV during decryption. Usually, it is sent along with the ciphertext by being prefixed to it. It is then sliced off before decryption:

    ct = codecs.decode('5b88aeb265712b6c8bfa8dbd63012d1e52eb42', 'hex') # I'm using Python 3
    counter = Counter.new(32, prefix=ct[:12], initial_value=0)
    cipher = AES.new("1234567890123456", AES.MODE_CTR, counter=counter)
    cipher.decrypt(ct[12:])
    

    Output:

    b'asasasa'
    

    Keep in mind that a key needs to be randomly chosen. You can generate a random key and keep it in an encoded form in the source code (i.e. as Hex). If you do that, you must not give the source code or the bytecode to anyone that you wouldn't trust the key with.