「暗号・復号(AES)」と「ハッシュ(SHA-2, BCrypt)」

Node.jsで「暗号・復号」「ハッシュ化」を行う方法について取り上げます。cryptoモジュールとbcryptモジュールを利用して動作確認します。

環境

$ node -v
v11.15.0

今回、 cryptoモジュールbcryptモジュール を利用します。

cryptoモジュール はNodeをインストールすれば利用できます。
bcryptモジュール は以下のようにnpmなどでインストールしておきます。

npm install bcrypt

暗号・復号( cipher・decipher )

利用できるアルゴリズム
( cryptoモジュール )

cryptoモジュール で利用できる暗号アルゴリズムについて確認します。

getCiphersメソッド で利用できる暗号アルゴリズムを確認できます。

> const crypto = require('crypto')
undefined
> const ciphers = crypto.getCiphers()
undefined
> ciphers.join(',')
'aes-128-cbc,aes-128-cbc-hmac-sha1,aes-128-cbc-hmac-sha256,aes-128-ccm,aes-128-cfb,aes-128-cfb1,aes-128-cfb8,aes-128-ctr,aes-128-ecb,aes-128-gcm,aes-128-ocb,aes-128-ofb,aes-128-xts,aes-192-cbc,aes-192-ccm,aes-192-cfb,aes-192-cfb1,aes-192-cfb8,aes-192-ctr,aes-192-ecb,aes-192-gcm,aes-192-ocb,aes-192-ofb,aes-256-cbc,aes-256-cbc-hmac-sha1,aes-256-cbc-hmac-sha256,aes-256-ccm,aes-256-cfb,aes-256-cfb1,aes-256-cfb8,aes-256-ctr,aes-256-ecb,aes-256-gcm,aes-256-ocb,aes-256-ofb,aes-256-xts,aes128,aes128-wrap,aes192,aes192-wrap,aes256,aes256-wrap,aria-128-cbc,aria-128-ccm,aria-128-cfb,aria-128-cfb1,aria-128-cfb8,aria-128-ctr,aria-128-ecb,aria-128-gcm,aria-128-ofb,aria-192-cbc,aria-192-ccm,aria-192-cfb,aria-192-cfb1,aria-192-cfb8,aria-192-ctr,aria-192-ecb,aria-192-gcm,aria-192-ofb,aria-256-cbc,aria-256-ccm,aria-256-cfb,aria-256-cfb1,aria-256-cfb8,aria-256-ctr,aria-256-ecb,aria-256-gcm,aria-256-ofb,aria128,aria192,aria256,bf,bf-cbc,bf-cfb,bf-ecb,bf-ofb,blowfish,camellia-128-cbc,camellia-128-cfb,camellia-128-cfb1,camellia-128-cfb8,camellia-128-ctr,camellia-128-ecb,camellia-128-ofb,camellia-192-cbc,camellia-192-cfb,camellia-192-cfb1,camellia-192-cfb8,camellia-192-ctr,camellia-192-ecb,camellia-192-ofb,camellia-256-cbc,camellia-256-cfb,camellia-256-cfb1,camellia-256-cfb8,camellia-256-ctr,camellia-256-ecb,camellia-256-ofb,camellia128,camellia192,camellia256,cast,cast-cbc,cast5-cbc,cast5-cfb,cast5-ecb,cast5-ofb,chacha20,chacha20-poly1305,des,des-cbc,des-cfb,des-cfb1,des-cfb8,des-ecb,des-ede,des-ede-cbc,des-ede-cfb,des-ede-ecb,des-ede-ofb,des-ede3,des-ede3-cbc,des-ede3-cfb,des-ede3-cfb1,des-ede3-cfb8,des-ede3-ecb,des-ede3-ofb,des-ofb,des3,des3-wrap,desx,desx-cbc,id-aes128-CCM,id-aes128-GCM,id-aes128-wrap,id-aes128-wrap-pad,id-aes192-CCM,id-aes192-GCM,id-aes192-wrap,id-aes192-wrap-pad,id-aes256-CCM,id-aes256-GCM,id-aes256-wrap,id-aes256-wrap-pad,id-smime-alg-CMS3DESwrap,idea,idea-cbc,idea-cfb,idea-ecb,idea-ofb,rc2,rc2-128,rc2-40,rc2-40-cbc,rc2-64,rc2-64-cbc,rc2-cbc,rc2-cfb,rc2-ecb,rc2-ofb,rc4,rc4-40,rc4-hmac-md5,seed,seed-cbc,seed-cfb,seed-ecb,seed-ofb,sm4,sm4-cbc,sm4-cfb,sm4-ctr,sm4-ecb,sm4-ofb'

cryptoモジュールで暗号・復号
( AES )

cipher_decipher.js というファイルを作成して以下処理を記述します。

共通鍵暗号方式である AES を暗号アルゴリズムに指定しています。

const crypto = require('crypto')

const algorithm = 'aes-256-cbc' // AESアルゴリズム[256ビット, CBCモード:Cipher Block Chaining mode (暗号ブロック連鎖モード)]
const password = 'abcdefg'
const salt = '12345678'
const key = crypto.scryptSync(password, salt, 32)
const iv = crypto.randomBytes(16)

const cipher = crypto.createCipheriv(algorithm, key, iv)      // 暗号用インスタンス
const decipher = crypto.createDecipheriv(algorithm, key, iv)  // 復号用インスタンス

const data = 'wakuwaku bank'
const inputEncoding = 'utf8'
const outputEncoding = 'hex' // hexadecimal(16進数)

// 暗号
let cipheredData = cipher.update(data, inputEncoding, outputEncoding)
cipheredData += cipher.final(outputEncoding)
console.log(`「${data}」を暗号化\n→ ${cipheredData}\n`)

// 復号
let decipheredData = decipher.update(cipheredData, outputEncoding, inputEncoding)
decipheredData += decipher.final(inputEncoding)
console.log(`「${cipheredData}」を復号化\n→ ${decipheredData}\n`)

実行してみます。

$ node cipher_decipher_iv.js 
「wakuwaku bank」を暗号化
→ 64412bdb11fba1d6eebd6c66f0538313

「64412bdb11fba1d6eebd6c66f0538313」を復号化
→ wakuwaku bank

暗号化したデータを復号することにより、元のデータに戻ったことを確認できました。

ハッシュ

一般に、パスワードをDBに保存する際、平文のまま保存せず、ハッシュ化したハッシュ値を保存します。

利用できるアルゴリズム
( cryptoモジュール )

cryptoモジュール で利用できるハッシュアルゴリズムについて確認します。

getHashesメソッド で利用できるハッシュアルゴリズムを確認できます。

> const crypto = require('crypto')
undefined
> const hashes = crypto.getHashes()
undefined
> hashes.join(',')
'RSA-MD4,RSA-MD5,RSA-MDC2,RSA-RIPEMD160,RSA-SHA1,RSA-SHA1-2,RSA-SHA224,RSA-SHA256,RSA-SHA3-224,RSA-SHA3-256,RSA-SHA3-384,RSA-SHA3-512,RSA-SHA384,RSA-SHA512,RSA-SHA512/224,RSA-SHA512/256,RSA-SM3,blake2b512,blake2s256,id-rsassa-pkcs1-v1_5-with-sha3-224,id-rsassa-pkcs1-v1_5-with-sha3-256,id-rsassa-pkcs1-v1_5-with-sha3-384,id-rsassa-pkcs1-v1_5-with-sha3-512,md4,md4WithRSAEncryption,md5,md5-sha1,md5WithRSAEncryption,mdc2,mdc2WithRSA,ripemd,ripemd160,ripemd160WithRSA,rmd160,sha1,sha1WithRSAEncryption,sha224,sha224WithRSAEncryption,sha256,sha256WithRSAEncryption,sha3-224,sha3-256,sha3-384,sha3-512,sha384,sha384WithRSAEncryption,sha512,sha512-224,sha512-224WithRSAEncryption,sha512-256,sha512-256WithRSAEncryption,sha512WithRSAEncryption,shake128,shake256,sm3,sm3WithRSAEncryption,ssl3-md5,ssl3-sha1,whirlpool'

cryptoモジュールでハッシュ化
( MD5, SHA-1, SHA-2 )

hash.js というファイルを作成して以下処理を記述します。

ハッシュアルゴリズムとして、MD5 SHA-1 SHA-2 を利用してみます。

const crypto = require('crypto')

const md5 = crypto.createHash('md5')
const sha1 = crypto.createHash('sha1')
const sha256 = crypto.createHash('sha256')
const sha512 = crypto.createHash('sha512')

const data = 'wakuwaku bank'
md5.update(data)
sha1.update(data)
sha256.update(data)
sha512.update(data)

const encoding = 'hex' // hexadecimal(16進数)
const md5Hash = md5.digest(encoding)
const sha1Hash = sha1.digest(encoding)
const sha256Hash = sha256.digest(encoding)
const sha512Hash = sha512.digest(encoding)

console.log('■ Data')
console.log(data, '\n')
console.log('■ Hash')
console.log('MD5: ', md5Hash)
console.log('SHA-1: ', sha1Hash)
console.log('SHA-2(256): ', sha256Hash)
console.log('SHA-2(512): ', sha512Hash)

実行してみます。

$ node hash.js 
■ Data
wakuwaku bank 

■ Hash
MD5:  f676c902b5c5e595809afde895e87e7a
SHA-1:  5c9586112785d0e148869a76b2550cad2d3a18e5
SHA-2(256):  bd2f7d71d5256da915178942749b300859f6842017f096018862502b489357a9
SHA-2(512):  1a62bd92a3c667113f7f505418c5733688cc17c5259e8d3449885b43b64394c151adb06fd5ff138aeab6d643b9f8c9dbb2f66e0babe2d9d5a71b6775b542c095

ハッシュ値から元のデータを復元することはできません。

なお、MD5 SHA-1 はハッシュ値の衝突耐性が低いので SHA-2 を利用することが推奨されています。

BCryptとは

ハッシュ化したパスワードに対する攻撃手法がいくつか存在します。BCrypt には、そういった攻撃に対する対策がいくつか組み込まれています。

  • レインボーテーブル攻撃対策
    • ソルト(Salt)の使用
      • パスワードごとに別々のソルトを用意して、同じパスワードでもハッシュ値が異なるようにする。
  • オフライン総当り攻撃対策
    • ストレッチング(ハッシュ関数を繰り返して実行)によって、ハッシュ化の処理を意図的に遅くする。
      • ただし、パスワード認証の時間が長くなるというトレードオフがある。

BCryptで生成された値には、以下情報が含まれています。

  • bcryptのバージョン
    • $1$ : MD5
    • $2$ : Blowfish
    • $sha1$ : SHA-1
    • $5$ : SHA-256
    • $6$ : SHA-512
  • ストレッチング回数
  • ソルト値
  • Hash化後の値

ソルトも含んでいるのでDBに格納する際に、別途ソルト用のカラムを追加せずに済みます。例で確認します。

$2b$10$DvCCoNxzW76rW1i2UpcObuaEihf2qpJu9HLc3Vl4bfhy3mM60ttrm
項目
bcryptのバージョン $2$
ストレッチング回数 10
ソルト値 $DvCCoNxzW76rW1i2UpcObu
Hash化後の値 aEihf2qpJu9HLc3Vl4bfhy3mM60ttrm

bcryptモジュールでハッシュ化

bcrypt.js というファイルを作成して以下処理を記述します。

const bcrypt = require('bcrypt')

const data1 = 'wakuwaku bank'
const data2 = 'xxxxxxxx yyyy'
const saltRounds = 10

const salt = bcrypt.genSaltSync(saltRounds)
console.log('salt: ', salt)

const hash = bcrypt.hashSync(data1, salt)
console.log('hash: ', hash)

console.log('data1: ', bcrypt.compareSync(data1, hash))
console.log('data2: ', bcrypt.compareSync(data2, hash))

実行してみます。

$ node bcrypt.js 
salt:  $2b$10$DvCCoNxzW76rW1i2UpcObu
hash:  $2b$10$DvCCoNxzW76rW1i2UpcObuaEihf2qpJu9HLc3Vl4bfhy3mM60ttrm
data1:  true
data2:  false

saltRounds = 10 としていますが、saltRounds = 15 にすると処理実行時間が長くなることを確認できます。

参考