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

# はじめに
## 背景
今回は、OpenSSLの「共通鍵暗号」の機能、中でも**鍵の取り扱い**に焦点をあてます。
OpenSSLは、ライブラリとして各種言語から機能を呼び出すこともできますが、それ自身が暗号化等の機能を使えるツールセットにもなっています。

そうすると、「opensslコマンドで暗号化して作ったデータを各種言語で復号したい」といった需要が一部出てきたりするわけですが、**鍵等のデータの取り扱い**を意識しないと、大抵うまく行きません。

今回は、「暗号化に必要な鍵等のデータ」に焦点をあてていきたいと思います。なお、**PBKDF2については取り扱いません**。

## 検証環境
ここでは、Ubuntu18/WSL1(Win10)付属のOpenSSL 1.1.1を検証環境としていきます。

# ツールによる暗号化
## 単純な暗号化
opensslコマンドは、サブコマンド `enc` により、共通鍵暗号による暗号化や復号を行うことができます。なので、単純に例えば 256bit AESで暗号化する場合、次のようなコマンドになります。

```shell-session:単純な暗号化
$ 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:文字列` の形式で指定することもできます。今度は逆に復号してみます。

```shell-session:パスワードを指定して復号
$ 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バイトのようです。

```shell-session: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` オプションにより得ることができるようになっています。

```shell-session:鍵・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を使った暗号化は次の通りです。

```shell-session:鍵・IVを指定した暗号化
$ openssl enc -aes256 -K 40F59A2C3AE6C310E4C45DE54A8041BA869865C1DF6B7D1D20D5986248580530 -iv 8816B5594C603BFF66CAE73B44CD3D7D -in abc.txt -out enc2.dat
```

つまり、`-K`および`-iv`オプションで、直接値を指定してしまう、という方法になります。
パスワード指定でできたファイルと比べると、先頭のsalt部分を除いて一致していることが分かります。

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

```shell-session:鍵・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ライブラリ](https://docs.ruby-lang.org/ja/latest/class/OpenSSL=3a=3aCipher.html)の場合、以下のように key, iv が分かっている前提で復号処理を行うことになっていますし、

>
```
# 復号化器を作成する
dec = OpenSSL::Cipher.new("AES-256-CBC")
dec.decrypt
# 鍵とIVを設定する
dec.key = key
dec.iv = iv
```

[PHPのOpenSSL関数](https://www.php.net/manual/ja/function.openssl-decrypt.php)の場合も、事情は同じです。
※`$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](https://github.com/openssl/openssl/blob/master/doc/man3/EVP_BytesToKey.pod)に対して、`count`引数を1とした処理を行っていることが分かっています。

このルーチンは、指定されたハッシュ関数を用いてデータ変換を行い、鍵等に使えるデータを作り出していくものです。以下、処理の概略を示します。

* パスワードとsalt(バイナリデータ)を連結し、そのハッシュ値 H0 を計算する。
* H0とパスワードとsaltを連結し、そのハッシュ値 H1 を計算する。
* H1と…と言うように、必要なデータが揃うまでハッシュ値計算を繰り返す。  
  ※計算したハッシュ値のデータ量合計が、鍵・IVのデータ量を賄えれば十分
* H0, H1, … を連結したデータの内、先頭データを鍵として切り出す。
* 残りの先頭データをIVとして切り出す。

このハッシュ関数はツールのオプション `-md` によって指定できます。デフォルトは sha256 であり、ハッシュ値は32バイト、256bit AESの鍵32バイトとIV 16バイトを賄うには、2回のハッシュ計算で十分です。

この計算で鍵・IVが生成されていることは、以下のようなコマンドで確認することができます。

```shell-session:鍵・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での回答](https://teratail.com/questions/366306#reply-497776)で示したコードの焼き直しです。

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