アプリごとに生成する乱数とハードコーディングした定数を組み合わせて暗号鍵を作る

# この記事の概要
アプリで暗号化を扱う場合、鍵の保存場所が問題となります。Android 4.3以降であれば、Android Keystoreが使えます。（別記事を参照: [Android 4.3 で Android Keystore を使う](https://qiita.com/Koganes/items/e8253f13ecb534ca11a1)）

しかし、Android4.3未満の場合や、4.3以上であっても何らかの問題でAndroid Keystoreが使えないケースがあります。Android Keystoreが登場する以前の鍵管理方法として、徳丸氏が以下の記事で次のような方法を提案しています。

[書籍「Android Security」の暗号鍵生成方法には課題がある（ockeghem(徳丸浩)の日記）](http://d.hatena.ne.jp/ockeghem/20120213/p1)
> 一案として、方法(A)と方法(B)のハイブリッドが考えられます。アプリケーションの初期設定時に乱数で生成してファイルに保存した鍵と、アプリケーション内に埋め込んだ定数の鍵を連結して、暗号鍵の元とする方法です

本稿では、Android Keystoreが利用できない場合のベストエフォート対応（の1つ）として、このハイブリッド式鍵管理の実装方法を説明します。

サンプルコードだけあれば充分という方は[こちら](https://gist.github.com/k-ohkura/fc346c6a854a2d505fb0b5cfbd2c4612)からどうぞ。

# 実装
まず、ハードコーディングする分の鍵を実装します。

```java
private static final byte[] HARD_CODED_KEY;

static {
    try {
        HARD_CODED_KEY = "0123456789abcdef".getBytes("US-ASCII"); // Change me
    } catch (UnsupportedEncodingException e) {
        throw new RuntimeException("Unsupported Encoding");
    }
}
```
鍵はbyte配列として保存します。ハードコーディングする鍵の長さは、全体の鍵長と相談して決めます。サンプルでは鍵長256 bitsのAESを使う想定で実装しているので、半分の128 bits分をハードコーディングに充てています（ASCII 16文字なので16 bytes * 8 == 128 bits）。

続いて、残りの128 bits分をランダム生成してファイルに保存する部分です。

```java
private static final int KEY_LENGTH = 256;
private static final String RANDOM_KEY_FILE_NAME = "key.piece";

public static byte[] getHybridKey(Context context) {
    byte[] key = new byte[KEY_LENGTH / 8];
    byte[] storedRandomKey;

    String keyFilePath = context.getFilesDir().getPath() + File.separator + RANDOM_KEY_FILE_NAME;
    if (new File(keyFilePath).exists()) {
        try {
            FileInputStream in = context.openFileInput(RANDOM_KEY_FILE_NAME);
            storedRandomKey = new byte[in.available()];
            in.read(storedRandomKey);
        } catch (IOException e) {
            throw new RuntimeException("Could't restore password.");
        }
    } else {
        try {
            // 初回
            storedRandomKey = createRandomPassword((KEY_LENGTH / 8) - HARD_CODED_KEY.length);
            FileOutputStream out = context.openFileOutput(RANDOM_KEY_FILE_NAME, Context.MODE_PRIVATE);
            out.write(storedRandomKey);
            out.flush();
        } catch (IOException e) {
            e.printStackTrace();
            throw new RuntimeException("Could't store password.");
        }
    }
```
初回はまだ乱数を保存したファイルがないため `else` に入ります。ここで、全体の鍵長256 bitsからハードコーディングした128 bits 分を引いた残りの128 bits分をランダム生成してファイルに保存しています。`context.getFilesDir()`は他のアプリからはアクセスできないアプリ内保存領域のパスを返します。`context.openFileOutput()`も同じ場所を参照する`FileOutputStream`を返します。
※`openFileOutput()`の第2引数には`MODE_WORLD_READABLE`と`MODE_WORLD_WRITEABLE`という大変危険なフラグ（他のアプリからの読み書きを許す）があるのですが、deprecatedな上にAndroid N以降は指定するとSecurityExceptionが発生します。必ず`MODE_PRIVATE`を指定して下さい。

`createRandomPassword()` は引数bytes分の乱数を返すメソッドです。


```java
public static byte[] createRandomPassword(int length) {
    byte[] key = new byte[length];
    new SecureRandom().nextBytes(key);
    return key;
}
```

最後に、ハードコーディング鍵とランダム生成鍵を合体させます。

```java
    // 鍵を合体させる
    int cursor;
    for (cursor = 0; cursor < HARD_CODED_KEY.length; cursor++) {
        key[cursor] = HARD_CODED_KEY[cursor];
    }

    for (int i = 0; cursor < key.length; cursor++) {
        key[cursor] = storedRandomKey[i++];
    }

    return key;
}
```
最終的な鍵を格納するbyte配列keyに、ハードコーディング鍵とランダム生成鍵を順番に入れているだけです。

あとは、このbyte配列keyを使ってSecretKeySpec等を作ればCipherなどで利用可能になります。以下はAES暗号化のサンプルです。

```java
SecretKeySpec secKey = new SecretKeySpec(getHybridKey(this), "AES"); // 共通鍵
byte[] plainText = "ほげほげ"
byte[] encryptedText;
try {
    Cipher cipher = Cipher.getInstance("AES");
    cipher.init(Cipher.ENCRYPT_MODE, secKey);
    encryptedText = cipher.doFinal(plainText.getBytes()); //暗号化完了
} catch (GeneralSecurityException e) {
    e.printStackTrace();
    throw new RuntimeException("Encryption failed.");
}
```
`Cipher.ENCRYPT_MODE`を`Cipher.DECRYPT_MODE`にすれば復号になります。

# サンプルコードの使い方
コードは[こちら](https://gist.github.com/k-ohkura/fc346c6a854a2d505fb0b5cfbd2c4612)。

- `HARD_CODED_KEY`のパスワードを書き換えて下さい
  - ビット数が`KEY_LENGTH`を超えないように注意してください
- package文を自分の環境に合わせて書き換えて下さい

```java
// 使用例
void testEncryption() {
    byte[] key = EncryptionUtil.getHybridKey(this);
    String plain = "ほげほげ";
    SecretKeySpec secKey = new SecretKeySpec(key, "AES");

    byte[] encText = EncryptionUtil.encryptByAES(plain.getBytes(), secKey);
    Log.d("Enc", new String(encText)); // => ����@��xJ��"

    byte[] decText = EncryptionUtil.decryptByAES(encText, secKey);
    Log.d("Dec", new String(decText)); // => ほげほげ
}
```
getHybridKey()に渡すContextはApplication Contextでも構いません。
なお、このサンプルはAPI Level 18以降で動作します。

# 注意点
- 暗号化にCipherを使う場合、`KEY_LENGTH`は指定する暗号化アルゴリズムによって制限を受けます
- Cipherで指定できる暗号化アルゴリズムは、API Levelの制限を受けます。
  - APIリファレンス https://developer.android.com/reference/javax/crypto/Cipher.html
- 本実装はあくまでAndroid Keystoreが利用できないときの代替案（妥協案）であり、セキュリティレベルは決して高くありません
  - 高いセキュリティレベルが求められるアプリの場合は、Android Keystore非対応端末をサポート外にすることを先に検討すべきです