hgot07 Hotspot Blog

主に無線LANや認証連携などの技術についてまとめるブログです。ネコは見る専。

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を使いまわせるようにするには、さらに困難がありました。これについては次の機会に。

 

おしまい