Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Migrate Function For KeystoreIdentityCredential #346

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view

Large diffs are not rendered by default.

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 @@ -26,7 +26,12 @@
import androidx.annotation.Nullable;
import androidx.biometric.BiometricPrompt;

import com.android.identity.android.securearea.AndroidKeystoreSecureArea;
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.securearea.SecureArea;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
Expand Down Expand Up @@ -818,4 +823,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();
AndroidKeystoreSecureArea.CreateKeySettings.Builder keySettingsBuilder = Utility.extractKeySettings(aliasForOldCredKey);
keySettingsBuilder.setEcCurve(SecureArea.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,38 @@

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.securearea.AndroidKeystoreSecureArea;
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.securearea.SecureArea;
import com.android.identity.util.Timestamp;
import com.android.identity.internal.Util;

import java.security.InvalidAlgorithmParameterException;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.io.IOException;
import java.security.KeyFactory;
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 +63,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 +357,70 @@ public static IdentityCredentialStore getIdentityCredentialStore(@NonNull Contex
//return IdentityCredentialStore.getHardwareInstance(context);
return IdentityCredentialStore.getKeystoreInstance(context, context.getNoBackupFilesDir());
}

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

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

public static @NonNull AndroidKeystoreSecureArea.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};
AndroidKeystoreSecureArea.CreateKeySettings.Builder keySettingsBuilder =
new AndroidKeystoreSecureArea.CreateKeySettings.Builder(credentialKeyAttestationChallenge);

keySettingsBuilder.setExistingKeyAlias(keyAlias);
keySettingsBuilder.setKeyPurposes(convertKeyPurpose(keyInfo));
if (keyInfo.isUserAuthenticationRequired()) {
long userAuthenticationTimeoutMillis = keyInfo.getUserAuthenticationValidityDurationSeconds() * 1000L;
keySettingsBuilder.setUserAuthenticationRequired(true, userAuthenticationTimeoutMillis, AndroidKeystoreSecureArea.USER_AUTHENTICATION_TYPE_LSKF);
}
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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,11 @@ public AndroidKeystoreSecureArea(@NonNull Context context,
public void createKey(@NonNull String alias,
@NonNull SecureArea.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 @@ -312,6 +317,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 @@ -835,6 +860,7 @@ public static class CreateKeySettings extends SecureArea.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 @@ -845,7 +871,8 @@ private CreateKeySettings(@KeyPurpose int keyPurpose,
boolean useStrongBox,
@Nullable String attestKeyAlias,
@Nullable Timestamp validFrom,
@Nullable Timestamp validUntil) {
@Nullable Timestamp validUntil,
@Nullable String existingKeyAlias) {
super(AndroidKeystoreSecureArea.class);
mKeyPurposes = keyPurpose;
mEcCurve = ecCurve;
Expand All @@ -857,6 +884,7 @@ private CreateKeySettings(@KeyPurpose int keyPurpose,
mAttestKeyAlias = attestKeyAlias;
mValidFrom = validFrom;
mValidUntil = validUntil;
mExistingKeyAlias = existingKeyAlias;
}

/**
Expand Down Expand Up @@ -950,6 +978,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 @@ -964,6 +1000,7 @@ public static class Builder {
private String mAttestKeyAlias;
private Timestamp mValidFrom;
private Timestamp mValidUntil;
private String mExistingKeyAlias;

/**
* Constructor.
Expand Down Expand Up @@ -1050,7 +1087,7 @@ public Builder(@NonNull byte[] attestationChallenge) {
/**
* Method to specify if StrongBox Android Keystore should be used, if available.
*
* By default StrongBox isn't used.
* <p>By default StrongBox isn't used.
*
* @param useStrongBox Whether to use StrongBox.
* @return the builder.
Expand Down Expand Up @@ -1091,6 +1128,21 @@ public Builder(@NonNull byte[] attestationChallenge) {
return this;
}

/**
* Method to specify both (1) if the {@link CreateKeySettings} should represent a key
* which already exists, and (2) the alias of said key.
*
* <p>By default the alias is <code>null</code>, indicating a new key should be created
* rather than repurposing an existing key.
*
* @param alias the alias of the key.
* @return the builder.
*/
public @NonNull Builder setExistingKeyAlias(@NonNull String alias) {
suzannajiwani marked this conversation as resolved.
Show resolved Hide resolved
mExistingKeyAlias = alias;
return this;
}

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

Expand Down
Loading
Loading