What is openssl_decrypt in Python? - python

I can't seem to do this PHP code in python:
openssl_decrypt( $acipher, "aes-256-gcm", $secret, OPENSSL_RAW_DATA, $bnonce, $tag);
This is my Python code:
from crypto.Cipher import AES
cipher = AES.new(acipher, AES.MODE_GCM, acipher)
plaintext = cipher.decrypt(secret)
# i don't even know where to put the "$bnonce, $tag", haha.
I'm quite stuck, please help.
I tried this but i got MAC check failed
secret = "b34f09fc80e7974a918b50cfda6f48ce" # after i use open-ssl on some string, this is what i received. Just for Extra Info.
content = "EZrjXFQ4o3oCVzy28q/IWYLgg9aP+VlmhQZ+wKs4C20D7k+lZMKlmi9UgZgINz6t/ucWmOzRFLo5DAZ3b6dYrMOz1lV2gJs9v9K6MDFeweREKRDupnTDKgx8AS7OaimofNK8wKxNp5QDnYBZNpY6BQ02mU5586LXGfwJJrAc3S7D85cZZofnUBpNcUztrHYOjCEtvB24p6j8W9ju3ALkAfd1Dk+UdKMVzp9sLa/qbzxPqQeZhPPS43A5b9Wa10DeKvMnMl4Z46M6j+AeudKmS46xCOM84zMbOonxIkCA+kS0BCPaDlpwOyiL31yeJqHw/dKhHuY0qVyHWgrlf3mdHizGKakeAoYsy4Wo1WjeTteSRnw713s="
# Content
bcontent = str( base64.b64decode(content) )
bcontent_total_characters = len(bcontent)
bcontent_total_characters_minus_12 = bcontent_total_characters - 12
bnonce = bcontent[ bcontent_total_characters_minus_12 : bcontent_total_characters - 12 + bcontent_total_characters ]
bcipher = bcontent[ 0 : 0 + bcontent_total_characters_minus_12 ]
# default tag
bcipher_total_characters = len(bcipher)
taglength = 16
tag = bcipher[ bcipher_total_characters - taglength : bcipher_total_characters - taglength + bcipher_total_characters ]
acipher = bcipher[ 0 : 0 + bcipher_total_characters - taglength]
cipher = AES.new(str.encode(secret), AES.MODE_GCM, str.encode(bnonce) )
decrypted = cipher.decrypt_and_verify(str.encode(content), str.encode(tag))

From php documentation i modified your function vars you can rename them if you like
openssl_decrypt( $acipher,
"aes-256-gcm",
$passphrase, //This is your secret
OPENSSL_RAW_DATA,
$iv, //This is your nonce
$tag //This is your tag
);
This is how it would look in python
#!/usr/bin/env python3
# pip install pycryptodome
import json
from base64 import b64encode,b64decode
from Crypto.Cipher import AES
from Crypto.Random import get_random_bytes
from Crypto.Random import get_random_bytes
def python_encrypt(data, passphrase, iv):
"""
Encrypt using AES-256-GCM
"""
cipher = AES.new(passphrase, AES.MODE_GCM,iv)
ciphertext, tag = cipher.encrypt_and_digest(data)
json_k = [ 'nonce', 'ciphertext', 'tag' ]
json_v = [ b64encode(x).decode('utf-8') for x in [cipher.nonce, ciphertext, tag ]]
result = json.dumps(dict(zip(json_k, json_v)))
return result
def python_decrypt(data, passphrase, iv, tag):
"""
Decrypt using AES-256-GCM
"""
cipher = AES.new(passphrase, AES.MODE_GCM, iv)
decrypted = cipher.decrypt_and_verify(data, tag)
return decrypted
#Testing code out
data = b"secret"
passphrase = get_random_bytes(16)
nonce = get_random_bytes(16)
enc_json = python_encrypt(data,passphrase,nonce)
print(enc_json)
b64 = json.loads(enc_json)
json_k = [ 'nonce', 'ciphertext', 'tag' ]
jv = {k:b64decode(b64[k]) for k in json_k}
print(python_decrypt(jv['ciphertext'], passphrase, jv['nonce'], jv['tag']))
Modified from the docs

Related

Python Crypto decoding

i try to encrypt and then decrypt text with Crypto and AWS KMS on Python, i have this code :
import base64
import boto3
from Crypto.Cipher import AES
PAD = lambda s: s + (256 - len(s) % 256) * ' '
def get_arn(aws_data):
return 'arn:aws:kms:{region}:{account_number}:key/{key_id}'.format(**aws_data)
def encrypt_data(aws_data, plaintext_message):
kms_client = boto3.client(
'kms',
region_name=aws_data['region'])
data_key = kms_client.generate_data_key(
KeyId=aws_data['key_id'],
KeySpec='AES_256')
cipher_text_blob = data_key.get('CiphertextBlob')
plaintext_key = data_key.get('Plaintext')
# Note, does not use IV or specify mode... for demo purposes only.
cypher = AES.new(plaintext_key, AES.MODE_CBC)
encrypted_data = base64.b64encode(cypher.encrypt(PAD(plaintext_message).encode("utf-8")))
# Need to preserve both of these data elements
return encrypted_data, cipher_text_blob
def decrypt_data(aws_data, encrypted_data, cipher_text_blob):
kms_client = boto3.client(
'kms',
region_name=aws_data['region'])
decrypted_key = kms_client.decrypt(CiphertextBlob=cipher_text_blob).get('Plaintext')
cypher = AES.new(decrypted_key, AES.MODE_CBC)
return cypher.decrypt(base64.b64decode(encrypted_data)).rstrip()
def main():
# Add your account number / region / KMS Key ID here.
aws_data = {
'region': 'eu-west-1',
'account_number': '7011777xxxxx',
'key_id': 'xxxxxx-83ac-xxxxxx-93d4-xxxxxx',
}
# And your super secret message to envelope encrypt...
plaintext = 'Hello, Worldas!'
# Store encrypted_data & cipher_text_blob in your persistent storage. You will need them both later.
encrypted_data, cipher_text_blob = encrypt_data(aws_data, plaintext)
print(encrypted_data)
decrypted_data = decrypt_data(aws_data, encrypted_data, cipher_text_blob)
print(decrypted_data)
if __name__ == '__main__':
main()
I encrypting for test message 'Hello, Worldas!' my encrypted_data in output looks like : b'ESsdSQv6JxpQptBmj321eX/bVj3gyGJ7AHtrH5qeIfTWbqSzIP7i6URrZFme1PGSNRGzl12B/NBFbK0nHBcCcaj9Wb9Qh+YMYJjeSTnGWOKFWmcIKYAAut9d040xiWG0KKBwHJTdl+41+g8F2ueSWqO1zR9Uuw1qyekF9s/141W7t+Le8IRe60tQKhgMAW5qxDVGluWZGJXLYDLIqFXszN9OhLmjwbMnF4g0ryMq41xbAXH77x0EJODhF1GQ+peHnKuexlhuzRjq1XVAvIgxQ1kYvBSE9AkqqCsO5BwIJuAlwfOWA93gSyTgLmWOg8bPTan4UnQNtTQ3vaRScffPgg=='
But then i try to decrypt i get output : b'-94\xc1\xee\xecF\xfbw9\x81o;\x9d\x1a\x10' instead of 'Hello, Worldas!'
Maybe whom know where is a problem? Why it happen? and how can i encrypt and decrypt my file properly ? please suggest!

Problems with encryption when converting Crypt:CBC example from Perl to Python (Blowfish)

I am trying to translate the program below to Python.
In the end the Python code should produce the same passphrase output as the Perl variant.
#!/usr/bin/env perl
use Crypt::CBC;
my $key = 'key to the gates';
my $cipher = Crypt::CBC->new(
-key => $key,
-cipher => 'Blowfish',
-salt => '12341234'
);
my $pass_phrase = "secret text";
print $cipher->encrypt_hex($pass_phrase),"\n";
print unpack('H*', $cipher->key()), "\n";
print unpack('H*', $cipher->iv()), "\n";
print unpack('H*', $cipher->salt()), "\n";
print unpack('H*', $cipher->keysize()), "\n";
#output:
#pass:53616c7465645f5f31323334313233344c0ad60f0eb9fdffc46b5cc02d76d473 <- hex enc "Salted__12341234<gibberish>"
#key:031f2cc96d063cf836ce42c77a8a3d25bdd959659d00a892a02b13930e92f47c82a7054256be4a0f1b3771bd36c07fe3ea4f6900f8ddebe5
#iv:f4d50b2385a2a996
#salt:3132333431323334
#keysize:3536
Below is my python code than that decrypts successfully, but encrypts unsuccessfully.
The successfull decryption of the perl passphrase was mostly for verify input params. (Taking IV straight from perl and removing Crypt::CBC's added 16 char of salt before decrypting made it work, various posts on SO helped me..).
Then is the unsuccessful attempt to encryption passhphrase and generate the same output as perl.
I think i just need to use the right combination of the verified input params to get it working...maybe the salt should be padded? Or padding in general is wrong? Or iv input should not be hexlified? (it need to be 8 chars..)
Any input is appreciated!
!/usr/bin/env python
from Crypto.Cipher import Blowfish
from binascii import hexlify, unhexlify
from struct import pack
import base64
# working decryption
passphrase = unhexlify("53616c7465645f5f31323334313233344c0ad60f0eb9fdffc46b5cc02d76d473"[32:])
key = unhexlify("031f2cc96d063cf836ce42c77a8a3d25bdd959659d00a892a02b13930e92f47c82a7054256be4a0f1b3771bd36c07fe3ea4f6900f8ddebe5")
iv = unhexlify('f4d50b2385a2a996')
num_padding = ord(Blowfish.new(key, Blowfish.MODE_CBC, iv).decrypt(passphrase)[-1])
print Blowfish.new(key, Blowfish.MODE_CBC, iv).decrypt(passphrase)[:(-1*num_padding)]
# --- non working encryption!
passphrase2 = "secret text"
key2 = 'key to the gates'
iv2 = unhexlify('f4d50b2385a2a996')
plength = Blowfish.block_size - len(passphrase2) % Blowfish.block_size
padding = [plength] * plength
pad_str = passphrase2 + pack('b' * plength, *padding)
cipher = Blowfish.new(key2, Blowfish.MODE_CBC, iv2)
print hexlify("Salted__12341234"+cipher.encrypt(pad_str))
#output:
#secret text
#53616c7465645f5f31323334313233346aa3f2169677cbf282b1330b46da3114
I guess you are not supposed to answer your own question, but i leave it here anyway:)
I think maybe padding was off in the original code, not really sure.
This time i used the nice "Padding" lib https://pypi.python.org/pypi/Padding
from Crypto.Cipher import Blowfish
from binascii import hexlify, unhexlify
import Padding
class BlowFishCipher:
def __init__( self, key,iv,salt ):
self.key = unhexlify(key)
self.iv = unhexlify(iv)
self.salt = unhexlify(salt)
def encrypt( self, raw ):
raw = Padding.appendPadding(raw, BS)
cipher = Blowfish.new( self.key, Blowfish.MODE_CBC, self.iv )
return hexlify("Salted__"+self.salt+cipher.encrypt(raw))
def decrypt( self, enc):
enc = unhexlify(enc)
cipher = Blowfish.new(self.key, Blowfish.MODE_CBC, self.iv )
return Padding.removePadding(cipher.decrypt( enc), BS)
if __name__== "__main__":
BS = Blowfish.block_size
key_perl = "031f2cc96d063cf836ce42c77a8a3d25bdd959659d00a892a02b13930e92f47c82a7054256be4a0f1b3771bd36c07fe3ea4f6900f8ddebe5"
iv_perl = "f4d50b2385a2a996"
salt_perl= "3132333431323334"
passphrase_perl = "53616c7465645f5f31323334313233344c0ad60f0eb9fdffc46b5cc02d76d473"
# remove "Salted__12341234" from passhphrase_perl by [32:]
passphrase = passphrase_perl[32:]
decryptor = BlowFishCipher(key_perl,iv_perl,salt_perl)
plaintext = decryptor.decrypt(passphrase)
print "decrypted {:>70}".format(plaintext)
ciphertext = "secret text"
encodedtext = decryptor.encrypt(ciphertext)
print "encrypted pyhton {:>70}".format(encodedtext)
print "encrypted perl {:>70}".format(passphrase_perl)
decrypted secret text
encrypted pyhton 53616c7465645f5f31323334313233344c0ad60f0eb9fdffc46b5cc02d76d473
encrypted perl 53616c7465645f5f31323334313233344c0ad60f0eb9fdffc46b5cc02d76d473

encrypt using node.js crypto aes256 and decrypt using python2.7 PyCrypto

I am trying to encrypt using node.js as follows (node.js v0.10.33):
var crypto = require('crypto');
var assert = require('assert');
var algorithm = 'aes256'; // or any other algorithm supported by OpenSSL
var key = 'mykey';
var text = 'this-needs-to-be-encrypted';
var cipher = crypto.createCipher(algorithm, key);
var encrypted = cipher.update(text, 'utf8', 'hex') + cipher.final('hex');
console.log('encrypted', encrypted, encrypted.length)
/*
var decipher = crypto.createDecipher(algorithm, key);
try {
var decrypted = decipher.update(encrypted, 'hex', 'utf8') + decipher.final('utf8');
} catch (e) {
console.error('Couldnt decipher encrypted text. Invalid key provided', e)
} finally {
assert.equal(decrypted, text);
}
*/
How can I decrypt the encrypted text using PyCrypto (v2.6.1) on py2.7?
You should be using crypto.createCipheriv as stated in https://nodejs.org/api/crypto.html#crypto_crypto_createcipher_algorithm_password.
The answer below assumes you change your snippet to use crypto.createCipheriv, as following:
var crypto = require('crypto');
var assert = require('assert');
var algorithm = 'aes256'; // or any other algorithm supported by OpenSSL
var key = '00000000000000000000000000000000';
var iv = '0000000000000000';
var text = 'this-needs-to-be-encrypted';
var cipher = crypto.createCipheriv(algorithm, key, iv);
var encrypted = cipher.update(text, 'utf8', 'hex') + cipher.final('hex');
console.log('encrypted', encrypted, encrypted.length)
which generates the encrypted text b88e5f69c7bd5cd67c9c12b9ad73e8c1ca948ab26da01e6dad0e7f95448e79f4.
Python Solution with explicit key and IV:
from Crypto import Random
from Crypto.Cipher import AES
BS = 16
def pad(data):
padding = BS - len(data) % BS
return data + padding * chr(padding)
def unpad(data):
return data[0:-ord(data[-1])]
def decrypt_node(hex_data, key='0'*32, iv='0'*16):
data = ''.join(map(chr, bytearray.fromhex(hex_data)))
aes = AES.new(key, AES.MODE_CBC, iv)
return unpad(aes.decrypt(data))
def encrypt_node(data, key='0'*32, iv='0'*16):
aes = AES.new(key, AES.MODE_CBC, iv)
return aes.encrypt(pad(data)).encode('hex')
print(encrypt_node('this-needs-to-be-encrypted'))
print(decrypt_node('b88e5f69c7bd5cd67c9c12b9ad73e8c1ca948ab26da01e6dad0e7f95448e79f4'))
If you keep using plain crypto.createCipher you will need to derive the key and iv from the password using https://www.openssl.org/docs/man1.0.2/crypto/EVP_BytesToKey.html.

Decrypting AES and HMAC with PyCrypto

Having a bit of trouble getting a AES cipher text to decrypt.
In this particular scenario, I am encrypting data on the client side with Crypto-JS and decrypting it back on a python server with PyCrypto.
encrypt.js:
var password = 'BJhtfRjKnTDTtPXUBnErKDxfkiMCOLyP';
var data = 'mytext';
var masterKey = CryptoJS.SHA256(password).toString();
// Derive keys for AES and HMAC
var length = masterKey.toString().length / 2
var encryptionKey = masterKey.substr(0, length);
var hmacKey = masterKey.substr(length);
var iv = CryptoJS.lib.WordArray.random(64/8);
var encrypted = CryptoJS.AES.encrypt(
data,
encryptionKey,
{
iv: iv,
mode: CryptoJS.mode.CFB
}
);
var concat = iv + encrypted;
// Calculate HMAC using iv and cipher text
var hash = CryptoJS.HmacSHA256(concat, hmacKey);
// Put it all together
var registrationKey = iv + encrypted + hash;
// Encode in Base64
var basemessage = btoa(registrationKey);
decrypt.py:
class AESCipher:
def __init__(self, key):
key_hash = SHA256.new(key).hexdigest()
# Derive keys
encryption_key = key_hash[:len(key_hash)/2]
self.key = encryption_key
self.hmac_key = key_hash[len(key_hash)/2:]
def verify_hmac(self, input_cipher, hmac_key):
# Calculate hash using inputted key
new_hash = HMAC.new(hmac_key, digestmod=SHA256)
new_hash.update(input_cipher)
digest = new_hash.hexdigest()
# Calculate hash using derived key from local password
local_hash = HMAC.new(self.hmac_key, digestmod=SHA256)
local_hash.update(input_cipher)
local_digest = local_hash.hexdigest()
return True if digest == local_digest else False
def decrypt(self, enc):
enc = base64.b64decode(enc)
iv = enc[:16]
hmac = enc[60:]
cipher_text = enc[16:60]
# Verify HMAC using concatenation of iv + cipher like in js
verified_hmac = self.verify_hmac((iv+cipher_text), self.hmac_key)
if verified_hmac:
cipher = AES.new(self.key, AES.MODE_CFB, iv)
return cipher.decrypt(cipher_text)
password = 'BJhtfRjKnTDTtPXUBnErKDxfkiMCOLyP'
input = 'long base64 registrationKey...'
cipher = AESCipher(password)
decrypted = cipher.decrypt(input)
I'm successful in re-calculating the HMAC but when I try and then decrypt the cipher I get something that seems encrypted with �'s in the result.
I was getting errors about input length of cipher text but when I switched to CFB mode that fixed it so I don't think it's a padding issue.
There are many problems with your code.
Client (JavaScript):
AES has a block size of 128 bit and CFB mode expects a full block for the IV. Use
var iv = CryptoJS.lib.WordArray.random(128/8);
The iv and hash variables are WordArray objects, but encrypted is not. When you force them to be converted to strings by concatenating them (+), iv and hash are Hex-encoded, but encrypted is formatted in an OpenSSL compatible format and Base64-encoded. You need to access the ciphertext property to get the encrypted WordArray:
var concat = iv + encrypted.ciphertext;
and
var registrationKey = iv + encrypted.ciphertext + hash;
registrationKey is hex-encoded. There is no need to encode it again with Base64 and bloat it even more:
var basemessage = registrationKey;
If you want to convert the hex encoded registrationKey to base64 encoding, use:
var basemessage = CryptoJS.enc.Hex.parse(registrationKey).toString(CryptoJS.enc.Base64);
concat is a hex-encoded string of the IV and ciphertext, because you forced the stringification by "adding" (+) iv and encrypted. The HmacSHA256() function takes either a WordArray object or a string. When you pass a string in, as you do, it will assume that the data is UTF-8 encoded and try to decode it as UTF-8. You need to parse the data yourself into a WordArray:
var hash = CryptoJS.HmacSHA256(CryptoJS.enc.Hex.parse(concat), hmacKey);
The CryptoJS.AES.encrypt() and CryptoJS.HmacSHA256() expect the key either as a WordArray object or as a string. As before, if the key is supplied as a string, a UTF-8 encoding is assumed which is not the case here. You better parse the strings into WordArrays yourself:
var encryptionKey = CryptoJS.enc.Hex.parse(masterKey.substr(0, length));
var hmacKey = CryptoJS.enc.Hex.parse(masterKey.substr(length));
Server (Python):
You're not verifying anything in verify_hmac(). You hash the same data with the same key twice. What you need to do is hash the IV+ciphertext and compare the result with the hash (called tag or HMAC-tag) that you slice off the full ciphertext.
def verify_hmac(self, input_cipher, mac):
# Calculate hash using derived key from local password
local_hash = HMAC.new(self.hmac_key, digestmod=SHA256)
local_hash.update(input_cipher)
local_digest = local_hash.digest()
return mac == local_digest
And later in decrypt():
verified_hmac = self.verify_hmac((iv+cipher_text), hmac)
You need to correctly slice off the MAC. The 60 that is hardcoded is a bad idea. Since you're using SHA-256 the MAC is 32 bytes long, so you do this
hmac = enc[-32:]
cipher_text = enc[16:-32]
The CFB mode is actually a set of similar modes. The actual mode is determined by the segment size. CryptoJS only supports segments of 128 bit. So you need tell pycrypto to use the same mode as in CryptoJS:
cipher = AES.new(self.key, AES.MODE_CFB, iv, segment_size=128)
If you want to use CFB mode with a segment size of 8 bit (default of pycrypto), you can use a modified version of CFB in CryptoJS from my project: Extension for CryptoJS
Full client code:
var password = 'BJhtfRjKnTDTtPXUBnErKDxfkiMCOLyP';
var data = 'mytext';
var masterKey = CryptoJS.SHA256(password).toString();
var length = masterKey.length / 2
var encryptionKey = CryptoJS.enc.Hex.parse(masterKey.substr(0, length));
var hmacKey = CryptoJS.enc.Hex.parse(masterKey.substr(length));
var iv = CryptoJS.lib.WordArray.random(128/8);
var encrypted = CryptoJS.AES.encrypt(
data,
encryptionKey,
{
iv: iv,
mode: CryptoJS.mode.CFB
}
);
var concat = iv + encrypted.ciphertext;
var hash = CryptoJS.HmacSHA256(CryptoJS.enc.Hex.parse(concat), hmacKey);
var registrationKey = iv + encrypted.ciphertext + hash;
console.log(CryptoJS.enc.Hex.parse(registrationKey).toString(CryptoJS.enc.Base64));
Full server code:
from Crypto.Cipher import AES
from Crypto.Hash import HMAC, SHA256
import base64
import binascii
class AESCipher:
def __init__(self, key):
key_hash = SHA256.new(key).hexdigest()
self.hmac_key = binascii.unhexlify(key_hash[len(key_hash)/2:])
self.key = binascii.unhexlify(key_hash[:len(key_hash)/2])
def verify_hmac(self, input_cipher, mac):
local_hash = HMAC.new(self.hmac_key, digestmod=SHA256)
local_hash.update(input_cipher)
local_digest = local_hash.digest()
return SHA256.new(mac).digest() == SHA256.new(local_digest).digest() # more or less constant-time comparison
def decrypt(self, enc):
enc = base64.b64decode(enc)
iv = enc[:16]
hmac = enc[-32:]
cipher_text = enc[16:-32]
verified_hmac = self.verify_hmac((iv+cipher_text), hmac)
if verified_hmac:
cipher = AES.new(self.key, AES.MODE_CFB, iv, segment_size=128)
return cipher.decrypt(cipher_text)
else:
return 'Bad Verify'
password = 'BJhtfRjKnTDTtPXUBnErKDxfkiMCOLyP'
input = "btu0CCFbvdYV4B/j7hezAra6Q6u6KB8n5QcyA32JFLU8QRd+jLGW0GxMQsTqxaNaNkcU2I9r1ls4QUPUpaLPQg=="
obj = AESCipher(password)
decryption = obj.decrypt(input)
print 'Decrypted message:', decryption

Encrypting and Decrypting with python and nodejs

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)
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)
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')
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 = Buffer.concat([cipher.update(data, 'utf8'), cipher.final()]).toString('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 = Buffer.concat([decipher.update(edata, 'binary'), decipher.final()]).toString('utf-8')
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:
Implementing AES/ECB/PKCS5 padding in Python
Node.js - Set padding in crypto module
Python Encrypting with PyCrypto AES

Categories