You have 2 free member-only stories left this month.
Goでのパスワードを使用したデータ暗号化(後半)
Password
さて、aes.NewCipher() は 16、24、または 32 バイトのキーを必要とし、この例では 32 バイトのキーを使用しています。しかし、私たちのパスワードは32バイトにはなりません。そこで、パスワードを適切なキーに変換する必要があります。これを行うには、鍵導出関数(KDF)11を使用して、パスワードを適切な暗号キーにするために「伸張」します。このキーストレッチ12は、それ自体が遅いという特徴を持っています。これは、アタッカーがパスワードにブルートフォース攻撃を試みるために多くのリソース(総当たり攻撃)を費やす必要があるためなのです。KDF にはいくつかのオプションがあります。そのオプションというのはArgon213、scrypt14、bcrypt15、pbkdf216です。どれを選ぶかはいくつかの要因に依存しますが、主にどのくらい安全であるか171819202122で選びます。
通常、KDFではパスワード、ソルト、および繰り返しの引数を持つ。salt23は、攻撃者がパスワードとキーのペアを保存するだけではなく、アタッカーが派生キーのディクショナリを事前にコンピューティングすることを防ぐために使用されます。というのも、異なるソルトは各パスワードは、異なるアウトプットを算出するからです。それぞれのパスワードはキーを導出するために使用されたソルトと照合しなければならりません。(Isom 2015; Wikipedia 2020) ソルトは、ランダムに生成される必要があるという点で nonce に関連しています。また、nonceと同様に、ソルトは秘密である必要はありませんが、ユニークである必要があります。繰り返し引数または difficulty パラメータは、処理を何回繰り返すかを表します。これは、ソルトを使用してもディクショナリ攻撃は可能ですが、反復回数を指定すると、パスワードからキーをコンピューティングするのにかかる時間が遅くなるからです(Viega and Messier 2003)。(Viega and Messier 2003, 141–42)
この例では scrypt を使用していますので、どのようにプログラムに実装するか見てみましょう。
import ("crypto/rand""golang.org/x/crypto/scrypt")func DeriveKey(password, salt []byte) ([]byte, []byte, error) {if salt == nil {salt = make([]byte, 32)if _, err := rand.Read(salt); err != nil {return nil, nil, err}}key, err := scrypt.Key(password, salt, 1048576, 8, 1, 32)if err != nil {return nil, nil, err}return key, salt, nil}
もう一度、コードを確認してみましょう。
func DeriveKey(password, salt []byte) ([]byte, []byte, error)
ここでは、引数としてのパスワードをバイトスライスとして受け取り、結果として得られたキーとソルトを返します。
salt := make([]byte, 32)if _, err := rand.Read(salt); err != nil {return err}
Encrypt関数と同じように、32バイトのランダムなバイトでソルトを作成します。
key, err := scrypt.Key(password, salt, 1048576, 8, 1, 32)
ここでは golang.org/x/ library.24 の scrypt パッケージを使用しています。
読むことのできるドキュメントからは、キー関数は以下の引数を受け取ることができます。
func Key(password, salt []byte, N, r, p, keyLen int) ([]byte, error)
引数のpasswordとsaltがそれを物語っています。Nは反復回数です。C. Percivalによるプレゼンテーションでは、対話型ログイン16384 (214)では繰り返し、ファイル暗号化では 1048576 (220) 回の繰り返しの使用が推奨されています。(Percival 2005a, 2005b; Isom 2015) 引数 r と p は r∗p<230 を満たさなければならず、満たしていない場合は nil のバイトスライスとエラーを返します。(Golang Documentation 2020). r 引数は相対メモリコストパラメータを定義し、ハッシュのブロックサイズをコントロールします。ここで推奨されている値は8です。 p 引数は相対 CPU コストパラメータで、推奨値は 1 です。(Isom 2015; Percival 2005a) keyLen 引数は、キーとして返されるバイト数を定義します。先ほどお話したようにこれは32バイトになります。
Result
DeriveKey 関数を作成したので、それに対応させるためにコードをアップデートする必要があります。それでは、以下のコードのようにしてみましょう。
// scrypt.gopackage mainimport ("crypto/aes""crypto/cipher""crypto/rand""crypto/sha256""encoding/hex""fmt""log""golang.org/x/crypto/scrypt")func Encrypt(key, data []byte) ([]byte, error) {key, salt, err := DeriveKey(key, nil)if err != nil {return nil, err}blockCipher, err := aes.NewCipher(key)if err != nil {return nil, err}gcm, err := cipher.NewGCM(blockCipher)if err != nil {return nil, err}nonce := make([]byte, gcm.NonceSize())if _, err = rand.Read(nonce); err != nil {return nil, err}ciphertext := gcm.Seal(nonce, nonce, data, nil)ciphertext = append(ciphertext, salt...)return ciphertext, nil}func Decrypt(key, data []byte) ([]byte, error) {salt, data := data[len(data)-32:], data[:len(data)-32]key, _, err := DeriveKey(key, salt)if err != nil {return nil, err}blockCipher, err := aes.NewCipher(key)if err != nil {return nil, err}gcm, err := cipher.NewGCM(blockCipher)if err != nil {return nil, err}nonce, ciphertext := data[:gcm.NonceSize()], data[gcm.NonceSize():]plaintext, err := gcm.Open(nil, nonce, ciphertext, nil)if err != nil {return nil, err}return plaintext, nil}func DeriveKey(password, salt []byte) ([]byte, []byte, error) {if salt == nil {salt = make([]byte, 32)if _, err := rand.Read(salt); err != nil {return nil, nil, err}}key, err := scrypt.Key(password, salt, 1048576, 8, 1, 32)if err != nil {return nil, nil, err}return key, salt, nil}func main() {var (password = []byte("mysecretpassword")data = []byte("our super secret text"))ciphertext, err := Encrypt(password, data)if err != nil {log.Fatal(err)}fmt.Printf("ciphertext: %s\n", hex.EncodeToString(ciphertext))plaintext, err := Decrypt(password, ciphertext)if err != nil {log.Fatal(err)}fmt.Printf("plaintext: %s\n", plaintext)}
それを実行してテストすることができるようになりました。
# First we need to get the scrypt package$ go get -u golang.org/x/crypto/scrypt$ go run scrypt.go
一部をアップデートしましたので、その様子を見ていきましょう。
key, salt, err := DeriveKey(key, nil)
Encrypt関数では、パスワードを渡してキーを作成します。それはキー関数に含まれます。salt 引数のように nil を渡します。これは、データを暗号化するのが初めてなので、ソルトを作成したいからです。
ciphertext = append(ciphertext, salt...)
さらに、Encrypt 関数では暗号文にソルトを追加します。
salt, data := data[len(data)-32:], data[:len(data)-32]
そして、暗号文にソルトを追加しているので、DeriveKey関数で使用するため、Decrypt関数で分割してスライスする必要があります。
key, _, err := DeriveKey(key, salt)
ここで表示されているように、ソルトをDeriveKey関数に渡すと、データを暗号化するために使用したキーを取得できるようになります。
まとめ
このように、Goでデータを暗号化して復号化するための2つの方法を作りました。まず、AES暗号化アルゴリズムを使用してデータを暗号化し、データを復号化するために使用するランダムキーを作成しました。その後、パスワードをキーとして使用できるようにコードを更新しました。これは、キーの派生関数を使用してパスワードをキーストレッチすることで実現しており、そのために scrypt を使用しています。この記事がみなさんのお役に立てれば大変嬉しく思います。また、私がリストアップしたソースを読んで見たり、他のソースをチェックしたりして、データを正しく安全に暗号化する方法の概要をしっかりと把握して頂けるとより理解が深まるでしょう。