Help us understand the problem. What is going on with this article?

Android 4.3 で Android Keystore を使う

More than 1 year has passed since last update.

この記事の概要

  • API Level 23 (Android 6.0) になってAndroid Keystore が色々と強化された
  • と同時にAPI Level 18 (Android 4.3) 時代の実装ドキュメントが公式から消えた
  • 4.3でも使えなきゃ困る。でも詳しい書き方載ってない。だから書き記す

サンプルコードだけあれば充分という方はこちらからどうぞ。
Android Keystoreが利用できない端末で鍵を保存したいときは、こちらの記事を参考にして下さい。

Android Keystore とは

Android 4.3 から登場した鍵管理の仕組み。アプリ内になんらかの機密データを保持する場合、例え暗号化して保存しても鍵が見つかってしまうと意味がありません。しかし、自前で鍵をセキュアに管理するのはなかなか難儀です。Android Keystoreを使えば鍵の生成や保持をどっか安全な場所でAndroidがうまいことやってくれます(よくしらない)。この鍵は、鍵が生成されたアプリ&デバイスのみで有効な、まさにアプリ内保存に特化したものとなっています。
※ 仕組みとしてはiOSのKeychainとよく似ています(あちらはアプリ間で鍵の共有もできるけど)。ややこしいことにAndroidにもKeychainというAPIがありますが、iOSのKeychainとは意味合いが違うものになっています

Android 4.3 -> Android 6.0 でAPIはどう変わったか

内部的には楕円曲線暗号などの新たなアルゴリズムが追加されたり、ようやく共通鍵暗号に対応したりと色々強化されているようですが、コード上の大きな違いは以下の2つです。

4.3時代と比べると幾分スマートに書けるようになっています。しかしこれらのクラスはAPI Level 23以降しか対応していません(特定のメソッドが、ではなくクラスそのものが23以降にしかない)。2016/05/04 現在、Android 6.0 のシェアは7.5%となっています。いくらdeprecatedと言われても、こんな状況でAndroid 6.0未満を切り捨てられる豪傑はそうそういないでしょう。

Android 4.3でも動くサンプル

もともとAndroid 4.3から使えるAPIなので「4.3でも動く」というのは少し変なのですが、公式のリファレンスはすっかり最新版のAPI Level 23のものに書き換えられ、4.3時代の書き方はWeb Archiveにすら残っていません。

というわけでdeprecatedになった旧式のサンプルを書き記しておきます。
トップダウンにいきましょう。まずはKeyPairを作るところ。

private KeyPair createKeyPair() {
    KeyPairGenerator kpg = null;
    try {
        kpg = KeyPairGenerator.getInstance("RSA", "AndroidKeyStore");
        kpg.initialize(createKeyPairGeneratorSpec());
    } catch (Exception e) {
        e.printStackTrace();
        return null;
    }
    return kpg.generateKeyPair();
}

戻り値が格納された変数をkeyPairとすると、keyPair.getPublic()で公開鍵、keyPair.getPrivate()で秘密鍵が得られます。鍵を生成しているだけのように見えますが、保存まで終わっています。"RSA""AndroidKeyStore"は固定の文字列です。API Level 18ではRSA以外の選択肢がありません
※ 結構色々な検査例外をスローしますが、ここでは個別のcatchは割愛しています。

続いてcreateKeyPairGeneratorSpec()の中身です。KeyPairGeneratorの初期化に使うKeyPairGeneratorSpecのインスタンスを生成します。いわゆるオレオレ証明書です。

private KeyPairGeneratorSpec createKeyPairGeneratorSpec() {
    Calendar start = Calendar.getInstance();
    Calendar end = Calendar.getInstance();
    end.add(Calendar.YEAR, 100);

    KeyPairGeneratorSpec spec = new KeyPairGeneratorSpec.Builder(mContext)
            .setAlias(KEY_STORE_ALIAS)
            .setSubject(new X500Principal(String.format("CN=%s", KEY_STORE_ALIAS)))
            .setSerialNumber(BigInteger.valueOf(1000000))
            .setStartDate(start.getTime())
            .setEndDate(end.getTime())
            .build();

    return spec;
}

暗号化のためにしか使われない証明書だと思うのでsetAlias(KEY_STORE_ALIAS)以外の値はテキトーです。有効期間はなんとなく100年にしときました。Aliasは次回以降KeyStoreから鍵を取り出すときに使います。面倒なのはBuilderにContextを渡さないといけないところです。ダイアログの表示(が必要なとき)にしか使われないようなので、Activityのインスタンスが取得しにくいときはApplication Contextでも渡しておけばよいでしょう。
※ 因みにAPI Level 23のKeyGenParameterSpec.Builderではこの辺りは簡略化され、指定が無ければ証明書はデフォルト値を使って勝手に作ってくれるようなりました。Contextも不要でした。

最後に、Android Keystoreからそれぞれの鍵を取り出す部分です。

KeyStore keyStore = KeyStore.getInstance("AndroidKeyStore");
keyStore.load(null);
PublicKey  publicKey  = keyStore.getCertificate(KEY_STORE_ALIAS).getPublicKey();
PrivateKey privateKey = keyStore.getKey(KEY_STORE_ALIAS, null);

keyStore.getKey()の第2引数はパスワードが入るみたいですが、Android Keystoreの場合は当然そんなものはないので(あったら意味無い)nullで良いっぽいです。このPublicKeyPrivateKeyCipherで使用できるので、以下のようなメソッドに渡せば任意のbyte列を暗号化/復号することができます。

static private final String CIPHER_ALGORITHM = "RSA/ECB/PKCS1Padding";

public byte[] encrypt(byte[] bytes, PublicKey publicKey) {
    try {
        Cipher cipher = Cipher.getInstance(CIPHER_ALGORITHM);
        cipher.init(Cipher.ENCRYPT_MODE, publicKey);

        return cipher.doFinal(bytes);

    } catch (GeneralSecurityException e) {
        e.printStackTrace();
        throw new RuntimeException("Encryption failed.");
    }
}

public byte[] decrypt(byte[] bytes, PrivateKey privateKey) {
    try {
        Cipher cipher = Cipher.getInstance(CIPHER_ALGORITHM);
        cipher.init(Cipher.DECRYPT_MODE, privateKey);

        return cipher.doFinal(bytes);

    } catch (GeneralSecurityException e) {
        e.printStackTrace();
        throw new RuntimeException("Decryption failed.");
    }
}

ソースコード

こちらからどうぞ。
※ 記事に載せたコードとは細部が異なっています

以下の行を適当な文字列に変更してください。

static private final String KEY_STORE_ALIAS = "sample_alias"; // Change me

以下のメソッドが使えます。
encrypt(byte[] bytes) decrypt(byte[] bytes) getPublicKey() getPrivateKey()
鍵の生成とか気にせず、いきなりコールしてOKです。初回なら鍵の生成、2回目からは取り出して使います。getPublicKey()getPrivateKey()はCipher以外で鍵を使う場合などにご利用ください。

// 使用例
private AndroidKeyStoreManager mKeyStoreManager;

protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);

    mKeyStoreManager = AndroidKeyStoreManager.getInstance(this);
    test();
}

private void test() {
    String plainText = "hogehoge";
    byte[] encryptedBytes = mKeyStoreManager.encrypt(plainText.getBytes());
    byte[] decryptedBytes = mKeyStoreManager.decrypt(encryptedBytes);
    String decryptedText = new String(decryptedBytes);
    Log.d("KeyStoreTest", decryptedText); // -> hogehoge
}
Koganes
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした