How to encrypt data that needs to be decrypted in node.js?
Asked Answered
S

10

109

We are using bcrypt for hashing passwords and data that never needs to be decrypted. What should we do to protect other user information that does need to be decrypted?

For example, let's say that we didn't want a user's real name to be in plain text in case someone was to obtain access to the database. This is somewhat sensitive data but also needs to be called from time to time and displayed in plain text. Is there a simple way to do this?

Serialize answered 5/8, 2011 at 8:10 Comment(2)
The Internet is so smart and helpful. It's great when people choose to demonstrate their intelligence by insult your approach rather than answering the question or providing substantive guidance.Cryostat
You’re not encrypting your data with bcrypt, because it doesn’t do encryption. You’re hashing it. They’re not the same thing.Shoplifter
M
177

You can use the crypto module:

var crypto = require('crypto');
var assert = require('assert');

var algorithm = 'aes256'; // or any other algorithm supported by OpenSSL
var key = 'password';
var text = 'I love kittens';

var cipher = crypto.createCipher(algorithm, key);  
var encrypted = cipher.update(text, 'utf8', 'hex') + cipher.final('hex');
var decipher = crypto.createDecipher(algorithm, key);
var decrypted = decipher.update(encrypted, 'hex', 'utf8') + decipher.final('utf8');

assert.equal(decrypted, text);

Edit

Now createCipher and createDecipher is deprecated instead use createCipheriv and createDecipheriv

Mossbunker answered 5/8, 2011 at 8:42 Comment(3)
Would you recommend adding an IV to this to make it more secure? If so, how would this be done with crypto?Garrick
@Fizzix, createCipheriv requires an IV if I am correct.Empathy
Not useful anymore! Once createCipher and createDecipher are deprecated, this solution won't work. They require more parameters as IV.Outcaste
B
73

Update on 12-DEC-2019

Unlike some other modes like CBC, GCM mode does not require the IV to be unpredictable. The only requirement is that the IV has to be unique for each invocation with a given key. If it repeats once for a given key, security can be compromised. An easy way to achieve this is to use a random IV from a strong pseudo random number generator as shown below.

Using a sequence or timestamp as IV is also possible, but it may not be as trivial as it may sound. For example, if the system does not correctly keep track of the sequences already used as IV in a persistent store, an invocation may repeat an IV after a system reboot. Likewise, there is no perfect clock. Computer clocks readjusts etc.

Also, the key should be rotated after every 2^32 invocations. For further details on the IV requirement, refer to this answer and the NIST recommendations.

Update on 30-JUL-2019

As the answer is getting more views and votes, I think it is worth mentioning that the code below has used a *Sync method - crypto.scryptSync. Now that is fine if the encryption or decryption is done during application initialization. Otherwise, consider using the asynchronous version of the function to avoid blocking the event loop. (A promise library like bluebird is useful).

Update on 23-JAN-2019

The bug in decryption logic has been fixed. Thanks @AlexisWilke for rightly pointing it out.


The accepted answer is 7 years old and doesn't look secured today. Hence, I'm answering it:

  1. Encryption Algorithm: Block cipher AES with 256 bits key is considered secure enough. To encrypt a complete message, a mode needs to be selected. Authenticated encryption (which provides both confidentiality and integrity) is recommended. GCM, CCM and EAX are most commonly used authenticated encryption modes. GCM is usually preferred and it performs well in Intel architectures which provide dedicated instructions for GCM. All these three modes are CTR-based (counter-based) modes and therefore they do not need padding. As a result they are not vulnerable to padding related attacks

  2. An initialization Vector (IV) is required for GCM. The IV is not a secret. The only requirement being it has to be random or unpredictable. In NodeJs, crypto.randomBytes() is meant to produce cryptographically strong pseudo random numbers.

  3. NIST recommends 96 bit IV for GCM to promote interoperability, efficiency, and simplicity of design

  4. The recipient needs to know the IV to be able to decrypt the cipher text. Therefore the IV needs to be transferred along with the cipher text. Some implementations send the IV as AD (Associated Data) which means that the authentication tag will be calculated on both the cipher text and the IV. However, that is not required. The IV can be simply pre-pended with the cipher text because if the IV is changed during transmission due to a deliberate attack or network/file system error, the authentication tag validation will fail anyway

  5. Strings should not be used to hold the clear text message, password or the key as Strings are immutable which means we cannot clear the strings after use and they will linger in the memory. Thus a memory dump can reveal the sensitive information. For the same reason, the client calling these encryption or decryption methods should clear all the Buffer holding the message, key or the password after they are no longer needed using bufferVal.fill(0).

  6. Finally for transmission over network or storage, the cipher text should be encoded using Base64 encoding. buffer.toString('base64'); can be used to convert the Buffer into Base64 encoded string.

  7. Note that the key derivation scrypt (crypto.scryptSync()) has been used to derive a key from a password. However, this function is available only in Node 10.* and later versions

The code goes here:

const crypto = require('crypto');

var exports = module.exports = {};

const ALGORITHM = {
    
    /**
     * GCM is an authenticated encryption mode that
     * not only provides confidentiality but also 
     * provides integrity in a secured way
     * */  
    BLOCK_CIPHER: 'aes-256-gcm',

    /**
     * 128 bit auth tag is recommended for GCM
     */
    AUTH_TAG_BYTE_LEN: 16,

    /**
     * NIST recommends 96 bits or 12 bytes IV for GCM
     * to promote interoperability, efficiency, and
     * simplicity of design
     */
    IV_BYTE_LEN: 12,

    /**
     * Note: 256 (in algorithm name) is key size. 
     * Block size for AES is always 128
     */
    KEY_BYTE_LEN: 32,

    /**
     * To prevent rainbow table attacks
     * */
    SALT_BYTE_LEN: 16
}

const getIV = () => crypto.randomBytes(ALGORITHM.IV_BYTE_LEN);
exports.getRandomKey = getRandomKey = () => crypto.randomBytes(ALGORITHM.KEY_BYTE_LEN);

/**
 * To prevent rainbow table attacks
 * */
exports.getSalt = getSalt = () => crypto.randomBytes(ALGORITHM.SALT_BYTE_LEN);

/**
 * 
 * @param {Buffer} password - The password to be used for generating key
 * 
 * To be used when key needs to be generated based on password.
 * The caller of this function has the responsibility to clear 
 * the Buffer after the key generation to prevent the password 
 * from lingering in the memory
 */
exports.getKeyFromPassword = getKeyFromPassword = (password, salt) => {
    return crypto.scryptSync(password, salt, ALGORITHM.KEY_BYTE_LEN);
}

/**
 * 
 * @param {Buffer} messagetext - The clear text message to be encrypted
 * @param {Buffer} key - The key to be used for encryption
 * 
 * The caller of this function has the responsibility to clear 
 * the Buffer after the encryption to prevent the message text 
 * and the key from lingering in the memory
 */
exports.encrypt = encrypt = (messagetext, key) => {
    const iv = getIV();
    const cipher = crypto.createCipheriv(
        ALGORITHM.BLOCK_CIPHER, key, iv,
        { 'authTagLength': ALGORITHM.AUTH_TAG_BYTE_LEN });
    let encryptedMessage = cipher.update(messagetext);
    encryptedMessage = Buffer.concat([encryptedMessage, cipher.final()]);
    return Buffer.concat([iv, encryptedMessage, cipher.getAuthTag()]);
}

/**
 * 
 * @param {Buffer} ciphertext - Cipher text
 * @param {Buffer} key - The key to be used for decryption
 * 
 * The caller of this function has the responsibility to clear 
 * the Buffer after the decryption to prevent the message text 
 * and the key from lingering in the memory
 */
exports.decrypt = decrypt = (ciphertext, key) => {
    const authTag = ciphertext.slice(-16);
    const iv = ciphertext.slice(0, 12);
    const encryptedMessage = ciphertext.slice(12, -16);
    const decipher = crypto.createDecipheriv(
        ALGORITHM.BLOCK_CIPHER, key, iv,
        { 'authTagLength': ALGORITHM.AUTH_TAG_BYTE_LEN });
    decipher.setAuthTag(authTag);
    let messagetext = decipher.update(encryptedMessage);
    messagetext = Buffer.concat([messagetext, decipher.final()]);
    return messagetext;
}

And the unit tests are also provided below:

const assert = require('assert');
const cryptoUtils = require('../lib/crypto_utils');
describe('CryptoUtils', function() {
  describe('decrypt()', function() {
    it('should return the same mesage text after decryption of text encrypted with a '
     + 'randomly generated key', function() {
      let plaintext = 'my message text';
      let key = cryptoUtils.getRandomKey();
      let ciphertext = cryptoUtils.encrypt(plaintext, key);

      let decryptOutput = cryptoUtils.decrypt(ciphertext, key);

      assert.equal(decryptOutput.toString('utf8'), plaintext);
    });

    it('should return the same mesage text after decryption of text excrypted with a '
     + 'key generated from a password', function() {
      let plaintext = 'my message text';
      /**
       * Ideally the password would be read from a file and will be in a Buffer
       */
      let key = cryptoUtils.getKeyFromPassword(
              Buffer.from('mysecretpassword'), cryptoUtils.getSalt());
      let ciphertext = cryptoUtils.encrypt(plaintext, key);

      let decryptOutput = cryptoUtils.decrypt(ciphertext, key);

      assert.equal(decryptOutput.toString('utf8'), plaintext);
    });
  });
});
Billiot answered 1/12, 2018 at 17:2 Comment(15)
I know you have a test and you probably ran a special case (small input)... but it looks like you have a bug in the decrypt() as you do nothing with the output of decipher.final(). It should be concatenated, right?Dignitary
@AlexisWilke, This is fixed now. Thank you for pointing it out.Billiot
Excellent and informative answer! Can you please ellaborate on (or perhaps put into practice in your test cases) your point in number 6? How should someone use the information without storing the value in a string as you have in the test cases as well? I feel this is great for academic purposes, but wouldn't it be better to use an open source library in production? ex: ThemisDrillmaster
@MatthewSanders The tests could have been written using buffers. But still you're right. There may be scenarios where you can't avoid using strings. However, such usages should be minimal for reasons mentioned above. There is no harm in using a battle-tested library. The problem happens when we assume that a library is always correct and forget the fact that it may have bugs or issues as well. We should know the science behind and peek into the library code to appreciate the security of the library code and be able to find work arounds when we do run into bugs and issues.Billiot
I agree completely! The majority of my career has been at Startups. Wearing many hats you typically need to excel at being a generalist which implies you can't be a specialist at everything. Knowing the high level approach does indeed help if you need to dig into the implementation details for some reason. As for avoiding strings I figured you would use the buffer interface, but as I thought there are situations you may need to expose to a string. Perhaps Wasm could avoid some of the GC uncertainty in some cases. However, there are unavoidable situations when communicating with clients.Drillmaster
I am getting Error: Unsupported state or unable to authenticate data, does anyone have a fix for this?Equally
Not practical at all. As soon as the program stops running IV is lost along with all your data.Moncton
@PhilipRego IV is not needed to be stored. You don't know that because you don't have the patience to read. Moreover, even if it were required, this is meant to show the best practices of encryption and not write an entire application with persistence.Billiot
I see now you're storing the iv in the encrypted value.Moncton
In case anyone tries to port this to Typescript... there is a small problem with type interference and you will have to change one line: ` BLOCK_CIPHER: 'aes-256-gcm' as crypto.CipherCCMTypes,` This should solve your problem :)Orthochromatic
I am getting TypeError: decipher.setAuthTag is not a function, does anyone have a fix for this?Goglet
Silly me, I was using createCipheriv instead of createDecipheriv hahaGoglet
What are some best practices for rotating the key and saving it for existing encrypted records? The example here auto-creates the key but this would disappear after the unit test execution is completeCabalistic
My question isn't directly related. It's discussed here: security.stackexchange.com/questions/198324/…Cabalistic
I'm not good about this but ... Is the numbers -16, and 12 in .slice() calls can be replaced with ALGORITHM.AUTH_TAG_BYTE_LEN, and ALGORITHM.IV_BYTE_LEN?Bebel
B
34

An update to @mak answer, crypto.createCipher and crypto.createDecipher has been deprecated. Latest working code would be:

var crypto = require("crypto");
var algorithm = "aes-192-cbc"; //algorithm to use
var secret = "your-secret-key";
const key = crypto.scryptSync(secret, 'salt', 24); //create key
var text= "this is the text to be encrypted"; //text to be encrypted

const iv = crypto.randomBytes(16); // generate different ciphertext everytime
const cipher = crypto.createCipheriv(algorithm, key, iv);
var encrypted = cipher.update(text, 'utf8', 'hex') + cipher.final('hex'); // encrypted text

const decipher = crypto.createDecipheriv(algorithm, key, iv);
var decrypted = decipher.update(encrypted, 'hex', 'utf8') + decipher.final('utf8'); //deciphered text
console.log(decrypted);
Bevy answered 21/4, 2020 at 6:38 Comment(5)
Hi good day, is var text = '...' is the secret key right?Spermatium
@AljohnYamaro text refers to the plain text string that needs to be encrypted. The secret key is key generated using password as baseBevy
Great, helpful answer! I'd like to add that a random IV can be used instead of the static one. So, iv = Buffer.alloc(...) can be changed to iv = crypto.randomBytes(16), for a different hash each time, hence defending against any rainbow-table attack.Francisfrancisca
Not practical because you're not saving iv for each encrypted value.Moncton
If you are storing encrypted and iv for later decryption, you also need to store the authTag. Fetch it via authTag = cipher.getAuthTag() and apply it via decipher.setAuthTag(authTag). Without this piece, I was getting failures on decipher.final('utf8'). See: github.com/nodejs/help/issues/1034Nonessential
S
26

While this has been answered correctly, a good pattern to use the crypto library is within a class wrapper, which I have copy/pasted over the years into various projects.

const crypto = require("crypto");

class Encrypter {
  constructor(encryptionKey) {
    this.algorithm = "aes-192-cbc";
    this.key = crypto.scryptSync(encryptionKey, "salt", 24);
  }

  encrypt(clearText) {
    const iv = crypto.randomBytes(16);
    const cipher = crypto.createCipheriv(this.algorithm, this.key, iv);
    const encrypted = cipher.update(clearText, "utf8", "hex");
    return [
      encrypted + cipher.final("hex"),
      Buffer.from(iv).toString("hex"),
    ].join("|");
  }

  dencrypt(encryptedText) {
    const [encrypted, iv] = encryptedText.split("|");
    if (!iv) throw new Error("IV not found");
    const decipher = crypto.createDecipheriv(
      this.algorithm,
      this.key,
      Buffer.from(iv, "hex")
    );
    return decipher.update(encrypted, "hex", "utf8") + decipher.final("utf8");
  }
}
// Usage

const encrypter = new Encrypter("secret");

const clearText = "adventure time";
const encrypted = encrypter.encrypt(clearText);
const dencrypted = encrypter.dencrypt(encrypted);

console.log({ worked: clearText === dencrypted });
Sunnisunnite answered 4/3, 2021 at 13:51 Comment(6)
Hello Expelledboy. Im using your code, but every time I run it it generates a different result when encrypting, which makes it difficult to encrypt again when saving a password encrypted. Any suggestions? Thanks.Allopatric
@Allopatric The purpose of the IV is to randomize the generated result, this is desired. If you want to compare the generated result you need to decrypt it, else look into hashing, or the now depreciated crypto.createCipher/2 used in the top answerSunnisunnite
Hey, I am using your code but I am getting Invalid key length error.Goldshell
@Goldshell inspect your encrypted text, its likely being modified. You could also print the variable encrypted and iv in the decrypt/1 function to debugSunnisunnite
I was using aes256 and when I switched to aes-192-cbc it worked. From what I've read in the documentation I could use any method that is supported by OpenSSL. Do I have to add some more code for aes256?Goldshell
@Goldshell If you are using aes256, you likely need to change the key length specified in scryptSync to 32.Deluxe
V
6

Accepted ansewr was right but there are few changes as createCipher and createDecipher is deprecated.

In new methods createCipheriv and createDecipheriv iv value is require and iv value length must be 128 bit and key must be 256 bit.

👇 code sample

const crypto = require('crypto');
const assert = require('assert');

let algorithm = 'aes256'; // or any other algorithm supported by OpenSSL
let key = 'ExchangePasswordPasswordExchange'; // or any key from .env
let text = 'I love kittens';
let iv = crypto.randomBytes(8).toString('hex'); // or you can add static value from .env

let cipher = crypto.createCipheriv(algorithm, key, iv);  
let encrypted = cipher.update(text, 'utf8', 'hex') + cipher.final('hex');
let decipher = crypto.createDecipheriv(algorithm, key, iv);
let decrypted = decipher.update(encrypted, 'hex', 'utf8') + decipher.final('utf8');

assert.equal(decrypted, text);
Vehement answered 29/1, 2022 at 9:4 Comment(1)
Thanks for this, it helped, but the "randomBytes(8).toString('hex')" confused me. It is 16 bytes. Better just to crypto.randomBytes(16), and pass that in. If it needs to be seen as hex, you could call toString('hex') on the 16 byte buffer, and use Buffer.from('...', 'hex') to turn that back into a buffer before passing that in.Dissonant
C
4

Here's a simplified version of the answer posted by Saptarshi Basu:

Changes:

  • Explicitly import Buffer from the buffer module
  • Remove unnecessary variable declarations
  • Convert once modified let variables into const variables (or omit them altogether)
  • Convert module.exports into a single object
  • Move exports.x = x = (...) declarations to the module.exports object
  • Simplify and/or reduce documentation for the ALGORITHM object

Code:

const crypto = require("crypto");
const { Buffer } = require("buffer");

const ALGORITHM = {
  // GCM is an authenticated encryption mode that not only provides confidentiality but also provides integrity in a secured way
  BLOCK_CIPHER: "aes-256-gcm",
  // 128 bit auth tag is recommended for GCM
  AUTH_TAG_BYTE_LEN: 16,
  // NIST recommends 96 bits or 12 bytes IV for GCM to promote interoperability, efficiency, and simplicity of design
  IV_BYTE_LEN: 12,
  // NOTE: 256 (in algorithm name) is key size (block size for AES is always 128)
  KEY_BYTE_LEN: 32,
  // to prevent rainbow table attacks
  SALT_BYTE_LEN: 16
};

module.exports = {
  getRandomKey() {
    return crypto.randomBytes(ALGORITHM.KEY_BYTE_LEN);
  },

  // to prevent rainbow table attacks
  getSalt() {
    return crypto.randomBytes(ALGORITHM.SALT_BYTE_LEN);
  },

  /**
   *
   * @param {Buffer} password - The password to be used for generating key
   *
   * To be used when key needs to be generated based on password.
   * The caller of this function has the responsibility to clear
   * the Buffer after the key generation to prevent the password
   * from lingering in the memory
   */
  getKeyFromPassword(password, salt) {
    return crypto.scryptSync(password, salt, ALGORITHM.KEY_BYTE_LEN);
  },

  /**
   *
   * @param {Buffer} messagetext - The clear text message to be encrypted
   * @param {Buffer} key - The key to be used for encryption
   *
   * The caller of this function has the responsibility to clear
   * the Buffer after the encryption to prevent the message text
   * and the key from lingering in the memory
   */
  encrypt(messagetext, key) {
    const iv = crypto.randomBytes(ALGORITHM.IV_BYTE_LEN);
    const cipher = crypto.createCipheriv(ALGORITHM.BLOCK_CIPHER, key, iv, {
      authTagLength: ALGORITHM.AUTH_TAG_BYTE_LEN
    });
    let encryptedMessage = cipher.update(messagetext);
    encryptedMessage = Buffer.concat([encryptedMessage, cipher.final()]);
    return Buffer.concat([iv, encryptedMessage, cipher.getAuthTag()]);
  },

  /**
   *
   * @param {Buffer} ciphertext - Cipher text
   * @param {Buffer} key - The key to be used for decryption
   *
   * The caller of this function has the responsibility to clear
   * the Buffer after the decryption to prevent the message text
   * and the key from lingering in the memory
   */
  decrypt(ciphertext, key) {
    const authTag = ciphertext.slice(-16);
    const iv = ciphertext.slice(0, 12);
    const encryptedMessage = ciphertext.slice(12, -16);
    const decipher = crypto.createDecipheriv(ALGORITHM.BLOCK_CIPHER, key, iv, {
      authTagLength: ALGORITHM.AUTH_TAG_BYTE_LEN
    });
    decipher.setAuthTag(authTag);
    const messagetext = decipher.update(encryptedMessage);
    return Buffer.concat([messagetext, decipher.final()]);
  }
};

Keep in mind that although simplified, this code is supposed to be functionally identical to Saptarshi Basu's code.

Good luck.

Cowgirl answered 29/6, 2020 at 14:50 Comment(0)
S
1

This helped me for TypeScript and aes256 using createCipheriv. Ref.

Encrypt.ts

import * as crypto from "crypto";

export class Encrypter {
  static algorithm = "aes256";
  static key = crypto.scryptSync("<Your-Secret-Key>", "salt", 32);

  static encrypt(clearText) {
    const iv = crypto.randomBytes(16);
    try {
      const cipher = crypto.createCipheriv(
        Encrypter.algorithm,
        Encrypter.key,
        iv
      );
      const encrypted = cipher.update(clearText, "utf8", "hex");
      return [
        encrypted + cipher.final("hex"),
        Buffer.from(iv).toString("hex"),
      ].join("|");
    } catch (error) {
      return error;
    }
  }

  static decrypt(encryptedText) {
    try {
      const [encrypted, iv] = encryptedText.split("|");
      if (!iv) throw new Error("IV not found");
      const decipher = crypto.createDecipheriv(
        Encrypter.algorithm,
        Encrypter.key,
        Buffer.from(iv, "hex")
      );
      return decipher.update(encrypted, "hex", "utf8") + decipher.final("utf8");
    } catch (error) {
      return error;
    }
  }
}

Usage:

//Encrypt
const encryptedPassword = Encrypter.encrypt("Password");

//Decrypt, Note: Here you need to provide encrypted value, to decrypt it
const decryptedPassword = Encrypter.decrypt(encryptedPassword);
Salisbury answered 6/8, 2022 at 15:32 Comment(0)
O
1

The simplest way to achieve this is by using a package called cryptr.

It can be done very quick, like:

// npm install cryptr

const Cryptr = require('cryptr');
const cryptr = new Cryptr('myTotallySecretKey');

const encryptedString = cryptr.encrypt('bacon');
const decryptedString = cryptr.decrypt(encryptedString);

console.log(encryptedString); // 2a3260f5ac4754b8ee3021ad413ddbc11f04138d01fe0c5889a0dd7b4a97e342a4f43bb43f3c83033626a76f7ace2479705ec7579e4c151f2e2196455be09b29bfc9055f82cdc92a1fe735825af1f75cfb9c94ad765c06a8abe9668fca5c42d45a7ec233f0
console.log(decryptedString); // bacon

Credits for Maurice Butler that built this lib.

Outcaste answered 27/10, 2022 at 22:41 Comment(0)
C
0

For multiple value encriptions in same page, we need to create separate Cipheriv like below with static iv:

const iv = 'xxxx';   /* replace with your iv */

const cipher1 = crypto.createCipheriv(algorithm, key, iv);
var encrypted1 = cipher1.update(val1, 'utf8', 'hex') + cipher1.final('hex');            // encrypted text
                    
const cipher2 = crypto.createCipheriv(algorithm, key, iv);
var encrypted2 = cipher2.update(val2, 'utf8', 'hex') + cipher2.final('hex');        // encrypted text
                    
const cipher3 = crypto.createCipheriv(algorithm, key, iv);
var encrypted3 = cipher3.update(val3, 'utf8', 'hex') + cipher3.final('hex');        // encrypted text
Chandos answered 3/8, 2022 at 6:56 Comment(0)
C
-3
var crypto = require('crypto'),
algorithm = 'aes-256-ctr',
password = 'RJ23edrf';

//Here "aes-256-cbc" is the advance encryption standard we are using for encryption.

function encrypt(text){
    var cipher = crypto.createCipher(algorithm,password)
    var crypted = cipher.update(text,'utf8','hex')
    crypted += cipher.final('hex');
    return crypted;
}


function decrypt(text){
   var decipher = crypto.createDecipher(algorithm,password)
   var dec = decipher.update(text,'hex','utf8')
   dec += decipher.final('utf8');
   return dec;
}

var salt = uuid.v4()

var e = encrypt();
console.log(e);
var d = decrypt(e);
console.log(d);
Cartilaginous answered 26/1, 2020 at 6:6 Comment(1)
Please don't just provide code, but add a description to what it does.Caudell

© 2022 - 2024 — McMap. All rights reserved.