You have 2 free member-only stories left this month.
Goでのパスワードを使用したデータ暗号化(前半)
Introduction
データを暗号化する場合、通常はそのデータを復号化できるランダムキーを作成します。特定のケースにおいては、パスワードのようにデータを復号化するためにユーザが指定したキーを使用する場合もあります。しかし、暗号化アルゴリズムに使用されるキーは、通常、少なくとも32バイトである必要があります。しかし、私たちのパスワードではその基準を満たしていない可能性が高いので、それを解決する方法が必要になります。最近、ずっと私は解決策を模索していたので、この記事ではその方法をご紹介します。しかし、その前に、私たちは重要な事を押さえておきましょう。
免責事項: 私は暗号化の専門家ではありませんが、この記事で提供されている解決策にたどり着くために使用したソースについて色々な見解を述べています。私はみなさんがよりよく理解するために、これらのソースを読んだり、見たりすることを強くお勧めします。
OKです。少し話がそれましたが、始めましょう!
Encrypt
まずはデータを暗号化することから始めましょう。まず、キーとデータの引数を受け取るEncrypt関数を作成します。これを元に、キーを使って復号できるデータを暗号化します。まず、32個のランダムなバイトを使ってキーを生成し、後でパスワードに置き換えます。次に生成されたキーでデータを暗号化するコードを示しておきます。
import ("crypto/aes""crypto/cipher""crypto/rand")func Encrypt(key, data []byte) ([]byte, error) {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)return ciphertext, nil}
では、コードを確認して、何が行われているのかを見てみましょう。
func Encrypt(key, data []byte) ([]byte, error)
まず、Encrypt関数を作成することから始めます。その関数でキーとデータ引数を受け取ります。データ引数として、io.Readerの代わりにバイトスライスを使用します。一方、io.Readerを使用すると、io.Readerインターフェースを実装している他のすべてのタイプでEncrypt関数を使用することができるようになります。(Ryer 2015) しかし、データのストリームであるio.Readerの性質上、暗号文を復号化したいときには、その全体を見る必要があります。解決策としては、ストリームを個別のチャンクに分割することが考えられますが、これでは問題がより複雑化する可能性があります。.1 (Isom 2015)
blockCipher, err := aes.NewCipher(key)
提供したキーをベースにしたブロック暗号を初期化しています。ここでは、AES34 (Advanced Encryption Standard:高度暗号化標準) 暗号化アルゴリズムを実装した crypto/aes2 パッケージを使用しています。AES はシンメトリックキー暗号化(対称鍵暗号)アルゴリズムで、現代のユースケースでも十分に安全です。さらに、AES はほとんどのプラットフォームでハードウェアアクセラレーションを使用しているので、かなり高速で使用できます。(Tankersley 2016)
gcm, err := cipher.NewGCM(blockCipher)
ここではブロック暗号を特定のモードでラップしています。これは cipher.Block インターフェイスを直接使うべきではないからです。これはブロック暗号が16バイトのデータを暗号化するだけで、それ以上は何もしないからです。つまり、blockCiper.Encrypt() を呼び出しても最初の 16 バイトしか暗号化されないのです。そこで、その上にブロック暗号をラップする何かが必要になります。それらのことをモードと呼びます。ここでもいくつかのモードがありますが、ここでは「Galois Counter Mode (GCM) 5:認証子付き暗号モード」 を使用します。
GCMだけが認証された暗号化を提供しており、cipher.AEADインターフェイス(Authenticated Encryption with Associated Data:認証付き暗号)6を実装しています。認証された暗号化とは、あなたのデータが機密、秘密、暗号化されるだけでなく、改ざん防止にもなることを意味します。誰かが暗号を改ざんしても、それを正当に復号化することはできません。認証された暗号化を使用している場合にもし誰かがあなたのデータに手を加えても、復号化に失敗するだけです。(Tankersley 2016; Isom 2015)
nonce := make([]byte, gcm.NonceSize())if _, err = rand.Read(nonce); err != nil {return nil, err}
バイトを暗号化する前に、ランダム化された nonce を生成する必要があります。そして、その長さはGCMが指定します。nonceとは「一度だけ使用された数」を意味していて、繰り返し使用してはならないデータであり、特定のキーと組み合わせて一度だけ使用されます。つまり、キーとnonceの組み合わせを2回以上繰り返してはいけないということです。しかし、それをどうやって記録しているのでしょうか?nonce に十分に大きな数を使えば、このユースケースでは問題ないでしょう。(Isom 2015; Viega and Messier 2003, 134–35) これを実行するためにGoのcrypto/randパッケージを使用して、ランダム化されたバイトをnonceのバイトスライス7に読み込ませます。
encryptedData := gcm.Seal(nonce, nonce, data, nil)
データを暗号化する際に使用する nonce は、復号化する際にも必要になります。そのため、復号中にnonce を参照できるようにする必要があり、暗号化されたデータに追加するのが戦略の一つです。この例では、暗号化されたデータに nonce を追加します。この例では、暗号化されたデータにnonceを前置します。nonceをSeal関数の第一引数dstとして渡すことで、暗号化されたデータがそれに8追加されます。nonceはシークレットで必要はなく、単にユニークであれさえすればいいので、これが可能なのです。(Tankersley 2016)
Decrypt
これでデータを暗号化できるようになったので、Decrypt関数を実装してみましょう。
import ("crypto/aes""crypto/cipher")func Decrypt(key, data []byte) ([]byte, error) {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}
もう一度、コードを確認してみましょう。大体はEncrypt関数と同じコードなので、異なる部分を確認してみましょう。
nonce, ciphertext := data[:gcm.NonceSize()], data[gcm.NonceSize():]
前のセクションで、gcm.Seal を使ってデータに nonce を付加して暗号文を作成したことを覚えていますか?今度はそれらを独立して使えるように分割する必要があります。そして、gcmが提供するnonceのサイズに基づいてデータをスライスすることで、これらの部分を作成していきます。
plaintext, err := gcm.Open(nil, nonce, ciphertext, nil)
それではgcmを使います。
Open を使って暗号文を平文9に復号化します。
Key
Encrypt関数とDecrypt関数の両方にキーを渡していますが、まだ作っていないので、それをやってみましょう。
import ("crypto/rand")func GenerateKey() ([]byte, error) {key := make([]byte, 32)_, err := rand.Read(key)if err != nil {return nil, err}return key, nil}
ここでは、Goのcrypto/randパッケージを使ってランダムキーを生成しています。AESの場合は32バイトの長さのキーが必要なので、サイズ32のバイトスライスを作成します。次に、rand.Read()でランダムなバイト数10でスライスを埋めます。
これでいくつかのデータを暗号化して復号化するのに十分な量になったので、それをまとめてテストしてみましょう。
// crypto.gopackage mainimport ("crypto/aes""crypto/cipher""crypto/rand""encoding/hex""fmt""log")func Encrypt(key, data []byte) ([]byte, error) {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)return ciphertext, nil}func Decrypt(key, data []byte) ([]byte, error) {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 GenerateKey() ([]byte, error) {key := make([]byte, 32)_, err := rand.Read(key)if err != nil {return nil, err}return key, nil}func main() {data := []byte("our super secret text")key, err := GenerateKey()if err != nil {log.Fatal(err)}ciphertext, err := Encrypt(key, data)if err != nil {log.Fatal(err)}fmt.Printf("ciphertext: %s\n", hex.EncodeToString(ciphertext))plaintext, err := Decrypt(key, ciphertext)if err != nil {log.Fatal(err)}fmt.Printf("plaintext: %s\n", plaintext)}
次のコードを使ってこの例を走らせることができます:
$ go run crypto.go
これで、ランダム化されたキーを使って データを暗号化したり復号化したりできるようになりました。最高ですよね。データを暗号化したり復号化したりできるキーが手に入りました。しかし、それはキーが今私たちのパスワードになり、自分でそれを選択することができなかったことを意味し、さらに、それは32バイトの長さを持っているということを意味します。
しかし、この記事の冒頭で述べたように、私たちは独自のキー、つまり私たちが使用することを選択したパスワードを提供することによって、データを暗号化したり復号化したりできるようにしたいと思います。それについては次のセクションで説明します。
Orangesys.ioでは、kuberneteの運用、DevOps、監視のお手伝いをさせていただいています。ぜひ私たちにおまかせください。