I'm currently working implementing AES encryption in the backend using Python, but I'm encountering some issues in ensuring compatibility between frontend and backedn. I need help in integrating the frontend JavaScript code to work with it.
My backend Python code:
class Crypt():
def pad(self, data):
BLOCK_SIZE = 16
length = BLOCK_SIZE - (len(data) % BLOCK_SIZE)
return data + (chr(length)*length)
def unpad(self, data):
return data[:-(data[-1] if type(data[-1]) == int else ord(data[-1]))]
def bytes_to_key(self, data, salt, output=48):
assert len(salt) == 8, len(salt)
data += salt
key = sha256(data).digest()
final_key = key
while len(final_key) < output:
key = sha256(key + data).digest()
final_key += key
return final_key[:output]
def bytes_to_key_md5(self, data, salt, output=48):
assert len(salt) == 8, len(salt)
data += salt
key = md5(data).digest()
final_key = key
while len(final_key) < output:
key = md5(key + data).digest()
final_key += key
return final_key[:output]
def encrypt(self, message):
passphrase = "<secret passpharse value>".encode()
salt = Random.new().read(8)
key_iv = self.bytes_to_key_md5(passphrase, salt, 32+16)
key = key_iv[:32]
iv = key_iv[32:]
aes = AES.new(key, AES.MODE_CBC, iv)
return base64.b64encode(b"Salted__" + salt + aes.encrypt(self.pad(message).encode()))
def decrypt(self, encrypted):
passphrase ="<secret passpharse value>".encode()
encrypted = base64.b64decode(encrypted)
assert encrypted[0:8] == b"Salted__"
salt = encrypted[8:16]
key_iv = self.bytes_to_key_md5(passphrase, salt, 32+16)
key = key_iv[:32]
iv = key_iv[32:]
aes = AES.new(key, AES.MODE_CBC, iv)
return self.unpad(aes.decrypt(encrypted[16:])).decode().strip('"')
def base64_decoding(self, encoded):
base64decode = base64.b64decode(encoded)
return base64decode.decode()
crypt = Crypt()
test = "secret message to be send over network"
encrypted_message = crypt.encrypt(test)
print("Encryp msg:", encrypted_message)
decrypted_message = crypt.decrypt(encrypted_message)
print("Decryp:", decrypted_message)
here's what I've tried so far on the frontend with React and CryptoJS:
import React from "react";
import CryptoJS from 'crypto-js';
const DecryptEncrypt = () => {
function bytesToKey(passphrase, salt, output = 48) {
if (salt.length !== 8) {
throw new Error('Salt must be 8 characters long.');
}
let data = CryptoJS.enc.Latin1.parse(passphrase + salt);
let key = CryptoJS.SHA256(data).toString(CryptoJS.enc.Latin1);
let finalKey = key;
while (finalKey.length < output) {
data = CryptoJS.enc.Latin1.parse(key + passphrase + salt);
key = CryptoJS.SHA256(data).toString(CryptoJS.enc.Latin1);
finalKey += key;
}
return finalKey.slice(0, output);
}
const decryptData = (encryptedData, key) => {
const decodedEncryptedData = atob(encryptedData);
const salt = CryptoJS.enc.Hex.parse(decodedEncryptedData.substring(8, 16));
const ciphertext = CryptoJS.enc.Hex.parse(decodedEncryptedData.substring(16));
const keyIv = bytesToKey(key, salt.toString(), 32 + 16);
const keyBytes = CryptoJS.enc.Hex.parse(keyIv.substring(0, 32));
const iv = CryptoJS.enc.Hex.parse(keyIv.substring(32));
const decrypted = CryptoJS.AES.decrypt(
{ ciphertext: ciphertext },
keyBytes,
{ iv: iv, mode: CryptoJS.mode.CBC, padding: CryptoJS.pad.Pkcs7 }
);
return decrypted.toString(CryptoJS.enc.Utf8);
};
const encryptData = (data, key) => {
const salt = CryptoJS.lib.WordArray.random(8); // Generate random salt
const keyIv = bytesToKey(key, salt.toString(), 32 + 16);
const keyBytes = CryptoJS.enc.Hex.parse(keyIv.substring(0, 32));
const iv = CryptoJS.enc.Hex.parse(keyIv.substring(32));
const encrypted = CryptoJS.AES.encrypt(data, keyBytes, {
iv: iv,
mode: CryptoJS.mode.CBC,
padding: CryptoJS.pad.Pkcs7
});
const ciphertext = encrypted.ciphertext.toString(CryptoJS.enc.Hex);
const saltedCiphertext = "Salted__" + salt.toString(CryptoJS.enc.Hex) + ciphertext;
return btoa(saltedCiphertext);
};
const dataToEncrypt = 'Data to be sent over network';
const encryptionKey = "<secret passpharse value>";
const encryptedData = encryptData(dataToEncrypt, encryptionKey);
console.log("Encrypted data:", encryptedData);
const decryptedData = decryptData(encryptedData, encryptionKey);
console.log("Decrypted data:", decryptedData);
return (<>
Check
</>);
}
export default DecryptEncrypt;
I'm encountering some issues in ensuring compatibility between frontend and backedn. Specifically, I'm struggling with properly deriving the key and IV, and encrypting/decrypting the data in a way that matches the backend implementation. Getting error as below when i try to send encrypted text to backend where it throws following error while decrypting,
packages\Crypto\Cipher\_mode_cbc.py", line 246, in decrypt
raise ValueError("Data must be padded to %d byte boundary in CBC mode" % self.block_size)
ValueError: Data must be padded to 16 byte boundary in CBC mode
I m bit new to implementing AES in a fullstack app, so learning and trying but still stuck with this issue. Could someone who has encountered similar issue or implemented encryption/decryption in JavaScript offer some guidance or suggestions on how to modify my frontend code to achieve compatibility with the backend?
It's much easier than you think.
The Python code performs an OpenSSL compliant encryption/decryption:
EVP_BytesToKey()
compatible. It uses MD5 as digest, an iteration count of 1 and an 8 bytes salt.Salted__
, followed by the 8 bytes of salt, followed by the actual ciphertext.CryptoJS is OpenSSL compatible (s. here) and supports the above encryption/decryption by default. All you need to do is pass the passphrase as string (s. here).
Note that when using bytes_to_key()
instead of bytes_to_key_md5()
in the Python code, SHA256 must be explicitly specified as digest on the CryptoJS side, as MD5 is the default.
CryptoJS sample code:
// MD5 sample
var ciphertextFromPython = "U2FsdGVkX18lJwVCQIbRWqiIycIZg4LRZFHq+ORvygkE/umH1Il3m/yzgu3n9jVQhUikwXeURBW9yAjMawTk3A==";
var passphrase = "<secret passpharse value>";
var decrypted = CryptoJS.AES.decrypt(ciphertextFromPython, passphrase);
console.log(decrypted.toString(CryptoJS.enc.Utf8));
var plaintext = "secret message to be send over network";
var passphrase = "<secret passpharse value>";
var ciphertextForPython = CryptoJS.AES.encrypt(plaintext, passphrase);
console.log(ciphertextForPython.toString()); // e.g. U2FsdGVkX18/aYM99XaqbT/GjFDAuNlGBMd2Wd7Vuum120DkmeItS7tJndPLbxDyNzEUBF28AOG5pOwLGvpSSA==
// SHA-256 sample
CryptoJS.algo.EvpKDF.cfg.hasher = CryptoJS.algo.SHA256.create();
var ciphertextFromPython = "U2FsdGVkX189ft5ncnmOK/rJIB2fkdrfdWQCbf6DgbXkWMXw7yjX2oRXbDgZTIt4LibWBPamalnKCZl3l1VnWQ==";
var passphrase = "<secret passpharse value>";
var decrypted = CryptoJS.AES.decrypt(ciphertextFromPython, passphrase);
console.log(decrypted.toString(CryptoJS.enc.Utf8));
var plaintext = "secret message to be send over network";
var passphrase = "<secret passpharse value>";
var ciphertextForPython = CryptoJS.AES.encrypt(plaintext, passphrase);
console.log(ciphertextForPython.toString()); // e.g. U2FsdGVkX188W7G1Xis9KZogKpVCvCVbDQHc1AIul+CSTjS8m+zdc4pPQ9jlunIP4jbTD49q82GV9ic/4HVNNA==
<script src="https://cdnjs.cloudflare.com/ajax/libs/crypto-js/4.2.0/crypto-js.min.js"></script>
In the CryptoJS code above, ciphertextFromPython
was generated with the posted Python code and ciphertextForPython
can be decrypted with the posted Python code.
The first example applies MD5 as digest (by default) and correponds to the key derivation function bytes_to_key_md5()
, the second applies SHA256 (explicitly specified) and correponds to bytes_to_key()
.
Security:
The key derivation function EVP_BytesToKey()
, especially in combination with the broken digest MD5 and an iteration count of 1, is considered insecure. Instead, a reliable key derivation function should be applied (at least PBKDF2, which is also supported by CryptoJS).
Note that CryptoJS has been discontinued. The last version is 4.2.0.