Add Android KeyStore API demo
Adds a sample application which uses the AndroidKeyStore and KeyPairGenerator for AndroidKeyStore. Bug: 8608817 Change-Id: Iec78a74461dc259e4080ed1fab7d923593fcdf75
This commit is contained in:
@@ -0,0 +1,526 @@
|
||||
/*
|
||||
* Copyright 2013 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License
|
||||
*/
|
||||
|
||||
package com.example.android.apis.security;
|
||||
|
||||
import com.example.android.apis.R;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.database.DataSetObserver;
|
||||
import android.os.AsyncTask;
|
||||
import android.os.Bundle;
|
||||
import android.security.KeyPairGeneratorSpec;
|
||||
import android.util.Base64;
|
||||
import android.util.Log;
|
||||
import android.view.View;
|
||||
import android.view.View.OnClickListener;
|
||||
import android.view.View.OnFocusChangeListener;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.AdapterView;
|
||||
import android.widget.AdapterView.OnItemClickListener;
|
||||
import android.widget.AdapterView.OnItemSelectedListener;
|
||||
import android.widget.ArrayAdapter;
|
||||
import android.widget.Button;
|
||||
import android.widget.EditText;
|
||||
import android.widget.ListAdapter;
|
||||
import android.widget.ListView;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.math.BigInteger;
|
||||
import java.security.InvalidAlgorithmParameterException;
|
||||
import java.security.InvalidKeyException;
|
||||
import java.security.KeyPair;
|
||||
import java.security.KeyPairGenerator;
|
||||
import java.security.KeyStore;
|
||||
import java.security.KeyStore.PrivateKeyEntry;
|
||||
import java.security.KeyStoreException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.NoSuchProviderException;
|
||||
import java.security.Signature;
|
||||
import java.security.SignatureException;
|
||||
import java.security.UnrecoverableEntryException;
|
||||
import java.security.cert.CertificateException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Calendar;
|
||||
import java.util.Date;
|
||||
import java.util.Enumeration;
|
||||
import java.util.List;
|
||||
|
||||
import javax.security.auth.x500.X500Principal;
|
||||
|
||||
public class KeyStoreUsage extends Activity {
|
||||
private static final String TAG = "AndroidKeyStoreUsage";
|
||||
|
||||
/**
|
||||
* An instance of {@link java.security.KeyStore} through which this app
|
||||
* talks to the {@code AndroidKeyStore}.
|
||||
*/
|
||||
KeyStore mKeyStore;
|
||||
|
||||
/**
|
||||
* Used by the {@code ListView} in our layout to list the keys available in
|
||||
* our {@code KeyStore} by their alias names.
|
||||
*/
|
||||
AliasAdapter mAdapter;
|
||||
|
||||
/**
|
||||
* Button in the UI that causes a new keypair to be generated in the
|
||||
* {@code KeyStore}.
|
||||
*/
|
||||
Button mGenerateButton;
|
||||
|
||||
/**
|
||||
* Button in the UI that causes data to be signed by a key we selected from
|
||||
* the list available in the {@code KeyStore}.
|
||||
*/
|
||||
Button mSignButton;
|
||||
|
||||
/**
|
||||
* Button in the UI that causes data to be signed by a key we selected from
|
||||
* the list available in the {@code KeyStore}.
|
||||
*/
|
||||
Button mVerifyButton;
|
||||
|
||||
/**
|
||||
* Button in the UI that causes a key entry to be deleted from the
|
||||
* {@code KeyStore}.
|
||||
*/
|
||||
Button mDeleteButton;
|
||||
|
||||
/**
|
||||
* Text field in the UI that holds plaintext.
|
||||
*/
|
||||
EditText mPlainText;
|
||||
|
||||
/**
|
||||
* Text field in the UI that holds the signature.
|
||||
*/
|
||||
EditText mCipherText;
|
||||
|
||||
/**
|
||||
* The alias of the selected entry in the KeyStore.
|
||||
*/
|
||||
private String mSelectedAlias;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
setContentView(R.layout.keystore_usage);
|
||||
|
||||
/*
|
||||
* Set up our {@code ListView} with an adapter that allows
|
||||
* us to choose from the available entry aliases.
|
||||
*/
|
||||
ListView lv = (ListView) findViewById(R.id.entries_list);
|
||||
mAdapter = new AliasAdapter(getApplicationContext());
|
||||
lv.setAdapter(mAdapter);
|
||||
lv.setOnItemClickListener(new OnItemClickListener() {
|
||||
@Override
|
||||
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
|
||||
mSelectedAlias = mAdapter.getItem(position);
|
||||
setKeyActionButtonsEnabled(true);
|
||||
}
|
||||
});
|
||||
|
||||
// This is alias the user wants for a generated key.
|
||||
final EditText aliasInput = (EditText) findViewById(R.id.entry_name);
|
||||
mGenerateButton = (Button) findViewById(R.id.generate_button);
|
||||
mGenerateButton.setOnClickListener(new OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
/*
|
||||
* When the user presses the "Generate" button, we'll
|
||||
* check the alias isn't blank here.
|
||||
*/
|
||||
final String alias = aliasInput.getText().toString();
|
||||
if (alias == null || alias.length() == 0) {
|
||||
aliasInput.setError(getResources().getText(R.string.keystore_no_alias_error));
|
||||
} else {
|
||||
/*
|
||||
* It's not blank, so disable the generate button while
|
||||
* the generation of the key is happening. It will be
|
||||
* enabled by the {@code AsyncTask} later after its
|
||||
* work is done.
|
||||
*/
|
||||
aliasInput.setError(null);
|
||||
mGenerateButton.setEnabled(false);
|
||||
new GenerateTask().execute(alias);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
mSignButton = (Button) findViewById(R.id.sign_button);
|
||||
mSignButton.setOnClickListener(new OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
final String alias = mSelectedAlias;
|
||||
final String data = mPlainText.getText().toString();
|
||||
if (alias != null) {
|
||||
setKeyActionButtonsEnabled(false);
|
||||
new SignTask().execute(alias, data);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
mVerifyButton = (Button) findViewById(R.id.verify_button);
|
||||
mVerifyButton.setOnClickListener(new OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
final String alias = mSelectedAlias;
|
||||
final String data = mPlainText.getText().toString();
|
||||
final String signature = mCipherText.getText().toString();
|
||||
if (alias != null) {
|
||||
setKeyActionButtonsEnabled(false);
|
||||
new VerifyTask().execute(alias, data, signature);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
mDeleteButton = (Button) findViewById(R.id.delete_button);
|
||||
mDeleteButton.setOnClickListener(new OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
final String alias = mSelectedAlias;
|
||||
if (alias != null) {
|
||||
setKeyActionButtonsEnabled(false);
|
||||
new DeleteTask().execute(alias);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
mPlainText = (EditText) findViewById(R.id.plaintext);
|
||||
mPlainText.setOnFocusChangeListener(new OnFocusChangeListener() {
|
||||
@Override
|
||||
public void onFocusChange(View v, boolean hasFocus) {
|
||||
mPlainText.setTextColor(getResources().getColor(android.R.color.primary_text_dark));
|
||||
}
|
||||
});
|
||||
|
||||
mCipherText = (EditText) findViewById(R.id.ciphertext);
|
||||
mCipherText.setOnFocusChangeListener(new OnFocusChangeListener() {
|
||||
@Override
|
||||
public void onFocusChange(View v, boolean hasFocus) {
|
||||
mCipherText
|
||||
.setTextColor(getResources().getColor(android.R.color.primary_text_dark));
|
||||
}
|
||||
});
|
||||
|
||||
updateKeyList();
|
||||
}
|
||||
|
||||
private class AliasAdapter extends ArrayAdapter<String> {
|
||||
public AliasAdapter(Context context) {
|
||||
// We want users to choose a key, so use the appropriate layout.
|
||||
super(context, android.R.layout.simple_list_item_single_choice);
|
||||
}
|
||||
|
||||
/**
|
||||
* This clears out all previous aliases and replaces it with the
|
||||
* current entries.
|
||||
*/
|
||||
public void setAliases(List<String> items) {
|
||||
clear();
|
||||
addAll(items);
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
}
|
||||
|
||||
private void updateKeyList() {
|
||||
setKeyActionButtonsEnabled(false);
|
||||
new UpdateKeyListTask().execute();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets all the buttons related to actions that act on an existing key to
|
||||
* enabled or disabled.
|
||||
*/
|
||||
private void setKeyActionButtonsEnabled(boolean enabled) {
|
||||
mSignButton.setEnabled(enabled);
|
||||
mVerifyButton.setEnabled(enabled);
|
||||
mDeleteButton.setEnabled(enabled);
|
||||
}
|
||||
|
||||
private class UpdateKeyListTask extends AsyncTask<Void, Void, Enumeration<String>> {
|
||||
@Override
|
||||
protected Enumeration<String> doInBackground(Void... params) {
|
||||
try {
|
||||
// BEGIN_INCLUDE(list)
|
||||
/*
|
||||
* Load the Android KeyStore instance using the the
|
||||
* "AndroidKeyStore" provider to list out what entries are
|
||||
* currently stored.
|
||||
*/
|
||||
KeyStore ks = KeyStore.getInstance("AndroidKeyStore");
|
||||
ks.load(null);
|
||||
Enumeration<String> aliases = ks.aliases();
|
||||
// END_INCLUDE(list)
|
||||
return aliases;
|
||||
} catch (KeyStoreException e) {
|
||||
Log.w(TAG, "Could not list keys", e);
|
||||
return null;
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
Log.w(TAG, "Could not list keys", e);
|
||||
return null;
|
||||
} catch (CertificateException e) {
|
||||
Log.w(TAG, "Could not list keys", e);
|
||||
return null;
|
||||
} catch (IOException e) {
|
||||
Log.w(TAG, "Could not list keys", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(Enumeration<String> result) {
|
||||
List<String> aliases = new ArrayList<String>();
|
||||
while (result.hasMoreElements()) {
|
||||
aliases.add(result.nextElement());
|
||||
}
|
||||
mAdapter.setAliases(aliases);
|
||||
}
|
||||
}
|
||||
|
||||
private class GenerateTask extends AsyncTask<String, Void, Boolean> {
|
||||
@Override
|
||||
protected Boolean doInBackground(String... params) {
|
||||
final String alias = params[0];
|
||||
try {
|
||||
// BEGIN_INCLUDE(generate)
|
||||
/*
|
||||
* Generate a new entry in the KeyStore by using the
|
||||
* KeyPairGenerator API. We have to specify the attributes for a
|
||||
* self-signed X.509 certificate here so the KeyStore can attach
|
||||
* the public key part to it. It can be replaced later with a
|
||||
* certificate signed by a Certificate Authority (CA) if needed.
|
||||
*/
|
||||
Calendar cal = Calendar.getInstance();
|
||||
Date now = cal.getTime();
|
||||
cal.add(Calendar.YEAR, 1);
|
||||
Date end = cal.getTime();
|
||||
|
||||
KeyPairGenerator kpg = KeyPairGenerator.getInstance("RSA", "AndroidKeyStore");
|
||||
kpg.initialize(new KeyPairGeneratorSpec.Builder(getApplicationContext())
|
||||
.setAlias(alias)
|
||||
.setStartDate(now)
|
||||
.setEndDate(end)
|
||||
.setSerialNumber(BigInteger.valueOf(1))
|
||||
.setSubject(new X500Principal("CN=test1"))
|
||||
.build());
|
||||
|
||||
KeyPair kp = kpg.generateKeyPair();
|
||||
// END_INCLUDE(generate)
|
||||
return true;
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
Log.w(TAG, "Could not generate key", e);
|
||||
return false;
|
||||
} catch (InvalidAlgorithmParameterException e) {
|
||||
Log.w(TAG, "Could not generate key", e);
|
||||
return false;
|
||||
} catch (NoSuchProviderException e) {
|
||||
Log.w(TAG, "Could not generate key", e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(Boolean result) {
|
||||
updateKeyList();
|
||||
mGenerateButton.setEnabled(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCancelled() {
|
||||
mGenerateButton.setEnabled(true);
|
||||
}
|
||||
}
|
||||
|
||||
private class SignTask extends AsyncTask<String, Void, String> {
|
||||
@Override
|
||||
protected String doInBackground(String... params) {
|
||||
final String alias = params[0];
|
||||
final String dataString = params[1];
|
||||
try {
|
||||
byte[] data = dataString.getBytes();
|
||||
// BEGIN_INCLUDE(sign)
|
||||
/*
|
||||
* Use a PrivateKey in the KeyStore to create a signature over
|
||||
* some data.
|
||||
*/
|
||||
KeyStore ks = KeyStore.getInstance("AndroidKeyStore");
|
||||
ks.load(null);
|
||||
KeyStore.Entry entry = ks.getEntry(alias, null);
|
||||
if (!(entry instanceof PrivateKeyEntry)) {
|
||||
Log.w(TAG, "Not an instance of a PrivateKeyEntry");
|
||||
return null;
|
||||
}
|
||||
Signature s = Signature.getInstance("SHA256withRSA");
|
||||
s.initSign(((PrivateKeyEntry) entry).getPrivateKey());
|
||||
s.update(data);
|
||||
byte[] signature = s.sign();
|
||||
// END_INCLUDE(sign)
|
||||
return Base64.encodeToString(signature, Base64.DEFAULT);
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
Log.w(TAG, "Could not generate key", e);
|
||||
return null;
|
||||
} catch (KeyStoreException e) {
|
||||
Log.w(TAG, "Could not generate key", e);
|
||||
return null;
|
||||
} catch (CertificateException e) {
|
||||
Log.w(TAG, "Could not generate key", e);
|
||||
return null;
|
||||
} catch (IOException e) {
|
||||
Log.w(TAG, "Could not generate key", e);
|
||||
return null;
|
||||
} catch (UnrecoverableEntryException e) {
|
||||
Log.w(TAG, "Could not generate key", e);
|
||||
return null;
|
||||
} catch (InvalidKeyException e) {
|
||||
Log.w(TAG, "Could not generate key", e);
|
||||
return null;
|
||||
} catch (SignatureException e) {
|
||||
Log.w(TAG, "Could not generate key", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(String result) {
|
||||
mCipherText.setText(result);
|
||||
setKeyActionButtonsEnabled(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCancelled() {
|
||||
mCipherText.setText("error!");
|
||||
setKeyActionButtonsEnabled(true);
|
||||
}
|
||||
}
|
||||
|
||||
private class VerifyTask extends AsyncTask<String, Void, Boolean> {
|
||||
@Override
|
||||
protected Boolean doInBackground(String... params) {
|
||||
final String alias = params[0];
|
||||
final String dataString = params[1];
|
||||
final String signatureString = params[2];
|
||||
try {
|
||||
byte[] data = dataString.getBytes();
|
||||
byte[] signature;
|
||||
try {
|
||||
signature = Base64.decode(signatureString, Base64.DEFAULT);
|
||||
} catch (IllegalArgumentException e) {
|
||||
signature = new byte[0];
|
||||
}
|
||||
// BEGIN_INCLUDE(verify)
|
||||
/*
|
||||
* Verify a signature previously made by a PrivateKey in our
|
||||
* KeyStore. This uses the X.509 certificate attached to our
|
||||
* private key in the KeyStore to validate a previously
|
||||
* generated signature.
|
||||
*/
|
||||
KeyStore ks = KeyStore.getInstance("AndroidKeyStore");
|
||||
ks.load(null);
|
||||
KeyStore.Entry entry = ks.getEntry(alias, null);
|
||||
if (!(entry instanceof PrivateKeyEntry)) {
|
||||
Log.w(TAG, "Not an instance of a PrivateKeyEntry");
|
||||
return false;
|
||||
}
|
||||
Signature s = Signature.getInstance("SHA256withRSA");
|
||||
s.initVerify(((PrivateKeyEntry) entry).getCertificate());
|
||||
s.update(data);
|
||||
boolean valid = s.verify(signature);
|
||||
// END_INCLUDE(verify)
|
||||
return valid;
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
Log.w(TAG, "Could not generate key", e);
|
||||
return false;
|
||||
} catch (KeyStoreException e) {
|
||||
Log.w(TAG, "Could not generate key", e);
|
||||
return false;
|
||||
} catch (CertificateException e) {
|
||||
Log.w(TAG, "Could not generate key", e);
|
||||
return false;
|
||||
} catch (IOException e) {
|
||||
Log.w(TAG, "Could not generate key", e);
|
||||
return false;
|
||||
} catch (UnrecoverableEntryException e) {
|
||||
Log.w(TAG, "Could not generate key", e);
|
||||
return false;
|
||||
} catch (InvalidKeyException e) {
|
||||
Log.w(TAG, "Could not generate key", e);
|
||||
return false;
|
||||
} catch (SignatureException e) {
|
||||
Log.w(TAG, "Could not generate key", e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(Boolean result) {
|
||||
if (result) {
|
||||
mCipherText.setTextColor(getResources().getColor(R.color.solid_green));
|
||||
} else {
|
||||
mCipherText.setTextColor(getResources().getColor(R.color.solid_red));
|
||||
}
|
||||
setKeyActionButtonsEnabled(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCancelled() {
|
||||
mCipherText.setText("error!");
|
||||
setKeyActionButtonsEnabled(true);
|
||||
mCipherText.setTextColor(getResources().getColor(android.R.color.primary_text_dark));
|
||||
}
|
||||
}
|
||||
|
||||
private class DeleteTask extends AsyncTask<String, Void, Void> {
|
||||
@Override
|
||||
protected Void doInBackground(String... params) {
|
||||
final String alias = params[0];
|
||||
try {
|
||||
// BEGIN_INCLUDE(delete)
|
||||
/*
|
||||
* Deletes a previously generated or stored entry in the
|
||||
* KeyStore.
|
||||
*/
|
||||
KeyStore ks = KeyStore.getInstance("AndroidKeyStore");
|
||||
ks.load(null);
|
||||
ks.deleteEntry(alias);
|
||||
// END_INCLUDE(delete)
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
Log.w(TAG, "Could not generate key", e);
|
||||
} catch (KeyStoreException e) {
|
||||
Log.w(TAG, "Could not generate key", e);
|
||||
} catch (CertificateException e) {
|
||||
Log.w(TAG, "Could not generate key", e);
|
||||
} catch (IOException e) {
|
||||
Log.w(TAG, "Could not generate key", e);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(Void result) {
|
||||
updateKeyList();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCancelled() {
|
||||
updateKeyList();
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user