openssl v3互換の暗号化をPerlで実現する方法
とある技術の実装 (インプリメンテーション) において、Perlスクリプトで暗号化を行う必要が生じました。opensslコマンドでも同じデータを扱えるようにしたかったので、色々と調べてみました。意外とハマりどころが多かったので、メモを残します。openssl 1系ではなく、3系です。
やりたいこと:
- openssl enc -aes256 と互換性のある暗号化・復号をPerlスクリプトで行いたい。
- もちろん既存ライブラリを使って楽をしたい、というより、ヘタに自前で実装するよりも安全なのでそうすべき。
- 頻繁に起動されるスクリプトなので、暗号強度が少し下がっても、それなりの速度が欲しい。
- 一定時間は同じ入力に対して同じ暗号文を生成したい。つまり、saltとIV (Initialization Vector)を使いまわせるようにしたい。(今回は省略、別記事を計画)
まずはopensslの使い方
暗号化と復号のコマンドは、こんな感じです。
$ openssl enc -pbkdf2 -salt -aes256 -base64 -k hogefugapiyopiyo -e -in plain.txt -out cipher.txt
$ openssl enc -pbkdf2 -salt -aes256 -base64 -k hogefugapiyopiyo -d -in cipher.txt -out decrypted.txt
ウェブ検索で見つかる古めの記事には -pbkdf2 がなかったりします。openssl v3では、-pbkdf2 がないとこんな風に怒られてしまいます。
*** WARNING : deprecated key derivation used.
Using -iter or -pbkdf2 would be better.
このたいそうな名前のオプションは一体何なのかというと、鍵導出関数 (KDF, Key Derivation Function) のアルゴリズムとして PBKDF2 を使えという意味です。パスワードをそのまま使うと安全性が低いので、ハッシュ値を新たな鍵として暗号化に使うというのが、現代的なやり方です。
なお、Diffie-Hellman鍵共有で導出された鍵も、ハッシュ化してから使うことが推奨されています。
Perlで実装するには
Crypt::CBCを使うのがメジャーなようで、ウェブ検索でよく引っかかります。ところが、どれを試してみても、エラーになるか、openssl v3で展開できないデータが出てくるかで、結構苦労しました。
GitHubのCopilot君にも色々と聞いてみましたが、なかなか解決のヒントに辿り着けませんでした。
とりあえず、手っ取り早く暗号化するなら、正解例はこれです。
use Crypt::CBC;
use Crypt::PRNG;
my $salt = Crypt::PRNG::random_bytes(8);
my $crypt = Crypt::CBC->new(
-pass => 'hogefugapiyopiyo',
-keysize => 32,
-cipher => 'Crypt::Rijndael',
-salt => $salt,
-iter => 10000,
-pbkdf => 'pbkdf2',
-header => 'salt',
);
my $cipher = $crypt->encrypt( 'I love you!' );
出力の $cipher はバイナリなので、必要に応じてBase64に変換すればよいです。これを cipher.txt に書き込めば、opensslで復号できるはずです。
ivはどこへ行った? はい、モジュールの内部で自動的に生成されます。
以下、ハマったところを説明していきます。
鍵導出のために Crypt::PBKDF2 を使うコードが、あちこちで例示されています。ところが、少なくとも現在の Crypt::CBC では、Crypt::PBKDF2 の出力をそのまま受け取ることができなくて、エラーになってしまいます。
次に、-cipher => 'Crypt::OpenSSL::AES' と書かれている解説もよくあるのですが、openssl v3互換にするなら Crypt::Rijndael にしなければなりません。
この例では、生のパスワードを -pass に渡していますが、心配には及びません。中できちんと PBKDF2 で鍵導出が行われます。
saltを求めるのに、Copilot君はrand関数と英数字だけを使った例を出してきました。saltはバイナリで与えることができるので、できるだけまともな乱数生成器を使って、バイナリデータで生成すべきでしょう。
高速化してみる
こいつ、遅いぞ!
そうです。100バイト程度のデータを暗号化するのに、スクリプト全体で100 ms以上かかりました。
暗号化の部分が遅いのではなくて、CPU時間を食っているのは PBKDF2 の計算です。10,000回 (暗号界の最低限の推奨回数で、opensslのデフォルト値) も回していれば、それは時間食うでしょうねぇ。
どうしても高速化したければ、強度の低下に目をつぶって、以前の実装で使われていた1000回ぐらいまで落とす方法があります。opensslコマンドでは -iter 1000 のように指定できます。自己責任で。
以上、openssl互換は実現できましたが、saltとIVを使いまわせるようにするには、さらに困難がありました。これについては次の機会に。
おしまい