Encrypt/Decrypt between Postgres and Node - node.js

Problem: We have to encrypt a certain column on a certain table (Postgres). It has to be decryptable in SQL queries and in our nodejs/sequelize application layer. The encryption can happen in either layer, but it must be decodable from either.
The issue I'm running into (and I'm sure it's user error) is that if I encrypt in the db I can only decrypt in the db, and the same for node.
I've tried using PGP_SYM_ENCRYPT and ENCRYPT in postgres and crypto and crypto-js/aes in node. I've gotten it to the point where it's decrypting without an error, but returns gibberish.
A few things I've tried so far (test key is thirtytwocharsthirtytwocharsplus):
set() {
this.setDataValue('field', seq.cast(seq.fn('PGP_SYM_ENCRYPT', val,
config.AES_KEY), 'text'))
}
This properly writes the field such that PGP_SYM_DECRYPT will decrypt it, but there's (apparently?) no way to tell Sequelize to wrap the field name with a function call so it's a lot of extra js that I feel is avoidable
const decipher = crypto.createDecipher('aes256', config.AES_KEY)
decipher.setAutoPadding(false);
return decipher.update(new Buffer(this.getDataValue('field', 'binary'), 'binary', 'ascii')) + decipher.final('ascii')
This will decode the field but returns gibberish (�Mq��8Ya�b) instead of the value (test)
aes.encrypt('test', config.AES_KEY)
aes.decrypt(field, config.AES_KEY).toString(CryptoJS.enc.Utf8)
This encrypts fine, decrypts fine, but Postgres errors when trying to decrypt (using either PGP_SYM_DECRYPT or DECRYPT). Casting the resulting field to ::TEXT and pasting it into an online AES Decrypter returns the expected value.
I really want to avoid having to add a bunch of boilerplate to our node repositories/queries, and I really feel like this should work. Using the same crypto algorithm should yield the same results
Any nudge or pointer would be greatly appreciated

Postgres has rather unclear documentation about the raw encryption functions. After a few tries and failures, I managed to replicate most of logic logic in nodejs.
Here is the program I used.
const crypto = require('crypto');
const iv = Buffer.alloc(16); // zeroed-out iv
function encrypt(plainText, algorithm, key) {
const cipher = crypto.createCipheriv(algorithm, key, iv);
let encrypted = cipher.update(plainText, 'utf8', 'base64');
encrypted += cipher.final('base64');
return encrypted;
}
function decrypt(encrypted, algorithm, key) {
const decrypt = crypto.createDecipheriv(algorithm, key, iv);
let text = decrypt.update(encrypted, 'base64', 'utf8');
text += decrypt.final('utf8')
return text;
}
const originalText = "hello world";
const userKey = 'abcd'
const algorithm = 'aes-128-cbc';
const paddedKey = Buffer.concat([Buffer.from(userKey), Buffer.alloc(12)]); // make it 128 bits key
const hw = encrypt(originalText, algorithm, paddedKey);
console.log("original", originalText);
console.log("encrypted:", hw);
console.log("decoded: ", decrypt(hw, algorithm, paddedKey).toString());
Also here is a list of things not documented for the raw functions of postgres:
the key will be auto padded to match one of the 3 lengths: 128 bits, 192 bits, 256 bits
algorithm is automatically upgraded, when key length exceeds the limit. e.g. if key exceeds 128bits, aes-192-cbc will be used to encrypt
if key exceeds 256 bits, it will be truncated to 256 bits.
It would be easier to replicate it in application language (either Javascript or Java), if Postgres has proper documentation of these functions.

Ok, I got it working, hopefully properly
What I did was:
Encrypting with crypto.createCipheriv('aes-256-cbc', new Buffer(config.AES_KEY), iv) in node, encrypt_iv in pgsql and storing as hex in the db, and decrypting with crypto.createDecipheriv/decrypt_iv into text/utf8
I don't know what part I was missing, but between specifying aes256, using the iv methods, and flipping juggling hex/text it seems to be working.
👍

Related

Why createCipheriv and createDecipheriv are not working in separate functions - crypto

I am starting to use crypto module for my NodeJS project.
My code is simple as below:
const { createCipheriv, randomBytes, createDecipheriv } = require('crypto');
const key = randomBytes(32);
const iv = randomBytes(16);
const cipher = createCipheriv('aes256', key, iv);
const decipher = createDecipheriv('aes256', key, iv);
const cipherTest = (message) => {
const encryptedMessage = cipher.update(message, 'utf8', 'hex') + cipher.final('hex');
const decryptedMessage = decipher.update(encryptedMessage, 'hex', 'utf-8') + decipher.final('utf8');
return decryptedMessage.toString('utf-8')
}
The cipherTest function returns the same result as input. Correct!
However, If I create the cipher and decipher functions separately as below , It can not decipher the encryptedMessage.
const encryptMessage = (message) => {
const encryptedMessage = cipher.update(message, 'utf8', 'hex') + cipher.final('hex');
return encryptedMessage;
}
const decryptMessage = (encryptedMessage) => {
const decryptedMessage = decipher.update(encryptedMessage, 'hex', 'utf-8') + decipher.final('utf8');
return decryptedMessage;
}
Could someone have a look? I am so thankful for that.
Ciphers are algorithms, but they contain state. Furthermore, they usually require an IV as input, which needs to be randomized for CBC which is used in this case. That IV needs to change for each ciphertext, assuming that the key remains the same.
As such, you should generally keep to the following structure in pseudo-code:
MessageEncryptor {
Key key
constructor(Key key) {
if (key = bad format) {
throw error // fail fast
this.key = key
}
String encryptMessage(String message) {
Cipher cipher = Cipher.Create(key)
Bytes iv = Rng.createBytes(cipher.blockSize)
cipher.setIV(iv)
Bytes encodedMessage = UTF8.encode(message)
// lower level runtimes may require padding for CBC mode
Bytes ciphertext = cipher.encrypt(encodedMessage)
ciphertext = Bytes.concat(ciphertext, cipher.final())
Bytes ciphertextMessage = Bytes.concat(iv, ciphertext)
// use HMAC here to append an authentication tag
String encodedCiphertextMessage = Base64.encode(ciphertextMessage)
return encodedCiphertextMessage
}
String decryptMessage(String encodedCiphertextMessage) {
Cipher cipher = Cipher.Create(key)
Bytes ciphertextMessage = Base64.decode(encodedCiphertextMessage)
// use HMAC here to verify an authentication tag
Bytes iv = Bytes.sub(0, cipher.blockSizeBytes)
cipher.setIV(iv)
Bytes encodedMessage = cipher.decrypt(ciphertextMessage, start = iv.Size)
encodedMessage = Bytes.concat(encodedMessage, cipher.final())
// lower level runtimes may require unpadding here
String message = UTF8.decode(encodedMessage)
return message
}
}
As you can see it is imperative that you:
only use the Cipher construct within the method bodies; these object carry state and are generally not thread-safe as they are mutable;
rely on the key as a field so that the message encryptor always uses the same key;
create a new MessageEncryptor instance in case you want to use another key;
create a new IV per message during encryption and extract it from the ciphertext message during decryption.
Notes:
if your scheme can handle bytes then there is no need to encode the ciphertextMessage;
the IV may have a different size or may not require random values for other modes such as GCM;
using an authenticated mode such as GCM is much safer, but hashing the IV + ciphertext using HMAC is also an option (use a separate key if you are unsure about possible issues for reusing the encryption key);
UTF8.encode and UTF8.decode can simply be replaced by any other codec in case your message is not a simple string (UTF8 is highly recommended for strings though);
a lot of libraries will have shortcuts for encoding and such, so your code may be shorter;
there are also pre-made container formats such as CMS or NaCL which are probably more secure and more flexible than any scheme you can come up with;
CBC, as used in this example, is vulnerable to plaintext oracle attacks including padding oracle attacks
for larger messages it makes more sense to use streaming for all encoding, decoding, encryption and decryption operations (the methods shown here have a high and unnecessary memory requirement);
create these kind of classes only for specific types of messages, and create a protocol + protocol description if you do so; most crypto API's don't need wrapper classes, it was created with this kind of flexibility on purpose;
The MessageEncryptor still is very lightweight - actually it has only one key as state (sometimes having a an additional key and an algorithm or two as well). If you are going to expand with fields that can change value or state such as a buffer then you want to create a new instance of the class each time you use it from a different thread. Otherwise you want to clear the state after each successful message encryption / decryption.
You may want to destroy the key securely once you've finished using it.

nodejs recover createCipher data with createCipheriv

I have some encrypted data in my database
I did it few years ago using crypto.createCipher
const cipher = crypto.createCipher('aes192', password);
As createCipher and createDecipher is deprecated, I would like to change to createCipheriv and createDecipheriv. The problem is that the data I have in my database are encoded without iv.
Is it possible to decode with createDecipheriv data encoded with createDecipher and to generate the same secret with createCipher and createCipheriv.
I tried setting the iv to null but not working
Thanks, because the database migration is an heavy work !
I tried setting the iv to null but not working
This is because this method didn’t allow for passing an initialization vector (IV), and instead derived the IV from the key using the OpenSSL EVP_BytesToKey derivation function, using a null salt meaning that the IV would be deterministic for a given key which is an issue for ciphers with counter mode like CTR, GCM and CCM.
Looking at your code:
const cipher = crypto.createCipher('aes192', password);
If you want to make this code backwards compatible, you need to call OpenSSL’s EVP_BytesToKey function yourself, typically through evp_bytestokey module which makes it available in JS userland.
Is it possible to decode with createDecipheriv data encoded with createDecipher and to generate the same secret with createCipher and createCipheriv.
Yes, you can. check out my example code here:
const crypto = require('crypto');
const EVP_BytesToKey = require('evp_bytestokey')
const ALGO = 'aes192';
const password = 'Your_Password_Here';
const KEY_SIZE = 24;
function decrypt_legacy_using_IV(text) {
const result = EVP_BytesToKey(
password,
null,
KEY_SIZE * 8, // byte to bit size
16
)
let decipher = crypto.createDecipheriv(ALGO, result.key, result.iv);
let decrypted = decipher.update(text, 'hex','utf8') + decipher.final('utf8');
return decrypted.toString();
}
function encrypt_legacy_using_IV(text) {
const result = EVP_BytesToKey(
password,
null,
KEY_SIZE * 8, // byte to bit size
16
)
var cipher = crypto.createCipheriv(ALGO, result.key, result.iv);
var encrypted = cipher.update(text, 'utf8', 'hex') + cipher.final('hex');
return encrypted.toString();
}
For complete running example, clone node-snippets and run node apogee-legacy-crypto-cipheriv.js.
However the reason this function is deprecated in the first place is because you shouldn’t use it, and instead use a random unpredictable IV, which requires you to change your code to something like this:
const iv = crypto.randomBytes(16)
const cipher = crypto.createCipheriv('aes192', password, iv)
Here, for AES-192 in CBC mode (aes192 being aliased to AES-192-CBC by OpenSSL), the IV size is expected to be the same as the block size, which is always 16 bytes.
In order to decrypt the message, you will need the IV as well. Typically you’d store the IV together with the message, as the important part is for the IV to not be predictable ahead of time.

Trouble with getting correct tag for 256-bit AES GCM encryption in Node.js

I need to write the reverse (encryption) of the following decryption function:
const crypto = require('crypto');
let AESDecrypt = (data, key) => {
const decoded = Buffer.from(data, 'binary');
const nonce = decoded.slice(0, 16);
const ciphertext = decoded.slice(16, decoded.length - 16);
const tag = decoded.slice(decoded.length - 16);
let decipher = crypto.createDecipheriv('aes-256-gcm', key, nonce);
decipher.setAuthTag(tag)
decipher.setAutoPadding(false);
try {
let plaintext = decipher.update(ciphertext, 'binary', 'binary');
plaintext += decipher.final('binary');
return Buffer.from(plaintext, 'binary');
} catch (ex) {
console.log('AES Decrypt Failed. Exception: ', ex);
throw ex;
}
}
This above function allows me to properly decrypt encrypted buffers following the spec:
| Nonce/IV (First 16 bytes) | Ciphertext | Authentication Tag (Last 16 bytes) |
The reason why AESDecrypt is written the way it (auth tag as the last 16 bytes) is because that is how the default standard library implementations of AES encrypts data in both Java and Go. I need to be able to bidirectionally decrypt/encrypt between Go, Java, and Node.js. The crypto library based encryption in Node.js does not put the auth tag anywhere, and it is left to the developer how they want to store it to pass to setAuthTag() during decryption. In the above code, I am baking the tag directly into the final encrypted buffer.
So the AES Encryption function I wrote needed to meet the above circumstances (without having to modify AESDecrypt since it is working properly) and I have the following code which is not working for me:
let AESEncrypt = (data, key) => {
const nonce = 'BfVsfgErXsbfiA00'; // Do not copy paste this line in production code (https://crypto.stackexchange.com/questions/26790/how-bad-it-is-using-the-same-iv-twice-with-aes-gcm)
const encoded = Buffer.from(data, 'binary');
const cipher = crypto.createCipheriv('aes-256-gcm', key, nonce);
try {
let encrypted = nonce;
encrypted += cipher.update(encoded, 'binary', 'binary')
encrypted += cipher.final('binary');
const tag = cipher.getAuthTag();
encrypted += tag;
return Buffer.from(encrypted, 'binary');
} catch (ex) {
console.log('AES Encrypt Failed. Exception: ', ex);
throw ex;
}
}
I am aware hardcoding the nonce is insecure. I have it this way to make it easier to compare properly encrypted files with my broken implementation using a binary file diff program like vbindiff
The more I looked at this in different ways, the more confounding this problem has become for me.
I am actually quite used to implementing 256-bit AES GCM based encryption/decryption, and have properly working implementations in Go and Java. Furthermore, because of certain circumstances, I had a working implementation of AES decryption in Node.js months ago.
I know this to be true because I can decrypt in Node.js, files that I encrypted in Java and Go. I put up a quick repository that contains the source code implementations of a Go server written just for this purpose and the broken Node.js code.
For easy access for people that understand Node.js, but not Go, I put up the following Go server web interface for encrypting and decrypting using the above algorithm hosted at https://go-aes.voiceit.io/. You can confirm my Node.js decrypt function works just fine by encrypting a file of your choice at https://go-aes.voiceit.io/, and decrypting the file using decrypt.js (Please look at the README for more information on how to run this if you need to confirm this works properly.)
Furthermore, I know this issue is specifically with the following lines of AESEncrypt:
const tag = cipher.getAuthTag();
encrypted += tag;
Running vbindiff against the same file encrypted in Go and Node.js, The files started showing differences only in the last 16 bytes (where the auth tag get's written). In other words, the nonce and the encrypted payload is identical in Go and Node.js.
Since the getAuthTag() is so simple, and I believe I am using it correctly, I have no idea what I could even change at this point. Hence, I have also considered the remote possibility that this is a bug in the standard library. However, I figured I'd try Stackoverflow first before posting an Github Issue as its most likely something I'm doing wrong.
I have a slightly more expanded description of the code, and proof of how I know what is working works, in the repo I set up to try to get help o solve this problem.
Thank you in advance.
Further info: Node: v14.15.4 Go: go version go1.15.6 darwin/amd64
In the NodeJS code, the ciphertext is generated as a binary string, i.e. using the binary/latin1 or ISO-8859-1 encoding. ISO-8859-1 is a single byte charset which uniquely assigns each value between 0x00 and 0xFF to a specific character, and therefore allows the conversion of arbitrary binary data into a string without corruption, see also here.
In contrast, the authentication tag is not returned as a binary string by cipher.getAuthTag(), but as a buffer.
When concatenating both parts with:
encrypted += tag;
the buffer is converted into a string implicitly using buf.toString(), which applies UTF-8 encoding by default.
Unlike ISO-8859-1, UTF-8 is a multi byte charset that defines specific byte sequences between 1 and 4 bytes in length that are assigned to characters, s. UTF-8 table. In arbitrary binary data (such as the authentication tag) there are generally byte sequences that are not defined for UTF-8 and therefore invalid. Invalid bytes are represented by the Unicode replacement character with the code point U+FFFD during conversion (see also the comment by #dave_thompson_085). This corrupts the data because the original values are lost. Thus UTF-8 encoding is not suitable for converting arbitrary binary data into a string.
During the subsequent conversion into a buffer with the single byte charset binary/latin1 with:
return Buffer.from(encrypted, 'binary');
only the last byte (0xFD) of the replacement character is taken into account.
The bytes marked in the screenshot (0xBB, 0xA7, 0xEA etc.) are all invalid UTF-8 byte sequences, s. UTF-8 table, and are therefore replaced by the NodeJS code with 0xFD, resulting in a corrupted tag.
To fix the bug, the tag must be converted with binary/latin1, i.e. consistent with the encoding of the ciphertext:
let AESEncrypt = (data, key) => {
const nonce = 'BfVsfgErXsbfiA00'; // Static IV for test purposes only
const encoded = Buffer.from(data, 'binary');
const cipher = crypto.createCipheriv('aes-256-gcm', key, nonce);
let encrypted = nonce;
encrypted += cipher.update(encoded, 'binary', 'binary');
encrypted += cipher.final('binary');
const tag = cipher.getAuthTag().toString('binary'); // Fix: Decode with binary/latin1!
encrypted += tag;
return Buffer.from(encrypted, 'binary');
}
Please note that in the update() call the input encoding (the 2nd 'binary' parameter) is ignored, since encoded is a buffer.
Alternatively, the buffers can be concatenated instead of the binary/latin1 converted strings:
let AESEncrypt_withBuffer = (data, key) => {
const nonce = 'BfVsfgErXsbfiA00'; // Static IV for test purposes only
const encoded = Buffer.from(data, 'binary');
const cipher = crypto.createCipheriv('aes-256-gcm', key, nonce);
return Buffer.concat([ // Fix: Concatenate buffers!
Buffer.from(nonce, 'binary'),
cipher.update(encoded),
cipher.final(),
cipher.getAuthTag()
]);
}
For the GCM mode, a nonce length of 12 bytes is recommended by NIST for performance and compatibility reasons, see here, chapter 5.2.1.1 and here. The Go code (via NewGCMWithNonceSize()) and the NodeJS code apply a nonce length of 16 bytes different from this.

Node.js crypto: too short encryption key

I want to encrypt a user's data with AES-256 to store it securely in my database. However, I have the problem that the key must be 32 characters long. But the passwords of my users are usually much shorter. Is there a way how I can "extend" the length of the passwords?
I also thought about the fact that human-made passwords are usually weak. So I would need some kind of function that "links" the password to the encryption key?
Here is my code, which I use to encrypt and decrypt:
const crypto = require('crypto');
const algorithm = 'aes-256-cbc';
const key; //Here I would get the password of the user
function encrypt(text) {
const iv = crypto.randomBytes(16);
let cipher = crypto.createCipheriv('aes-256-cbc', Buffer.from(key), iv);
let encrypted = cipher.update(text);
encrypted = Buffer.concat([encrypted, cipher.final()]);
return { iv: iv.toString('hex'), encryptedData: encrypted.toString('hex') };
}
function decrypt(text) {
let iv = Buffer.from(text.iv, 'hex');
let encryptedText = Buffer.from(text.encryptedData, 'hex');
let decipher = crypto.createDecipheriv('aes-256-cbc', Buffer.from(key), iv);
let decrypted = decipher.update(encryptedText);
decrypted = Buffer.concat([decrypted, decipher.final()]);
return decrypted.toString();
}
Many thanks for answers in advance.
Update 1.0:
After some research I have found the following code: (Source)
const crypto = require('crypto');
// Uses the PBKDF2 algorithm to stretch the string 's' to an arbitrary size,
// in a way that is completely deterministic yet impossible to guess without
// knowing the original string
function stretchString(s, outputLength) {
var salt = crypto.randomBytes(16);
return crypto.pbkdf2Sync(s, salt, 100000, outputLength, 'sha512');
}
// Stretches the password in order to generate a key (for encrypting)
// and a large salt (for hashing)
function keyFromPassword(password) {
// We need 32 bytes for the key
const keyPlusHashingSalt = stretchString(password, 32 + 16);
return {
cipherKey: keyPlusHashingSalt.slice(0, 32),
hashingSalt: keyPlusHashingSalt.slice(16)
};
}
If I got everything right, this should solve my problem: From any password I can generate a secure encryption key with a given length using the above function. The same password always generates the same encryption key with the function keyFromPassword(password), right?
Update 2.0:
Thanks to #President James K. Polk, who gave me some important tips, I have now updated my code. I hope that everything is fine now.
You should not be using your user's passwords directly as keys. Instead, you can use them as input to a key-derivation algorithm like pkbdf2.
As this paper on PKBDF2 explains:
Unfortunately, user-chosen passwords are generally short and
lack enough entropy [11], [21], [18]. For these reasons, they cannot
be directly used as a key to implement secure cryptographic systems. A possible solution to this issue is to adopt a key derivation
function (KDF), that is a function which takes a source of initial
keying material and derives from it one or more pseudorandom keys.
A library for computing pkbdf2 for Node.js, for example is found here.
Always add a salt to the text, and append it to the end of the text. The salt can be a randomly generated string of arbitrary length and can easily satisfy the 32-char length limit. In addition, adding salts before encryption strengthens the encryption.

How to derive IV and key to crypto.createCipheriv for decryption?

I have seen other questions which ask about creating the initialization vector (IV) for encryption and it seems using a random value is one option. However, I need to generate the IV for decryption, so I have to use the same one that the data was encrypted with based on some salt.
The node.js crypto function createDecipher says:
The implementation of crypto.createDecipher() derives keys using the
OpenSSL function EVP_BytesToKey with the digest algorithm set to MD5,
one iteration, and no salt.
For backwards compatibility with assets encrypted by other software, I need a different number of iterations, and a salt that I specify.
Continuing to read the documentation, it further says:
In line with OpenSSL's recommendation to use PBKDF2 instead of
EVP_BytesToKey it is recommended that developers derive a key and IV
on their own using crypto.pbkdf2() and to use crypto.createDecipheriv()
to create the Decipher object.
Ok, that sounds good. The data I need to decrypt was encrypted using EVP_BytesToKey to get the key and IV, so I need to be compatible with that, though.
Anyway, the crypto.pbkdf2 function appears to take all the parameters I need it to, but the problem is, it does not appear to create an initialization vector.
The corresponding C code which did the decryption which this needs to be compatible with looks like this:
// parameters to function:
// unsigned char *decrypt_salt
// int nrounds
// unsigned char *decrypt_key_data <- the password
// int decrypt_key_data_len <- password length
// the following is not initialized before the call to EVP_BytesToKey
unsigned char decrypt_key[32], decrypt_iv[32];
EVP_BytesToKey(EVP_aes_256_cbc(), EVP_md5(), decrypt_salt, decrypt_key_data,
decrypt_key_data_len, nrounds, decrypt_key, decrypt_iv);
My attempt to use crypto.pbkdf2 to replicate this behavior:
crypto.pbkdf2(password, salt, nrounds, 32, "md5", (err, derivedKey) => {
if (err) throw err
console.log(derivedKey.toString("hex"))
})
The derivedKey also does not match the key produced by the C code above. I'm not sure if that's even expected! I also tried key lengths of 48 and 64 but those didn't generate anything similar to the expected key and IV either.
Given the correct password, salt, and hashing rounds, how do I generate the same key and IV to decrypt with?
To start, the reason you are not getting your desired result is because the C code you have does use EVP_BytesToKey, whereas your NodeJS code uses PBKDF2. I think you may have misunderstood the recommendation of OpenSSL. They recommend PBKDF2, not as a better way to produce the same result, but as a better way to solve the problem. PBKDF2 is simply a better key derivation function, but it will not produce the same result as EVP_BytesToKey.
Further, the way you are handling your IV generation in the first place is quite poor. Using a KDF to generate your key is excellent, well done. Using a KDF to generate an IV is, frankly, quite a poor idea. Your initial readings, where you found that generating an IV randomly is a good idea, are correct. All IVs/nonces should be generated randomly. Always. The important thing to keep in mind here is that an IV is not a secret. You can pass it publicly.
Most implementations will randomly generate an IV and then prefix it to the ciphertext. Then, when it comes to decrypting, you can simply remove the first 128-bits (AES) worth of bytes and use that as the IV. This covers all your bases and means you don't have to derive your IV from the same place as the key material (which is yucky).
For further information, see the examples in this GitHub repository. I have included the NodeJS one below, which is an example of best-practice modern encryption in NodeJS:
const crypto = require("crypto");
const ALGORITHM_NAME = "aes-128-gcm";
const ALGORITHM_NONCE_SIZE = 12;
const ALGORITHM_TAG_SIZE = 16;
const ALGORITHM_KEY_SIZE = 16;
const PBKDF2_NAME = "sha256";
const PBKDF2_SALT_SIZE = 16;
const PBKDF2_ITERATIONS = 32767;
function encryptString(plaintext, password) {
// Generate a 128-bit salt using a CSPRNG.
let salt = crypto.randomBytes(PBKDF2_SALT_SIZE);
// Derive a key using PBKDF2.
let key = crypto.pbkdf2Sync(new Buffer(password, "utf8"), salt, PBKDF2_ITERATIONS, ALGORITHM_KEY_SIZE, PBKDF2_NAME);
// Encrypt and prepend salt.
let ciphertextAndNonceAndSalt = Buffer.concat([ salt, encrypt(new Buffer(plaintext, "utf8"), key) ]);
// Return as base64 string.
return ciphertextAndNonceAndSalt.toString("base64");
}
function decryptString(base64CiphertextAndNonceAndSalt, password) {
// Decode the base64.
let ciphertextAndNonceAndSalt = new Buffer(base64CiphertextAndNonceAndSalt, "base64");
// Create buffers of salt and ciphertextAndNonce.
let salt = ciphertextAndNonceAndSalt.slice(0, PBKDF2_SALT_SIZE);
let ciphertextAndNonce = ciphertextAndNonceAndSalt.slice(PBKDF2_SALT_SIZE);
// Derive the key using PBKDF2.
let key = crypto.pbkdf2Sync(new Buffer(password, "utf8"), salt, PBKDF2_ITERATIONS, ALGORITHM_KEY_SIZE, PBKDF2_NAME);
// Decrypt and return result.
return decrypt(ciphertextAndNonce, key).toString("utf8");
}
function encrypt(plaintext, key) {
// Generate a 96-bit nonce using a CSPRNG.
let nonce = crypto.randomBytes(ALGORITHM_NONCE_SIZE);
// Create the cipher instance.
let cipher = crypto.createCipheriv(ALGORITHM_NAME, key, nonce);
// Encrypt and prepend nonce.
let ciphertext = Buffer.concat([ cipher.update(plaintext), cipher.final() ]);
return Buffer.concat([ nonce, ciphertext, cipher.getAuthTag() ]);
}
function decrypt(ciphertextAndNonce, key) {
// Create buffers of nonce, ciphertext and tag.
let nonce = ciphertextAndNonce.slice(0, ALGORITHM_NONCE_SIZE);
let ciphertext = ciphertextAndNonce.slice(ALGORITHM_NONCE_SIZE, ciphertextAndNonce.length - ALGORITHM_TAG_SIZE);
let tag = ciphertextAndNonce.slice(ciphertext.length + ALGORITHM_NONCE_SIZE);
// Create the cipher instance.
let cipher = crypto.createDecipheriv(ALGORITHM_NAME, key, nonce);
// Decrypt and return result.
cipher.setAuthTag(tag);
return Buffer.concat([ cipher.update(ciphertext), cipher.final() ]);
}

Resources