Encrypting and Decrypting with python and nodejs
Question:
I’m trying to encrypt some content in Python and decrypt it in a nodejs application.
I’m struggling to get the two AES implementations to work together though. Here is where I am at.
In node:
var crypto = require('crypto');
var password = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa';
var input = 'hello world';
var encrypt = function (input, password, callback) {
var m = crypto.createHash('md5');
m.update(password)
var key = m.digest('hex');
m = crypto.createHash('md5');
m.update(password + key)
var iv = m.digest('hex');
// add padding
while (input.length % 16 !== 0) {
input += ' ';
}
var data = new Buffer(input, 'utf8').toString('binary');
var cipher = crypto.createCipheriv('aes-256-cbc', key, iv.slice(0,16));
var encrypted = cipher.update(data, 'binary') + cipher.final('binary');
var encoded = new Buffer(encrypted, 'binary').toString('base64');
callback(encoded);
};
var decrypt = function (input, password, callback) {
// Convert urlsafe base64 to normal base64
var input = input.replace('-', '+').replace('/', '_');
// Convert from base64 to binary string
var edata = new Buffer(input, 'base64').toString('binary')
// Create key from password
var m = crypto.createHash('md5');
m.update(password)
var key = m.digest('hex');
// Create iv from password and key
m = crypto.createHash('md5');
m.update(password + key)
var iv = m.digest('hex');
// Decipher encrypted data
var decipher = crypto.createDecipheriv('aes-256-cbc', key, iv.slice(0,16));
var decrypted = decipher.update(edata, 'binary') + decipher.final('binary');
var plaintext = new Buffer(decrypted, 'binary').toString('utf8');
callback(plaintext);
};
encrypt(input, password, function (encoded) {
console.log(encoded);
decrypt(encoded, password, function (output) {
console.log(output);
});
});
This produces the output:
BXSGjDAYKeXlaRXVVJGuREKTPiiXeam8W9e96Nknt3E=
hello world
In python
from Crypto.Cipher import AES
from hashlib import md5
import base64
password = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'
input = 'hello world'
def _encrypt(data, nonce, password):
m = md5()
m.update(password)
key = m.hexdigest()
m = md5()
m.update(password + key)
iv = m.hexdigest()
# pad to 16 bytes
data = data + " " * (16 - len(data) % 16)
aes = AES.new(key, AES.MODE_CBC, iv[:16])
encrypted = aes.encrypt(data)
return base64.urlsafe_b64encode(encrypted)
def _decrypt(edata, nonce, password):
edata = base64.urlsafe_b64decode(edata)
m = md5()
m.update(password)
key = m.hexdigest()
m = md5()
m.update(password + key)
iv = m.hexdigest()
aes = AES.new(key, AES.MODE_CBC, iv[:16])
return aes.decrypt(edata)
output = _encrypt(input, "", password)
print(output)
plaintext = _decrypt(output, "", password)
print(plaintext)
This produces the output
BXSGjDAYKeXlaRXVVJGuRA==
hello world
Clearly they are very close, but node seems to be padding the output with something. Any ideas how I can get the two to interoperate?
Answers:
OK, I’ve figured it out, node uses OpenSSL which uses PKCS5 to do padding. PyCrypto doesn’t handle the padding so I was doing it myself just add ‘ ‘ in both.
If I add PKCS5 padding in the python code and remove the padding in the node code, it works.
So updated working code.
Node:
var crypto = require('crypto');
var password = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa';
var input = 'hello world';
var encrypt = function (input, password, callback) {
var m = crypto.createHash('md5');
m.update(password)
var key = m.digest('hex');
m = crypto.createHash('md5');
m.update(password + key)
var iv = m.digest('hex');
var data = new Buffer(input, 'utf8').toString('binary');
var cipher = crypto.createCipheriv('aes-256-cbc', key, iv.slice(0,16));
// UPDATE: crypto changed in v0.10
// https://github.com/joyent/node/wiki/Api-changes-between-v0.8-and-v0.10
var nodev = process.version.match(/^v(d+).(d+)/);
var encrypted;
if( nodev[1] === '0' && parseInt(nodev[2]) < 10) {
encrypted = cipher.update(data, 'binary') + cipher.final('binary');
} else {
encrypted = cipher.update(data, 'utf8', 'binary') + cipher.final('binary');
}
var encoded = new Buffer(encrypted, 'binary').toString('base64');
callback(encoded);
};
var decrypt = function (input, password, callback) {
// Convert urlsafe base64 to normal base64
var input = input.replace(/-/g, '+').replace(/_/g, '/');
// Convert from base64 to binary string
var edata = new Buffer(input, 'base64').toString('binary')
// Create key from password
var m = crypto.createHash('md5');
m.update(password)
var key = m.digest('hex');
// Create iv from password and key
m = crypto.createHash('md5');
m.update(password + key)
var iv = m.digest('hex');
// Decipher encrypted data
var decipher = crypto.createDecipheriv('aes-256-cbc', key, iv.slice(0,16));
// UPDATE: crypto changed in v0.10
// https://github.com/joyent/node/wiki/Api-changes-between-v0.8-and-v0.10
var nodev = process.version.match(/^v(d+).(d+)/);
var decrypted, plaintext;
if( nodev[1] === '0' && parseInt(nodev[2]) < 10) {
decrypted = decipher.update(edata, 'binary') + decipher.final('binary');
plaintext = new Buffer(decrypted, 'binary').toString('utf8');
} else {
plaintext = (decipher.update(edata, 'binary', 'utf8') + decipher.final('utf8'));
}
callback(plaintext);
};
encrypt(input, password, function (encoded) {
console.log(encoded);
decrypt(encoded, password, function (output) {
console.log(output);
});
});
Python:
from Crypto.Cipher import AES
from hashlib import md5
import base64
password = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'
input = 'hello world'
BLOCK_SIZE = 16
def pad (data):
pad = BLOCK_SIZE - len(data) % BLOCK_SIZE
return data + pad * chr(pad)
def unpad (padded):
pad = ord(chr(padded[-1]))
return padded[:-pad]
def get_key_iv (password):
m = md5()
m.update(password.encode('utf-8'))
key = m.hexdigest()
m = md5()
m.update((password + key).encode('utf-8'))
iv = m.hexdigest()
return [key,iv]
def _encrypt(data, password):
key,iv = get_key_iv(password)
data = pad(data)
aes = AES.new(key, AES.MODE_CBC, iv[:16])
encrypted = aes.encrypt(data)
return base64.urlsafe_b64encode(encrypted)
def _decrypt(edata, password):
edata = base64.urlsafe_b64decode(edata)
key,iv = get_key_iv(password)
aes = AES.new(key, AES.MODE_CBC, iv[:16])
return unpad(aes.decrypt(edata))
output = _encrypt(input, password)
print(output)
plaintext = _decrypt(output, password)
print(plaintext)
Just for any one that is similar to me, who was finding a simple way to do the encryption and decryption for AES in python that is doing the same thing in node.js. The class here supports different bits of AES and both hex and base64 encoding that produces same result in node.js.
Also noted that if you are missing the package Crypto, you can simply install it by
pip install pycrypto
The code for python is as follows:
import base64
import hashlib
from Crypto.Cipher import AES
class AESCrypto(object):
def __init__(self, algorithm, password):
self.algorithm = filter(lambda x: not x.isdigit(), algorithm).lower()
self.bits = int(filter(str.isdigit, algorithm))
self.bs = 16
if not self.algorithm == 'aes':
raise Exception('Only AES crypto is supported')
if not self.bits % 8 == 0:
raise Exception('Bits of crypto must be a multiply of 8.')
self.bytes = self.bits / 8
self.password = password
self.generateKeyAndIv()
def generateKeyAndIv(self):
last = ''
allBytes = ''
maxBytes = self.bytes + self.bs
while len(allBytes) < maxBytes:
last = hashlib.md5(last + self.password).digest()
allBytes += last
self.key = allBytes[:self.bytes]
self.iv = allBytes[self.bytes:maxBytes]
def encrypt(self, raw, outputEncoding):
outputEncoding = outputEncoding.lower()
raw = self._pad(raw)
cipher = AES.new(self.key, AES.MODE_CBC, self.iv)
encrypted = cipher.encrypt(raw)
if outputEncoding == 'hex':
return encrypted.encode('hex')
elif outputEncoding == 'base64':
return base64.b64encode(encrypted)
else:
raise Exception('Encoding is not supported.')
def decrypt(self, data, inputEncoding):
inputEncoding = inputEncoding.lower()
if inputEncoding == 'hex':
data = ''.join(map(chr, bytearray.fromhex(data)))
elif inputEncoding == 'base64':
data = base64.b64decode(data)
cipher = AES.new(self.key, AES.MODE_CBC, self.iv)
return self._unpad(cipher.decrypt(data))
def _pad(self, data):
padding = self.bs - len(data) % self.bs
return data + padding * chr(padding)
@staticmethod
def _unpad(data):
return data[0:-ord(data[-1])]
The following are examples to use the class:
Encryption Example:
password = 'some_random_password'
content = 'content_to_be_encrypted'
cipher = AESCrypto('aes192', password)
encrypted = cipher.encrypt(content, 'hex')
Decryption Example:
password = 'some_random_password'
content = 'encrypted_content'
cipher = AESCrypto('aes192', password)
decrypted = cipher.decrypt(content, 'hex')
while trying to run the Python script using Python 3.8 I encountered the following error:
m.update(password)
TypeError: Unicode-objects must be encoded before hashing
the password should be :
password = b'abcd'
I also got the following error :
m.update(password + key)
TypeError: can't concat str to bytes
I was able to fix it by adding the following line after key:
key = bytes.fromhex(key_)
The python script should work this way :
from Crypto.Cipher import AES
from hashlib import md5
import base64
password = b'abcd'
input = 'hello world'
BLOCK_SIZE = 16
def pad (data):
pad = BLOCK_SIZE - len(data) % BLOCK_SIZE
return data + pad * chr(pad)
def unpad (padded):
pad = ord(chr(padded[-1]))
return padded[:-pad]
def _encrypt(data, nonce, password):
m = md5()
m.update(password)
key_ = m.hexdigest()
key = bytes.fromhex(key_)
m = md5()
m.update(password + key)
iv = m.hexdigest()
iv = bytes.fromhex(iv)
data = pad(data)
aes = AES.new(key, AES.MODE_CBC, iv[:16])
encrypted = aes.encrypt(data.encode('utf-8'))
return base64.urlsafe_b64encode(encrypted)
def _decrypt(edata, nonce, password):
edata = base64.urlsafe_b64decode(edata)
m = md5()
m.update(password)
key = m.hexdigest()
key = bytes.fromhex(key)
m = md5()
m.update(password + key)
iv = m.hexdigest()
iv = bytes.fromhex(iv)
aes = AES.new(key, AES.MODE_CBC, iv[:16])
return unpad(aes.decrypt(edata))
output = _encrypt(input, "", password)
print(output)
plaintext = _decrypt(output, "", password)
print(plaintext)
Because I spent way too much time on this with Python 3.10.7 and Node.js v18.6.0.
Here is a working code totally compatible between two languages with examples.
Only the secret is needed for getting same values as expected 🙂
Note pycryptodome
is needed for Python. Code should be tweaked for supporting different algorithms.
const crypto = require('crypto')
function get_crypto(secret, encode) {
// Create hashed key from password/key
let m = crypto.createHash('md5').update(secret)
const key = m.digest('hex')
m = crypto.createHash('md5').update(secret + key)
const iv = m.digest('hex').slice(0, 16) // only in aes-256
return encode
? crypto.createCipheriv('aes-256-cbc', key, iv)
: crypto.createDecipheriv('aes-256-cbc', key, iv)
}
const secret = 'f8abb29f13cb932704badb0de414ab08ca9f6c63' // crypto.randomBytes(20).toString('hex')
const value = 'hello world'
const data = Buffer.from(value, 'utf8').toString('binary')
const cipher = get_crypto(secret, true)
const encrypted = cipher.update(data, 'utf8', 'binary') + cipher.final('binary')
const encoded = Buffer.from(encrypted, 'binary').toString('base64')
console.log('encoded:', encoded)
const edata = Buffer.from(encoded, 'base64').toString('binary')
const decipher = get_crypto(secret, false)
const decoded = decipher.update(edata, 'binary', 'utf8') + decipher.final('utf8')
console.log('decoded:', decoded)
# This script needs pycryptodome dependency
# pip install pycryptodome
from Crypto.Cipher import AES
from hashlib import md5
import base64
BLOCK_SIZE = AES.block_size
def get_aes(s):
m = md5()
m.update(s.encode('utf-8'))
key = m.hexdigest()
m = md5()
m.update((s + key).encode('utf-8'))
iv = m.hexdigest()
return AES.new(key.encode("utf8"), AES.MODE_CBC, iv.encode("utf8")[:BLOCK_SIZE])
# pkcs5 padding
def pad(byte_array):
pad_len = BLOCK_SIZE - len(byte_array) % BLOCK_SIZE
return byte_array + (bytes([pad_len]) * pad_len)
# pkcs5 - unpadding
def unpad(byte_array):
return byte_array[:-ord(byte_array[-1:])]
def _encrypt(s, data):
data = pad(data.encode("UTF-8"))
aes = get_aes(s)
encrypted = aes.encrypt(data)
return base64.urlsafe_b64encode(encrypted).decode('utf-8')
def _decrypt(s, edata):
edata = base64.urlsafe_b64decode(edata)
aes = get_aes(s)
return unpad(aes.decrypt(edata)).decode('utf-8')
if __name__ == '__main__':
secret = 'f8abb29f13cb932704badb0de414ab08ca9f6c63'
value = 'hello world'
encoded = _encrypt(secret, value)
print('encoded:', encoded)
decoded = _decrypt(secret, encoded)
print('decoded:', decoded)
Help from:
I’m trying to encrypt some content in Python and decrypt it in a nodejs application.
I’m struggling to get the two AES implementations to work together though. Here is where I am at.
In node:
var crypto = require('crypto');
var password = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa';
var input = 'hello world';
var encrypt = function (input, password, callback) {
var m = crypto.createHash('md5');
m.update(password)
var key = m.digest('hex');
m = crypto.createHash('md5');
m.update(password + key)
var iv = m.digest('hex');
// add padding
while (input.length % 16 !== 0) {
input += ' ';
}
var data = new Buffer(input, 'utf8').toString('binary');
var cipher = crypto.createCipheriv('aes-256-cbc', key, iv.slice(0,16));
var encrypted = cipher.update(data, 'binary') + cipher.final('binary');
var encoded = new Buffer(encrypted, 'binary').toString('base64');
callback(encoded);
};
var decrypt = function (input, password, callback) {
// Convert urlsafe base64 to normal base64
var input = input.replace('-', '+').replace('/', '_');
// Convert from base64 to binary string
var edata = new Buffer(input, 'base64').toString('binary')
// Create key from password
var m = crypto.createHash('md5');
m.update(password)
var key = m.digest('hex');
// Create iv from password and key
m = crypto.createHash('md5');
m.update(password + key)
var iv = m.digest('hex');
// Decipher encrypted data
var decipher = crypto.createDecipheriv('aes-256-cbc', key, iv.slice(0,16));
var decrypted = decipher.update(edata, 'binary') + decipher.final('binary');
var plaintext = new Buffer(decrypted, 'binary').toString('utf8');
callback(plaintext);
};
encrypt(input, password, function (encoded) {
console.log(encoded);
decrypt(encoded, password, function (output) {
console.log(output);
});
});
This produces the output:
BXSGjDAYKeXlaRXVVJGuREKTPiiXeam8W9e96Nknt3E=
hello world
In python
from Crypto.Cipher import AES
from hashlib import md5
import base64
password = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'
input = 'hello world'
def _encrypt(data, nonce, password):
m = md5()
m.update(password)
key = m.hexdigest()
m = md5()
m.update(password + key)
iv = m.hexdigest()
# pad to 16 bytes
data = data + " " * (16 - len(data) % 16)
aes = AES.new(key, AES.MODE_CBC, iv[:16])
encrypted = aes.encrypt(data)
return base64.urlsafe_b64encode(encrypted)
def _decrypt(edata, nonce, password):
edata = base64.urlsafe_b64decode(edata)
m = md5()
m.update(password)
key = m.hexdigest()
m = md5()
m.update(password + key)
iv = m.hexdigest()
aes = AES.new(key, AES.MODE_CBC, iv[:16])
return aes.decrypt(edata)
output = _encrypt(input, "", password)
print(output)
plaintext = _decrypt(output, "", password)
print(plaintext)
This produces the output
BXSGjDAYKeXlaRXVVJGuRA==
hello world
Clearly they are very close, but node seems to be padding the output with something. Any ideas how I can get the two to interoperate?
OK, I’ve figured it out, node uses OpenSSL which uses PKCS5 to do padding. PyCrypto doesn’t handle the padding so I was doing it myself just add ‘ ‘ in both.
If I add PKCS5 padding in the python code and remove the padding in the node code, it works.
So updated working code.
Node:
var crypto = require('crypto');
var password = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa';
var input = 'hello world';
var encrypt = function (input, password, callback) {
var m = crypto.createHash('md5');
m.update(password)
var key = m.digest('hex');
m = crypto.createHash('md5');
m.update(password + key)
var iv = m.digest('hex');
var data = new Buffer(input, 'utf8').toString('binary');
var cipher = crypto.createCipheriv('aes-256-cbc', key, iv.slice(0,16));
// UPDATE: crypto changed in v0.10
// https://github.com/joyent/node/wiki/Api-changes-between-v0.8-and-v0.10
var nodev = process.version.match(/^v(d+).(d+)/);
var encrypted;
if( nodev[1] === '0' && parseInt(nodev[2]) < 10) {
encrypted = cipher.update(data, 'binary') + cipher.final('binary');
} else {
encrypted = cipher.update(data, 'utf8', 'binary') + cipher.final('binary');
}
var encoded = new Buffer(encrypted, 'binary').toString('base64');
callback(encoded);
};
var decrypt = function (input, password, callback) {
// Convert urlsafe base64 to normal base64
var input = input.replace(/-/g, '+').replace(/_/g, '/');
// Convert from base64 to binary string
var edata = new Buffer(input, 'base64').toString('binary')
// Create key from password
var m = crypto.createHash('md5');
m.update(password)
var key = m.digest('hex');
// Create iv from password and key
m = crypto.createHash('md5');
m.update(password + key)
var iv = m.digest('hex');
// Decipher encrypted data
var decipher = crypto.createDecipheriv('aes-256-cbc', key, iv.slice(0,16));
// UPDATE: crypto changed in v0.10
// https://github.com/joyent/node/wiki/Api-changes-between-v0.8-and-v0.10
var nodev = process.version.match(/^v(d+).(d+)/);
var decrypted, plaintext;
if( nodev[1] === '0' && parseInt(nodev[2]) < 10) {
decrypted = decipher.update(edata, 'binary') + decipher.final('binary');
plaintext = new Buffer(decrypted, 'binary').toString('utf8');
} else {
plaintext = (decipher.update(edata, 'binary', 'utf8') + decipher.final('utf8'));
}
callback(plaintext);
};
encrypt(input, password, function (encoded) {
console.log(encoded);
decrypt(encoded, password, function (output) {
console.log(output);
});
});
Python:
from Crypto.Cipher import AES
from hashlib import md5
import base64
password = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'
input = 'hello world'
BLOCK_SIZE = 16
def pad (data):
pad = BLOCK_SIZE - len(data) % BLOCK_SIZE
return data + pad * chr(pad)
def unpad (padded):
pad = ord(chr(padded[-1]))
return padded[:-pad]
def get_key_iv (password):
m = md5()
m.update(password.encode('utf-8'))
key = m.hexdigest()
m = md5()
m.update((password + key).encode('utf-8'))
iv = m.hexdigest()
return [key,iv]
def _encrypt(data, password):
key,iv = get_key_iv(password)
data = pad(data)
aes = AES.new(key, AES.MODE_CBC, iv[:16])
encrypted = aes.encrypt(data)
return base64.urlsafe_b64encode(encrypted)
def _decrypt(edata, password):
edata = base64.urlsafe_b64decode(edata)
key,iv = get_key_iv(password)
aes = AES.new(key, AES.MODE_CBC, iv[:16])
return unpad(aes.decrypt(edata))
output = _encrypt(input, password)
print(output)
plaintext = _decrypt(output, password)
print(plaintext)
Just for any one that is similar to me, who was finding a simple way to do the encryption and decryption for AES in python that is doing the same thing in node.js. The class here supports different bits of AES and both hex and base64 encoding that produces same result in node.js.
Also noted that if you are missing the package Crypto, you can simply install it by
pip install pycrypto
The code for python is as follows:
import base64
import hashlib
from Crypto.Cipher import AES
class AESCrypto(object):
def __init__(self, algorithm, password):
self.algorithm = filter(lambda x: not x.isdigit(), algorithm).lower()
self.bits = int(filter(str.isdigit, algorithm))
self.bs = 16
if not self.algorithm == 'aes':
raise Exception('Only AES crypto is supported')
if not self.bits % 8 == 0:
raise Exception('Bits of crypto must be a multiply of 8.')
self.bytes = self.bits / 8
self.password = password
self.generateKeyAndIv()
def generateKeyAndIv(self):
last = ''
allBytes = ''
maxBytes = self.bytes + self.bs
while len(allBytes) < maxBytes:
last = hashlib.md5(last + self.password).digest()
allBytes += last
self.key = allBytes[:self.bytes]
self.iv = allBytes[self.bytes:maxBytes]
def encrypt(self, raw, outputEncoding):
outputEncoding = outputEncoding.lower()
raw = self._pad(raw)
cipher = AES.new(self.key, AES.MODE_CBC, self.iv)
encrypted = cipher.encrypt(raw)
if outputEncoding == 'hex':
return encrypted.encode('hex')
elif outputEncoding == 'base64':
return base64.b64encode(encrypted)
else:
raise Exception('Encoding is not supported.')
def decrypt(self, data, inputEncoding):
inputEncoding = inputEncoding.lower()
if inputEncoding == 'hex':
data = ''.join(map(chr, bytearray.fromhex(data)))
elif inputEncoding == 'base64':
data = base64.b64decode(data)
cipher = AES.new(self.key, AES.MODE_CBC, self.iv)
return self._unpad(cipher.decrypt(data))
def _pad(self, data):
padding = self.bs - len(data) % self.bs
return data + padding * chr(padding)
@staticmethod
def _unpad(data):
return data[0:-ord(data[-1])]
The following are examples to use the class:
Encryption Example:
password = 'some_random_password'
content = 'content_to_be_encrypted'
cipher = AESCrypto('aes192', password)
encrypted = cipher.encrypt(content, 'hex')
Decryption Example:
password = 'some_random_password'
content = 'encrypted_content'
cipher = AESCrypto('aes192', password)
decrypted = cipher.decrypt(content, 'hex')
while trying to run the Python script using Python 3.8 I encountered the following error:
m.update(password)
TypeError: Unicode-objects must be encoded before hashing
the password should be :
password = b'abcd'
I also got the following error :
m.update(password + key)
TypeError: can't concat str to bytes
I was able to fix it by adding the following line after key:
key = bytes.fromhex(key_)
The python script should work this way :
from Crypto.Cipher import AES
from hashlib import md5
import base64
password = b'abcd'
input = 'hello world'
BLOCK_SIZE = 16
def pad (data):
pad = BLOCK_SIZE - len(data) % BLOCK_SIZE
return data + pad * chr(pad)
def unpad (padded):
pad = ord(chr(padded[-1]))
return padded[:-pad]
def _encrypt(data, nonce, password):
m = md5()
m.update(password)
key_ = m.hexdigest()
key = bytes.fromhex(key_)
m = md5()
m.update(password + key)
iv = m.hexdigest()
iv = bytes.fromhex(iv)
data = pad(data)
aes = AES.new(key, AES.MODE_CBC, iv[:16])
encrypted = aes.encrypt(data.encode('utf-8'))
return base64.urlsafe_b64encode(encrypted)
def _decrypt(edata, nonce, password):
edata = base64.urlsafe_b64decode(edata)
m = md5()
m.update(password)
key = m.hexdigest()
key = bytes.fromhex(key)
m = md5()
m.update(password + key)
iv = m.hexdigest()
iv = bytes.fromhex(iv)
aes = AES.new(key, AES.MODE_CBC, iv[:16])
return unpad(aes.decrypt(edata))
output = _encrypt(input, "", password)
print(output)
plaintext = _decrypt(output, "", password)
print(plaintext)
Because I spent way too much time on this with Python 3.10.7 and Node.js v18.6.0.
Here is a working code totally compatible between two languages with examples.
Only the secret is needed for getting same values as expected 🙂
Note pycryptodome
is needed for Python. Code should be tweaked for supporting different algorithms.
const crypto = require('crypto')
function get_crypto(secret, encode) {
// Create hashed key from password/key
let m = crypto.createHash('md5').update(secret)
const key = m.digest('hex')
m = crypto.createHash('md5').update(secret + key)
const iv = m.digest('hex').slice(0, 16) // only in aes-256
return encode
? crypto.createCipheriv('aes-256-cbc', key, iv)
: crypto.createDecipheriv('aes-256-cbc', key, iv)
}
const secret = 'f8abb29f13cb932704badb0de414ab08ca9f6c63' // crypto.randomBytes(20).toString('hex')
const value = 'hello world'
const data = Buffer.from(value, 'utf8').toString('binary')
const cipher = get_crypto(secret, true)
const encrypted = cipher.update(data, 'utf8', 'binary') + cipher.final('binary')
const encoded = Buffer.from(encrypted, 'binary').toString('base64')
console.log('encoded:', encoded)
const edata = Buffer.from(encoded, 'base64').toString('binary')
const decipher = get_crypto(secret, false)
const decoded = decipher.update(edata, 'binary', 'utf8') + decipher.final('utf8')
console.log('decoded:', decoded)
# This script needs pycryptodome dependency
# pip install pycryptodome
from Crypto.Cipher import AES
from hashlib import md5
import base64
BLOCK_SIZE = AES.block_size
def get_aes(s):
m = md5()
m.update(s.encode('utf-8'))
key = m.hexdigest()
m = md5()
m.update((s + key).encode('utf-8'))
iv = m.hexdigest()
return AES.new(key.encode("utf8"), AES.MODE_CBC, iv.encode("utf8")[:BLOCK_SIZE])
# pkcs5 padding
def pad(byte_array):
pad_len = BLOCK_SIZE - len(byte_array) % BLOCK_SIZE
return byte_array + (bytes([pad_len]) * pad_len)
# pkcs5 - unpadding
def unpad(byte_array):
return byte_array[:-ord(byte_array[-1:])]
def _encrypt(s, data):
data = pad(data.encode("UTF-8"))
aes = get_aes(s)
encrypted = aes.encrypt(data)
return base64.urlsafe_b64encode(encrypted).decode('utf-8')
def _decrypt(s, edata):
edata = base64.urlsafe_b64decode(edata)
aes = get_aes(s)
return unpad(aes.decrypt(edata)).decode('utf-8')
if __name__ == '__main__':
secret = 'f8abb29f13cb932704badb0de414ab08ca9f6c63'
value = 'hello world'
encoded = _encrypt(secret, value)
print('encoded:', encoded)
decoded = _decrypt(secret, encoded)
print('decoded:', decoded)
Help from: