node.js:加密需要解密的数据?

fan*_*ncy 50 javascript cryptography node.js

我们使用bcrypt来获取永远不需要解密的密码和数据.

应采取哪些措施来保护其他用户信息.对于这个例子,我们可以说,如果有人要获取数据库,我们不希望用户的真实姓名是纯文本.

这是一些有些敏感的数据,但也需要不时调用并以纯文本显示.有一个简单的方法吗?

mak*_*mak 117

您可以使用加密模块:

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);
Run Code Online (Sandbox Code Playgroud)

  • 您会建议为此添加一个 IV 以使其更安全吗?如果是这样,这将如何使用“crypto”来完成? (7认同)
  • @Fizzix,如果我是正确的,createCipheriv 需要 IV。 (3认同)
  • 不再有用了!一旦“createCipher”和“createDecipher”被弃用,此解决方案将不起作用。它们需要更多参数,如“IV”。 (2认同)

exp*_*boy 24

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

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");
  }
}
Run Code Online (Sandbox Code Playgroud)
// 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 });
Run Code Online (Sandbox Code Playgroud)

  • @TheWhiteFang如果您使用“aes256”,您可能需要将 scryptSync 中指定的密钥长度更改为 32。 (2认同)

Sap*_*asu 17

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不是秘密。唯一的要求是它必须是随机的或不可预测的。在NodeJ中,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())已用于从密码派生密钥。但是,此功能仅在节点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;
}
Run Code Online (Sandbox Code Playgroud)

并且下面还提供了单元测试:

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);
    });
  });
});
Run Code Online (Sandbox Code Playgroud)

  • 我知道您已经进行了测试,并且可能运行了一个特例(少量输入)...但是看起来您在`decrypt()`中有一个错误,因为您对`decipher.final()`的输出不执行任何操作。应该串联起来吧? (3认同)
  • 我收到“错误:不受支持的状态或无法验证数据”,有人可以解决此问题吗? (2认同)
  • 万一有人尝试将其移植到 Typescript...存在类型干扰的小问题,您必须更改一行: ` BLOCK_CIPHER: 'aes-256-gcm' as crypto.CipherCCMTypes,` 这应该可以解决您的问题:) (2认同)

小智 17

更新到@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);
Run Code Online (Sandbox Code Playgroud)

  • 不实用,因为您没有为每个加密值保存 iv 。 (2认同)

Ami*_*mir 6

接受的答案是正确的,但几乎没有什么变化,因为createCiphercreateDecipher已被弃用。

在新方法中createCipheriv,要求createDecipheriv iv值,并且iv值长度必须为 128 位,密钥必须为 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);
Run Code Online (Sandbox Code Playgroud)