Skip to content

Instantly share code, notes, and snippets.

@bertramn
Created December 26, 2017 15:09
  • Star 6 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
Star You must be signed in to star a gist
Save bertramn/d22e88eba2ce432be016ab5d61595ab4 to your computer and use it in GitHub Desktop.
Jasypt/Bouncycastle PBEWITHSHA256AND256BITAES-CBC-BC en/decryption in Python
# Make coding more python3-ish
from __future__ import (absolute_import, division, print_function)
from abc import ABCMeta
from array import array
from base64 import b64encode, b64decode
from Crypto import Random
from Crypto.Cipher import AES
from Crypto.Hash import SHA256
class PBEParameterGenerator(object):
__metaclass__ = ABCMeta
@staticmethod
def pad(block_size, s):
"""
Pad a string to the provided block size when using fixed block ciphers.
:param block_size: int - the cipher block size
:param s: str - the string to pad
:return: a padded string that can be fed to the cipher
"""
return s + (block_size - len(s) % block_size) * chr(block_size - len(s) % block_size)
@staticmethod
def unpad(s):
"""
Remove padding from the string after decryption when using fixed block ciphers.
:param s: str - the string to remove padding from
:return: the unpadded string
"""
return s[0:-ord(s[-1])]
@staticmethod
def adjust(a, a_off, b):
"""
Adjusts the byte array as per PKCS12 spec
:param a: byte[] - the target array
:param a_off: int - offset to operate on
:param b: byte[] - the bitsy array to pick from
:return: nothing as operating on array by reference
"""
x = (b[len(b) - 1] & 0xff) + (a[a_off + len(b) - 1] & 0xff) + 1
a[a_off + len(b) - 1] = x & 0xff
x = x >> 8
for i in range(len(b) - 2, -1, -1):
x = x + (b[i] & 0xff) + (a[a_off + i] & 0xff)
a[a_off + i] = x & 0xff
x = x >> 8
@staticmethod
def pkcs12_password_to_bytes(password):
"""
Converts a password string to a PKCS12 v1.0 compliant byte array.
:param password: byte[] - the password as simple string
:return: The unsigned byte array holding the password
"""
pkcs12_pwd = [0x00] * (len(password) + 1) * 2
for i in range(0, len(password)):
digit = ord(password[i])
pkcs12_pwd[i * 2] = int(digit >> 8)
pkcs12_pwd[i * 2 + 1] = int(digit)
return array('B', pkcs12_pwd)
class PKCS12ParameterGenerator(PBEParameterGenerator):
"""
Equivalent of the Bouncycastle PKCS12ParameterGenerator.
"""
__metaclass__ = ABCMeta
KEY_MATERIAL = 1
IV_MATERIAL = 2
MAC_MATERIAL = 3
SALT_SIZE_BYTE = 16
def __init__(self, digest_factory):
super(PBEParameterGenerator, self).__init__()
self.digest_factory = digest_factory
def generate_derived_parameters(self, password, salt, iterations, key_size, iv_size):
"""
Generates the key and iv that can be used with the cipher.
:param password: str - the password used for the key material
:param salt: byte[] - random salt
:param iterations: int - number if hash iterations for key material
:param key_size: int - key size in bits
:param iv_size: int - iv size in bits
:return: key and iv that can be used to setup the cipher
"""
key_size = int(key_size / 8)
iv_size = int(iv_size / 8)
# pkcs12 padded password (unicode byte array with 2 trailing 0x0 bytes)
password_bytes = PKCS12ParameterGenerator.pkcs12_password_to_bytes(password)
d_key = self.generate_derived_key(password_bytes, salt, iterations, self.KEY_MATERIAL, key_size)
if iv_size and iv_size > 0:
d_iv = self.generate_derived_key(password_bytes, salt, iterations, self.IV_MATERIAL, iv_size)
else:
d_iv = None
return d_key, d_iv
def generate_derived_key(self, password, salt, iterations, id_byte, key_size):
"""
Generate a derived key as per PKCS12 v1.0 spec
:param password: byte[] - pkcs12 padded password (unicode byte array with 2 trailing 0x0 bytes)
:param salt: byte[] - random salt
:param iterations: int - number if hash iterations for key material
:param id_byte: int - the material padding
:param key_size: int - the key size in bytes (e.g. AES is 256/8 = 32, IV is 128/8 = 16)
:return: the sha256 digested pkcs12 key
"""
u = int(self.digest_factory.digest_size)
v = int(self.digest_factory.block_size)
d_key = [0x00] * key_size
# Step 1
D = [id_byte] * v
# Step 2
S = []
if salt and len(salt) != 0:
s_size = v * int((len(salt) + v - 1) / v)
S = [0x00] * s_size
salt_size = len(salt)
for i in range(s_size):
S[i] = salt[i % salt_size]
# Step 3
P = []
if password and len(password) != 0:
p_size = v * int((len(password) + v - 1) / v)
P = [0x00] * p_size
password_size = len(password)
for i in range(p_size):
P[i] = password[i % password_size]
# Step 4
I = array('B', S + P)
B = array('B', [0x00] * v)
# Step 5
c = int((key_size + u - 1) / u)
# Step 6
for i in range(1, c + 1):
# Step 6 - a
digest = self.digest_factory.new()
digest.update(array('B', D))
digest.update(I)
A = array('B', digest.digest()) # bouncycastle now resets the digest, we will create a new digest
for j in range(1, iterations):
A = array('B', self.digest_factory.new(A).digest())
# Step 6 - b
for k in range(0, v):
B[k] = A[k % u]
# Step 6 - c
for j in range(0, int(len(I) / v)):
self.adjust(I, j * v, B)
if i == c:
for j in range(0, key_size - ((i - 1) * u)):
d_key[(i - 1) * u + j] = A[j]
else:
for j in range(0, u):
d_key[(i - 1) * u + j] = A[j]
return array('B', d_key)
def encrypt(password, text, iterations=1000):
# some default from somewhere
key_size_bits = 256
iv_size_bits = 128
# create sha256 PKCS12 secret generator
generator = PKCS12ParameterGenerator(SHA256)
# from PBE.Util.makePBEParameters()
# generate a 16 byte salt which is used to generate key material and iv
salt = array('B', Random.get_random_bytes(PKCS12ParameterGenerator.SALT_SIZE_BYTE))
# print('enc-salt = %s' % binascii.hexlify(salt))
# generate key material as per PBEWITHSHA256AND256BITAES-CBC-BC
key, iv = generator.generate_derived_parameters(password, salt, iterations, key_size_bits, iv_size_bits)
# setup AES cipher
cipher = AES.new(key, AES.MODE_CBC, iv)
# pad the plain text secret to AES block size
encrypted_message = cipher.encrypt(generator.pad(AES.block_size, text))
# concatenate salt + encrypted message
return b64encode(salt + array('B', encrypted_message))
def decrypt(password, ciphertext, iterations=1000):
# some default from somewhere
key_size_bits = 256
iv_size_bits = 128
# create sha256 PKCS12 secret generator
generator = PKCS12ParameterGenerator(SHA256)
# decode the base64 encoded and encrypted secret
n_cipher_bytes = b64decode(ciphertext)
# extract salt bytes 0 - SALT_SIZE
salt = array('B', n_cipher_bytes[:PKCS12ParameterGenerator.SALT_SIZE_BYTE])
# print('dec-salt = %s' % binascii.hexlify(salt))
# create reverse key material
key, iv = generator.generate_derived_parameters(password, salt, iterations, key_size_bits, iv_size_bits)
cipher = AES.new(key, AES.MODE_CBC, iv)
# extract encrypted message bytes SALT_SIZE - len(cipher)
n_cipher_message = array('B', n_cipher_bytes[PKCS12ParameterGenerator.SALT_SIZE_BYTE:])
# decode the message and unpad
decoded = cipher.decrypt(n_cipher_message.tostring())
return generator.unpad(decoded)
if __name__ == "__main__":
passcode = 'pssst...don\'t tell anyone'
result = encrypt(passcode, 'secret value', 4000)
print('enc = %s' % result)
reverse = decrypt(passcode, result, 4000)
print('dec = %s' % reverse)
# run something like this on the jasypt command line
# $JASYPT_HOME/bin/decrypt.sh keyObtentionIterations = 4000 \
# providerClassName = "org.bouncycastle.jce.provider.BouncyCastleProvider" \
# saltGeneratorClassName = "org.jasypt.salt.RandomSaltGenerator" \
# algorithm = "PBEWITHSHA256AND256BITAES-CBC-BC" \
# password = 'pssst...don\'t tell anyone' \
# input = 'xgX5+yRbKhs4zSubkAPkg9gSBkZU6XWt7csceM/3xDY='
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment