OpenSSLで共通鍵暗号を使う場合の鍵の指定


はじめに

背景

今回は、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バイトのようです。

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..

では肝心の鍵・IVは? というと、パスワードを知っているという前提で、enc サブコマンドの中で -P オプションにより得ることができるようになっています。

鍵・IVの確認
$ 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を使った暗号化は次の通りです。

鍵・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があると却って邪魔になるので、パスワードベースで作成した暗号データの場合には、その分を取り除く必要があります。

鍵・IVを指定した復号
$ 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が生成されていることは、以下のようなコマンドで確認することができます。

鍵・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での復号実装例
<?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を使うことが推奨です。ただそのやり方にも応用が利くと思いますので(多分)、備忘録としてデフォルトの方式をまとめました。
ちょっとした小ネタですが、お役に立てば幸いです。