package org.nick.androidkeystore; import java.math.BigInteger; import java.security.KeyPair; import java.security.interfaces.ECPublicKey; import java.security.interfaces.RSAPublicKey; import javax.crypto.SecretKey; import javax.crypto.spec.SecretKeySpec; import org.nick.androidkeystore.android.security.KeyStore; import org.nick.androidkeystore.android.security.KeyStoreJb43; import org.nick.androidkeystore.android.security.KeyStoreKk; import android.annotation.SuppressLint; import android.annotation.TargetApi; import android.app.Activity; import android.content.ActivityNotFoundException; import android.content.Intent; import android.os.AsyncTask; import android.os.Build; import android.os.Bundle; import android.security.KeyChain; import android.util.Log; import android.view.View; import android.view.View.OnClickListener; import android.view.Window; import android.widget.ArrayAdapter; import android.widget.Button; import android.widget.ListView; import android.widget.TextView; import android.widget.Toast; public class KeystoreActivity extends Activity implements OnClickListener { private static final String TAG = KeystoreActivity.class.getSimpleName(); private static final boolean IS_JB43 = Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2; private static final boolean IS_JB = Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN; private static final boolean IS_KK = Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT; private static final String EXTRA_CIPHERTEXT = "org.nick.androidkeystore.CIPHERTEXT"; private static final String EXTRA_KEY_NAME = "org.nick.androidkeystore.KEY_NAME"; private static final String EXTRA_PLAINTEXT = "org.nick.androidkeystore.PLAINTEXT"; public static final String OLD_UNLOCK_ACTION = "android.credentials.UNLOCK"; public static final String UNLOCK_ACTION = "com.android.credentials.UNLOCK"; public static final String RESET_ACTION = "com.android.credentials.RESET"; private static final String KEY_NAME = "aes_key"; private static final String RSA_KEY_NAME = "rsa_key"; private static final String EC_KEY_NAME = "ec_key"; private static final String PLAIN_TEXT = "Hello, KeyStore!"; private static int keyNum = 0; private TextView encryptedText; private TextView decryptedText; private Button encryptButton; private Button decryptButton; private Button signButton; private Button verifyButton; private Button encryptRsaButton; private Button decryptRsaButton; private Button signEcButton; private Button verifyEcButton; private Button listButton; private Button resetButton; private ListView keyList; private KeyStore ks; private String encryptionKeyName; private String signKeyName; private String rsaEncryptKeyName; /** Called when the activity is first created. */ @Override public void onCreate(Bundle savedInstanceState) { requestWindowFeature(Window.FEATURE_INDETERMINATE_PROGRESS); super.onCreate(savedInstanceState); PRNGFixes.apply(); setContentView(R.layout.main); findViews(); if (savedInstanceState != null) { encryptedText.setText(savedInstanceState .getString(EXTRA_CIPHERTEXT)); encryptionKeyName = savedInstanceState.getString(EXTRA_KEY_NAME); String plaintext = savedInstanceState.getString(EXTRA_PLAINTEXT); decryptedText.setText(plaintext); } if (IS_KK) { ks = KeyStoreKk.getInstance(); } else if (IS_JB43) { ks = KeyStoreJb43.getInstance(); } else { ks = KeyStore.getInstance(); } displayKeystoreState(); } abstract class KeystoreTask extends AsyncTask<Void, Void, String[]> { Exception error; @Override protected void onPreExecute() { setProgressBarIndeterminateVisibility(true); toggleControls(false); } @Override protected String[] doInBackground(Void... params) { try { return doWork(); } catch (Exception e) { error = e; Log.e(TAG, "Error: " + e.getMessage(), e); return null; } } protected abstract String[] doWork() throws Exception; @Override protected void onPostExecute(String[] result) { setProgressBarIndeterminateVisibility(false); toggleControls(true); if (error != null) { Toast.makeText(KeystoreActivity.this, "Error: " + error.getMessage(), Toast.LENGTH_LONG) .show(); return; } updateUi(result); } protected abstract void updateUi(String[] result); } private void toggleControls(boolean enable) { encryptButton.setEnabled(enable); decryptButton.setEnabled(enable); listButton.setEnabled(enable); resetButton.setEnabled(enable); if (IS_JB) { signButton.setEnabled(enable); verifyButton.setEnabled(enable); encryptRsaButton.setEnabled(enable); decryptRsaButton.setEnabled(enable); } if (IS_KK) { signEcButton.setEnabled(enable); verifyEcButton.setEnabled(enable); } } private void displayKeystoreState() { new KeystoreTask() { @Override protected String[] doWork() { Log.d(TAG, "Keystore state: " + ks.state()); String status = String.format("Keystore state:%s", ks.state() .toString()); String storeType = null; if (IS_KK) { storeType = ((KeyStoreKk) ks).isHardwareBacked() ? "HW-backed" : "SW only"; } else if (IS_JB43) { storeType = ((KeyStoreJb43) ks).isHardwareBacked() ? "HW-backed" : "SW only"; } return new String[] { status, storeType }; } @Override @SuppressLint("NewApi") protected void updateUi(String[] result) { setTitle(result[0]); if (result[1] != null) { if (IS_JB43) { getActionBar().setSubtitle(result[1]); } } } }.execute(); } private void findViews() { encryptedText = (TextView) findViewById(R.id.encrypted_text); decryptedText = (TextView) findViewById(R.id.decrypted_text); encryptButton = (Button) findViewById(R.id.encrypt_button); encryptButton.setOnClickListener(this); decryptButton = (Button) findViewById(R.id.decrypt_button); decryptButton.setOnClickListener(this); signButton = (Button) findViewById(R.id.sign_rsa_button); signButton.setOnClickListener(this); signButton.setEnabled(IS_JB); verifyButton = (Button) findViewById(R.id.verify_rsa_button); verifyButton.setOnClickListener(this); verifyButton.setEnabled(IS_JB); encryptRsaButton = (Button) findViewById(R.id.encrypt_rsa_button); encryptRsaButton.setOnClickListener(this); encryptRsaButton.setEnabled(IS_JB); decryptRsaButton = (Button) findViewById(R.id.decrypt_rsa_button); decryptRsaButton.setOnClickListener(this); decryptRsaButton.setEnabled(IS_JB); signEcButton = (Button) findViewById(R.id.sign_ec_button); signEcButton.setOnClickListener(this); signEcButton.setEnabled(IS_KK); verifyEcButton = (Button) findViewById(R.id.verify_ec_button); verifyEcButton.setOnClickListener(this); verifyEcButton.setEnabled(IS_KK); listButton = (Button) findViewById(R.id.list_button); listButton.setOnClickListener(this); resetButton = (Button) findViewById(R.id.reset_button); resetButton.setOnClickListener(this); keyList = (ListView) findViewById(R.id.key_list); } @TargetApi(18) @Override protected void onResume() { super.onResume(); displayKeystoreState(); if (ks.state() == KeyStore.State.UNLOCKED) { showKeys(); } if (IS_KK) { Log.d(TAG, "RSA supported " + KeyChain.isKeyAlgorithmSupported("RSA")); Log.d(TAG, "RSA bound " + KeyChain.isBoundKeyAlgorithm("RSA")); Log.d(TAG, "DSA supported " + KeyChain.isKeyAlgorithmSupported("DSA")); Log.d(TAG, "DSA bound " + KeyChain.isBoundKeyAlgorithm("DSA")); Log.d(TAG, "EC supported " + KeyChain.isKeyAlgorithmSupported("EC")); Log.d(TAG, "EC bound " + KeyChain.isBoundKeyAlgorithm("EC")); } else if (IS_JB43) { Log.d(TAG, "RSA supported " + KeyChain.isKeyAlgorithmSupported("RSA")); Log.d(TAG, "RSA bound " + KeyChain.isBoundKeyAlgorithm("RSA")); } } @Override protected void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); String ciphertext = encryptedText.getText().toString(); String plaintext = decryptedText.getText().toString(); if (ciphertext != null) { outState.putString(EXTRA_CIPHERTEXT, encryptedText.getText() .toString()); outState.putString(EXTRA_KEY_NAME, encryptionKeyName); } if (plaintext != null) { outState.putString(EXTRA_PLAINTEXT, plaintext); } } @Override public void onClick(View v) { if (ks.state() != KeyStore.State.UNLOCKED) { Toast.makeText( this, "Keystore is locked or not initialized. Retry operation " + "after unlock activity returns.", Toast.LENGTH_LONG).show(); unlock(); // unlocking is in a separate activity, stop here return; } try { if (v.getId() == R.id.reset_button) { deleteAllKeys(); // resetKeystore(); } else if (v.getId() == R.id.list_button) { showKeys(); // testKeystore(); } else if (v.getId() == R.id.encrypt_button) { encrypt(); } else if (v.getId() == R.id.decrypt_button) { if (encryptedText.getText() == null || encryptedText.getText().equals("")) { Toast.makeText(this, "No encrypted text found.", Toast.LENGTH_SHORT).show(); return; } decrypt(); } else if (v.getId() == R.id.sign_rsa_button) { signRsaPss(); } else if (v.getId() == R.id.verify_rsa_button) { verifyRsaPss(); } else if (v.getId() == R.id.encrypt_rsa_button) { encryptRsaOaep(); } else if (v.getId() == R.id.decrypt_rsa_button) { decryptRsa(); } else if (v.getId() == R.id.sign_ec_button) { signEcDsa(); } else if (v.getId() == R.id.verify_ec_button) { verifyEcDsa(); } } catch (Exception e) { Log.e(TAG, "Unexpected error: " + e.getMessage(), e); Toast.makeText(this, "Unexpected error: " + e.getMessage(), Toast.LENGTH_LONG).show(); } } private void encryptRsaOaep() { new KeystoreTask() { @Override protected String[] doWork() throws Exception { String alias = RSA_KEY_NAME + keyNum; KeyPair kp = Crypto.generateRsaPairWithGenerator( KeystoreActivity.this, alias); Log.d(TAG, String.format("Genarated %d bit RSA key pair", ((RSAPublicKey) kp.getPublic()).getModulus() .bitLength())); rsaEncryptKeyName = alias; keyNum++; String ciphertext = Crypto.encryptRsaOaep(PLAIN_TEXT, rsaEncryptKeyName); return new String[] { ciphertext }; } @Override protected void updateUi(String[] result) { encryptedText.setText(result[0]); decryptedText.setText(""); showKeys(); } }.execute(); } private void decryptRsa() { new KeystoreTask() { @Override protected String[] doWork() throws Exception { String plainStr = Crypto.decryptRsaOaep(encryptedText.getText() .toString(), rsaEncryptKeyName); return new String[] { plainStr }; } @Override protected void updateUi(String[] result) { decryptedText.setText(result[0]); showKeys(); } }.execute(); } private void decrypt() { final String ciphertext = encryptedText.getText().toString(); new KeystoreTask() { @Override protected String[] doWork() { byte[] keyBytes = ks.get(encryptionKeyName); if (keyBytes == null) { Log.w(TAG, "Encryption key not found in keystore: " + encryptionKeyName); return null; } SecretKeySpec key = new SecretKeySpec(keyBytes, "AES"); String plaintext = Crypto.decryptAesCbc(ciphertext, key); return new String[] { plaintext }; } @Override protected void updateUi(String[] result) { if (result == null) { Toast.makeText(KeystoreActivity.this, "Encryption key not found in keystore.", Toast.LENGTH_SHORT).show(); return; } decryptedText.setText(result[0]); } }.execute(); } private void encrypt() { new KeystoreTask() { @Override protected String[] doWork() { SecretKey key = Crypto.generateAesKey(); encryptionKeyName = KEY_NAME + keyNum; boolean success = ks.put(encryptionKeyName, key.getEncoded()); Log.d(TAG, "put key success: " + success); checkRc(success); keyNum++; String ciphertext = Crypto.encryptAesCbc(PLAIN_TEXT, key); return new String[] { ciphertext }; } @Override protected void updateUi(String[] result) { encryptedText.setText(result[0]); decryptedText.setText(""); showKeys(); } }.execute(); } private void signRsaPss() { new KeystoreTask() { @Override protected String[] doWork() throws Exception { String alias = RSA_KEY_NAME + keyNum; KeyPair kp = Crypto.generateRsaPairWithGenerator( KeystoreActivity.this, alias); Log.d(TAG, String.format("Genarated %d bit RSA key pair", ((RSAPublicKey) kp.getPublic()).getModulus() .bitLength())); signKeyName = alias; keyNum++; String signature = Crypto.signRsaPss(signKeyName, PLAIN_TEXT); return new String[] { signature }; } @Override protected void updateUi(String[] result) { encryptedText.setText(result[0]); decryptedText.setText(""); showKeys(); } }.execute(); } private void verifyRsaPss() { new KeystoreTask() { @Override protected String[] doWork() throws Exception { String signatureStr = encryptedText.getText().toString(); boolean verified = Crypto.verifyRsaPss(signatureStr, PLAIN_TEXT, signKeyName); Log.d(TAG, "RSA PSS signature verification result: " + verified); return new String[] { verified ? "Signature verifies" : "Invalid signature" }; } @Override protected void updateUi(String[] result) { if (result == null) { Toast.makeText(KeystoreActivity.this, "Signature key key not found in keystore.", Toast.LENGTH_SHORT).show(); return; } decryptedText.setText(result[0]); } }.execute(); } private void signEcDsa() { new KeystoreTask() { @Override protected String[] doWork() throws Exception { String alias = EC_KEY_NAME + keyNum; KeyPair kp = Crypto.generateEcPairWithGenerator( KeystoreActivity.this, alias); Log.d(TAG, String.format("Genarated %d bit EC key pair", ((ECPublicKey) kp.getPublic()).getParams().getCurve() .getField().getFieldSize())); signKeyName = alias; keyNum++; String signature = Crypto.signEc(signKeyName, PLAIN_TEXT); return new String[] { signature }; } @Override protected void updateUi(String[] result) { encryptedText.setText(result[0]); decryptedText.setText(""); showKeys(); } }.execute(); } private void verifyEcDsa() { new KeystoreTask() { @Override protected String[] doWork() throws Exception { String signatureStr = encryptedText.getText().toString(); boolean verified = Crypto.verifyEc(signatureStr, PLAIN_TEXT, signKeyName); Log.d(TAG, "RSA PSS signature verification result: " + verified); return new String[] { verified ? "Signature verifies" : "Invalid signature" }; } @Override protected void updateUi(String[] result) { if (result == null) { Toast.makeText(KeystoreActivity.this, "Signature key key not found in keystore.", Toast.LENGTH_SHORT).show(); return; } decryptedText.setText(result[0]); } }.execute(); } private void deleteAllKeys() { new KeystoreTask() { @Override protected String[] doWork() throws Exception { String[] keys = ks.saw(""); for (String key : keys) { boolean success = ks.delete(key); Log.d(TAG, String.format("delete key '%s' success: %s", key, success)); if (!success && IS_JB) { success = ks.delKey(key); Log.d(TAG, String.format("delKey '%s' success: %s", key, success)); } // delete_keypair() is optional, don't fail // checkRc(success); } return null; } @Override protected void updateUi(String[] result) { encryptionKeyName = null; signKeyName = null; encryptedText.setText(""); decryptedText.setText(""); showKeys(); } }.execute(); } private void showKeys() { new KeystoreTask() { @Override protected String[] doWork() { String[] keyNames = ks.saw(""); return keyNames; } @Override protected void updateUi(String[] keyNames) { ArrayAdapter<String> adapter = new ArrayAdapter<String>( KeystoreActivity.this, android.R.layout.simple_list_item_1, keyNames); keyList.setAdapter(adapter); Log.d(TAG, "Keys: "); for (String keyName : keyNames) { byte[] keyBytes = ks.get(keyName); if (keyBytes != null) { Log.d(TAG, String.format("\t%s: %s", keyName, new BigInteger(keyBytes).toString())); } else { Log.d(TAG, String.format("\t%s: %s", keyName, "RSA unexportable")); } } } }.execute(); } private void unlock() { if (ks.state() == KeyStore.State.UNLOCKED) { return; } try { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB) { startActivity(new Intent(OLD_UNLOCK_ACTION)); } else { startActivity(new Intent(UNLOCK_ACTION)); } } catch (ActivityNotFoundException e) { Log.e(TAG, "No UNLOCK activity: " + e.getMessage(), e); Toast.makeText(this, "No keystore unlock activity found.", Toast.LENGTH_SHORT).show(); return; } } @SuppressWarnings("unused") private void resetKeystore() { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.ICE_CREAM_SANDWICH) { Toast.makeText(this, "Reset not supported on pre-ICS devices", Toast.LENGTH_SHORT).show(); } else { startActivity(new Intent(RESET_ACTION)); } } @SuppressWarnings("unused") private void testKeystore() { boolean success = ks.lock(); if (!success) { Log.d(TAG, "lock() last error = " + rcToStr(ks.getLastError())); } success = ks.unlock("foo"); if (!success) { Log.d(TAG, "unlock() last error = " + rcToStr(ks.getLastError())); } success = ks.password("foobar"); if (!success) { Log.d(TAG, "password() last error = " + rcToStr(ks.getLastError())); } success = ks.reset(); if (!success) { Log.d(TAG, "reset() last error = " + rcToStr(ks.getLastError())); } success = ks.isEmpty(); if (!success) { Log.d(TAG, "isEmpty() last error = " + rcToStr(ks.getLastError())); } } private void checkRc(boolean success) { if (!success) { String errorStr = rcToStr(ks.getLastError()); Log.d(TAG, "last error = " + errorStr); throw new RuntimeException("Keystore error: " + errorStr); } } private static final String rcToStr(int rc) { switch (rc) { case KeyStore.NO_ERROR: return "NO_ERROR"; case KeyStore.LOCKED: return "LOCKED"; case KeyStore.UNINITIALIZED: return "UNINITIALIZED"; case KeyStore.SYSTEM_ERROR: return "SYSTEM_ERROR"; case KeyStore.PROTOCOL_ERROR: return "PROTOCOL_ERROR"; case KeyStore.PERMISSION_DENIED: return "PERMISSION_DENIED"; case KeyStore.KEY_NOT_FOUND: return "KEY_NOT_FOUND"; case KeyStore.VALUE_CORRUPTED: return "VALUE_CORRUPTED"; case KeyStore.UNDEFINED_ACTION: return "UNDEFINED_ACTION"; case KeyStore.WRONG_PASSWORD: return "WRONG_PASSWORD"; default: return "Unknown RC"; } } }