Node.js Crypto AES-128 encrypt stream with padding - node.js

I am successfully using NodeJS to encrypt a file in the following manner:
let cipher = crypto.createCipheriv('aes-128-cbc', key, iv),
crypted = cipher.update(file_data, 'binary');
return Buffer.concat([crypted, cipher.final()]);
This generates a file with a hash that exactly matches our target encryption, which is server-side OpenSSL command:
openssl aes-128-cbc -e -in file -out file.enc -nosalt -iv <iv_str> -K <k_str>
The goal is to upgrade our systems to use nodejs streams. Node documentation and other posts suggest the following way to encrypt streams:
cipher = crypto.createCipheriv('aes-128-cbc', key, iv);
let file_stream = fs.createReadStream(local_file, 'binary'),
write_stream = fs.createWriteStream(`${local_file}.enc`, 'binary');
file_stream.pipe(cipher).pipe(write_stream);
however this ignores the cipher.final() appending. Without it the encryption always results in a file with the wrong hash. I've tried:
creating a Transform that would append the remaining padding. That doesn't work because you can't call final on a cipher that hasn't had update called on it
setting the autopadding with setAutoPadding. In all cases that results in the error Error: error:0607F08A:digital envelope routines:EVP_EncryptFinal_ex:data not multiple of block length
How can I cipher with streams in the same way I was originally encrypting?

Related

Trying to symmetrically encrypt a value for storage in the client (httpOnly cookie) and having an issue decrypting

I am trying to encrypt a value on my server with a private key to store it on the client within an httpOnly cookie.
I am having trouble with the encryption/decryption lifecycle
function encrypt(input) {
const encryptedData = crypto.privateEncrypt(
privateKey,
Buffer.from(input)
)
return encryptedData.toString('base64')
}
function decrypt(input) {
const decryptedData = crypto.privateDecrypt(
{ key: privateKey },
Buffer.from(input, 'base64'),
)
return decryptedData.toString()
}
const enc = encrypt('something moderately secret')
const dec = decrypt(enc)
console.log(dec) // 'something moderately secret'
However the crypto.privateDecrypt function is throwing with
Error: error:04099079:rsa routines:RSA_padding_check_PKCS1_OAEP_mgf1:oaep decoding error
Side question, is it safe to reuse the same private key the server uses to sign JWTs. It's an rsa key generated using ssh-keygen -t rsa -b 4096 -m PEM -f RS256.key
So, you don't use crypto.privateEncrypt() with crypto.privateDecrypt(). That's not how they work. Those functions are for asymmetric encryption, not for symmetric encryption. You use either of these two pairs:
crypto.publicEncrypt() ==> crypto.privateDescrypt()
crypto.privateEncrypt() ==> crypto.publicDecrypt()
So, that's why you're getting the error you're getting. The nodejs doc for crypto.privateDecript() says this:
Decrypts buffer with privateKey. buffer was previously encrypted using the corresponding public key, for example using crypto.publicEncrypt().
If what you really want is symmetric encryption, there are a bunch of options in the crypto module for that. There are some examples shown here: https://www.section.io/engineering-education/data-encryption-and-decryption-in-node-js-using-crypto/ and https://fireship.io/lessons/node-crypto-examples/#symmetric-encryption-in-nodejs.

error:0409A06E:rsa routines data too large for key size

I'm generating a SAML response and it needs to be encrypted and signed with public and private keys. I generated private.pem and public.pem in the terminal with the commands
openssl genrsa -out private.pem 2048
openssl rsa -in ./private.pem -pubout -out public.pem
Then in nodeJS.
encrypt: function(message) {
return new Promise(function (resolve, reject) {
var publicKey = require("fs").readFileSync(__dirname + "/public.pem", "utf8");
var encrypted = require("crypto").publicEncrypt(publicKey, new Buffer(message));
resolve(encrypted.toString("base64"));
});
},
Once I call the message encrypt(xml), I get the following error
{
library: 'rsa routines',
function: 'RSA_padding_add_PKCS1_OAEP_mgf1',
reason: 'data too large for key size',
code: 'ERR_OSSL_RSA_DATA_TOO_LARGE_FOR_KEY_SIZE'
}
Objective:
I've to sign the message as per the demo here samltools.com (Mode: SignMessage), my SAML message looks like this. (see SAML Response section).
Sign the message
Base64Encode the message
The problem here is that you cannot directly encrypted with RSA, a piece of data which is larger than the key size.
Surprising I know, it surprised me too.
In reality very little payload data is encrypted directly with RSA or even elliptic curves.
You should be using RSA Diffie-Hellman to generate a shared secret.
Signature of the file, is really signature of the hash of the file.

Is there a way to create an AES cipher from a hashed key-secret?

Having lost the plaintext secret, but having the hashed key, it's posible to encript and decript in openssl like so (keys and iv are not the actual ones):
Input:
printf "ciphertext" | base64 -d | openssl enc -aes-256-cbc -d -nosalt -K "0000000000000000000000000000000000000000000000000000000000000000" -iv "00"
Output:
"plaintext"
Now, to be able to do this in a NodeJs application, openssl is called as child_process. As you can guess, spawning openssl calls is not very performant.
To be able to do it in node crypto, the plaintext "secret" is needed in creating the keys.
Is there a way yo generate the cipher from the hashed key?
I have tried doing it like so with no succes.
var crypto=require('crypto')
var iv = Buffer.alloc(16, 0);
var key = '0000000000000000000000000000000000000000000000000000000000000000'
var cipher=crypto.createDecipher('aes-256-cbc', newBuffer(key).toString('binary'), new Buffer('0000000000000000', 'hex').toString('binary'));
var enc = cipher.update("ciphertext", 'base64', 'utf8')
enc += cipher.final('utf8')
console.log(enc);
Output:
internal/crypto/cipher.js:164
const ret = this._handle.final();
^
Error: error:06065064:digital envelope routines:EVP_DecryptFinal_ex:bad decrypt
Try
new Buffer('0000000000000000000000000000000000000000000000000000000000000000', 'hex')
for the key and and
new Buffer('00000000000000000000000000000000', 'hex')
for the IV. Currently you are encoding to a binary string representation of the bytes, instead of (just) decoding the hexadecimal values to 32 bytes and 16 bytes respectively.
To use this you should use createDecipheriv as createDecipher will still generate the key using the password.

Decrypt a node.js aes-256-cbc string in openssl

I have the following example code that generates an encrypted value in node.js using the aes-256-cbc algorithm, a fixed key, and an iv in node.js.
const crypto = require('crypto');
const iv = '0f9387b3f94bf06f';
const key = 'ZTk0YTU5YjNhMjk4NGI3NmIxNWExNzdi';
const encrypt = (value) => {
const cipher = crypto.createCipheriv('AES-256-CBC', key, iv);
let encrypted = cipher.update(value, 'utf8', 'hex');
encrypted += cipher.final('hex');
return encrypted;
};
console.log('Encrypted value: ', encrypt('HelloWorld'));
The encrypted value that is created by the above script, encrypted the string HelloWorld is the following hex value:
0c491f8c5256b9744550688fc54926e8
In bash and using openssl what script can I execute with the given encrypted value above to give me a decrypted value of HeloWorld?
I have spent lots of time struggling on this, using openssl, various options and accompanying commands but am struggling to generate the same output.
Any gurus out there who can help?
According to Node.js documentation, the function crypto.createCipher() doesn't use salt. You then need to explicitly say it to openssl.
Moreover the function you use doesn't expect IV. If you would require IV, then use crypto.createCipheriv().
So the openssl command is the following:
$ echo dd64232d380669d1a09378e1dc9de762 | xxd -ps -r | openssl enc -d -aes-256-cbc -k ZTk0YTU5YjNhMjk4NGI3NmIxNWExNzdi -nosalt
HelloWorld
Note that I'm using xxd to convert the string into bytes. You could use hexdump, od or other...
In response of the last OP's change, here the correct openssl command.
Obviously if you specify explicitly the IV, you don't need the salt and need to the key in bytes format (option -K)
echo 0c491f8c5256b9744550688fc54926e8 | xxd -ps -r | openssl enc -d -aes-256-cbc -iv 30663933383762336639346266303666 -K 5a546b3059545535596a4e684d6a6b344e4749334e6d49784e5745784e7a6469

What's wrong with nodejs crypto decipher?

I have the following encrypted data:
U2FsdGVkX1+21O5RB08bavFTq7Yq/gChmXrO3f00tvJaT55A5pPvqw0zFVnHSW1o
The pass to decrypt it is: password
(it's the example from gibberish-aes)
In the command line using openssl:
echo "U2FsdGVkX1+21O5RB08bavFTq7Yq/gChmXrO3f00tvJaT55A5pPvqw0zFVnHSW1o" | openssl enc -d -aes-256-cbc -a -k password
The output is:
Made with Gibberish\n
With my NodeJS application:
var decipher = crypto.createDecipher('aes-256-cbc', "password");
var dec = decipher.update("U2FsdGVkX1+21O5RB08bavFTq7Yq/gChmXrO3f00tvJaT55A5pPvqw0zFVnHSW1o",
'base64', 'utf8');
dec += decipher.final('utf8');
I have the following error TypeError: DecipherFinal fail at the decipher.final line.
Am I missing something ? Thanks.
The encrypted data starts with a 8 byte "magic" indicating that there is a salt (the ASCII encoding of "Salted__"). Then the next 8 bytes is the salt. Now the bad news: Node.js does not seem to use the salt for the EVP_BytesToKey method:
int key_len = EVP_BytesToKey(cipher, EVP_md5(), NULL,
(unsigned char*) key_buf, key_buf_len, 1, key, iv);
That NULL is the salt.
This has been verified using a Java test application (using the right salt) - the result string was returned.
Please leave out the salt using the OpenSSL -nosalt switch and try again.
[EXAMPLE]
OpenSSL CLI:
openssl enc -aes-256-cbc -nosalt -a -k password
owlstead
Mh5yxIyZH+fSMTkSgkLa5w==
NodeJS crypto:
var crypto=require('crypto')
var cipher=crypto.createDecipher('aes-256-cbc', "password")
var enc = cipher.update("Mh5yxIyZH+fSMTkSgkLa5w==", 'base64', 'utf8')
enc += cipher.final('utf8')
[LATE EDIT] Note that using secret key derivation with a salt and large work factor may be paramount to security. You'd better use a very unique, high entropy password otherwise your encrypted data may be at risk.
[REALLY LATE EDIT] OpenSSL 1.1.0c changed the digest algorithm used in some internal components. Formerly, MD5 was used, and 1.1.0 switched to SHA256. Be careful the change is not affecting you in both EVP_BytesToKey and commands like openssl enc.

Resources