如何加密需要在 node.js 中解密的数据?

我们正在使用 地下室散列密码和数据,永远不需要解密。我们应该做些什么来保护需要解密的其他用户信息?

例如,假设我们不希望用户的真实姓名为纯文本,以防有人获得访问数据库的权限。这是一些比较敏感的数据,但也需要不时调用并以纯文本显示。有什么简单的方法吗?

90264 次浏览

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

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

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

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.

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

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

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

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

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.