OpenPGPはPGP(Pretty Good Privacy)をベースとした暗号化フォーマットです。 Go言語でもgolang.org/x/crypto/openpgp という準標準パッケージで提供されています。

PGPは公開鍵暗号としてメールの暗号化等でよく使用されますが、パスフレーズを用いた対称暗号として使用することもできますので、今回はこちらを紹介します。

TL;DR

  • 暗号化にはSymmetricallyEncrypt()を使用する
  • 復号にはReadMessage()を使用する
    • prompt次第で無限ループする恐れがあるので注意

暗号化

x/crypto/openpgpパッケージでパスフレーズを用いてファイルを暗号化するには、SymmetricallyEncrypt関数を使用します。 シグネチャは次のようになっています。

1
func SymmetricallyEncrypt(ciphertext io.Writer, passphrase []byte, hints *FileHints, config *packet.Config) (plaintext io.WriteCloser, err error)

順番に見て行きます。まずは引数から。

第一引数であるciphertextには、暗号化されたテキストを出力するio.Writerを与えます。*os.Fileなどを与えてもいいですが、*bytes.Bufferなどを与えてその後*os.Fileにコピーする方が良いでしょう。可読なテキストではなく、バイト列が出力されます。

第二引数のpassphraseはその名の通り、パスフレーズを与えます。

第三引数のhintsには暗号化するファイルのメタデータなどを含むことができますが、単純にnilを与えても良いです。

第四引数のconfigで暗号化方式や乱数エントロピーソース、圧縮アルゴリズムなどを設定することができます。設定しなければ乱数としてcrypto/rand.Readerが、ハッシュ関数としてSHA-256が、暗号化関数としてAES-128が、現在時刻としてtime.Nowが、RSAのビット数として2048がそれぞれ使用されます。圧縮はされません。

返り値は二値で、io.WriteClosererrorです。返り値のio.WriteCloserに暗号化したい内容を書き込むことで暗号化が行われます。必ずCloseする必要があるので忘れないように注意しましょう。

使用例

1
2
3
4
5
6
7
8
func encrypt(in io.Reader, out io.Writer, passphrase []byte) error {
    // omit error handling
    w, _ := openpgp.SymmetricallyEncrypt(out, passphrase, nil, nil)
    defer w.Close()
    
    io.Copy(w, in)
    return nil
}

復号

暗号化したファイルを復号するには、ReadMessage関数を使用します。Decrypt〜のような関数ではないので注意が必要でしょう。関数のシグネチャは次のようになっています。

1
func ReadMessage(r io.Reader, keyring KeyRing, prompt PromptFunction, config *packet.Config) (*MessageDetails, error)

こちらも順に見て行きましょう。

第一引数のio.Readerには暗号化されたファイルを与えます。読み込みですから、*os.Fileを直接与えてもいいかもしれません。

第二引数は復号に使用する鍵へのKeyRingですから、パスフレーズで暗号化した今回はnilを与えます。

第三引数であるpromptがこの関数の肝で、パスフレーズを返すコールバック関数を与えます。PromptFunctionの定義は次のようになっています。

1
type PromptFunction func(keys []Key, symmetric bool) ([]byte, error)

今回の用途の場合は引数を使用する必要はありません。基本的には単純にパスフレーズを返す関数とするか、標準入力等からパスフレーズを読み込んで返す、という関数として作成すれば良いでしょう。

一点だけ注意点があり、ドキュメントに次のような記載があります。

If the decrypted private key or given passphrase isn’t correct, the function will be called again, forever.

返されたパスワードが間違っていると、永遠にこの関数が呼ばれ続けます。そのため、二度目に呼ばれた時にエラーを返すように、なんらかの対策が必要です。 例えば、次のようなクロージャを作成しても良いでしょう。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
func promptFn(passphrase []byte) openpgp.PromptFunction {
    var called bool
    return func([]Key, bool) ([]byte, error) {
        if called {
            return nil, errors.New("the passphrase is invalid")
        }
        called = true
        return passphrase, nil
    }
}

第四引数のconfigは暗号化の時に使用したものと同じものを使用します。

返り値の*MessageDetailsに復号された内容が含まれています。この構造体はいくつかのメタデータを含みますが、実際のデータはMessageDetails.UnverifiedBodyというio.Readerに格納されています。

使用例

前節で紹介したpromptFnクロージャを使用した例です。

1
2
3
4
5
6
func decrypt(in io.Reader, out io.Writer, passphrase []byte) error {
    // omit error handling
    md, _ := openpgp.ReadMessage(in, nil, promptFn(passphrase), nil)
    io.Copy(out, md.UnverifiedBody)
    return nil
}