hgot07 Hotspot Blog

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

openssl v3互換の暗号化をPerlで実現する方法・高速化編

前回のこれの続きです。日が開くと面倒くさくなるのでサクサクと。

hgot07.hatenablog.com

前回課題になっていた部分です。

  • 頻繁に起動されるスクリプトなので、暗号強度が少し下がっても、それなりの速度が欲しい。
  • 一定時間は同じ入力に対して同じ暗号文を生成したい。つまり、saltとIV (Initialization Vector)を使いまわせるようにしたい。

openssl v3 のコマンドは、以下を想定しています。

$ 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

暗号化のモードは、AES256 CBCモードです。同じsaltとIVが指定されると、同じ入力に対して同じ出力が得られます。つまり、処理時間のかかる PBKDF2 などの鍵導出は最初の一回だけにして、しばらくsaltとIVを使い回すことで、高速化できそうです。

 

[2025/7/1追記]

重要:IVの再利用は暗号化にとって致命的な問題を生むことがあります。モードによっては即時アウトです (AES-GCMなど)。CBCモードでも安全性が著しく低下するとされているので、用途を十分に検討すべきです。

saltとIVを明示的に与える暗号化

結論から言うと、正解例はこれです。PBKDF2の計算を先に行い、求めたsaltとIVを使ってCrypt::CBCで暗号化します

use MIME::Base64;
use Crypt::CBC;
use Crypt::PRNG;
use Crypt::PBKDF2;

my $salt = Crypt::PRNG::random_bytes(8);

my $pbkdf2 = Crypt::PBKDF2->new(
        hash_class => 'HMACSHA2',
        iterations => 10000,
        output_len => 32+16,
        salt_len => 8,
);
my $ekr = $pbkdf2->generate('hogefugapiyopiyo', $salt);
my @ek = split(/:/, $ekr);
my $key_iv = decode_base64($ek[3]);

my $key = substr($key_iv, 0, 32);
my $iv = substr($key_iv, 32, 16);

my $crypt = Crypt::CBC->new(
        -pass    => $key,
        -keysize => 32,
        -cipher => 'Crypt::Rijndael',
        -iv => $iv,
        -pbkdf  => 'none',
        -header => 'none',
);

my $cipher = "Salted\x5f\x5f".$salt.$crypt->encrypt( 'I love you, too!' );

前回と比べるとだいぶ長くなりました。

この出力 $cipher をBase64エンコードして cipher.txt に書き込めば、前述の openssl enc -d で復号できるはずです。

 

ハマったところの説明

ウェブ検索で見つかる記事や、GitHubのCopilot君では、なかなか解決のヒントに辿り着けませんでした。ハマりどころは以下のとおりです

まずCrypt::PBKDF2 の出力をそのまま Crypt::CBC に渡すコードをよく見かけるのですが、形式が違います。 Crypt::PBKDF2 は RFC 2307 形式で鍵を出力します。printしてみるとこんな感じです。

{X-PBKDF2}HMACSHA2+256:AAAnEA:aTLiFshBDHw=:33CUEBmlL9J42BVhbfH2i7X0NGtAwIQbkj0NJExNA0/nxnPYpNAHEbtfOzAt0FL1

コロンで区切られた最後のフィールドが、Base64エンコードされた鍵です。

次に、IVをどうしたらよいのかというところで、なかなか正解にたどり着けなくて苦労しました。ウェブには任意の乱数をIVとして使うコードばかりありますが、これではダメです。自前で生成した乱数saltと、前回のコードで Crypt::CBC の中で計算されたIVのペアを使うと、今回のコードの後半部分は正常に動きます。ところが、IVを別に与えると復号できないのです。

正解は「暗号化の鍵とIVをPBKDF2の中で一緒に求める」というものです。

わかるかーい!・・・・・(ノ`Д´)ノ彡┻━┻

AES256 (CBC) では、鍵長が32バイトです。これにIV分の16バイトを足した値を PBKDF2 で生成して、前半を鍵、後半をIVとして使います。

みっつめは、Crypt::CBC のencryptの出力にsaltが付いていないことです。仕方がないので自分でヘッダーを付けます。

 

PBKDF2::Tiny で高速化する

そうです。同じスクリプトの中で PBKDF2 の計算をしているので、前回のコードから高速化できていません。Ryzen 7 PRO 4750GのPCで動かしてみると、user時間が70~80 msもかかっています。realは100 msぐらいです。おっそ……

調べてみると、実は Crypt::PBKDF2 のモジュールをロードする部分でも時間を食われていることがわかりました。

手っ取り早く高速化するのに、PBKDF2::Tiny を使う方法があります。"Minimalist PBKDF2 (RFC 2898) with HMAC-SHA1 or HMAC-SHA2" だそうです。これを使うと、ロード時間が短く、コードもグッとシンプルになります。

require PBKDF2::Tiny;
        $key_iv = PBKDF2::Tiny::derive('SHA-256', 'hogefugapiyopiyo', $salt, 10000, 48);

user時間が40~50 ms、realが60 ms程度になりました。

 

saltとIVをKVSに保存して高速化してみる

スクリプトが呼ばれるたびにsalt/IVが新しくなってしまうので、当初の目的が達成できていません。毎回 PBKDF2 しなくても済むように、salt/IVを再利用することを考えます。といっても、ファイルに書き出すのでは遅いので、オンメモリの KVS (Key Value Store) などを使うのがよさそうです。Redis / Valkey でできそうですね。

というわけで、試してみたのですが、なんと Redis のモジュールをロードするのに60 msぐらいかかることが判明_('、3」∠)_ 本末転倒な結果に。

そこに神様がいました。Redis::Fast というモジュールを使うと、ロード時間が大幅に短縮されます。キャッシュしたsalt/IVを再利用することで、user時間が20 ms程度まで短縮できました。

 

(応用先が) 芽出たし、芽出たし。