Node.js crypto key and iv to match java SecretKeySpec / IvParameterSpec - node.js

I'm trying to to port a Java (simple) encryption algorythm to Node JS. I will need to be able to decrypt/encrypt stuff encrypted/decrypted from the Java side.
I'm stuck at the very beginning, the initialization of the cipher.
In Java, I get the key with SecretKeySpec, and the Initialization Vector with IvParameterSpec:
public CryptStuff(String password) throws zillion_exceptions {
if (password==null) throw new InvalidKeyException("No encryption password is set!");
key = new SecretKeySpec(password.getBytes("UTF-8"), "AES");
cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
ivSpec=new IvParameterSpec(new byte[cipher.getBlockSize()]);
cipher.init(Cipher.ENCRYPT_MODE, key, ivSpec);
}
NodeJS requires a Key Buffer and an IV buffer, however, I don't know how to calculate them from scratch:
var mCrypto = require('crypto'),
key=[0,0,0,0,0,0,.......],
iv=[0,0,0,0,0,.........];
function init (password) {
// generate key from password
// generate IV from blocksize?
var aesCipher = mCrypto.createCipheriv("aes-????????", (new Buffer(key)), (new Buffer(iv)));
.
.
.
}
Also, what's the matching algorithm string for AES/CBC/PKCS5Padding?

Assuming you have the same password string as in the Java code, you can create a key buffer like this in node:
var key = new Buffer(password, "utf8");
Since you're using a zero filled IV (bad!) in Java, this is the equivalent code in node:
var iv = new Buffer(16); // 16 byte buffer with random data
iv.fill(0); // fill with zeros
Since you're using CBC mode in Java, you have to do the same in node. Note that you have to select the correct key size when selecting the cipher string depending on your "password" length:
var aesCipher = mCrypto.createCipheriv("aes-128-cbc", key, iv);
// or
var aesCipher = mCrypto.createCipheriv("aes-192-cbc", key, iv);
// or
var aesCipher = mCrypto.createCipheriv("aes-256-cbc", key, iv);
Node will automatically apply PKCS#7 padding which is the same as PKCS#5 padding for AES.
A password is not a key!
A password has usually not the appropriate length to be used as a key (valid lengths are 16 byte, 24 byte and 32 byte for AES) and it is comprised of only printable characters which might make it easier for an attacker to brute force the key.
What you would need to create a key from a password is key derivation function. Popular ones are PBKDF2, bcrypt and scrypt (with increasing cost).
Random IV!
You really should be generating a new random IV for every ciphertext that you produce. If you use a static IV, an attacker that observes your ciphertexts can determine that you sent the same or even similar messages. If you use a random IV, then the ciphertexts differ so much that an attacker cannot determine whether two different ciphertexts where created from the same plaintext or not. This is called semantic security.
The random IV itself doesn't have to be secret, so you can easily prepend it to the ciphertext and slice it off before decryption.
You can even combine this with the key derivation function (KDF). Simply generate a random salt for the KDF. A KDF is usually able to derive a variable amount of output bytes, so simply let it derive key || IV (concatenation) and then split them. Now, you only need to prepend the salt to the ciphertext.
Authentication!
Depending on your system, you might be vulnerable to attacks such as the padding oracle attack. The best defense against this is to authenticate the ciphertext. So you can either use an encrypt-then-MAC scheme with a strong MAC such as HMAC-SHA256 or an authenticated mode of operation such as GCM or EAX. Java and node both support GCM, but there is a little more work involved.

Related

How can I quickly encrypt a 128 bit value into another equal length value in NodeJS 14

My server will be sending many IDs to the browser/user, and, for that session, the user might operate using those IDs. Between users, and between multiple sessions of a single user, I need IDs to be encrypted, so they cannot be traced except within the context of a single session. Due to the number of IDs that will be used in each session, it is not reasonable to dereference or hash them and store lookup tables for each session.
The IDs are effectively UUIDs, unique 128 bit values. The server will encrypt them in the context of a session, and when the user queries using them, and the server can decrypt those values within the context of that same session. I would like for the encrypted output to also be 128 bits in length (for example, so they could be rendered as UUIDs even in their encrypted state). What is the best way for me to achieve this?
This is my sample code, demonstrating that I can encrypt 16 bytes (the size of a block), but the cipher extends it to 2 blocks, doubling the size to 32 bytes, when I finalize it. I think because it is OK for a value to be encrypted the same way twice in the context of a single session, it is acceptable to reuse the same IV for each item; so the server stores a key and IV for the session, and can encrypt and decrypt all of the IDs with those.
async function sampleCrypt() {
const algorithm = 'aes-128-cbc';
crypto.scrypt("samplePassword", "salty", 16, (err, key) => {
const iv: Buffer = crypto.randomBytes(16);
const cipher: crypto.Cipher = crypto.createCipheriv(algorithm, key, iv);
const inbuffer = Buffer.allocUnsafe(16);
inbuffer.writeUInt32BE(1960); // just some sample data
console.log(cipher.update(inbuffer, undefined, 'hex')); // loads the whole buffer in
console.log(cipher.final('hex'));
});
}
sampleCrypt();
/* Sample output:
83134f7dc2f9b175bd70a7dd0512eaf7
9495e0cfceab0439fddc92f3fffa48c2
*/
Please advise if I have made any incorrect assumptions here as well. Thanks!
Block ciphers such as AES require plaintexts whose lengths are an integer multiple of the blocksize (16 bytes for AES). If this isn't the case, padding must be used. NodeJS applies PKCS7 padding by default. Here a complete padding block is appended if the plaintext length is already an integer multiple of the blocksize. This is the reason why in your case a 16 bytes plaintext results in a 32 bytes ciphertext. But since the plaintext is always exactly one block long, there is actually no need for padding. In NodeJS the padding can be disabled with cipher.setAutoPadding(false), so in your case plaintext and ciphertext are both 16 bytes long.
A block cipher only encrypts one block. To encrypt longer plaintexts, an operation mode must be used, e.g. CBC as in the posted code. Generally, these operation modes use an initialization vector (IV) whose size is equal to the blocksize (16 bytes for AES). The IV must meet certain conditions, e.g. a key/IV pair may only be used once. Since the IV isn't secret it's usually placed before the ciphertext. In your case, this would result in a 32 bytes result (IV + ciphertext). The condition mentioned also means that the concept you use (one key/IV pair for all encryptions) is inherently insecure.
An operating mode that doesn't require an IV is ECB. ECB generates the same ciphertext for the same plaintext, which generallay allows conclusions from the ciphertext to the plaintext. This problem doesn't exist for a mode with an IV. Therefore ECB is more insecure compared to a mode with an IV. However, the severity of this insecurity ultimately depends on the characteristics of the plaintext and the particular application, and the respective requirements determine whether this disadvantage is tolerable or not. 1-block plaintexts containing a GUID are less vulnerable in this respect than multi-block plaintexts with some message content, so ECB may be an option here.
With disabled padding and ECB mode a 16 bytes plaintext results in a 16 bytes ciphertext, as the following TypeScript code demonstrates:
import * as crypto from "crypto";
const algorithm:string = 'aes-128-ecb';
const key:Buffer = crypto.randomBytes(16);
// Encryption
const plaintextEnc:Buffer = Buffer.from('0123456789012345');
const cipherEnc:crypto.Cipher = crypto.createCipheriv(algorithm, key, null);
cipherEnc.setAutoPadding(false);
const ciphertext:Buffer = Buffer.concat([cipherEnc.update(plaintextEnc), cipherEnc.final()]);
console.log(ciphertext.toString('hex'));
// Decryption
const cipherDec:crypto.Decipher = crypto.createDecipheriv(algorithm, key, null);
cipherDec.setAutoPadding(false);
const plaintextDec:Buffer = Buffer.concat([cipherDec.update(ciphertext), cipherDec.final()]);
console.log(plaintextDec.toString('hex'));
If the limitation to 16 bytes is dropped, GCM would be a recommendable mode that provides besides confidentiality also authenticity and integrity. GCM uses a 12 bytes IV (nonce) and generates a tag (typically 16 bytes) that is used for authentication. In your case, the result (IV + ciphertext + tag) would have a length of 44 bytes. Note that if a key/IV pair is used more than once for GCM, security is lost.

Crypto.decipher.final for 'aes-256-cbc' algorithm with invalid key fails with bad decrypt

I am able to use use node.js Crypto module to encrypt and decrypt a message using Cipher and Decipher classes with 'aes-256-cbc' algorithm like so:
var crypto = require('crypto');
var cipherKey = crypto.randomBytes(32); // aes-256 => key length is 256 bits => 32 bytes
var cipherIV = crypto.randomBytes(16); // aes block size = initialization vector size = 128 bits => 16 bytes
var cipher = crypto.createCipheriv('aes-256-cbc', cipherKey, cipherIV);
var message = 'Hello world';
var encrypted = cipher.update(message, 'utf8', 'hex') + cipher.final('hex');
console.log('Encrypted \'' + message + '\' as \'' + encrypted + '\' with key \''+ cipherKey.toString('hex') + '\' and IV \'' + cipherIV.toString('hex') + '\'');
// Outputs: Encrypted 'Hello world' as '2b8559ce4227c3c3c200ea126cb50957' with key '50f7a656cfa3c4f90796a972b2f6eedf41b589da705fdec95b9d25c180c16cf0' and IV '6b28c13d63af14cf05059a2a2caf370c'
var decipher = crypto.createDecipheriv('aes-256-cbc', cipherKey, cipherIV);
var decrypted = decipher.update(encrypted, 'hex', 'utf8') + decipher.final('utf8');
console.log('Decrypted \'' + encrypted + '\' as \'' + decrypted + '\' with key \''+ cipherKey.toString('hex') + '\' and IV \'' + cipherIV.toString('hex') + '\'');
// Outputs: Decrypted '2b8559ce4227c3c3c200ea126cb50957' as 'Hello world' with key '50f7a656cfa3c4f90796a972b2f6eedf41b589da705fdec95b9d25c180c16cf0' and IV '6b28c13d63af14cf05059a2a2caf370c'
However when I try to decrypt the message using a wrong key to, perhaps naively, demonstrate an attacker will not be able decrypt the message unless the key is known, I get Error: error:06065064:digital envelope routines:EVP_DecryptFinal_ex:bad decrypt at Decipheriv.final (internal/crypto/cipher.js:164:28):
var differentCipherKey = crypto.randomBytes(32);
var decipherDifferentKey = crypto.createDecipheriv('aes-256-cbc', differentCipherKey, cipherIV);
decrypted = decipherDifferentKey.update(encrypted, 'hex', 'utf8') + decipherDifferentKey.final('utf8');
What was I was hoping to get is unintelligible text. bad decrypt was featured in other SO questions either regarding openssl version mismatch between encrypting and decrypting or too-short initialization vector in the same case but I believe my case is a different scenario. Does AES somehow known that encrypted text was generated with a different key?
Tested on node v12.13.0 on Windows 10 and also in repl.it running v10.16.0.
EDIT:
As suggested in the answers the issue was with default padding, in order to see unintelligible output one needs to disable auto-padding on both cipher and deciphers and pad manually:
var requirePadding = 16 - Buffer.byteLength(message, 'utf8');
var paddedMessage = Buffer.alloc(requirePadding, 0).toString('utf8') + message;
cipher.setAutoPadding(false)
Full example here
Another answer has correctly identified the issue as a padding problem. I might summarize the issue like so:
Block ciphers can only operate on data that has a length that is a multiple of the cipher's block size. (AES has a block size of 128 bits.)
In order to make variously-sized inputs conform to the block size, the library adds padding. This padding has a particular format (For example, when adding padding of length N, repeat the value N for the last N bytes of the input.)
When decrypting, the library checks that correct padding exists. Since your badly-decrypted data is arbitrary noise, it is very unlikely to have a valid pad.
You may turn this check off with decipher.setAutoPadding(false) before you do update. However, note that this will include the padding in your decrypted output. Here is a modified repl.it instance that uses setAutoPadding.
The CBC mode requires padding, you did not define one, but the library applied one for you as default. The default is PKCS7Padding which supports from 1 to up to 256 bytes of the block size.
Each padding has a specific format so that it can be uniquely removed from the decrypted text without ambiguity. For example, if the plaintext missing two characters to match the block size, 16-byte in AES, then the PKCS7 padding adds 0202 (in hex) indicating that 2 characters are added and each has value as the number of added characters. If 5 missing 0505050505, etc. In the below xy is a byte.
xyxyxyxyxyxyxyxyxyxyxyxyxyxyxy01
xyxyxyxyxyxyxyxyxyxyxyxyxyxy0202
xyxyxyxyxyxyxyxyxyxyxyxyxy030303
...
xyxy0E0E0E0E0E0E0E0E0E0E0E0E0E0E
xy0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F
and if the last block is a full block, a new block completely filled with padding
xyxyxyxyxyxyxyxyxyxyxyxyxyxyxyxy 10101010101010101010101010101010
After the decryption, firstly the padding is checked. if the padding has not a correct format as specified in rfc 2315, then one can say that there is a padding error.
In this case, while decrypting the library checks the padding and warns you about this. To prevent the padding oracle attacks you don't get an incorrect padding warning. You get a bad decrypt.
The library knows that the key results with a valid padding or not, nothing more. There may be more than one key that results with valid padding even with a negligible probability where integrity is helpful.
In modern Cryptography, we don't use CBC mode anymore. We prefer Authenticated Encryption (AE) modes like AES-GCM or ChaCha20-Poly1305. AE modes provide confidentiality, integrity, and authentication in a bundle.
THE Galois Counter Mode (GCM), internally uses CTR mode in which there is no padding therefore they are free from padding oracle attacks.

How to encrypt in Node.js?

I'm attempting to perform encryption in Node.js 7.5.0 using the 'des-cbc' algorithm. According to RFC 1423, this algorithm requires a 64-bit cryptographic key, and a 64-bit initialization vector.
I'm trying to use a key and iv composed of 8 Latin-1 characters; however, Node is saying, "Error: Invalid IV length". Here's some example code:
let crypto = require('crypto');
let key = '\xb1\xb2\xb3\xb4\xb5\xb6\xb7\xb8',
iv = '\xb8\xb7\xb6\xb5\xb4\xb3\xb2\xb1';
let cipher = crypto.createCipheriv('des-cbc', Buffer.from(key), Buffer.from(iv));
If I change the iv to 8 ASCII characters, then Node is saying, "Error: Invalid key length":
let crypto = require('crypto');
let key = '\xb1\xb2\xb3\xb4\xb5\xb6\xb7\xb8',
iv = 'abcdefgh';
let cipher = crypto.createCipheriv('des-cbc', Buffer.from(key), Buffer.from(iv));
But if both the key and iv are 8 ASCII characters, it works:
let crypto = require('crypto');
let key = 'hgfedcba',
iv = 'abcdefgh';
let cipher = crypto.createCipheriv('des-cbc', Buffer.from(key), Buffer.from(iv));
Why can't the Latin-1 characters be used for key and iv?
Solution
You should either use
Buffer.from('\xb1\xb2\xb3\xb4\xb5\xb6\xb7\xb8', 'binary')
or even more cleaner as Maarten Bodewes points out
Buffer.from([0xb1, 0xb2, 0xb3, 0xb4, 0xb5, 0xb6, 0xb7, 0xb8'])
The same goes for your IV.
Reason
Buffer.from('\xb1\xb2\xb3\xb4\xb5\xb6\xb7\xb8').length results in 16 which is not a valid length for a DES key. DES expects exactly 64 bits or 8 bytes as a key. That is also why DES is very insecure. The key size is simply too small.
The reason the above Buffer has a size of 16 bytes instead of 8 bytes is that the default encoding is UTF-8. If a code point is larger than 127 (decimal) or 0x7F (hexadecimal) it will encode into at least two bytes instead of one. Each and every code point (character) of your key is larger than 0x7F. So, each of them is encoded into two bytes.
Things to think about
Don't use DES nowadays. It only provides 56 bit of security. AES would be a much better, because it's more secure with the lowest key size of 128 bit. There is also a practical limit on the maximum ciphertext size with DES. See Security comparison of 3DES and AES.
The IV must be unpredictable (read: random). Don't use a static IV, because that makes the cipher deterministic and therefore not semantically secure. An attacker who observes ciphertexts can determine when the same message prefix was sent before. The IV is not secret, so you can send it along with the ciphertext. Usually, it is simply prepended to the ciphertext and sliced off before decryption.
The key is supposed to be indistinguishable from random noise. It is best to just generate it randomly and use it in your code in encoded form.
It is better to authenticate your ciphertexts so that attacks like a padding oracle attack are not possible. This can be done with authenticated modes like GCM or EAX, or with an encrypt-then-MAC scheme.

HMAC secret key length and Node's crypto library

I'm trying to use HMAC SHA256 for JWT. I've read that I should have 256 bits in my key. However, Node's crypto library accepts a string as a key. Does it convert each character to its unicode number? If I just use single-byte utf8 characters, will 32 characters create a 256-bit key (256/8=32)? Since Javascript uses utf8 strings and utf8 pads each byte with a 0, won't there be a 0 every 8th bit?
How do I ensure that my secret key has 256 bits?
Here's what I have:
let hmac = crypto.createHmac('sha256', SECRET);
hmac.update(payload);
let signature = toUrlSafe(hmac.digest('base64'));

Is this kind of encryption "safe"?

I must first say I have never studied cryptography, and everything I know on this topic is just basic notions.
We were looking at a fast and easy way to encrypt some data (to be stored into a database) using a password.
I know the "safest" algorithm is AES, but it's probably too complicated for us and I know it requires us to obtain authorizations from the US government, etc.
We thought about this (simple) algorithm, which reminds me (but I may be wrong) a sort of "One time pad".
(it's not written in any specific language... it's just the idea :) )
// The string we need to encrypt
string data = "hello world";
// Long string of random bytes that will be generated the first time we need to encrypt something
string randomData = "aajdfskjefafdsgsdewrbhf";
// The passphrase the user selected
string passphrase = "foo";
// Let's generate the encryption key, using randomData XOR passphrase (repeating this one)
string theKey = "";
j = 0;
for(i = 0; i < randomData.length; i++)
{
theKey += randomData[i] ^ passphrase[j];
j++;
if(j == passphrase.length) j = 0;
}
// Encrypt the data, using data XOR theKey (with theKey.length >= data.length)
string encryptedData = "";
for(i = 0; i < data.length; i++)
{
encryptedData += data[i] ^ theKey[i];
}
On disk, we will store then only randomData and encryptedData.
passphrase will be asked to the user every time.
How safe will an algorithm like this be?
Except with a brute force, are there other ways this could be cracked? I don't think statistical analysis will work on this, does it?
Is it "as safe as" a One Time Pad?
Thank you!
You can just import an AES library and let it do all the heavy work. Authorizations from the US government? It is a public function, and the US government also uses it.
No, this is not secure.
If the random data is stored alongside the encrypted data, then it is simply equivalent to XORing with the passphrase: this is because the attacker can simply XOR the encrypted data with the random data, and obtain plaintext XOR passphrase as the result.
This is extremely weak. Statistical analysis would crack it in the blink of an eye. Some diligent pen-and-paper guesswork would probably crack it pretty quickly too.
The only exception would be if (1) randomData was taken from a truly crypto-strength source, (2) randomData was at least as long as your plaintext data, (3) randomData was never, ever re-used for a different message, and (4) you got rid of passphrase altogether and treated randomData as your key. In that case you'd have what amounts to a one-time pad.
No, it isn't safe. Using xor with random data and password this way is completely wrong.
A one time pad cryptograpy needs the random data to be the same length as the data to be encrypted.

Resources