"Unsupported state or unable to authenticate data" with aes-128-gcm in Node - node.js

I'm trying to implement encrypt/decrypt functions using aes-128-gcm as provided by node crypto. From my understanding, gcm encrypts the ciphertext but also hashes it and provides this as an 'authentication tag'. However, I keep getting the error: "Unsupported state or unable to authenticate data".
I'm not sure if this is an error in my code - looking at the encrypted ciphertext and auth tag, the one being fetched by the decrypt function is the same as the one produced by the encrypt function.
function encrypt(plaintext) {
// IV is being generated for each encryption
var iv = crypto.randomBytes(12),
cipher = crypto.createCipheriv(aes,key,iv),
encryptedData = cipher.update(plaintext),
tag;
// Cipher.final has been called, so no more encryption/updates can take place
encryptedData += cipher.final();
// Auth tag must be generated after cipher.final()
tag = cipher.getAuthTag();
return encryptedData + "$$" + tag.toString('hex') + "$$" + iv.toString('hex');
}
function decrypt(ciphertext) {
var cipherSplit = ciphertext.split("$$"),
text = cipherSplit[0],
tag = Buffer.from(cipherSplit[1], 'hex'),
iv = Buffer.from(cipherSplit[2], 'hex'),
decipher = crypto.createDecipheriv(aes,key,iv);
decipher.setAuthTag(tag);
var decryptedData = decipher.update(text);
decryptedData += decipher.final();
}
The error is being thrown by decipher.final().

In case if someone still tries to get a working example of encryption and decryption process.
I've left some comments that should be taken into consideration.
import * as crypto from 'crypto';
const textToEncode = 'some secret text'; // utf-8
const algo = 'aes-128-gcm';
// Key bytes length depends on algorithm being used:
// 'aes-128-gcm' = 16 bytes
// 'aes-192-gcm' = 24 bytes
// 'aes-256-gcm' = 32 bytes
const key = crypto.randomBytes(16);
const iv = crypto.randomBytes(16);
const cipher = crypto.createCipheriv(algo, key, iv);
const encrypted = Buffer.concat([
cipher.update(Buffer.from(textToEncode, 'utf-8')),
cipher.final(),
]);
const authTag = cipher.getAuthTag();
console.info('Value encrypted', {
valueToEncrypt: textToEncode,
encryptedValue: encrypted.toString('hex'),
authTag: authTag.toString('hex'),
});
// It's important to use the same authTag and IV that were used during encoding
const decipher = crypto.createDecipheriv(algo, key, iv);
decipher.setAuthTag(authTag);
const decrypted = Buffer.concat([
decipher.update(encrypted),
decipher.final(),
]);
console.info('Value decrypted', {
valueToDecrypt: encrypted.toString('hex'),
decryptedValue: decrypted.toString('utf-8'),
});

I managed to fix this: the issue was that I wasn't specifying an encoding type for cipher.final() and I was returning it within a String, so it wasn't returning a Buffer object, which decipher.final() was expecting.
To fix, I add 'utf-8' to 'hex' encoding parameters within my cipher.update and cipher.final, and vice versa in decipher.
Edited to add code example - note this is from 2018, so may be outdated now.
function encrypt(plaintext) {
// IV is being generated for each encryption
var iv = crypto.randomBytes(12),
cipher = crypto.createCipheriv(aes,key,iv),
encryptedData = cipher.update(plaintext, 'utf-8', 'hex'),
tag;
// Cipher.final has been called, so no more encryption/updates can take place
encryptedData += cipher.final('hex');
// Auth tag must be generated after cipher.final()
tag = cipher.getAuthTag();
return encryptedData + "$$" + tag.toString('hex') + "$$" + iv.toString('hex');
}
function decrypt(ciphertext) {
var cipherSplit = ciphertext.split("$$"),
text = cipherSplit[0],
tag = Buffer.from(cipherSplit[1], 'hex'),
iv = Buffer.from(cipherSplit[2], 'hex'),
decipher = crypto.createDecipheriv(aes, key, iv);
decipher.setAuthTag(tag);
var decryptedData = decipher.update(text, 'hex', 'utf-8');
decryptedData += decipher.final('utf-8');
}

Related

NodeJS AESCFB + pkcs7 padding decryption

I'm trying to port the following Go functions to nodeJS using crypt or crypt-js but i'm having issues trying to figure out what's wrong:
The Go encryption code is available at https://go.dev/play/p/O88Bslwd-qh ( both encrypt and decrypt work)
The current nodejs implementation is:
var decryptKey= "93D87FF936DAB334C2B3CC771C9DC833B517920683C63971AA36EBC3F2A83C24";
const crypto = require('crypto');
const algorithm = 'aes-256-cfb';
const BLOCK_SIZE = 16;
var message = "8a0f6b165236391ac081f5c614265b280f84df882fb6ee14dd8b0f7020962fdd"
function encryptText(keyStr, text) {
const hash = crypto.createHash('sha256');
//Decode hex key
keyStr = Buffer.from(keyStr, "hex")
hash.update(keyStr);
const keyBytes = hash.digest();
const iv = crypto.randomBytes(BLOCK_SIZE);
const cipher = crypto.createCipheriv(algorithm, keyBytes, iv);
cipher.setAutoPadding(true);
let enc = [iv, cipher.update(text,'latin1')];
enc.push(cipher.final());
return Buffer.concat(enc).toString('hex');
}
function decryptText(keyStr, text) {
const hash = crypto.createHash('sha256');
//Decode hex key
keyStr = Buffer.from(keyStr, "hex")
hash.update(keyStr);
const keyBytes = hash.digest();
const contents = Buffer.from(text, 'hex');
const iv = contents.slice(0, BLOCK_SIZE);
const textBytes = contents.slice(BLOCK_SIZE);
const decipher = crypto.createDecipheriv(algorithm, keyBytes, iv);
decipher.setAutoPadding(true);
let res = decipher.update(textBytes,'latin1');
res += decipher.final('latin1');
return res;
}
console.log(message)
result = decryptText(decryptKey,message);
console.log(result);
message = encryptText(decryptKey,'hola').toString();
console.log(message)
result = decryptText(decryptKey,message);
console.log(result);
Any idea why it is not working as expected?
Note: I know that padding is not required with cfb but i can't modify the encryption code, it just for reference.
I don't know Go or the specifics of aes.NewCipher(key), but from its documentation it doesn't look like it's hashing the key in any way. The Go code you're linking to also doesn't hash it, so I'm not sure why you're hashing it in the Node.js code.
This should be sufficient:
function encryptText(keyStr, text) {
const keyBytes = Buffer.from(keyStr, "hex")
…
}
function decryptText(keyStr, text) {
const keyBytes = Buffer.from(keyStr, 'hex');
…
}
As an aside: it looks like you may be encrypting JSON blocks with these functions. If so, I would suggest not using any encoding (like latin1) during the encryption/decryption process, given that JSON text must be encoded using UTF-8.

Decrypting AES-256-CBC Cipher using Node.js

I'm trying to decrypt an AES-256-CBC ciphertext from a web service using Node.js but can't seem to make it work and honestly lost.
const raw = `{
"iv":"uUwGJgxslfYiahji3+e2jA==",
"docMimeType":"text\/xml",
"doc":"1XLjWZlMMrgcpR6QtfwExQSOOPag1BJZTo1QEkcDrY6PFesWoVw8xrbHFsEYyMVDeemzk+5kBnb3\r\nqBmcUtkSFs7zDsxjYZkkEU9nyq1jXFz99fGylIealw37FPMaK0gviXESRO5AHMs46tpgSQcuWX0Z\r\nV7+mnTvjmaRHi4p1Cvg8aYfDO1aIWWWjAwOTCyopyCwribbGoEdiYDc5pERHpw=="
}`;
const cleanedEncryptedDataAsJsonStr = raw.replace(/\r?\n|\r/g, " ")
const data = JSON.parse(cleanedEncryptedDataAsJsonStr)
const ivBase64 = data.iv
const iv = Buffer.from(ivBase64, 'base64').toString('hex').substring(0, 16)
const plainKey = 'PHilheaLthDuMmyciPHerKeyS'
const hashKey = crypto.createHash('sha256');
hashKey.update(plainKey)
const key = hashKey.digest('hex').substring(0, 32)
let encrypted = Buffer.from(data.doc, 'base64').toString('hex')
const decipher = crypto.createDecipheriv('aes-256-cbc', key, iv)
let decrypted = decipher.update(encrypted, 'hex', 'utf-8')
decrypted += decipher.final('utf-8')
so I'm receiving a json object which contains the iv and the encrypted data(doc), and I have a copy of the cipher key which according to the docs needs to be hashed during the decryption.
doc: base64 encoding of the encrypted data
iv: base64 encoded value of initialization vector during encryption
When I run my code, the error is:
Error: error:0606506D:digital envelope routines:EVP_DecryptFinal_ex:wrong final block length
and also not sure on how to handle \r\n in the raw json string.
The decrypted message should be:
<eEMPLOYERS ASOF="07-02-2022"><employer pPEN="110474000002" pEmployerName="JOSE A TERAMOTO ODM" pEmployerAddress="ORANBO, PASIG CITY"/></eEMPLOYERS
There are the following issues in the code:
IV and key must not be hex encoded.
The default PKCS#7 padding must be disabled since Zero padding was applied during encryption (if desired, explicitly remove the trailing 0x00 padding bytes).
Fixed code:
var crypto = require('crypto')
const raw = `{
"iv":"uUwGJgxslfYiahji3+e2jA==",
"docMimeType":"text\/xml",
"doc":"1XLjWZlMMrgcpR6QtfwExQSOOPag1BJZTo1QEkcDrY6PFesWoVw8xrbHFsEYyMVDeemzk+5kBnb3\r\nqBmcUtkSFs7zDsxjYZkkEU9nyq1jXFz99fGylIealw37FPMaK0gviXESRO5AHMs46tpgSQcuWX0Z\r\nV7+mnTvjmaRHi4p1Cvg8aYfDO1aIWWWjAwOTCyopyCwribbGoEdiYDc5pERHpw=="
}`;
const cleanedEncryptedDataAsJsonStr = raw.replace(/\r?\n|\r/g, " ")
const data = JSON.parse(cleanedEncryptedDataAsJsonStr)
const ivBase64 = data.iv
const iv = Buffer.from(ivBase64, 'base64') // .toString('hex').substring(0, 16) // Fix 1: no hex encoding
const plainKey = 'PHilheaLthDuMmyciPHerKeyS'
const hashKey = crypto.createHash('sha256')
hashKey.update(plainKey)
const key = hashKey.digest() // .digest('hex').substring(0, 32) // Fix 2: no hex encoding
let encrypted = Buffer.from(data.doc, 'base64').toString('hex')
const decipher = crypto.createDecipheriv('aes-256-cbc', key, iv)
decipher.setAutoPadding(false) // Fix 3: disable default PKCS#7 padding
let decrypted = decipher.update(encrypted, 'hex', 'utf-8')
decrypted += decipher.final('utf-8')
console.log(decrypted) // plaintext zero-padded, if necessary remove trailing 0x00 values, e.g. as in the following:
console.log(decrypted.replace(new RegExp("\0+$"), "")) // <eEMPLOYERS ESOF="07-02-2022"><employer pPEN="110474000002" pEmployerName="JOSE A TERAMOTO ODM" pEmployerAddress="ORANBO, PASIG CITY"/></eEMPLOYERS>

Nodejs AES-256-GCM decrypt the encrypted client message by webcrypto api

I've Encrypted my text by a key in Client by AES-256-GCM algorithm and I can decrypt it in Client, But when I send it to the Backend which has a SharedKey(the same as the Client has), it can decrypt the message by AES-256-CTR algorithm(I used this algo because the AES-256-GCM in Nodejs needs authTag that I don't create it in Client and iv is the only thing I have).
When I decrypt the message on the Backend side, it works with no error, but the result is not what I encrypted in the Client
Here is what I wrote:
Client:
async function encrypt(text: string) {
const encodedText = new TextEncoder().encode(text);
const aesKey = await generateAesKey();
const iv = window.crypto.getRandomValues(
new Uint8Array(SERVER_ENCRYPTION_IV_LENGTH)
);
const encrypted = await window.crypto.subtle.encrypt(
{
name: 'AES-GCM',
iv,
},
aesKey,
encodedText
);
const concatenatedData = new Uint8Array(
iv.byteLength + encrypted.byteLength
);
concatenatedData.set(iv);
concatenatedData.set(new Uint8Array(encrypted), iv.byteLength);
return arrayBufferToBase64(concatenatedData),
}
Backend:
export function decrypt(sharedKey: string, message: string) {
const messageBuffer = new Uint8Array(base64ToArrayBuffer(message));
const iv = messageBuffer.subarray(0, 16);
const data = messageBuffer.subarray(16);
const decipher = crypto.createDecipheriv(
'aes-256-ctr',
Buffer.from(sharedKey, 'base64'),
iv
);
const decrypted =
decipher.update(data, 'binary', 'hex') + decipher.final('hex');
return Buffer.from(decrypted, 'hex').toString('base64');
}
Sample usage:
const encrypted = encrypt("Hi Everybody");
// send the encrypted message to the server
// Response is: Ô\tp\x8F\x03$\f\x91m\x8B B\x1CkQPQ=\x85\x97\x8AêsÌG0¸Ê
Since GCM is based on CTR, decryption with CTR is in principle also possible. However, this should generally not be done in practice, since it skips the authentication of the ciphertext, which is the added value of GCM over CTR.
The correct way is to decrypt on the NodeJS side with GCM and properly consider the authentication tag.
The authentication tag is automatically appended to the ciphertext by the WebCrypto API, while the crypto module of NodeJS handles ciphertext and tag separately. Therefore, not only the nonce but also the authentication tag must be separated on the NodeJS side.
The following JavaScript/WebCrypto code demonstrates the encryption:
(async () => {
var nonce = crypto.getRandomValues(new Uint8Array(12));
var plaintext = 'The quick brown fox jumps over the lazy dog';
var plaintextEncoded = new TextEncoder().encode(plaintext);
var aesKey = base64ToArrayBuffer('a068Sk+PXECrysAIN+fEGDzMQ3xlpWgE1bWXHVLb0AQ=');
var aesCryptoKey = await crypto.subtle.importKey('raw', aesKey, 'AES-GCM', true, ['encrypt', 'decrypt']);
var ciphertextTag = await crypto.subtle.encrypt({name: 'AES-GCM', iv: nonce}, aesCryptoKey, plaintextEncoded);
ciphertextTag = new Uint8Array(ciphertextTag);
var nonceCiphertextTag = new Uint8Array(nonce.length + ciphertextTag.length);
nonceCiphertextTag.set(nonce);
nonceCiphertextTag.set(ciphertextTag, nonce.length);
nonceCiphertextTag = arrayBufferToBase64(nonceCiphertextTag.buffer);
document.getElementById("nonceCiphertextTag").innerHTML = nonceCiphertextTag; // ihAdhr6595oyQ3koj52cnZp7VeB1fzWuY1v7vqFdSQGxK0VQxIXUegB1mVG4rC5Aymij7bQ9rmnFWbpo7C2znN4ROnnChB0=
})();
// Helper
// https://stackoverflow.com/a/9458996/9014097
function arrayBufferToBase64(buffer){
var binary = '';
var bytes = new Uint8Array(buffer);
var len = bytes.byteLength;
for (var i = 0; i < len; i++) {
binary += String.fromCharCode(bytes[i]);
}
return window.btoa(binary);
}
// https://stackoverflow.com/a/21797381/9014097
function base64ToArrayBuffer(base64) {
var binary_string = window.atob(base64);
var len = binary_string.length;
var bytes = new Uint8Array(len);
for (var i = 0; i < len; i++) {
bytes[i] = binary_string.charCodeAt(i);
}
return bytes.buffer;
}
<p style="font-family:'Courier New', monospace;" id="nonceCiphertextTag"></p>
This code is basically the same as your code, with some changes needed because of methods you didn't post like generateAesKey() or arrayBufferToBase64().
Example output:
ihAdhr6595oyQ3koj52cnZp7VeB1fzWuY1v7vqFdSQGxK0VQxIXUegB1mVG4rC5Aymij7bQ9rmnFWbpo7C2znN4ROnnChB0=
The following NodeJS/crypto code demonstrates the decryption. Note the tag separation and explicit passing with setAuthTag():
var crypto = require('crypto');
function decrypt(key, nonceCiphertextTag) {
key = Buffer.from(key, 'base64');
nonceCiphertextTag = Buffer.from(nonceCiphertextTag, 'base64');
var nonce = nonceCiphertextTag.slice(0, 12);
var ciphertext = nonceCiphertextTag.slice(12, -16);
var tag = nonceCiphertextTag.slice(-16); // Separate tag!
var decipher = crypto.createDecipheriv('aes-256-gcm', key, nonce);
decipher.setAuthTag(tag); // Set tag!
var decrypted = decipher.update(ciphertext, '', 'utf8') + decipher.final('utf8');
return decrypted;
}
var nonceCiphertextTag = 'ihAdhr6595oyQ3koj52cnZp7VeB1fzWuY1v7vqFdSQGxK0VQxIXUegB1mVG4rC5Aymij7bQ9rmnFWbpo7C2znN4ROnnChB0=';
var key = 'a068Sk+PXECrysAIN+fEGDzMQ3xlpWgE1bWXHVLb0AQ=';
var decrypted = decrypt(key, nonceCiphertextTag);
console.log(decrypted);
Output:
The quick brown fox jumps over the lazy dog
For completeness: Decryption of a GCM ciphertext with CTR is also possible by appending 4 bytes to the 12 bytes nonce (0x00000002). For other nonce sizes the relation is more complex, see e.g. Relationship between AES GCM and AES CTR. However, as already said, this should not be done in practice, since it bypasses the authentication of the ciphertext and is thus insecure.

node.js randomBytes() issue - use crypto to encrypt and decrypt data

I'm learning how to use the crypto module of node.js to encrypt and decrypt data. I've readed this article and this other article, they will explain how to do the same thing but in a different way. I've read in the first article that there is an issue with randomBytes() but it's dated 2017 and maybe it's foxed in node 15. Since I want to pass a custom user password for the encryption key, can I use the sha256 to hash the password and pass it for encryption? Something like
import crypto from 'crypto'
ipcMain.on('encryptMessage', (event, data) => {
let iv = crypto.randomBytes(16).toString('hex'); //I'm not sure if the string conversion is needed?
let key = crypto.createHash('sha256')
.update(data.password)
.digest(); // If I understand digest will provide an human readable hash?
//encryption code here
});
Will be this ok for the encryption process, the password will be ok if hashed to be lenght almost 32 char?
Below you find a simple program that does a complete encryption - decryption using AES in CBC mode with a 32 byte long key derived from a passphrase with PBKDF2.
As the program is used within a cross platform project the output looks strange and you can adjust it to your needs. It runs on Node 12 without any problems, see it running in an online compiler here: https://repl.it/#javacrypto/CpcNodeJsCryptoAesCbc256Pbkdf2StringEncryptionFull#index.js
The issue in the linked article was a Node 5->6 migration issue.
Security warning: the program has no exception handling and should be used for educational purpose only.
This is the output:
AES CBC 256 String encryption with PBKDF2 derived key
plaintext: The quick brown fox jumps over the lazy dog
* * * Encryption * * *
ciphertext (Base64): VUH/MSEjdMOeLySYoVbQaaA30EVioZ9CXUXUDX69LG0=:kkz49Z23b0Q+D9LX1FHzaA==:VDWsYRpRmC5i2OyiMlBQLIzw2cW2Kdp2kxZKNzh/5vNcbCdMdEGInDoT7VIUCzzj
output is (Base64) salt : (Base64) iv : (Base64) ciphertext
* * * Decryption * * *
ciphertext (Base64): VUH/MSEjdMOeLySYoVbQaaA30EVioZ9CXUXUDX69LG0=:kkz49Z23b0Q+D9LX1FHzaA==:VDWsYRpRmC5i2OyiMlBQLIzw2cW2Kdp2kxZKNzh/5vNcbCdMdEGInDoT7VIUCzzj
input is (Base64) salt : (Base64) iv : (Base64) ciphertext
plaintext: The quick brown fox jumps over the lazy dog
code:
var crypto = require('crypto');
console.log('AES CBC 256 String encryption with PBKDF2 derived key');
var plaintext = 'The quick brown fox jumps over the lazy dog';
console.log('plaintext: ', plaintext);
var password = "secret password";
console.log('\n* * * Encryption * * *');
var ciphertextBase64 = aesCbcPbkdf2EncryptToBase64(password, plaintext);
console.log('ciphertext (Base64): ' + ciphertextBase64);
console.log('output is (Base64) salt : (Base64) iv : (Base64) ciphertext');
console.log('\n* * * Decryption * * *');
var ciphertextDecryptionBase64 = ciphertextBase64;
console.log('ciphertext (Base64): ', ciphertextDecryptionBase64);
console.log('input is (Base64) salt : (Base64) iv : (Base64) ciphertext');
var decryptedtext = aesCbcPbkdf2DecryptFromBase64(password, ciphertextBase64);
console.log('plaintext: ', decryptedtext);
function aesCbcPbkdf2EncryptToBase64(password, data) {
var PBKDF2_ITERATIONS = 15000;
var salt = generateSalt32Byte();
var key = crypto.pbkdf2Sync(password, salt, PBKDF2_ITERATIONS, 32, 'sha256');
var iv = generateRandomInitvector();
const cipher = crypto.createCipheriv('aes-256-cbc', key, iv);
let encryptedBase64 = '';
cipher.setEncoding('base64');
cipher.on('data', (chunk) => encryptedBase64 += chunk);
cipher.on('end', () => {
// do nothing console.log(encryptedBase64);
// Prints: some clear text data
});
cipher.write(data);
cipher.end();
var saltBase64 = base64Encoding(salt);
var ivBase64 = base64Encoding(iv);
return saltBase64 + ':' + ivBase64 + ':' + encryptedBase64;
}
function aesCbcPbkdf2DecryptFromBase64(password, data) {
var PBKDF2_ITERATIONS = 15000;
var dataSplit = data.split(":");
var salt = base64Decoding(dataSplit[0]);
var key = crypto.pbkdf2Sync(password, salt, PBKDF2_ITERATIONS, 32, 'sha256');
var iv = base64Decoding(dataSplit[1]);
var ciphertext = dataSplit[2];
const decipher = crypto.createDecipheriv('aes-256-cbc', key, iv);
let decrypted = '';
decipher.on('readable', () => {
while (null !== (chunk = decipher.read())) {
decrypted += chunk.toString('utf8');
}
});
decipher.on('end', () => {
// do nothing console.log(decrypted);
});
decipher.write(ciphertext, 'base64');
decipher.end();
return decrypted;
}
function generateSalt32Byte() {
return crypto.randomBytes(32);
}
function generateRandomInitvector() {
return crypto.randomBytes(16);
}
function base64Encoding(input) {
return input.toString('base64');
}
function base64Decoding(input) {
return Buffer.from(input, 'base64')
}

Getting error of Invalid IV Length while using aes-256-cbc for encryption in node

Code Sample is as follows:
var crypto = require('crypto');
var key = 'ExchangePasswordPasswordExchange';
var plaintext = '150.01';
var iv = new Buffer(crypto.randomBytes(16))
ivstring = iv.toString('hex');
var cipher = crypto.createCipheriv('aes-256-cbc', key, ivstring)
var decipher = crypto.createDecipheriv('aes-256-cbc', key,ivstring);
cipher.update(plaintext, 'utf8', 'base64');
var encryptedPassword = cipher.final('base64');
Getting error of invalid IV length.
From https://github.com/nodejs/node/issues/6696#issuecomment-218575039 -
The default string encoding used by the crypto module changed in
v6.0.0 from binary to utf8. So your binary string is being interpreted
as utf8 and is most likely becoming larger than 16 bytes during that
conversion process (rather than smaller than 16 bytes) due to invalid
utf8 character bytes being added.
Modifying your code so that ivstring is always 16 characters in length should solve your issue.
var ivstring = iv.toString('hex').slice(0, 16);
The above answer adds more overhead than needed, since you converted each byte to a hexidecimal representation that requires twice as many bytes all you need to do is generate half the number of bytes
var crypto = require('crypto');
var key = 'ExchangePasswordPasswordExchange';
var plaintext = '150.01';
var iv = new Buffer(crypto.randomBytes(8))
ivstring = iv.toString('hex');
var cipher = crypto.createCipheriv('aes-256-cbc', key, ivstring)
var decipher = crypto.createDecipheriv('aes-256-cbc', key,ivstring);
cipher.update(plaintext, 'utf8', 'base64');
var encryptedPassword = cipher.final('base64');
In Node.js 10 I had to use a 12 bytes string for it to work... const iv = crypto.pseudoRandomBytes(6).toString('hex');. 16 bytes gave me an error. I had this problem when I was running Node.js 10 globally, and then uploading it to a Cloud Functions server with Node.js 8. Since Cloud Functions have Node.js 10 in beta, I just switched to that and now it works with the 12 bytes string. It didn't even work with a 16 bytes string on Node.js 8 on the Cloud Functions server...
When you really need Key/Iv from legacy Crypto
In case of cypher aes-256-cbc, required length for Key and IV is 32 Bytes and 16 Bytes.
You can calculate Key length by dividing 256 bits by 8 bits, equals 32 bytes.
Following GetUnsafeKeyIvSync(password) uses exactly same behavior as previous crypto did in old days.
There is no salt, and single iteration with MD5 digest, so anyone can generate exactly same Key and Iv.
This is why deprecated.
However, you may still need to use this approach only if your encrypted data is stored and cannot be changed(or upgraded.).
Do NOT use this function for new project.
This is provided only for who cannot upgrade previously encrypted data for other reason.
import * as crypto from 'node:crypto';
import { Buffer } from 'node:buffer';
export function GetUnsafeKeyIvSync(password) {
try {
const key1hash = crypto.createHash('MD5').update(password, 'utf8');
const key2hash = crypto.createHash('MD5').update(key1hash.copy().digest('binary') + password, 'binary');
const ivhash = crypto.createHash('MD5').update(key2hash.copy().digest('binary') + password, 'binary');
const Key = Buffer.from(key1hash.digest('hex') + key2hash.digest('hex'), 'hex');
const IV = Buffer.from(ivhash.digest('hex'), 'hex');
return { Key, IV };
}
catch (error) {
console.error(error);
}
}
export function DecryptSync(data, KeyIv) {
let decrypted;
try {
const decipher = crypto.createDecipheriv('aes-256-cbc', KeyIv.Key, KeyIv.IV);
decrypted = decipher.update(data, 'hex', 'utf8');
decrypted += decipher.final('utf8');
}
catch (error) {
console.error(error);
decrypted = '';
}
return decrypted;
}
export function EncryptSync(data, KeyIv) {
let encrypted;
try {
const cipher = crypto.createCipheriv('aes-256-cbc', KeyIv.Key, KeyIv.IV);
encrypted = cipher.update(data, 'utf8', 'hex');
encrypted += cipher.final('hex');
}
catch (error) {
console.error(error);
encrypted = '';
}
return encrypted;
}
For testing,
export function Test() {
const password = 'my plain text password which is free from length requirment';
const data = 'this is data to be encrypted and decrypted';
// Use same logic to retrieve as legacy crypto did.
// It is unsafe because there is no salt and single iteration.
// Anyone can generate exactly same Key/Iv with the same password.
// We would only need to use this only if stored encrypted data must be decrypted from previous result.
// Do NOT use this for new project.
const KeyIv = GetUnsafeKeyIvSync(password);
// Key is in binary format, for human reading, converted to hex, but Hex string is not passed to Cypher.
// Length of Key is 32 bytes, for aes-256-cbc
console.log(`Key=${KeyIv.Key.toString('hex')}`);
// Key is in binary format , for human reading, converted to hex, but Hex string is not passed to Cypher.
// Length of IV is 16 bytes, for aes-256-cbc
console.log(`IV=${KeyIv.IV.toString('hex')}`);
const encrypted = EncryptSync(data, KeyIv);
console.log(`enc=${encrypted}`);
const decrypted = DecryptSync(encrypted, KeyIv);
console.log(`dec=${decrypted}`);
console.log(`Equals ${decrypted === data}`);
return decrypted === data;
}

Resources