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

104
我们正在使用bcrypt来对密码和永远不需要解密的数据进行哈希处理。那么,我们该如何保护还需要解密的其他用户信息呢?
例如,假设我们不想以明文形式存储用户的真实姓名,以防万一有人获取了数据库访问权限。虽然这是一些敏感数据,但有时也需要显示为明文。那么,有没有一种简单的方法来解决这个问题呢?

7
攻击者可以获取您的数据库,但无法获取您也存储在磁盘上的加密密钥的情况并不常见。你确定你已经彻底考虑过这个问题了吗? - Nick Johnson
73
互联网是如此聪明和有帮助。当人们选择通过侮辱你的方法来展示他们的智慧,而不是回答问题或提供实质性的指导时,这非常出色。 - Secesh
4
你没有使用bcrypt来加密你的数据,因为它并不能做到加密。你正在对其进行哈希处理。这两者并不相同。 - Mike Scott
10个回答

175
你可以使用 crypto 模块:
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);

编辑

现在,createCiphercreateDecipher已被弃用,改用createCipherivcreateDecipheriv


8
您建议在此添加IV以使其更安全吗?如果是,请问如何使用crypto来实现? - Fizzix
3
@Fizzix,如果我没记错的话,createCipheriv需要一个IV。 - Royal
不再有用了!一旦createCiphercreateDecipher被弃用,这个解决方案就无法工作。它们需要更多的参数,如IV - Willian

68

2019年12月12日更新:

GCM模式不像其他模式(如CBC),不要求IV值是不可预测的。唯一要求的是,使用给定密钥时,每次调用都必须使用唯一的IV。如果为给定密钥重复使用IV,则会导致安全性被破坏。下面展示了一种简单实现这一要求的方法,即使用一个强伪随机数生成器生成随机IV。

使用序列或时间戳作为IV也是可以的,但实现起来可能并非像听起来那么简单。例如,如果系统没有正确跟踪在持久存储中已使用的IV序列,则在系统重新启动后,会有调用重复使用IV的风险。同样,也不存在完美的时钟,计算机时钟会重新调整等等。

另外,应该在每2^32次调用后轮换密钥。关于IV要求的更多细节,请参考此答案NIST建议

2019年7月30日更新:

鉴于本答案的点击量和投票数不断增加,我认为有必要提到下面的代码使用了一个同步方法——crypto.scryptSync。如果加密或解密是在应用程序初始化期间完成,则没有问题。否则,请考虑使用该函数的异步版本以避免阻塞事件循环(如使用bluebird等Promise库)。

2019年1月23日更新:

修复了解密逻辑中的错误。感谢@AlexisWilke正确指出这一点。


接受的答案已经有7年历史,在今天看来不再安全。因此,我来回答:

  1. 加密算法:使用256位密钥的块密码AES被视为足够安全。要加密完整的消息,需要选择一种模式。建议使用认证加密(同时提供机密性和完整性)。GCM、CCM和EAX是最常用的认证加密模式。GCM通常被优先选择,并且它在提供专门用于GCM的指令的Intel架构中表现良好。所有这三种模式都基于CTR(计数器)模式,因此它们不需要填充。因此,它们不易受到与填充相关的攻击。

  2. 需要使用IV来加密GCM。IV不是秘密。唯一的要求是它必须是随机或不可预测的。在NodeJs中,crypto.randomBytes()用于生成具有密码学强度的伪随机数。

  3. NIST建议使用96位IV以促进互操作性、效率和设计的简单性。

  4. 接收者需要知道IV才能解密密文。因此,IV需要与密文一起传输。一些实现将IV作为AD(相关数据)发送,这意味着认证标记将计算密文和IV的内容。但是,这并非必需。IV可以简单地添加到密文前面,因为如果在传输过程中由于恶意攻击或网络/文件系统错误更改了IV,则认证标记验证

    为避免敏感信息被泄露,不应使用字符串来保存明文消息、密码或密钥,因为字符串是不可变的,这意味着我们无法在使用后清除字符串,它们将在内存中残留。因此,内存转储可以揭示敏感信息。出于同样的原因,调用这些加密或解密方法的客户端应该使用bufferVal.fill(0)清除所有保存消息、密钥或密码的Buffer,一旦不再需要。

    最后,在网络传输或存储时,密文应使用Base64编码。可以使用buffer.toString('base64');Buffer转换为Base64编码的字符串。

    请注意,从密码派生密钥的关键导出函数scrypt (crypto.scryptSync()) 仅适用于Node 10.*及更高版本。

    代码如下:

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

    以下还提供了单元测试:

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

3
我知道你有一项测试,可能在跑一个特殊情况(小输入)... 但是看起来你的 decrypt() 函数有一个 bug,因为你没有对 decipher.final() 的输出做任何处理。应该将其连接起来,对吗? - Alexis Wilke
1
@MatthewSanders 这些测试本来可以使用缓冲区编写。但你说得对。可能有一些情况下无法避免使用字符串。然而,出于上述原因,这种用法应该尽量少。使用经过实战检验的库是没有害处的。问题在于我们假设库总是正确的,并忘记了它也可能存在错误或问题。我们应该了解其背后的科学知识并查看库代码,以欣赏库代码的安全性,并能够找到解决错误和问题的方法。 - Saptarshi Basu
4
我遇到了“错误:不支持的状态或无法验证数据”的问题,有没有解决方法? - Abana Clara
2
如果有人尝试将此代码移植到Typescript,会遇到一个类型干扰的小问题,您需要更改以下一行代码: BLOCK_CIPHER:'aes-256-gcm' as crypto.CipherCCMTypes,这应该可以解决您的问题 :) - pa1nd
1
旋转密钥并保存现有加密记录的一些最佳实践是什么?此处的示例自动创建该密钥,但此后该密钥将在单元测试执行完成后消失。 - AliAvci
显示剩余10条评论

32

对@mak答案的更新,crypto.createCiphercrypto.createDecipher已被弃用。最新可用的代码如下:

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

您好,今天过得怎么样?var text = '...' 是秘密密钥对吗? - Aljohn Yamaro
@AljohnYamaro 的文本是指需要加密的纯文本字符串。私钥是使用基于“密码”的 key 生成的。 - Bhumij Gupta
非常好的、有用的回答!我想补充一点,可以使用随机 IV 代替静态 IV。因此,iv = Buffer.alloc(...) 可以改为 iv = crypto.randomBytes(16),每次都会生成不同的哈希值,从而防御任何彩虹表攻击。 - roshnet
3
不切实际,因为您没有为每个加密值保存iv。 - Philip Rego
1
如果您正在存储加密iv以供以后解密使用,那么您还需要存储authTag。通过authTag = cipher.getAuthTag()获取它,并通过decipher.setAuthTag(authTag)应用它。如果没有这个步骤,我在decipher.final('utf8')上会失败。请参见:https://github.com/nodejs/help/issues/1034 - WGriffing

22

虽然已经有正确的答案了,但是一个好的模式是在类包装器中使用加密库,这个模式我已经复制/粘贴多年,用于各种项目。

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

你好,Expelledboy。我正在使用你的代码,但每次运行它时,加密时生成的结果都不同,这使得在保存加密密码时再次加密变得困难。有什么建议吗?谢谢。 - Danielle
@Danielle IV 的目的是为了随机生成的结果,这是期望的。如果您想比较生成的结果,则需要解密它,否则请查看哈希或现在已弃用的 crypto.createCipher/2(在顶部答案中使用)。 - expelledboy
嘿,我正在使用你的代码,但是出现了“无效密钥长度”错误。 - TheWhiteFang
@TheWhiteFang 检查一下您的加密文本,可能已被修改。您还可以在decrypt/1函数中打印变量 encryptediv 进行调试。 - expelledboy
我之前使用的是aes256,当我切换到aes-192-cbc时它正常工作。根据我在文档中阅读的内容,我可以使用任何OpenSSL支持的方法。我需要为aes256添加更多代码吗? - TheWhiteFang
2
@TheWhiteFang 如果您正在使用 aes256,您可能需要将 scryptSync 中指定的密钥长度更改为 32。 - CookieEater

6

接受的答案是正确的,但有一些更改,因为 createCiphercreateDecipher 已被弃用。

在新的方法中,createCipherivcreateDecipheriv 要求提供 iv 值,并且 iv 值长度必须为128位,key 必须为256位。

代码示例

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

谢谢这个,它帮了我很多,但是"randomBytes(8).toString('hex')"让我有些困惑。它是16个字节。最好直接使用crypto.randomBytes(16),然后将其传入。如果需要将其视为十六进制,可以在16字节缓冲区上调用toString('hex'),然后使用Buffer.from('...', 'hex')将其转换回缓冲区,然后再传入。 - undefined

3
这是Saptarshi Basu发布的答案的简化版本:
更改:
  • buffer模块显式导入Buffer
  • 删除不必要的变量声明
  • 将修改过的let变量转换为const变量(或完全省略)
  • module.exports转换为单个对象
  • exports.x = x = (...)声明移动到module.exports对象
  • 简化和/或减少ALGORITHM对象的文档
代码:
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()]);
  }
};

请记住,尽管简化了代码,但这段代码应该在功能上与Saptarshi Basu的代码完全相同。
祝你好运。

1

最简单的方法是使用一个名为 cryptr 的包。

可以非常快速地完成,例如:

// 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

感谢 Maurice Butler 创建了这个库。


0

对于同一页中的多个值加密,我们需要创建单独的Cipheriv,如下所示,使用静态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

0
这对我在使用 TypeScript 和 aes256 时使用 createCipheriv 很有帮助。参考链接:RefEncrypt.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;
    }
  }
}

使用方法:

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

//Decrypt, Note: Here you need to provide encrypted value, to decrypt it
const decryptedPassword = Encrypter.decrypt(encryptedPassword);

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

5
请勿仅提供代码,还需添加描述其作用。 - sportzpikachu

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接