Go言語でOpenPGPの共通鍵暗号化と復号をストリーミングしながら行う
やりたいこと
Go言語で平文のio.Readerを与えて、暗号化後のio.Readerを得ることをしたい。 復号であれば、暗号化のio.Readerを与えて、復号後のio.Readerを得たい。
ストリーミングできることで無限のデータに対して暗号化・復号が可能。
時間的・空間的に効率が良い。
割と探してもなかなか見つからなかった。いくつかのソースからコードを読み解いて本家のテストコードなどを実際に動くものを作ったのでその実装をここに載せる。
実装
すぐ再利用できるように関数にした。
openpgpSymmetricallyEncrypt()で暗号化。
openpgpSymmetricallyDecrypt()で復号。
code:go
import (
"golang.org/x/crypto/openpgp"
"os"
"io"
)
func openpgpSymmetricallyEncrypt(plain io.Reader, passphrase []byte) io.Reader {
pr, pw := io.Pipe()
go func() {
defer pw.Close()
w, err := openpgp.SymmetricallyEncrypt(pw, passphrase, nil, nil)
if err != nil {
panic(err)
}
_, err = io.Copy(w, plain)
if err != nil {
panic(err)
}
w.Close()
}()
return pr
}
func openpgpSymmetricallyDecrypt(encrypted io.Reader, passphrase []byte) (io.Reader, error) {
md, err := openpgp.ReadMessage(encrypted, nil, func(keys []openpgp.Key, symmetric bool) ([]byte, error) {
return passphrase, nil
}, nil)
if err != nil {
return nil, err
}
return md.UnverifiedBody, nil
}
使用例
使い方の例は以下。
やっていることは、平文を標準入力から取得して、暗号化して、復号して、標準出力からだす。
つまり見た目上は標準入力がそのまま標準出力流れるように見える。
code:main.go
func main() {
plain := os.Stdin
output := os.Stdout
passphrase := "my passphrase"
encrypted := openpgpSymmetricallyEncrypt(plain, []byte(passphrase))
decrypted, _ := openpgpSymmetricallyDecrypt(encrypted, []byte(passphrase))
io.Copy(output, decrypted)
}
例えば、ストリーミングされることを確認するには上記のファイルをmain.goとしてseq inf | go run main.go とすれば良い。ストリーミングされて暗号化と復号が同時にされるため、無限のバイト列にも対応できていることが確認できる。
上記のopenpgpSymmetricallyEncrypt()がgpgコマンドとの互換性があることも確認した。 inputをstrings.NewReader("hello")にして、encryptedをos.Create("enc.txt.gpg")に書き出して、gpg enc.txt.gpgして"my passphrase"を指定するとちゃんと"hello"が得られた。
上記のopenpgpSymmetricallyDecrypt()がgpgコマンドとの互換性があることも確認した。 gpg -c mydata.txtしたものをopenpgpSymmetricallyDecrypt()で復号できた。
参考
Go言語のopenpgpの使い方が書かれている例。 ライブラリ自体のテストコード。これで復号の処理方法がわかる。
おまけ
openpgp.SymmetricallyEncrypt()の定義から使用法を見いだすのはすこし非直感的。
以下がその定義。
code:go
func SymmetricallyEncrypt(ciphertext io.Writer, passphrase []byte, hints *FileHints, config *packet.Config) (plaintext io.WriteCloser, err error)
暗号化なので引数に平文を渡して、戻り値が暗号化されたものになりそうに思う。
だが、引数に暗号化結果を書き込むためのio.Writerを与えて、戻り値に平文を書き込むためのio.Writerが返る実装になっている。
それを直感的(主観)にするために、openpgpSymmetricallyEncrypt()ではio.Pipe()を使ってる。
JavaのPipedInputStreamやPipedOutputStreamに似ている。 あらかじめciphertextとしてpwを与えてpwに暗号化結果が流れ込むようにして、
それに対応するprを戻り値にすることで暗号化結果のio.Readerを得ている。
これで平文のio.Readerを引数に与えて、暗号化後のio.Readerが戻り値として返るようになっている。
openpgp.SymmetricallyEncrypt()があってもそれに対応するDecryptのついた関数がなく対称性がないので多少直感的ではないと思った。
openpgpSymmetricallyEncrypt()のgoの中のerrはpanicしているけど、これをうまいことハンドリングするのはどうするべきなのかはちょっと放置している。