HMAC secret key length and Node's crypto library - node.js

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'));

Related

Invalid length key or iv in node.js crypto

I know this topic have a couple answer, but I have problem about details key length.
I would like to encryption data in AES algorithm with CTR. The encryption will have length of 256 bits.
If I do encryption data by key length 256 and IV length 16, I get a error Invalid key length. I thought IV must be the same key length. I change length of IV to 256, but I get an error Invalid iv length. I found out IV must be 16 bits long.
My code works only if my key is 16 bits and my IV is 16 bits long. So my code didn't encrypt with length of 256 bits.
My code
const crypto = require('crypto');
let key = crypto.randomBytes(16).toString('hex'); // Key is static
let iv = crypto.randomBytes(16);
let cipher = crypto.createCipheriv('aes-256-ctr', process.env.KEY, iv);
let encrypte = cipher.update("Example_data", 'utf-8', 'hex');
encrypte += cipher.final('hex');
What should I do to encrypt data with a key length of 256 bits?
First of all: AES uses a 16 bytes IV (corresponding to the AES block size) and a 16, 24 or 32 bytes key. In the posted code aes-256-ctr is specified, so a 256 bits = 32 bytes key is needed.
And this is exactly the size of the key applied, not 16 bytes as you assume, which is why the code is executed in the first place (at least if the undefined process.env.KEY is replaced by key): crypto.randomBytes(16) creates a 16 bytes buffer and toString('hex') converts it to a hex string, doubling the size to 32 bytes (since 1 byte in the buffer is represented by 2 characters (and thus 2 bytes) in the string). So technically, with UTF-8 encoding (the default for crypto.createCipheriv()), this results in a 32 bytes key which can be used for aes-256-ctr. However, each byte in this key can only take 16 values (corresponding to the digits 0-9 and a-f), which corresponds to 1632 = 25616 = 2128 possible values and thus not 256, but only 128 bit security strength.
You prevent the reduction of the security strength by using directly the binary data for the key, so crypto.randomBytes(32). Here each byte can take 256 different values, which corresponds for a 32 bytes key to 25632 = 2256 possible values and thus a 256 bit security strength. If the key is hex encoded, it must not be UTF8 encoded, but hex decoded.
If the key is derived e.g. using a key derivation function, the hex encoding can cause another problem across platforms. If one platform applies uppercase letters for hex encoding and the other lowercase letters, different keys will result.
Long story short: Avoid using the UTF8 encoded data of a hex string for a key (or at least be aware of the implications).

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.

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

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.

Resources