センシティブなデータを DB に保存するにあたって、Go で暗号化を実装した。
その過程で暗号化についていろいろ調べたので、Go で暗号化したいけど暗号化のことはよくわからないという人を対象にまとめる。
今回は鍵の導出に PBKDF2 を、暗号アルゴリズムに AES-256-GCM を採用したのでこれをベースに説明していく1。
PBKDF2
前提として、暗号化には鍵と呼ばれるデータが必要になる。
暗号文 (暗号化の結果) は、暗号アルゴリズムと平文、そして鍵の組み合わせで決定される。
また、鍵は復号にも用いられるため第三者に知られてはならない。
この鍵に human-readable な文字列を使うと辞書攻撃や総当たり攻撃に弱いという問題がある。
対策として、文字列のハッシュ化を繰り返し行うストレッチングと呼ばれる処理をすることで攻撃を困難にする。
PBKDF2 は任意のハッシュ関数に基づいて上記を行うアルゴリズムで、これによる鍵の導出の実装はこうなる。
package main
import (
"crypto/sha256"
"golang.org/x/crypto/pbkdf2"
)
func main() {
password := []byte("password")
salt := []byte("salt")
key := pbkdf2.Key(password, salt, 1000, 32, sha256.New)
}
今回はベースとなるハッシュ関数 h を SHA-256、ストレッチング回数 iter を 1000 とした2。
鍵長 keyLen を 32 bytes にしていること、また salt については後述する。
AES-256-GCM
次に AES-256-GCM について。このままだと難しいので AES と 256 と GCM に分解する。
AES
共通鍵暗号のブロック暗号アルゴリズム。このままだと難しいので共通鍵暗号とブロック暗号に分解する。
暗号化と復号に同一の鍵を用いる暗号方式のこと。
暗号化に用いた鍵があればデータを復号できる。
ブロック暗号4
固定長のデータ (ブロックと呼ばれる) を単位に暗号化する暗号のこと。
256
AES で暗号化する際に用いる鍵のサイズ (ビット数)。
256 の他に 128 と 192 がある。
長ければ長いほど計算量が増えて安全になる。
GCM
ブロック暗号では固定長のデータしか扱えないため、ブロック長より長いデータを暗号化するためにはブロック暗号を繰り返して適用する必要がある。
この繰り返しの方法を暗号利用モードといって、GCM はそのひとつ。
ここで、AES-256-GCM を扱う実装についてまとめる。
package main
import (
"crypto/aes"
"crypto/cipher"
)
func main() {
var key [32]byte
block, err := aes.NewCipher(key[:])
if err != nil {
panic(err)
}
aead, err := cipher.NewGCM(block)
if err != nil {
panic(err)
}
}
コメントにも書いたが aes.NewCipher() に 32 bytes の鍵を渡すことで AES-256 が選択される。
PBKDF2 で導出する鍵長を 32 bytes にしていたのはこのためだ。
また cipher.NewGCM() の戻り値に aead と名前をつけているが、次はこれについて説明する。
AEAD
暗号化に加えて、認証を同時に行う暗号利用モードを AEAD という。
認証によって、暗号文が改ざんされていないこと、また正規に生成されたものであることが保証される5。
GCM は認証付きの暗号利用モードのため、cipher.NewGCM() はcipher.AEAD を返す。
この cipher.AEAD を用いた暗号化の実装は以下になる。
package main
import (
"crypto/cipher"
"crypto/rand"
"io"
)
func main() {
var aead cipher.AEAD
plaintext := []byte("plaintext")
iv := make([]byte, aead.NonceSize())
if _, err := io.ReadFull(rand.Reader, iv); err != nil {
panic(err)
}
encryptedData := aead.Seal(iv, iv, plaintext, nil)
}
今度は iv という名前がいっぱい出てきた。
IV
暗号文は暗号アルゴリズムと平文と鍵の組み合わせで決定されると前述した。
これは組み合わせが一致していれば暗号文は常に同じになるということであり、暗号初期化ベクトルは役に立つのか?|徳丸 浩|note で紹介されているような危険性がある。
そこで、IV (Initial Vector) と呼ばれる一意な値を付加することで6、組み合わせが一致していても異なる暗号文が出力されるようにする7。
復号には暗号化のときに付加した IV が必要になるのだが、それを踏まえて先ほどのコードの
encryptedData := aead.Seal(iv, iv, plaintext, nil)
の部分を詳しくみていく。まずは Seal() のシグネチャを以下に示す8。
func (cipher.AEAD).Seal(dst []byte, nonce []byte, plaintext []byte, additionalData []byte) []byte
ここで引数 dst に iv を渡すことによって、戻り値が IV と暗号文を連結したバイトスライスになる。
IV のサイズは aead.NonceSize() で取得可能なため、このバイトスライスを暗号データとして保存しておくことで復号時に以下で IV を取得できる。
package main
func main() {
var encryptedData []byte
iv, ciphertext := encryptedData[:aead.NonceSize()], encryptedData[aead.NonceSize():]
}
また、この IV と暗号文をもとに復号するコードは以下になる。
package main
import "crypto/cipher"
func main() {
var aead cipher.AEAD
var iv, ciphertext []byte
plaintext, err := aead.Open(nil, iv, ciphertext, nil)
if err != nil {
panic(err)
}
}
まとめ
PBKDF2 による鍵の導出と AES-256-GCM による暗号化を Go で実装した。
package main
import (
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"crypto/sha256"
"fmt"
"io"
"golang.org/x/crypto/pbkdf2"
)
func main() {
password := []byte("password")
salt := []byte("salt")
key := pbkdf2.Key(password, salt, 1000, 32, sha256.New)
block, err := aes.NewCipher(key)
if err != nil {
panic(err)
}
aead, err := cipher.NewGCM(block)
if err != nil {
panic(err)
}
plaintext := []byte("plaintext")
iv := make([]byte, aead.NonceSize())
if _, err := io.ReadFull(rand.Reader, iv); err != nil {
panic(err)
}
encryptedData := aead.Seal(iv, iv, plaintext, nil)
iv2, ciphertext := encryptedData[:aead.NonceSize()], encryptedData[aead.NonceSize():]
plaintext2, err := aead.Open(nil, iv2, ciphertext, nil)
if err != nil {
panic(err)
}
fmt.Printf("%s\n", plaintext2)
}