web-dev-qa-db-ja.com

node.js:復号化する必要があるデータを暗号化しますか?

解読する必要のないパスワードとデータにはbcryptを使用しています。

他のユーザー情報を保護するために何をすべきか。この例では、誰かがデータベースを取得する場合に備えて、ユーザーの実際の名前をプレーンテキストにしたくないと言えます。

これはやや機密性の高いデータですが、時々呼び出してプレーンテキストで表示する必要もあります。これを行う簡単な方法はありますか?

49
fancy

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

2019年7月30日に更新

答えがより多くのビューと投票を得ているので、以下のコードが* Syncメソッド-_crypto.scryptSync_を使用していることに言及する価値があると思います。これで、アプリケーションの初期化中に暗号化または復号化が行われれば問題ありません。そうでない場合は、イベントループのブロックを回避するために、非同期バージョンの関数の使用を検討してください。 (bluebirdのようなpromiseライブラリが便利です)。

2019年1月23日に更新

復号化ロジックのバグが修正されました。 @AlexisWilkeが正しく指摘してくれてありがとう。


受け入れられた答えは7歳で、今日は安全に見えません。したがって、私はそれに答えています:

  1. 暗号化アルゴリズム:256ビットキーのブロック暗号AESは十分に安全であると見なされます。完全なメッセージを暗号化するには、モードを選択する必要があります。認証された暗号化(機密性と整合性の両方を提供する)が推奨されます。 GCM、CCM、およびEAXは、最も一般的に使用される認証済み暗号化モードです。通常、GCMが好まれ、GCM専用の命令を提供するIntelアーキテクチャで良好に機能します。これら3つのモードはすべてCTRベース(カウンターベース)モードであるため、パディングは必要ありません。結果として、それらはパディング関連の攻撃に対して脆弱ではありません

  2. GCMには初期化ベクトル(IV)が必要です。 IVは秘密ではありません。唯一の要件は、ランダムまたは予測不能でなければなりません。 NodeJでは、crypto.randomBytes()は暗号的に強力な擬似乱数を生成することを意図しています。

  3. NISTは、設計の相互運用性、効率性、および簡素化を促進するために、GCMに96ビットIVを推奨しています

  4. 受信者は、暗号テキストを復号化できるようにIVを知っている必要があります。したがって、IVは暗号文とともに転送する必要があります。一部の実装では、IVをAD(関連データ)として送信します。これは、認証タグが暗号テキストとIVの両方で計算されることを意味します。ただし、これは必須ではありません。意図的な攻撃またはネットワーク/ファイルシステムエラーのためにIVが送信中に変更された場合、認証タグの検証はいずれにせよ失敗するため、IVに暗号テキストを単に付加することができます

  5. 文字列は不変であるため、クリアテキストメッセージ、パスワード、またはキーを保持するために文字列を使用しないでください。これは、使用後に文字列を消去できず、メモリに残ることを意味します。したがって、メモリダンプは機密情報を明らかにする可能性があります。同じ理由で、これらの暗号化または復号化メソッドを呼び出すクライアントは、bufferVal.fill(0)を使用して不要になった後、メッセージ、キー、またはパスワードを保持するすべてのBufferをクリアする必要があります。

  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);
    });
  });
});
_
13
Saptarshi Basu