OpenSSLの暗号文をJava/Perl/Rubyで開く
秘密鍵やプライベートな情報などを秘匿するためにパスワードでデータを暗号化・復号したい場合があります。このとき、暗号化と復号するアプリケーションが同じであれば簡単ですが、例えばCで暗号化してJava、Perl、Rubyで復号するといった風に異なるプラットフォームで暗号データをやりとりする場合には、いくつか気 をつけなければいけないポイントがあります。
使用するハッシュ関数や疑似乱数生成器が異なっていたり、変換処理のくり返し回数が異なったりすると、同じパスワードとsaltを入力しても同じ鍵を得られず正しく復号できません。そのため使用する鍵導出関数の仕様とそのパラメータも、暗号の相互運用のためには重要な情報と言えます。
さらにPKCS #5では鍵導出関数の他にも、
OpenSSLによる暗号化
OpenSSLはWebサーバのSSL/TLSサポートに利用されますが、その他にも付属しているopensslコマンドから基本的な暗号アルゴリズムを利用できます。次のような簡単なコマンドで、パスワードを使ってデータを暗号化したり復号したりすることができます:$ echo 'Hello World!' | openssl enc -e -aes-128-cbc > cipher.txt enter aes-128-cbc encryption password: ********この例では、文字列 "Hello World!"を暗号化してファイル cipher.txt に出力しています。また、暗号化に際して次のパラメータを使用しました:
- 暗号アルゴリズムはAES
- 秘密鍵の長さは128bit
- 暗号利用モードはCBCモード
- 秘密鍵はパスワードをもとにopensslコマンドが自動生成
$ openssl enc -d -aes-128-cbc < cipher.txt enter aes-128-cbc decryption password:******** Hello World!あたりまえですが、暗号化したときに使ったパスワードとパラメータは、復号するときにも同じものを使用します。 ちなみに、この場合のopensslコマンドは次のような書式で暗号文を出力します。
'Salted__' + Salt(8 byte) + CipherText(16 x n byte)ヘッダとして文字列 "Salted__"、続いてランダムに生成された8バイトのsalt、最後に暗号文本体が続きます。
別のプラットフォームで復号する
それでは、暗号化したデータをPerl, Ruby, Javaで復号する例を順番に見てみます。Perlで復号する
Perlにはopensslコマンドと暗号文をやりとりできるCrypt::CBCモジュールがあります。こんな感じ:
#!/usr/bin/perl
use strict;
use Crypt::CBC 2.13;
my $pbe = Crypt::CBC->new(
-key => 'my secret password',
-cipher => 'Crypt::Rijndael',
-keysize => 128/8,
# -salt => 1, # use random salt
# -header => 'salt', # use OpenSSL-compatible salted header
# -padding => 'standard', # use PKCS#5 PBES1|2 padding
);
my $cipher_text = scalar <>;
my $plain_text = $pbe->decrypt($cipher_text);
print $plain_text;
Crypt::CBCは-keyで与えられたパスワードを、暗号アルゴリズムが要求する形式の秘密鍵に変換して使用します。
Rubyで復号する
RubyにはOpenSSLのcryptoライブラリをwrapしたOpenSSL::Cipherクラスがあります。PerlのCrypt::CBCと異なり、明示的にpkcs5_keyivgenメソッドでパスワードを秘密鍵に変換して使用します。
#!/usr/bin/ruby
require 'openssl'
include OpenSSL::Cipher
# 'Salted__' | SALT(0..7) | CIPHER-TEXT
@cipher_text = gets.unpack("a8a8a*")
pbes = Cipher.new("aes-128-cbc")
pbes.pkcs5_keyivgen(
"my secret password",
@cipher_text[1], # salt
1 # iteration count
# ,OpenSSL::Digest::MD5.new() # Hash function
)
pbes.decrypt()
plain_text = pbes.update(@cipher_text[2]) + pbes.final
print plain_text
Javaで復号する
Javaの場合サードパーティの暗号化サービスプロバイダを導入せずに、Sun JDK付属の暗号化サービスプロバイダのみを使用すると、次のようになります:
import javax.crypto.*;
import javax.crypto.spec.*;
import java.io.*;
import java.security.*;
public class DecryptOpensslPBES {
private static boolean openSSLBytesToKey(Cipher type, MessageDigest md,
byte[] salt, char[] password,
int ic, byte[] key, byte[] iv) {
int keyLen = key.length;
int ivLen = type.getBlockSize();
byte[] dk = new byte[keyLen+ivLen];
try {
byte[] pw = new String(password).getBytes("UTF-8");
int pos = 0;
int hashLen = md.getDigestLength();
while (pos < dk.length) {
md.update(dk, 0, pos);
md.update(pw);
md.update(salt);
md.digest(dk, pos, hashLen);
for (int i = 1; i < ic; i++) {
md.update(dk);
md.digest(dk, pos, hashLen);
}
pos += hashLen;
}
}
catch (DigestException e) {
return false;
}
catch (UnsupportedEncodingException e) {
return false;
};
for (int i = 0; i < keyLen; i++)
key[i] = dk[i];
for (int i = 0; i < ivLen; i++)
iv[i] = dk[keyLen + i];
return true;
}
public static void main(String[] args) throws Exception {
byte header[] = new byte[8];
byte salt[] = new byte[8];
byte cipherText[] = new byte[8142];
int cipherLength;
PBEKeySpec spec;
SecretKeySpec key;
IvParameterSpec param;
byte rawKey[] = new byte[16];
byte iv[] = new byte[16];
byte plain_text[];
/* 'Salted__' | SALT(0..7) | CIPHER-TEXT */
if (System.in.read(header, 0, header.length) <= 0)
return;
if (System.in.read(salt, 0, salt.length) <= 0)
return;
if ((cipherLength = System.in.read(cipherText,
0, cipherText.length)) <= 0)
{
return;
}
Cipher cbc = Cipher.getInstance("AES/CBC/PKCS5Padding");
if (!openSSLBytesToKey(cbc, MessageDigest.getInstance("MD5"),
salt, "my secret password".toCharArray(),
1, rawKey, iv))
{
System.out.println("Cannot execute PBKDF");
return;
}
key = new SecretKeySpec(rawKey, "AES");
param = new IvParameterSpec(iv);
cbc.init(Cipher.DECRYPT_MODE, key, param);
plain_text = cbc.doFinal(cipherText, 0, cipherLength);
System.out.print(new String(plain_text));
}
}
「Sun JDKにもパスワードベース暗号のPBE系エンジンがバンドルされているのになぜ使わないのか?」と疑問がわいてきます。残念ながらSun JDK付属の暗号化サービスプロバイダは、AESなど128bitブロック暗号を使う場合、opensslコマンドと互換性のある鍵変換処理をもつPBE系エンジンが用意されていません。そのために次のような手続きを自前で記述しています:
- パスワードを鍵に変換する処理を自前で実装する
- 鍵の前半128bitを秘密鍵に使い
- 鍵の後半128bitをCBCモードのInitialization Vectorに使い暗号化する
鍵導出関数
ここまで漠然と「パスワードを鍵に変換する」と書いてきたので、具体的にどう変換するのか補足します。一般的なブロック暗号の秘密鍵は、アルゴリズムによって128bitや256bitなどと長さが決まっています。通常パスワードは長さが定まってない文字列なので、秘密鍵として使うには一定の長さに変換したりする処理が必要です。この変換処理を鍵導出関数(KDF - Key Derivation Function)と言います。
鍵導出関数には様々な方式が考えられますが、一般的にはPKCS #5で定めるPBKDF1 (Password-Based Key Derivation Function 1)やPBKDF2が広く利用されているようです。
これらの鍵導出関数は、暗号化用の鍵を得るためのパラメータとして- パスワード
- ランダムに生成したsalt
- 変換処理のくり返し回数(iteration count)
- 出力する鍵の長さ
| 鍵導出関数 | 変換処理の基盤 | 出力できる鍵長 | 推奨 |
|---|---|---|---|
| PBKDF1 | ハッシュ関数(MD2, MD5, SHA-1) | 最大128bit(SHA-1は160bit)の鍵を生成 | △ |
| PBKDF2 | 疑似乱数生成器(HMAC-SHA-1, HMAC-SHA-2など) | 最大で 疑似乱数生成器の出力幅 x (2^32-1) の鍵を生成 | ○ |
- 暗号化と復号の手続き(Encryption Scheme)を定めるPBES1, PBES2
- メッセージ認証コード計算の手続き(Message Authentication Scheme)を定めるPBMAC1
- PBKDF1で128bitの鍵を導出する
- 導出した鍵の前半64bitを秘密鍵に使用し
- 導出した鍵の後半64bitをCBCモードのInitialization Vectorに使用して
- CBCモードで暗号化する
- PBKDF1を拡張した鍵導出関数で鍵を導出する
- 導出した鍵の前半128〜256bitを秘密鍵に使用し
- 導出した鍵の後半128bitをCBCモードのInitialization Vectorに使用して
- CBCモードで暗号化する
まとめ
パスワードベースで暗号文をやり取りする場合は、- 使用する暗号アルゴリズム
- 暗号利用モード
- パディングルールなどのパラメータ
- 鍵の導出手段
ちなみにopensslコマンドは鍵導出関数に繰り返し回数"1"を指定しているので、辞書攻撃や総当たり攻撃に対して比較的不利な鍵が導出されているのは秘密です。いちおう64bitのsaltがあるので、攻撃1回あたりのコストは2の64乗倍に増加しますが、世間的には繰り返し回数を1,000〜5,000回以上にするのがお勧めみたいです。
あと、ここから重要なオチなんですが、PBKDF1はけっこう前から新規開発するアプリケーションでの採用を推奨されていません。現在は代わりにPBKDF2の採用が推奨されています。これに限らず暗号のアプリケーションまわりの話題は陳腐化が激しいので、定期的に棚卸ししておくと夢見が良さそうですね。