DB 接続用のパスワードだったり、Basic/Digest 認証のときに使うパスワードだったり、世の中にはパスワードが溢れていて、もうパスワードを個別にして記憶しておくとか絶対にムリ、ムリムリムリムリかたつむりであるから、みんなどの Web サービスのアカウントにも共通のパスワードを使い回したりする。 パスワードを使い回していると発言すると、どこからともなくセキュリティおじさんが飛んできて握りこぶしでゲンコツをしていくので、ぼくたちはもうパスワードを使いまわしできなくなる。こうなるともはや IT の力に頼らざるをえなくて、iCloud とか 1Passsword とか、Chrome の Smart Lock for Passwords とかで、暗号化したパスワードを保存しておいて、使いたいときに使えるようにする、みたいなソリューションを採る。
こういうのはシステムにおいても同じであって、1 つのシステムを作ると、たくさんのパスワードを使う状況に陥る。これを平文で保持しておくと、どこからともなくセキュリティおじさんが飛んできて頬を平手で殴っていくので、ぼくたちはパスワードを平文で保存できなくなる。こうなるともはや IT の力に頼らざるを得なくなる。
パスワード管理をどうしようか
パスワード管理をどうしようと考えると、やっぱしパスワードを暗号化してどっかに持たせておくしかなくて、Java だとそういうのに KeyStore がある。
KeyStore はそもそも暗号化に使う鍵とかを保持するものだけれど、一種の KVS としても使うことができて、一応その Value もなんちゃって暗号化される。
というわけで、パスワードもこの KeyStore に放りこんでしまおうという前提において、その実装を考えてみる。
KeyStore の種類をどうするか
KeyStore にはいくつか種類がある。Java の世界で(現状)の標準としては JKS という形式のものがあり、他にも JCEKS とか PKCS12、PKCS11 とかある。
また、KeyStore に投入できるものとしては
- 秘密鍵
- 共通鍵
- 証明書
が定義されているのだけれど、たとえば JKS には共通鍵が保存できなかったり、何でもかんでも互換性があるわけではない。
今回、パスワードを暗号化して KeyStore に放り込もうとすると、任意の文字列(暗号文) を KeyStore に放り込む必要があり、これを実現できるのはこのうちの共通鍵のみとなる。共通鍵に対応した KeyStore の形式は、というと、JCEKS、PKCS12、PKCS11 あたりが候補になる (JKS は共通鍵には対応していない)。
どうやって任意の文字列を KeyStore に投入するか
KeyStore に投入できるオブジェクトというのは、基本的には KeyStore.Entry Interface の実装クラスのみであり、上記に示した投入できるものとの対応関係は以下のようになる。
- 秘密鍵:
KeyStore.PrivateKeyEntry - 共通鍵:
KeyStore.SecretKeyEntry - 証明書:
KeyStore.TrustedCertificateEntry
このため、今回は「パスワードを暗号化した任意の文字列」 を KeyStore.SecretKeyEntry として構築できれば良い。
パスワードの暗号化については非常に単純に記述でき、以下に示したような Java プログラムで暗号化できる。
Cipher cipher = Cipher.getInstance(algorithm);
cipher.init(Cipher.ENCRYPT_MODE, getSecretKey(hexEncodedKey), getIv(hexEncodedIv));
byte[] encryptedSecret = cipher.doFinal(secret.getBytes(StandardCharsets.UTF_8));
なので、後は任意の暗号化文 (byte 配列) を KeyStore.SecretKeyEntry として構築するところを考える。
Key と KeySpec
実は Java における「鍵」には大きく 2 つあり、Key (鍵) と KeySpec (鍵仕様) である。
この 2 つの概念の違いは以下のページに記載があるが、これを一部運用とすると以下のとおり。
KeyとKeySpecはあまり変わらない表現に見えますが、実はこの2つには明確な違いがあります。 Keyインタフェースからはその鍵のデータに直接アクセスすることができません。このためKeyは不透明な鍵表現と 呼ばれ、それに対してKeySpecは鍵のデータに直接アクセスできるため透明な鍵表現と呼ばれます。 またKeyを鍵と呼ぶのに対して、KeySpecは鍵仕様と呼ばれます。
要するに具体的な値を持つのは KeySpec である。そして共通鍵における KeySpec は SecretKeySpec であるので、これを構築すれば良い。さらにいえば、SecretKeySpec は Key でもあり、KeySpec でもある (両者は Interface であり、SecretKeySpec は両 Interface を実装している)ので、両者を変換する必要はない。
あとは、KeyStore.SecretKeyEntry のコンストラクタが以下のように SecretKey を引数に取るメソッドシグニチャであるので、これを使って SecretKeyEntry を構築し、それを KeyStore に登録すれば良い。
public KeyStore.SecretKeyEntry(SecretKey secretKey)
具体的なコードは次のようになる。これで KeyStore に任意の暗号文が登録できた。
KeyStore ks = getKeyStore(keyStoreUrl, keyStorePassword, keyStoreType);
Cipher cipher = Cipher.getInstance(algorithm);
cipher.init(Cipher.ENCRYPT_MODE, getSecretKey(hexEncodedKey), getIv(hexEncodedIv));
byte[] encryptedSecret = cipher.doFinal(secret.getBytes(StandardCharsets.UTF_8));
SecretKeySpec keySpec = new SecretKeySpec(encryptedSecret, "AES");
KeyStore.SecretKeyEntry entry = new SecretKeyEntry(keySpec);
ks.setEntry(alias, entry, new KeyStore.PasswordProtection(keyStorePassword.toCharArray()));
try (FileOutputStream fos = new FileOutputStream(keyStoreUrl)) {
ks.store(fos, keyStorePassword.toCharArray());
}
これができれば、KeyStore からパスワードを取り出すのは簡単で、上記の対称形になる。
KeyStore ks = getKeyStore(keyStoreUrl, keyStorePassword, keyStoreType);
KeyStore.SecretKeyEntry secretKeyEntry = (KeyStore.SecretKeyEntry) ks.getEntry(alias,
new KeyStore.PasswordProtection(keyStorePassword.toCharArray()));
byte[] encrypted = secretKeyEntry.getSecretKey().getEncoded();
Cipher cipher = Cipher.getInstance(algorithm);
cipher.init(Cipher.DECRYPT_MODE, getSecretKey(hexEncodedKey), getIv(hexEncodedIv));
byte[] decrypted = cipher.doFinal(encrypted);
return new String(decrypted, StandardCharsets.UTF_8);
とりあえずの実装は、https://github.com/kiririmode/vault に。