Node.jsでAESを使った暗号化及び復号の処理

Published:

AES256 CBCでの暗号化

コード

import crypto from 'crypto'

function encrypt(plaintext: string, password: string) {
    const iv = crypto.randomBytes(16)
    const salt = crypto.randomBytes(16)
    const key = crypto.scryptSync(password, salt, 32)
    const cipher = crypto.createCipheriv('aes-256-cbc', key, iv)
    const ciphertext = Buffer.concat([cipher.update(plaintext, 'utf8'), cipher.final()])
    
    return {ciphertext, iv, salt}
}

IV(Initialization Vector、初期化ベクトル)

IV(初期化ベクトル)は、暗号化時に最初のブロックを暗号化するために利用される、ランダムな値です。 同じ鍵を使用して複数暗号化する場合、異なるIVを使うことで、同じ暗号結果にならないようにするために利用されます。

CBCでは、同じ文で同じ暗号結果が生成されないように、1個前のブロックと今のブロックとのXORを取ってから、暗号化を行います。 最初のブロックでは前のブロックが存在しないため、適当な値を用意する必要があります。 暗号化のブロックサイズと同じにする必要があり、CBCにおいては128bit(16Byte)です。

同じIVを使い回した場合、暗号化をする前のデータが推測されやすくなる(例えば同じような出だしで始まる住所など)ため、一回ごとに生成する必要があります( CWE-1204)。

IVは他人に知られても問題ありません。

Salt(ソルト)

Salt(ソルト)は、ハッシュ関数を用いてパスワードをハッシュ化する際に利用される、ランダムな値です。

ソルトを利用しない場合同じパスワードでは同じハッシュ値が生成されるため、レインボーテーブルを用いたハッシュ値の事前計算をすることが容易になります( CWE-759)。 ソルトを利用することで同じパスワードでも異なるハッシュ値が生成されるので、ハッシュ化したパスワードが流出した場合でも元々のパスワードを特定するのが難しくなります。

ただし、全てのパスワードに対して同じソルトを利用してしまうと計算が容易になるため、一回ごとに生成する必要があります( CWE-760)。

また、ソルトを十分に機能させるにはある程度の長さが必要になります。米国国立標準技術研究所が出している NIST SP 800-132では128bit(16Byte)以上が推奨されています。

Saltも他人に知られても問題ありません。

前述のSaltと、任意のパスワードを用いてscryptSync関数でハッシュ値を生成します。 AES256なら256bit(32Byte)必要なので、第三引数には32を指定しています。

任意のパスワードを知られてしまうと復号に必要な鍵を生成する出来てしまうため、安全な方法で管理する必要があります。

暗号化処理

createCipheriv関数で、暗号化アルゴリズム、鍵、IVを指定します。

update関数で暗号化した結果が返ってきます。上記の実装では一回しか使っていませんが、何らかの理由で分割する場合はupdate関数を繰り返します。

update関数の第一引数は暗号化する文です。 第二引数は入力エンコード(utf8asciibinary)です。 第三引数は出力エンコード(binarybase64hex)です。出力のエンコードは最終的な利用方法に合わせて設定します。DBに保存する場合はBase64やHEXにするのが扱いやすいと思います。

最後まで暗号化できたら、final関数で終端を取得します。引数はupdate関数の第三引数と同様に出力エンコードになります。

これにより暗号化したデータを取得出来ます。

AES256 CBCでの復号(復号化)

import crypto from 'crypto'

function decrypt(ciphertext: Buffer, password: string, salt: Buffer, iv: Buffer) {
    const key = crypto.scryptSync(password, salt, 32)
    const decipher = crypto.createDecipheriv('aes-256-cbc', key, iv)

    return decipher.update(ciphertext, 'binary', 'utf8') + decipher.final('utf8')
}

復号処理

まずは暗号化と同様にパスワードとSaltをscryptSync関数に通して、ハッシュ化した鍵を計算します。

次にcreateDecipheriv関数で、暗号化したときに利用した、暗号化アルゴリズム、鍵、IVを指定します。

後は暗号化の時と同様に、update関数を繰り返し、最後にfinal関数で終端を取得すると、復号されたデータになります。