はじめに
背景
今回は、OpenSSLの「共通鍵暗号」の機能、中でも鍵の取り扱いに焦点をあてます。
OpenSSLは、ライブラリとして各種言語から機能を呼び出すこともできますが、それ自身が暗号化等の機能を使えるツールセットにもなっています。
そうすると、「opensslコマンドで暗号化して作ったデータを各種言語で復号したい」といった需要が一部出てきたりするわけですが、鍵等のデータの取り扱いを意識しないと、大抵うまく行きません。
今回は、「暗号化に必要な鍵等のデータ」に焦点をあてていきたいと思います。なお、PBKDF2については取り扱いません。
検証環境
ここでは、Ubuntu18/WSL1(Win10)付属のOpenSSL 1.1.1を検証環境としていきます。
ツールによる暗号化
単純な暗号化
opensslコマンドは、サブコマンド enc
により、共通鍵暗号による暗号化や復号を行うことができます。なので、単純に例えば 256bit AESで暗号化する場合、次のようなコマンドになります。
$ openssl enc -aes256 -in abc.txt -out enc.dat
enter aes-256-cbc encryption password: ******
Verifying - enter aes-256-cbc encryption password: *******
*** WARNING : deprecated key derivation used.
Using -iter or -pbkdf2 would be better.
この場合 -in
で指定したのが平文データ、-out
で指定したのが暗号文保存先です。
そして、******
で表現してますが ( 実際は表示されません )、パスワードを入力しています。
※最後の2行のメッセージは、今回取り上げないPBKDF2を使えという内容なので、以降無視し、コマンド実行例からも省略します。
ちなみに、このパスワードは -pass pass:文字列
の形式で指定することもできます。今度は逆に復号してみます。
$ openssl enc -d -aes256 -pass pass:mypassword -in enc.dat
abcdefghijklmnopqrstuvwxyz
$ cat abc.txt
abcdefghijklmnopqrstuvwxyz
今回は mypassword
というパスワードを使っているという想定です。
この実行例のように、復号して元のデータが得られていることが分かります。
なお、今回は取り扱いませんが、上記のような実行例では大抵暗号データがバイナリになりますので、base64により印字可能なデータにする -a
オプションというのもあります。
鍵データの正体
さて、前項のようにパスワードを指定して暗号化・復号を行う例を見ますと、「共通鍵暗号は、共通の鍵を使う…。てことは!?」とある程度の人が、「このパスワードこそ鍵なんだ!」と思うこともあるようです。
しかし、「鍵データに使えるデータ長は方式毎に決まっている」 「鍵だけではなくIV(初期化ベクトル)というデータも大抵必要になる」という2点から、「パスワード=鍵」という考えは誤りです。
実際には、opensslが与えられたパスワードから鍵・IVを生成しているというのが正解です。
ただ、パスワードをそのまま使うと同じパスワードを使用することであっさり同じ鍵・IVが生成されてしまうので、saltと呼ばれるデータも併用します。このsaltは、ツールのオプションでも指定できますが、デフォルトではツールがランダムに生成します。
saltの情報が失われると、パスワードから鍵・IVの生成ができないため、ツールの作成した暗号文に埋め込まれます。以下のコマンドでダンプすると、先頭に Salted__
とあり、その次の8バイト分のsaltを確認することができます。
※方式により長さが変わる可能性もありますが、大抵8バイトのようです。
$ xxd enc.dat
00000000: 5361 6c74 6564 5f5f 3bde ed65 e2ff 50b1 Salted__;..e..P.
00000010: 0f41 c5b9 0dda 672d 1e20 3aa1 979b e267 .A....g-. :....g
00000020: edd3 68f0 c25c 7971 b036 ef10 1b4c c18a ..h..\yq.6...L..
では肝心の鍵・IVは? というと、パスワードを知っているという前提で、enc サブコマンドの中で -P
オプションにより得ることができるようになっています。
$ openssl enc -P -d -aes256 -pass pass:mypassword -in enc.dat
salt=3BDEED65E2FF50B1
key=40F59A2C3AE6C310E4C45DE54A8041BA869865C1DF6B7D1D20D5986248580530
iv =8816B5594C603BFF66CAE73B44CD3D7D
saltに加えて、鍵(key)およびIVが16進ダンプとして出力されます。
※ダンプだとものすごく長いデータに見えますが、この場合の鍵,IVの実データは32バイト,16バイトになります。
これが、暗号化・復号で内部的に使われている鍵・IVということです。
鍵・IVの直接指定
ここまででお気付きかと思いますが、なにもパスワードを使わなくても直接に鍵・IVを指定して処理を行うことも可能です。
※パスワードの方が扱いは楽ですが。
前述の実行例でのパスワード・saltに相当する鍵・IVを使った暗号化は次の通りです。
$ openssl enc -aes256 -K 40F59A2C3AE6C310E4C45DE54A8041BA869865C1DF6B7D1D20D5986248580530 -iv 8816B5594C603BFF66CAE73B44CD3D7D -in abc.txt -out enc2.dat
つまり、-K
および-iv
オプションで、直接値を指定してしまう、という方法になります。
パスワード指定でできたファイルと比べると、先頭のsalt部分を除いて一致していることが分かります。
$ xxd enc.dat
00000000: 5361 6c74 6564 5f5f 3bde ed65 e2ff 50b1 Salted__;..e..P.
00000010: 0f41 c5b9 0dda 672d 1e20 3aa1 979b e267 .A....g-. :....g
00000020: edd3 68f0 c25c 7971 b036 ef10 1b4c c18a ..h..\yq.6...L..
$ xxd enc2.dat
00000000: 0f41 c5b9 0dda 672d 1e20 3aa1 979b e267 .A....g-. :....g
00000010: edd3 68f0 c25c 7971 b036 ef10 1b4c c18a ..h..\yq.6...L..
復号する時も、同様に -K
,-iv
の指定で処理できます。ただ、先頭のsaltがあると却って邪魔になるので、パスワードベースで作成した暗号データの場合には、その分を取り除く必要があります。
$ openssl enc -d -aes256 -K 40F59A2C3AE6C310E4C45DE54A8041BA869865C1DF6B7D1D20D5986248580530 -iv 8816B5594C603BFF66CAE73B44CD3D7D -in enc2.dat
abcdefghijklmnopqrstuvwxyz
$ tail -c +17 enc.dat | openssl enc -d -aes256 -K 40F59A2C3AE6C310E4C45DE54A8041BA869865C1DF6B7D1D20D5986248580530 -iv 8816B5594C603BFF66CAE73B44CD3D7D
abcdefghijklmnopqrstuvwxyz
ツール以外での処理を見据えて
パスワード→鍵・IVの法則の必要性
ここまでで、パスワードは鍵そのものではなく、処理に実際に使う鍵・IVの2種類のデータがあることを見てきたわけですが、opensslコマンドを使うだけなら別に気にする必要はありません。
しかし、暗号化したデータを各種プログラミング言語から扱いたいと言うような場合、OpenSSLライブラリを使ったとしても、鍵・IVの存在を意識しなければ処理できません。
例えば、RubyのOpenSSLライブラリの場合、以下のように key, iv が分かっている前提で復号処理を行うことになっていますし、
# 復号化器を作成する
dec = OpenSSL::Cipher.new("AES-256-CBC")
dec.decrypt
# 鍵とIVを設定する
dec.key = key
dec.iv = iv
PHPのOpenSSL関数の場合も、事情は同じです。
※$passphrase
となっていて紛らわしいのですが、これはパスワードではなく ( バイナリデータとしての ) 鍵を意味します。
openssl_decrypt(
string $data,
string $cipher_algo,
string $passphrase,
int $options = 0,
string $iv = "",
string $tag = "",
string $aad = ""
): string|false
パスワードからの鍵・IVの生成
さて、では肝心のパスワードからの鍵・IVの生成ですが、これはOpenSSLの内部ルーチンEVP_BytesToKeyに対して、count
引数を1とした処理を行っていることが分かっています。
このルーチンは、指定されたハッシュ関数を用いてデータ変換を行い、鍵等に使えるデータを作り出していくものです。以下、処理の概略を示します。
- パスワードとsalt(バイナリデータ)を連結し、そのハッシュ値 H0 を計算する。
- H0とパスワードとsaltを連結し、そのハッシュ値 H1 を計算する。
- H1と…と言うように、必要なデータが揃うまでハッシュ値計算を繰り返す。
※計算したハッシュ値のデータ量合計が、鍵・IVのデータ量を賄えれば十分 - H0, H1, … を連結したデータの内、先頭データを鍵として切り出す。
- 残りの先頭データをIVとして切り出す。
このハッシュ関数はツールのオプション -md
によって指定できます。デフォルトは sha256 であり、ハッシュ値は32バイト、256bit AESの鍵32バイトとIV 16バイトを賄うには、2回のハッシュ計算で十分です。
この計算で鍵・IVが生成されていることは、以下のようなコマンドで確認することができます。
$ ( echo -n mypassword; xxd -p -r <<< 3BDEED65E2FF50B1 ) | openssl dgst -sha256
(stdin)= 40f59a2c3ae6c310e4c45de54a8041ba869865c1df6b7d1d20d5986248580530
$ (
> ( echo -n mypassword; xxd -p -r <<< 3BDEED65E2FF50B1 ) | openssl dgst -sha256 -binary
> echo -n mypassword; xxd -p -r <<< 3BDEED65E2FF50B1
> ) | openssl dgst -sha256
(stdin)= 8816b5594c603bff66cae73b44cd3d7dcbe2c368a284a9264ac689cebcad6bb5
$ openssl enc -P -d -aes256 -pass pass:mypassword -in enc.dat
salt=3BDEED65E2FF50B1
key=40F59A2C3AE6C310E4C45DE54A8041BA869865C1DF6B7D1D20D5986248580530
iv =8816B5594C603BFF66CAE73B44CD3D7D
この例の16進ダンプ出力のように、1回目のハッシュ値計算の値がそのまま key に、2回目のハッシュ値計算の先頭16バイト ( 16進ダンプで32文字分 ) が iv に一致していることが分かります。
なお、salt の値も16進ダンプで得られている状態なので、xxd コマンドでバイナリに変換して使っています。
PHPでの実装例
最後におまけして、PHPでの実装例です。
暗号データの先頭からsaltを読み取り、パスワードを併せて鍵・IVを生成、しかる後にsalt部分を除いた暗号データを、鍵・IVを指定して復号しています。
※Teratailでの回答で示したコードの焼き直しです。
<?php
$filename="enc.dat";
$pass="mypassword";
$fh=fopen($filename,"r");
$enc=fread($fh,filesize($filename));
$salt=substr($enc,8,8);
$key=openssl_digest($pass.$salt,"sha256",true);
$iv=substr(openssl_digest($key.$pass.$salt,"sha256",true),0,16);
echo openssl_decrypt(substr($enc,16),"aes256",$key,OPENSSL_RAW_DATA,$iv);
?>
終わりに
このパスワードから鍵・IVを生成する部分、方式としては弱いものなので、本来はPBKDF2を使うことが推奨です。ただそのやり方にも応用が利くと思いますので(多分)、備忘録としてデフォルトの方式をまとめました。
ちょっとした小ネタですが、お役に立てば幸いです。