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

IT技术 javascript node.js encryption cryptography node-crypto
2021-03-06 23:50:42

我们使用bcrypt来散列不需要解密的密码和数据。我们应该如何保护其他需要解密的用户信息?

例如,假设我们不希望用户的真实姓名为纯文本,以防有人获得对数据库的访问权限。这是有点敏感的数据,但也需要不时调用并以纯文本形式显示。有没有一种简单的方法可以做到这一点?

6个回答

您可以使用加密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);

编辑

现在createCiphercreateDecipher已被弃用,而不是使用createCipherivcreateDecipheriv

您会建议为此添加一个 IV 以使其更安全吗?如果是这样,这将如何完成crypto
2021-04-21 23:50:42
@Fizzix,如果我是对的,createCipheriv 需要一个 IV。
2021-04-28 23:50:42

2019 年 12 月 12 日更新

与 CBC 等其他一些模式不同,GCM 模式不需要 IV 不可预测。唯一的要求是对于使用给定键的每次调用,IV 必须是唯一的。如果对给定的密钥重复一次,则安全性可能会受到影响。实现此目的的一种简单方法是使用来自强伪随机数生成器的随机 IV,如下所示。

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

此外,应在每 2^32 次调用后轮换密钥。有关 IV 要求的更多详细信息,请参阅此答案NIST 建议

2019 年 7 月 30 日更新

由于答案获得了更多的观点和投票,我认为值得一提的是,下面的代码使用了 *Sync 方法 - crypto.scryptSync现在,如果在应用程序初始化期间完成加密或解密就可以了。否则,请考虑使用该函数的异步版本以避免阻塞事件循环。(像这样的Promise库bluebird很有用)。

2019 年 1 月 23 日更新

解密逻辑中的错误已得到修复。感谢@AlexisWilke 正确地指出它。


接受的答案是 7 岁,今天看起来并不安全。因此,我是这样回答的:

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

  2. GCM 需要初始化向量 (IV)。IV 不是秘密。唯一的要求是它必须是随机的或不可预测的。在 NodeJs 中,crypto.randomBytes()旨在产生加密强的伪随机数。

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

  4. 接收者需要知道 IV 才能解密密文。因此,IV 需要与密文一起传输。一些实现将 IV 作为 AD(关联数据)发送,这意味着将在密文和 IV 上计算身份验证标签。然而,这不是必需的。可以简单地在 IV 前面加上密文,因为如果在传输过程中由于故意攻击或网络/文件系统错误而更改了 IV,则身份验证标签验证无论如何都会失败

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

  6. 最后,为了通过网络或存储进行传输,密文应使用 Base64 编码进行编码。buffer.toString('base64');可用于将 转换Buffer为 Base64 编码的字符串。

  7. 请注意,密钥派生 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);
    });
  });
});
@AlexisWilke,现在已修复。谢谢你指出。
2021-04-15 23:50:42
我得到了Error: Unsupported state or unable to authenticate data,有没有人解决这个问题?
2021-04-20 23:50:42
如果有人试图将其移植到 Typescript... 类型干扰存在一个小问题,您将不得不更改一行:` BLOCK_CIPHER: 'aes-256-gcm' as crypto.CipherCCMTypes,` 这应该可以解决您的问题:)
2021-04-24 23:50:42
我知道你有一个测试,你可能运行了一个特殊情况(小输入)......但看起来你有一个错误,decrypt()因为你没有对decipher.final(). 应该是串联的吧?
2021-04-27 23:50:42
优秀而翔实的答案!您能否详细说明(或在您的测试用例中付诸实践)您在第 6 点中的观点?有人应该如何使用信息而不像测试用例中那样将值存储在字符串中?我觉得这对于学术目的来说很棒,但是在生产中使用开源库不是更好吗?例如:忒弥斯
2021-05-04 23:50:42

更新到@mak答案,crypto.createCiphercrypto.createDecipher已被弃用。最新的工作代码是:

var crypto = require("crypto");
var algorithm = "aes-192-cbc"; //algorithm to use
var password = "Hello darkness";
const key = crypto.scryptSync(password, '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);
很好,很有帮助的答案!我想补充一点,可以使用随机 IV 而不是静态 IV。因此,iv = Buffer.alloc(...)可以更改为iv = crypto.randomBytes(16), 每次使用不同的哈希值,从而防御任何彩虹表攻击。
2021-04-17 23:50:42
@AljohnYamaro 文本是指需要加密的纯文本字符串。密钥是key使用password作为基础生成的
2021-04-25 23:50:42
嗨,美好的一天, var text = '...' 是密钥吗?
2021-04-27 23:50:42
如果您正在存储encryptediv以后解密,您还需要存储authTag. 通过获取它并通过authTag = cipher.getAuthTag()应用它decipher.setAuthTag(authTag)没有这件作品,我就失败了decipher.final('utf8')参见:github.com/nodejs/help/issues/1034
2021-05-03 23:50:42
不切实际,因为您没有为每个加密值保存 iv。
2021-05-13 23:50:42

虽然这已得到正确回答,但使用加密库的一个好模式是在类包装器中,多年来我已将其复制/粘贴到各种项目中。

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 });
我正在使用aes256,当我切换到aes-192-cbc它时。根据我在文档中阅读的内容,我可以使用 OpenSSL 支持的任何方法。我是否必须为它添加更多代码aes256
2021-04-16 23:50:42
嘿,我正在使用您的代码,但Invalid key length出现错误。
2021-04-19 23:50:42
@TheWhiteFang 检查您的加密文本,它可能已被修改。您还可以打印变量encryptedivdecrypt/1函数中进行调试
2021-04-23 23:50:42
你好被驱逐的男孩。我正在使用您的代码,但是每次运行它时,它都会在加密时生成不同的结果,这使得在保存加密密码时很难再次加密。有什么建议?谢谢。
2021-04-27 23:50:42
@Danielle IV 的目的是随机化生成的结果,这是需要的。如果您想比较生成的结果,您需要对其进行解密,否则请查看散列,或crypto.createCipher/2在最佳答案中使用的现在折旧
2021-05-13 23:50:42

这是Saptarshi Basu发布的答案的简化版本

变化:

  • Bufferbuffermodule显式导入
  • 删除不必要的变量声明
  • 将修改过的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的代码相同

祝你好运。