Skip to content

Commit

Permalink
Add Migrate Function For KeystoreIdentityCredential
Browse files Browse the repository at this point in the history
Added a function to the KeystoreIdentityCredential class that creates a new
Credential (identity/src/main/java/com/android/identity/credential/Credential.java)
using the information in the given KeystoreIdentityCredential and deletes the
KeystoreIdentityCredential once the new Credential is created. This change required
additional functions in the Credential, CredentialStore, and AndroidKeystore classes
which allow Credential creation with an existing key so the credential key from the
KeystoreIdentityCredential could be preserved. A function in Utility.java to extract
key settings from an existing key as well as a function in CredentialData.java
to delete credential information while preserving the key were also added to aid in
this process.

Tested in MigrateFromKeystoreICStoreTest.
  • Loading branch information
suzannajiwani committed Aug 11, 2023
1 parent 982ad4b commit ff7da86
Show file tree
Hide file tree
Showing 7 changed files with 834 additions and 8 deletions.

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,11 @@ public AndroidKeystore(@NonNull Context context,
public void createKey(@NonNull String alias,
@NonNull KeystoreEngine.CreateKeySettings createKeySettings) {
CreateKeySettings aSettings = (CreateKeySettings) createKeySettings;
if (aSettings.getExistingKeyAlias() != null) {
createFromExistingKey(aSettings.getExistingKeyAlias(), aSettings);
return;
}

KeyPairGenerator kpg = null;
try {
kpg = KeyPairGenerator.getInstance(
Expand Down Expand Up @@ -271,6 +276,26 @@ public void createKey(@NonNull String alias,
saveKeyMetadata(alias, aSettings, attestation);
}

private void createFromExistingKey(@NonNull String existingKeyAlias, CreateKeySettings aSettings) {
List<X509Certificate> attestation = new ArrayList<>();
try {
KeyStore ks = KeyStore.getInstance("AndroidKeyStore");
ks.load(null);
Certificate[] certificates = ks.getCertificateChain(existingKeyAlias);
for (Certificate certificate : certificates) {
attestation.add((X509Certificate) certificate);
}
} catch (CertificateException
| KeyStoreException
| IOException
| NoSuchAlgorithmException e) {
throw new IllegalStateException("Error generating certificate chain", e);
}

saveKeyMetadata(existingKeyAlias, aSettings, attestation);
Logger.d(TAG, "EC key with alias '" + existingKeyAlias + "' transferred");
}

@Override
public void deleteKey(@NonNull String alias) {
KeyStore ks;
Expand Down Expand Up @@ -765,6 +790,7 @@ public static class CreateKeySettings extends KeystoreEngine.CreateKeySettings {
private final String mAttestKeyAlias;
private final Timestamp mValidFrom;
private final Timestamp mValidUntil;
private final String mExistingKeyAlias;

private CreateKeySettings(@KeyPurpose int keyPurpose,
@EcCurve int ecCurve,
Expand All @@ -774,7 +800,8 @@ private CreateKeySettings(@KeyPurpose int keyPurpose,
boolean useStrongBox,
@Nullable String attestKeyAlias,
@Nullable Timestamp validFrom,
@Nullable Timestamp validUntil) {
@Nullable Timestamp validUntil,
@Nullable String existingKeyAlias) {
super(AndroidKeystore.class);
mKeyPurposes = keyPurpose;
mEcCurve = ecCurve;
Expand All @@ -785,6 +812,7 @@ private CreateKeySettings(@KeyPurpose int keyPurpose,
mAttestKeyAlias = attestKeyAlias;
mValidFrom = validFrom;
mValidUntil = validUntil;
mExistingKeyAlias = existingKeyAlias;
}

/**
Expand Down Expand Up @@ -868,6 +896,14 @@ public boolean getUseStrongBox() {
return mValidUntil;
}

/**
* If the CreateKeySettings represents a key which already exists, returns the alias of the
* key which it represents. Otherwise, returns {@code null}.
*
* @return the alias or {@code null} if not set.
*/
public @Nullable String getExistingKeyAlias() { return mExistingKeyAlias; }

/**
* A builder for {@link CreateKeySettings}.
*/
Expand All @@ -881,6 +917,7 @@ public static class Builder {
private String mAttestKeyAlias;
private Timestamp mValidFrom;
private Timestamp mValidUntil;
private String mExistingKeyAlias;

/**
* Constructor.
Expand Down Expand Up @@ -982,6 +1019,11 @@ public Builder(@NonNull byte[] attestationChallenge) {
return this;
}

public @NonNull Builder setExistingKeyAlias(@NonNull String alias) {
mExistingKeyAlias = alias;
return this;
}

/**
* Builds the {@link CreateKeySettings}.
*
Expand All @@ -997,7 +1039,8 @@ public Builder(@NonNull byte[] attestationChallenge) {
mUseStrongBox,
mAttestKeyAlias,
mValidFrom,
mValidUntil);
mValidUntil,
mExistingKeyAlias);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -655,6 +655,59 @@ static byte[] delete(@NonNull Context context, @NonNull File storageDirectory,
return signature;
}

static boolean deleteForMigration(@NonNull Context context, @NonNull File storageDirectory,
@NonNull String credentialName) {
String filename = getFilenameForCredentialData(credentialName);
AtomicFile file = new AtomicFile(new File(storageDirectory, filename));
try {
file.openRead();
} catch (FileNotFoundException e) {
return false;
}

CredentialData data = new CredentialData(context, storageDirectory, credentialName);
String dataKeyAlias = getDataKeyAliasFromCredentialName(credentialName);
try {
data.loadFromDisk(dataKeyAlias);
} catch (RuntimeException e) {
Log.e(TAG, "Error parsing file on disk (old version?). Deleting anyway.");
}
file.delete();

KeyStore ks;
try {
ks = KeyStore.getInstance("AndroidKeyStore");
ks.load(null);
} catch (CertificateException
| IOException
| NoSuchAlgorithmException
| KeyStoreException e) {
throw new RuntimeException("Error loading keystore", e);
}

// Nuke all keys.
try {
if (!data.mPerReaderSessionKeyAlias.isEmpty()) {
ks.deleteEntry(data.mPerReaderSessionKeyAlias);
}
for (String alias : data.mAcpTimeoutKeyAliases.values()) {
ks.deleteEntry(alias);
}
for (AuthKeyData authKeyData : data.mAuthKeyDatas) {
if (!authKeyData.mAlias.isEmpty()) {
ks.deleteEntry(authKeyData.mAlias);
}
if (!authKeyData.mPendingAlias.isEmpty()) {
ks.deleteEntry(authKeyData.mPendingAlias);
}
}
} catch (KeyStoreException e) {
throw new RuntimeException("Error deleting key", e);
}

return true;
}

private void createDataEncryptionKey() {
// TODO: it could maybe be nice to encrypt data with the appropriate auth-bound
// key (the one associated with the ACP with the longest timeout), if it doesn't
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,18 +19,25 @@
import android.content.Context;
import android.icu.util.Calendar;
import android.security.keystore.KeyProperties;
import android.util.AtomicFile;
import android.util.Log;
import android.util.Pair;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.biometric.BiometricPrompt;

import com.android.identity.android.keystore.AndroidKeystore;
import com.android.identity.credential.Credential;
import com.android.identity.credential.CredentialStore;
import com.android.identity.credential.NameSpacedData;
import com.android.identity.internal.Util;
import com.android.identity.keystore.KeystoreEngine;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.security.InvalidAlgorithmParameterException;
Expand Down Expand Up @@ -818,4 +825,52 @@ List<Calendar> getAuthenticationDataExpirations() {
return mData.getAuthKeyExpirations();
}

// tested in identity-android/src/androidTest/java/com/android/identity/android/legacy/MigrateFromKeystoreICStoreTest.java
/**
* Gathers all the {@link PersonalizationData.NamespaceData} from this credential and creates a
* new {@link Credential} with this data inside the given {@link CredentialStore}. The key used
* by the {@link CredentialData} in this credential is preserved, and this method will also pass
* metadata for this key to the new {@link Credential} for future usage. Once the new
* {@link Credential} is created, this method will delete the file with encrypted
* {@link CredentialData} as well as any per reader session keys, acp timeout keys, and auth keys.
*
* <p> The returned {@link Credential} will also have the same name as this credential, so it can
* be retrieved from the given {@link CredentialStore} using
* {@link CredentialStore#lookupCredential(String)} with the same name.
*
* <p> In total, the data within each namespace and the credential key will be migrated to the
* new {@link Credential}, while the access control profile information, per reader session/acp
* timeout/auth keys will not be transferred.
*
* @param credentialStore the credential store where the new {@link Credential} should be stored.
* @return the new {@link Credential}.
*/
public @NonNull Credential migrateToCredentialStore(@NonNull CredentialStore credentialStore) {
loadData();

if (mData == null) {
throw new IllegalStateException("The credential has been deleted prior to migration.");
}
String aliasForOldCredKey = mData.getCredentialKeyAlias();
AndroidKeystore.CreateKeySettings.Builder keySettingsBuilder = Utility.extractKeySettings(aliasForOldCredKey);
keySettingsBuilder.setEcCurve(KeystoreEngine.EC_CURVE_P256);

Credential newCred = credentialStore.createCredentialWithExistingKey(mCredentialName,
keySettingsBuilder.build(), aliasForOldCredKey);

NameSpacedData.Builder nsBuilder = new NameSpacedData.Builder();
for (PersonalizationData.NamespaceData namespaceData : mData.getNamespaceDatas()) {
for (String entryName : namespaceData.getEntryNames()) {
byte[] value = namespaceData.getEntryValue(entryName);
nsBuilder.putEntry(namespaceData.mNamespace, entryName, value);
}
}

newCred.setNameSpacedData(nsBuilder.build());

CredentialData.deleteForMigration(mContext, mStorageDirectory, mCredentialName);

return newCred;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -20,32 +20,43 @@

import android.content.Context;
import android.icu.util.Calendar;
import android.os.Build;
import android.security.keystore.KeyInfo;
import android.security.keystore.KeyProperties;
import android.util.Log;

import androidx.annotation.Nullable;
import androidx.core.util.Pair;

import androidx.annotation.NonNull;

import com.android.identity.android.keystore.AndroidKeystore;
import com.android.identity.keystore.KeystoreEngine;
import com.android.identity.mdoc.mso.StaticAuthDataGenerator;
import com.android.identity.mdoc.response.DeviceResponseGenerator;
import com.android.identity.mdoc.mso.MobileSecurityObjectGenerator;
import com.android.identity.util.Constants;
import com.android.identity.util.Timestamp;
import com.android.identity.internal.Util;

import java.io.IOException;
import java.security.InvalidAlgorithmParameterException;
import java.security.KeyFactory;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.SecureRandom;
import java.security.UnrecoverableKeyException;
import java.security.cert.CertificateEncodingException;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.security.spec.ECGenParameterSpec;
import java.security.spec.InvalidKeySpecException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
Expand All @@ -57,12 +68,7 @@
import java.util.Random;

import co.nstant.in.cbor.CborBuilder;
import co.nstant.in.cbor.builder.ArrayBuilder;
import co.nstant.in.cbor.builder.MapBuilder;
import co.nstant.in.cbor.model.ByteString;
import co.nstant.in.cbor.model.DataItem;
import co.nstant.in.cbor.model.SimpleValue;
import co.nstant.in.cbor.model.SimpleValueType;
import co.nstant.in.cbor.model.UnicodeString;

/**
Expand Down Expand Up @@ -356,4 +362,70 @@ public static IdentityCredentialStore getIdentityCredentialStore(@NonNull Contex
//return IdentityCredentialStore.getHardwareInstance(context);
return IdentityCredentialStore.getKeystoreInstance(context, context.getNoBackupFilesDir());
}

private static @KeystoreEngine.KeyPurpose int convertKeyPurpose(KeyInfo keyInfo) {
@KeystoreEngine.KeyPurpose int keyPurposeIC = 0;
int keyPurposesAndroid = keyInfo.getPurposes();

if ((keyPurposesAndroid & KeyProperties.PURPOSE_AGREE_KEY) == KeyProperties.PURPOSE_AGREE_KEY) {
keyPurposeIC = keyPurposeIC | KeystoreEngine.KEY_PURPOSE_AGREE_KEY;
}
if ((keyPurposesAndroid & KeyProperties.PURPOSE_SIGN) == KeyProperties.PURPOSE_SIGN) {
keyPurposeIC = keyPurposeIC | KeystoreEngine.KEY_PURPOSE_SIGN;
}
return keyPurposeIC;
}

public static @NonNull AndroidKeystore.CreateKeySettings.Builder extractKeySettings(String keyAlias) {
KeyStore ks;
PrivateKey privateKey;
try {
ks = KeyStore.getInstance("AndroidKeyStore");
ks.load(null);
privateKey = (PrivateKey) ks.getKey(keyAlias, null);
} catch (CertificateException
| KeyStoreException
| IOException
| NoSuchAlgorithmException e) {
throw new IllegalStateException("Error generate certificate chain", e);
} catch (UnrecoverableKeyException e) {
throw new IllegalStateException("Error retrieving key", e);
}

KeyInfo keyInfo;
try {
KeyFactory factory = KeyFactory.getInstance(privateKey.getAlgorithm(), "AndroidKeyStore");
keyInfo = factory.getKeySpec(privateKey, KeyInfo.class);
} catch (InvalidKeySpecException e) {
throw new IllegalStateException("Unrecoverable Key: Not an Android KeyStore key", e);
} catch (NoSuchAlgorithmException
| NoSuchProviderException e) {
throw new RuntimeException(e);
}

// attestation challenge will have no impact since a key will not be created with these settings
byte[] credentialKeyAttestationChallenge = new byte[] {10, 11, 12};
AndroidKeystore.CreateKeySettings.Builder keySettingsBuilder =
new AndroidKeystore.CreateKeySettings.Builder(credentialKeyAttestationChallenge);

keySettingsBuilder.setExistingKeyAlias(keyAlias);
keySettingsBuilder.setKeyPurposes(convertKeyPurpose(keyInfo));
if (keyInfo.isUserAuthenticationRequired()) {
long userAuthenticationTimeoutMillis = keyInfo.getUserAuthenticationValidityDurationSeconds() * 1000L;
keySettingsBuilder.setUserAuthenticationRequired(true, userAuthenticationTimeoutMillis);
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
if ((keyInfo.getSecurityLevel() & KeyProperties.SECURITY_LEVEL_STRONGBOX) ==
KeyProperties.SECURITY_LEVEL_STRONGBOX) {
keySettingsBuilder.setUseStrongBox(true);
}
}
if (keyInfo.getKeyValidityStart() != null & keyInfo.getKeyValidityForOriginationEnd() != null) {
Timestamp validFrom = Timestamp.ofEpochMilli(keyInfo.getKeyValidityStart().getTime());
Timestamp validUntil = Timestamp.ofEpochMilli(keyInfo.getKeyValidityForOriginationEnd().getTime());
keySettingsBuilder.setValidityPeriod(validFrom, validUntil);
}

return keySettingsBuilder;
}
}
Loading

0 comments on commit ff7da86

Please sign in to comment.