Android Keystoreを使って秘匿情報を保持する

Androidのアプリで、tokenやユーザの個人情報などの秘匿情報をセキュアに保持するために、Android Keystore を使って暗号化してShared preferenceやsqliteに保持する簡単なコードサンプル。

# Android Keystore System
[Android Keystore System](http://developer.android.com/intl/ja/training/articles/keystore.html)を使って情報を暗号化して保持する手順は以下のようになる。

## Android Keystoreのロード

```java
  mKeyStore = KeyStore.getInstance("AndroidKeyStore");
  mKeyStore.load(null);
```

## 鍵ペアの生成
鍵ペアがなければ[KeyPairGenerator](http://developer.android.com/intl/ja/reference/java/security/KeyPairGenerator.html)を使って新しくpublid/private keyを生成する。

```java
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance(
    KeyProperties.KEY_ALGORITHM_RSA, KEY_PROVIDER);
keyPairGenerator.initialize(
    new KeyGenParameterSpec.Builder(...) /* key parameters */
    .build());
keyPairGenerator.generateKeyPair();
```

## 暗号化
public keyで暗号化したデータを保持する（サンプルコードでは暗号化と同時にBase64 encodeしている）。
CipherOutputStreamに暗号データが出力される。

```java
PublicKey publicKey = keyStore.getCertificate(alias).getPublicKey();

Cipher cipher = Cipher.getInstance(ALGORITHM);
cipher.init(Cipher.ENCRYPT_MODE, publicKey);

ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
CipherOutputStream cipherOutputStream = new CipherOutputStream(
    outputStream, cipher);
cipherOutputStream.write(plainText.getBytes("UTF-8"));
cipherOutputStream.close();
```

## 復号
保持したデータはprivate keyで復号して利用する。CipherInputStreamから復号データを読み出すことができる。

```java
PrivateKey privateKey = (PrivateKey) keyStore.getKey(alias, null);

Cipher cipher = Cipher.getInstance(ALGORITHM);
cipher.init(Cipher.DECRYPT_MODE, privateKey);
CipherInputStream cipherInputStream = new CipherInputStream(
    new ByteArrayInputStream(Base64.decode(encryptedText, Base64.DEFAULT)), cipher);
```

# サンプルコード

```java:MainActivity.java
package jp.gr.java_conf.fofn.sample.keystoreprovider;

import android.app.Activity;
import android.os.Bundle;

import android.security.keystore.KeyGenParameterSpec;
import android.security.keystore.KeyProperties;
import android.util.Base64;
import android.util.Log;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;

import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.InputStreamReader;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.KeyStore;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.util.ArrayList;

import javax.crypto.Cipher;
import javax.crypto.CipherInputStream;
import javax.crypto.CipherOutputStream;

public class MainActivity extends Activity {
    private static final String TAG = "KeyStoreProviderSample";

    private static final String KEY_PROVIDER = "AndroidKeyStore";
    private static final String KEY_ALIAS = "sample key";
    private static final String ALGORITHM = "RSA/ECB/OAEPWithSHA-256AndMGF1Padding";

    private KeyStore mKeyStore = null;

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

        prepareKeyStore();

        Button button = (Button)findViewById(R.id.button);
        button.setOnClickListener(new View.OnClickListener(){
            @Override
            public void onClick(View v) {
                TextView view;
                view = (TextView)findViewById(R.id.text);
                String plainText = view.getText().toString();

                // encrypt
                String encryptedText = encryptString(mKeyStore, KEY_ALIAS, plainText);
                view = (TextView)findViewById(R.id.enc);
                view.setText(encryptedText);

                // decrypt
                String decryptedText = decryptString(mKeyStore, KEY_ALIAS, encryptedText);
                view = (TextView)findViewById(R.id.dec);
                view.setText(decryptedText);
            }
        });
    }

    private void prepareKeyStore() {
        try {
            mKeyStore = KeyStore.getInstance("AndroidKeyStore");
            mKeyStore.load(null);
            createNewKey(mKeyStore, KEY_ALIAS);
        } catch (Exception e) {
            Log.e(TAG, e.toString());
        }
    }

    /**
     * Create new key pair if needed.
     *
     * Create RSA key pair for encryption/decryption using RSA OAEP.
     * See KeyGenParameterSpec document.
     *
     * @param keyStore key store
     * @param alias key alias
     */
    private void createNewKey(KeyStore keyStore, String alias) {
        try {
            // Create new key if needed
            if (!keyStore.containsAlias(alias)) {
                KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance(
                    KeyProperties.KEY_ALGORITHM_RSA, KEY_PROVIDER);
                keyPairGenerator.initialize(
                    new KeyGenParameterSpec.Builder(
                        alias,
                        KeyProperties.PURPOSE_DECRYPT)
                        .setDigests(KeyProperties.DIGEST_SHA256, KeyProperties.DIGEST_SHA512)
                        .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_RSA_OAEP)
                        .build());
                keyPairGenerator.generateKeyPair();
            }
        } catch (Exception e) {
            Log.e(TAG, e.toString());
        }
    }

    /**
     * Encrypt string text
     *
     * @param keyStore key store used
     * @param alias key alias
     * @param plainText string to be encrypted
     *
     * @return base64 encoded cipher text
     */
    private String encryptString(KeyStore keyStore, String alias, String plainText) {
        String encryptedText = null;
        try {
            PublicKey publicKey = keyStore.getCertificate(alias).getPublicKey();

            Cipher cipher = Cipher.getInstance(ALGORITHM);
            cipher.init(Cipher.ENCRYPT_MODE, publicKey);

            ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
            CipherOutputStream cipherOutputStream = new CipherOutputStream(
                outputStream, cipher);
            cipherOutputStream.write(plainText.getBytes("UTF-8"));
            cipherOutputStream.close();

            byte [] bytes = outputStream.toByteArray();
            encryptedText = Base64.encodeToString(bytes, Base64.DEFAULT);
        } catch (Exception e) {
            Log.e(TAG, e.toString());
        }
        return encryptedText;
    }

    /**
     * Decrypt base64 encoded cipher text
     *
     * @param keyStore key store used
     * @param alias key alias
     * @param encryptedText base64 encoded cipher text
     *
     * @return plain text string
     */
    private String decryptString(KeyStore keyStore, String alias, String encryptedText) {
        String plainText = null;
        try {
            PrivateKey privateKey = (PrivateKey) keyStore.getKey(alias, null);

            Cipher cipher = Cipher.getInstance(ALGORITHM);
            cipher.init(Cipher.DECRYPT_MODE, privateKey);

            CipherInputStream cipherInputStream = new CipherInputStream(
                new ByteArrayInputStream(Base64.decode(encryptedText, Base64.DEFAULT)), cipher);

            ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
            int b;
            while ((b = cipherInputStream.read()) != -1) {
                outputStream.write(b);
            }
            outputStream.close();
            plainText = outputStream.toString("UTF-8");
        } catch (Exception e) {
            Log.e(TAG, e.toString());
        }
        return plainText;
    }
}
```
実行中のScreenshot:
![screen_1.png](https://qiita-image-store.s3.amazonaws.com/0/88598/7fa05ddd-1ddc-ba90-122c-83b127a272c7.png)

# 追記 (2020-02-26)
Jetpack Securityを使えば手軽にShared preferenceやFileなどのデータを暗号化できる環境が整いました！
https://android-developers.googleblog.com/2020/02/data-encryption-on-android-with-jetpack.html