Why not login to Qiita and try out its useful features?

We'll deliver articles that match you.

You can read useful information later.

86
72

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

Android 4.3 で Android Keystore を使う

Last updated at Posted at 2016-05-04

#この記事の概要

  • 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
}
86
72
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up

Qiita Advent Calendar is held!

Qiita Advent Calendar is an article posting event where you post articles by filling a calendar 🎅

Some calendars come with gifts and some gifts are drawn from all calendars 👀

Please tie the article to your calendar and let's enjoy Christmas together!

86
72

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?