diff options
author | Jeff Davidson <jpd@google.com> | 2018-02-08 15:30:06 -0800 |
---|---|---|
committer | Jeff Davidson <jpd@google.com> | 2018-02-08 15:30:06 -0800 |
commit | a192cc2a132cb0ee8588e2df755563ec7008c179 (patch) | |
tree | 380e4db22df19c819bd37df34bf06e7568916a50 /android/security | |
parent | 98fe7819c6d14f4f464a5cac047f9e82dee5da58 (diff) | |
download | android-28-a192cc2a132cb0ee8588e2df755563ec7008c179.tar.gz |
Update fullsdk to 4575844
/google/data/ro/projects/android/fetch_artifact \
--bid 4575844 \
--target sdk_phone_x86_64-sdk \
sdk-repo-linux-sources-4575844.zip
Test: TreeHugger
Change-Id: I81e0eb157b4ac3b38408d0ef86f9d6286471f87a
Diffstat (limited to 'android/security')
45 files changed, 4027 insertions, 916 deletions
diff --git a/android/security/KeyStore.java b/android/security/KeyStore.java index fabcdf00..e25386ba 100644 --- a/android/security/KeyStore.java +++ b/android/security/KeyStore.java @@ -424,15 +424,6 @@ public class KeyStore { return getmtime(key, UID_SELF); } - public boolean duplicate(String srcKey, int srcUid, String destKey, int destUid) { - try { - return mBinder.duplicate(srcKey, srcUid, destKey, destUid) == NO_ERROR; - } catch (RemoteException e) { - Log.w(TAG, "Cannot connect to keystore", e); - return false; - } - } - // TODO: remove this when it's removed from Settings public boolean isHardwareBacked() { return isHardwareBacked("RSA"); @@ -519,6 +510,19 @@ public class KeyStore { return importKey(alias, args, format, keyData, UID_SELF, flags, outCharacteristics); } + public int importWrappedKey(String wrappedKeyAlias, byte[] wrappedKey, + String wrappingKeyAlias, + byte[] maskingKey, KeymasterArguments args, long rootSid, long fingerprintSid, int uid, + KeyCharacteristics outCharacteristics) { + try { + return mBinder.importWrappedKey(wrappedKeyAlias, wrappedKey, wrappingKeyAlias, + maskingKey, args, rootSid, fingerprintSid, outCharacteristics); + } catch (RemoteException e) { + Log.w(TAG, "Cannot connect to keystore", e); + return SYSTEM_ERROR; + } + } + public ExportResult exportKey(String alias, int format, KeymasterBlob clientId, KeymasterBlob appId, int uid) { try { diff --git a/android/security/keymaster/KeymasterDefs.java b/android/security/keymaster/KeymasterDefs.java index f409e5b7..34643703 100644 --- a/android/security/keymaster/KeymasterDefs.java +++ b/android/security/keymaster/KeymasterDefs.java @@ -73,6 +73,7 @@ public final class KeymasterDefs { public static final int KM_TAG_USER_AUTH_TYPE = KM_ENUM | 504; public static final int KM_TAG_AUTH_TIMEOUT = KM_UINT | 505; public static final int KM_TAG_ALLOW_WHILE_ON_BODY = KM_BOOL | 506; + public static final int KM_TAG_TRUSTED_USER_PRESENCE_REQUIRED = KM_BOOL | 507; public static final int KM_TAG_ALL_APPLICATIONS = KM_BOOL | 600; public static final int KM_TAG_APPLICATION_ID = KM_BYTES | 601; @@ -101,6 +102,7 @@ public final class KeymasterDefs { public static final int KM_ALGORITHM_RSA = 1; public static final int KM_ALGORITHM_EC = 3; public static final int KM_ALGORITHM_AES = 32; + public static final int KM_ALGORITHM_3DES = 33; public static final int KM_ALGORITHM_HMAC = 128; // Block modes. @@ -130,6 +132,7 @@ public final class KeymasterDefs { public static final int KM_ORIGIN_GENERATED = 0; public static final int KM_ORIGIN_IMPORTED = 2; public static final int KM_ORIGIN_UNKNOWN = 3; + public static final int KM_ORIGIN_SECURELY_IMPORTED = 4; // Key usability requirements. public static final int KM_BLOB_STANDALONE = 0; @@ -140,6 +143,7 @@ public final class KeymasterDefs { public static final int KM_PURPOSE_DECRYPT = 1; public static final int KM_PURPOSE_SIGN = 2; public static final int KM_PURPOSE_VERIFY = 3; + public static final int KM_PURPOSE_WRAP = 5; // Key formats. public static final int KM_KEY_FORMAT_X509 = 0; diff --git a/android/security/keystore/AndroidKeyStore3DESCipherSpi.java b/android/security/keystore/AndroidKeyStore3DESCipherSpi.java new file mode 100644 index 00000000..01fd0624 --- /dev/null +++ b/android/security/keystore/AndroidKeyStore3DESCipherSpi.java @@ -0,0 +1,298 @@ +/* + * Copyright (C) 2018 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 android.security.keystore; + +import android.security.keymaster.KeymasterArguments; +import android.security.keymaster.KeymasterDefs; + +import java.security.AlgorithmParameters; +import java.security.InvalidAlgorithmParameterException; +import java.security.InvalidKeyException; +import java.security.Key; +import java.security.NoSuchAlgorithmException; +import java.security.ProviderException; +import java.security.spec.AlgorithmParameterSpec; +import java.security.spec.InvalidParameterSpecException; +import java.util.Arrays; + +import javax.crypto.CipherSpi; +import javax.crypto.spec.IvParameterSpec; + +/** + * Base class for Android Keystore 3DES {@link CipherSpi} implementations. + * + * @hide + */ +public class AndroidKeyStore3DESCipherSpi extends AndroidKeyStoreCipherSpiBase { + + private static final int BLOCK_SIZE_BYTES = 8; + + private final int mKeymasterBlockMode; + private final int mKeymasterPadding; + /** Whether this transformation requires an IV. */ + private final boolean mIvRequired; + + private byte[] mIv; + + /** Whether the current {@code #mIv} has been used by the underlying crypto operation. */ + private boolean mIvHasBeenUsed; + + AndroidKeyStore3DESCipherSpi( + int keymasterBlockMode, + int keymasterPadding, + boolean ivRequired) { + mKeymasterBlockMode = keymasterBlockMode; + mKeymasterPadding = keymasterPadding; + mIvRequired = ivRequired; + } + + abstract static class ECB extends AndroidKeyStore3DESCipherSpi { + protected ECB(int keymasterPadding) { + super(KeymasterDefs.KM_MODE_ECB, keymasterPadding, false); + } + + public static class NoPadding extends ECB { + public NoPadding() { + super(KeymasterDefs.KM_PAD_NONE); + } + } + + public static class PKCS7Padding extends ECB { + public PKCS7Padding() { + super(KeymasterDefs.KM_PAD_PKCS7); + } + } + } + + abstract static class CBC extends AndroidKeyStore3DESCipherSpi { + protected CBC(int keymasterPadding) { + super(KeymasterDefs.KM_MODE_CBC, keymasterPadding, true); + } + + public static class NoPadding extends CBC { + public NoPadding() { + super(KeymasterDefs.KM_PAD_NONE); + } + } + + public static class PKCS7Padding extends CBC { + public PKCS7Padding() { + super(KeymasterDefs.KM_PAD_PKCS7); + } + } + } + + @Override + protected void initKey(int i, Key key) throws InvalidKeyException { + if (!(key instanceof AndroidKeyStoreSecretKey)) { + throw new InvalidKeyException( + "Unsupported key: " + ((key != null) ? key.getClass().getName() : "null")); + } + if (!KeyProperties.KEY_ALGORITHM_3DES.equalsIgnoreCase(key.getAlgorithm())) { + throw new InvalidKeyException( + "Unsupported key algorithm: " + key.getAlgorithm() + ". Only " + + KeyProperties.KEY_ALGORITHM_3DES + " supported"); + } + setKey((AndroidKeyStoreSecretKey) key); + } + + @Override + protected int engineGetBlockSize() { + return BLOCK_SIZE_BYTES; + } + + @Override + protected int engineGetOutputSize(int inputLen) { + return inputLen + 3 * BLOCK_SIZE_BYTES; + } + + @Override + protected final byte[] engineGetIV() { + return ArrayUtils.cloneIfNotEmpty(mIv); + } + + @Override + protected AlgorithmParameters engineGetParameters() { + if (!mIvRequired) { + return null; + } + if ((mIv != null) && (mIv.length > 0)) { + try { + AlgorithmParameters params = AlgorithmParameters.getInstance("DESede"); + params.init(new IvParameterSpec(mIv)); + return params; + } catch (NoSuchAlgorithmException e) { + throw new ProviderException( + "Failed to obtain 3DES AlgorithmParameters", e); + } catch (InvalidParameterSpecException e) { + throw new ProviderException( + "Failed to initialize 3DES AlgorithmParameters with an IV", + e); + } + } + return null; + } + + @Override + protected void initAlgorithmSpecificParameters() throws InvalidKeyException { + if (!mIvRequired) { + return; + } + + // IV is used + if (!isEncrypting()) { + throw new InvalidKeyException("IV required when decrypting" + + ". Use IvParameterSpec or AlgorithmParameters to provide it."); + } + } + + @Override + protected void initAlgorithmSpecificParameters(AlgorithmParameterSpec params) + throws InvalidAlgorithmParameterException { + if (!mIvRequired) { + if (params != null) { + throw new InvalidAlgorithmParameterException("Unsupported parameters: " + params); + } + return; + } + + // IV is used + if (params == null) { + if (!isEncrypting()) { + // IV must be provided by the caller + throw new InvalidAlgorithmParameterException( + "IvParameterSpec must be provided when decrypting"); + } + return; + } + if (!(params instanceof IvParameterSpec)) { + throw new InvalidAlgorithmParameterException("Only IvParameterSpec supported"); + } + mIv = ((IvParameterSpec) params).getIV(); + if (mIv == null) { + throw new InvalidAlgorithmParameterException("Null IV in IvParameterSpec"); + } + } + + @Override + protected void initAlgorithmSpecificParameters(AlgorithmParameters params) + throws InvalidAlgorithmParameterException { + if (!mIvRequired) { + if (params != null) { + throw new InvalidAlgorithmParameterException("Unsupported parameters: " + params); + } + return; + } + + // IV is used + if (params == null) { + if (!isEncrypting()) { + // IV must be provided by the caller + throw new InvalidAlgorithmParameterException("IV required when decrypting" + + ". Use IvParameterSpec or AlgorithmParameters to provide it."); + } + return; + } + + if (!"DESede".equalsIgnoreCase(params.getAlgorithm())) { + throw new InvalidAlgorithmParameterException( + "Unsupported AlgorithmParameters algorithm: " + params.getAlgorithm() + + ". Supported: DESede"); + } + + IvParameterSpec ivSpec; + try { + ivSpec = params.getParameterSpec(IvParameterSpec.class); + } catch (InvalidParameterSpecException e) { + if (!isEncrypting()) { + // IV must be provided by the caller + throw new InvalidAlgorithmParameterException("IV required when decrypting" + + ", but not found in parameters: " + params, e); + } + mIv = null; + return; + } + mIv = ivSpec.getIV(); + if (mIv == null) { + throw new InvalidAlgorithmParameterException("Null IV in AlgorithmParameters"); + } + } + + @Override + protected final int getAdditionalEntropyAmountForBegin() { + if ((mIvRequired) && (mIv == null) && (isEncrypting())) { + // IV will need to be generated + return BLOCK_SIZE_BYTES; + } + + return 0; + } + + @Override + protected int getAdditionalEntropyAmountForFinish() { + return 0; + } + + @Override + protected void addAlgorithmSpecificParametersToBegin(KeymasterArguments keymasterArgs) { + if ((isEncrypting()) && (mIvRequired) && (mIvHasBeenUsed)) { + // IV is being reused for encryption: this violates security best practices. + throw new IllegalStateException( + "IV has already been used. Reusing IV in encryption mode violates security best" + + " practices."); + } + + keymasterArgs.addEnum(KeymasterDefs.KM_TAG_ALGORITHM, KeymasterDefs.KM_ALGORITHM_3DES); + keymasterArgs.addEnum(KeymasterDefs.KM_TAG_BLOCK_MODE, mKeymasterBlockMode); + keymasterArgs.addEnum(KeymasterDefs.KM_TAG_PADDING, mKeymasterPadding); + if ((mIvRequired) && (mIv != null)) { + keymasterArgs.addBytes(KeymasterDefs.KM_TAG_NONCE, mIv); + } + } + + @Override + protected void loadAlgorithmSpecificParametersFromBeginResult( + KeymasterArguments keymasterArgs) { + mIvHasBeenUsed = true; + + // NOTE: Keymaster doesn't always return an IV, even if it's used. + byte[] returnedIv = keymasterArgs.getBytes(KeymasterDefs.KM_TAG_NONCE, null); + if ((returnedIv != null) && (returnedIv.length == 0)) { + returnedIv = null; + } + + if (mIvRequired) { + if (mIv == null) { + mIv = returnedIv; + } else if ((returnedIv != null) && (!Arrays.equals(returnedIv, mIv))) { + throw new ProviderException("IV in use differs from provided IV"); + } + } else { + if (returnedIv != null) { + throw new ProviderException( + "IV in use despite IV not being used by this transformation"); + } + } + } + + @Override + protected final void resetAll() { + mIv = null; + mIvHasBeenUsed = false; + super.resetAll(); + } +} diff --git a/android/security/keystore/AndroidKeyStoreBCWorkaroundProvider.java b/android/security/keystore/AndroidKeyStoreBCWorkaroundProvider.java index be390ffc..e4cf84af 100644 --- a/android/security/keystore/AndroidKeyStoreBCWorkaroundProvider.java +++ b/android/security/keystore/AndroidKeyStoreBCWorkaroundProvider.java @@ -93,6 +93,16 @@ class AndroidKeyStoreBCWorkaroundProvider extends Provider { putSymmetricCipherImpl("AES/CTR/NoPadding", PACKAGE_NAME + ".AndroidKeyStoreUnauthenticatedAESCipherSpi$CTR$NoPadding"); + putSymmetricCipherImpl("DESede/CBC/NoPadding", + PACKAGE_NAME + ".AndroidKeyStore3DESCipherSpi$CBC$NoPadding"); + putSymmetricCipherImpl("DESede/CBC/PKCS7Padding", + PACKAGE_NAME + ".AndroidKeyStore3DESCipherSpi$CBC$PKCS7Padding"); + + putSymmetricCipherImpl("DESede/ECB/NoPadding", + PACKAGE_NAME + ".AndroidKeyStore3DESCipherSpi$ECB$NoPadding"); + putSymmetricCipherImpl("DESede/ECB/PKCS7Padding", + PACKAGE_NAME + ".AndroidKeyStore3DESCipherSpi$ECB$PKCS7Padding"); + putSymmetricCipherImpl("AES/GCM/NoPadding", PACKAGE_NAME + ".AndroidKeyStoreAuthenticatedAESCipherSpi$GCM$NoPadding"); diff --git a/android/security/keystore/AndroidKeyStoreCipherSpiBase.java b/android/security/keystore/AndroidKeyStoreCipherSpiBase.java index fdebf379..5bcb34a6 100644 --- a/android/security/keystore/AndroidKeyStoreCipherSpiBase.java +++ b/android/security/keystore/AndroidKeyStoreCipherSpiBase.java @@ -307,7 +307,7 @@ abstract class AndroidKeyStoreCipherSpiBase extends CipherSpi implements KeyStor * * <p>This implementation returns {@code null}. * - * @returns stream or {@code null} if AAD is not supported by this cipher. + * @return stream or {@code null} if AAD is not supported by this cipher. */ @Nullable protected KeyStoreCryptoOperationStreamer createAdditionalAuthenticationDataStreamer( diff --git a/android/security/keystore/AndroidKeyStoreKeyGeneratorSpi.java b/android/security/keystore/AndroidKeyStoreKeyGeneratorSpi.java index f1d1e166..379e1770 100644 --- a/android/security/keystore/AndroidKeyStoreKeyGeneratorSpi.java +++ b/android/security/keystore/AndroidKeyStoreKeyGeneratorSpi.java @@ -60,6 +60,12 @@ public abstract class AndroidKeyStoreKeyGeneratorSpi extends KeyGeneratorSpi { } } + public static class DESede extends AndroidKeyStoreKeyGeneratorSpi { + public DESede() { + super(KeymasterDefs.KM_ALGORITHM_3DES, 168); + } + } + protected static abstract class HmacBase extends AndroidKeyStoreKeyGeneratorSpi { protected HmacBase(int keymasterDigest) { super(KeymasterDefs.KM_ALGORITHM_HMAC, diff --git a/android/security/keystore/AndroidKeyStoreProvider.java b/android/security/keystore/AndroidKeyStoreProvider.java index 55e6519d..10189263 100644 --- a/android/security/keystore/AndroidKeyStoreProvider.java +++ b/android/security/keystore/AndroidKeyStoreProvider.java @@ -80,6 +80,7 @@ public class AndroidKeyStoreProvider extends Provider { // javax.crypto.KeyGenerator put("KeyGenerator.AES", PACKAGE_NAME + ".AndroidKeyStoreKeyGeneratorSpi$AES"); + put("KeyGenerator.DESede", PACKAGE_NAME + ".AndroidKeyStoreKeyGeneratorSpi$DESede"); put("KeyGenerator.HmacSHA1", PACKAGE_NAME + ".AndroidKeyStoreKeyGeneratorSpi$HmacSHA1"); put("KeyGenerator.HmacSHA224", PACKAGE_NAME + ".AndroidKeyStoreKeyGeneratorSpi$HmacSHA224"); put("KeyGenerator.HmacSHA256", PACKAGE_NAME + ".AndroidKeyStoreKeyGeneratorSpi$HmacSHA256"); @@ -88,6 +89,7 @@ public class AndroidKeyStoreProvider extends Provider { // java.security.SecretKeyFactory putSecretKeyFactoryImpl("AES"); + putSecretKeyFactoryImpl("DESede"); putSecretKeyFactoryImpl("HmacSHA1"); putSecretKeyFactoryImpl("HmacSHA224"); putSecretKeyFactoryImpl("HmacSHA256"); @@ -348,7 +350,8 @@ public class AndroidKeyStoreProvider extends Provider { } if (keymasterAlgorithm == KeymasterDefs.KM_ALGORITHM_HMAC || - keymasterAlgorithm == KeymasterDefs.KM_ALGORITHM_AES) { + keymasterAlgorithm == KeymasterDefs.KM_ALGORITHM_AES || + keymasterAlgorithm == KeymasterDefs.KM_ALGORITHM_3DES) { return loadAndroidKeyStoreSecretKeyFromKeystore(userKeyAlias, uid, keyCharacteristics); } else if (keymasterAlgorithm == KeymasterDefs.KM_ALGORITHM_RSA || diff --git a/android/security/keystore/AndroidKeyStoreSpi.java b/android/security/keystore/AndroidKeyStoreSpi.java index d73a9e29..440e0863 100644 --- a/android/security/keystore/AndroidKeyStoreSpi.java +++ b/android/security/keystore/AndroidKeyStoreSpi.java @@ -18,6 +18,7 @@ package android.security.keystore; import libcore.util.EmptyArray; import android.security.Credentials; +import android.security.GateKeeper; import android.security.KeyStore; import android.security.KeyStoreParameter; import android.security.keymaster.KeyCharacteristics; @@ -25,6 +26,7 @@ import android.security.keymaster.KeymasterArguments; import android.security.keymaster.KeymasterDefs; import android.security.keystore.KeyProperties; import android.security.keystore.KeyProtection; +import android.security.keystore.WrappedKeyEntry; import android.util.Log; import java.io.ByteArrayInputStream; @@ -744,6 +746,31 @@ public class AndroidKeyStoreSpi extends KeyStoreSpi { } } + private void setWrappedKeyEntry(String alias, byte[] wrappedKeyBytes, String wrappingKeyAlias, + java.security.KeyStore.ProtectionParameter param) throws KeyStoreException { + if (param != null) { + throw new KeyStoreException("Protection parameters are specified inside wrapped keys"); + } + + byte[] maskingKey = new byte[32]; + KeymasterArguments args = new KeymasterArguments(); // TODO: populate wrapping key args. + + int errorCode = mKeyStore.importWrappedKey( + Credentials.USER_SECRET_KEY + alias, + wrappedKeyBytes, + Credentials.USER_PRIVATE_KEY + wrappingKeyAlias, + maskingKey, + args, + GateKeeper.getSecureUserId(), + 0, // FIXME fingerprint id? + mUid, + new KeyCharacteristics()); + if (errorCode != KeyStore.NO_ERROR) { + throw new KeyStoreException("Failed to import wrapped key. Keystore error code: " + + errorCode); + } + } + @Override public void engineSetKeyEntry(String alias, byte[] userKey, Certificate[] chain) throws KeyStoreException { @@ -974,6 +1001,9 @@ public class AndroidKeyStoreSpi extends KeyStoreSpi { } else if (entry instanceof SecretKeyEntry) { SecretKeyEntry secE = (SecretKeyEntry) entry; setSecretKeyEntry(alias, secE.getSecretKey(), param); + } else if (entry instanceof WrappedKeyEntry) { + WrappedKeyEntry wke = (WrappedKeyEntry) entry; + setWrappedKeyEntry(alias, wke.getWrappedKeyBytes(), wke.getWrappingKeyAlias(), param); } else { throw new KeyStoreException( "Entry must be a PrivateKeyEntry, SecretKeyEntry or TrustedCertificateEntry" diff --git a/android/security/keystore/AttestationUtils.java b/android/security/keystore/AttestationUtils.java index 0811100f..efee8b49 100644 --- a/android/security/keystore/AttestationUtils.java +++ b/android/security/keystore/AttestationUtils.java @@ -99,48 +99,35 @@ public abstract class AttestationUtils { } } + @NonNull private static KeymasterArguments prepareAttestationArgumentsForDeviceId( + Context context, @NonNull int[] idTypes, @NonNull byte[] attestationChallenge) throws + DeviceIdAttestationException { + // Verify that device ID attestation types are provided. + if (idTypes == null) { + throw new NullPointerException("Missing id types"); + } + + return prepareAttestationArguments(context, idTypes, attestationChallenge); + } + /** - * Performs attestation of the device's identifiers. This method returns a certificate chain - * whose first element contains the requested device identifiers in an extension. The device's - * manufacturer, model, brand, device and product are always also included in the attestation. - * If the device supports attestation in secure hardware, the chain will be rooted at a - * trustworthy CA key. Otherwise, the chain will be rooted at an untrusted certificate. See - * <a href="https://developer.android.com/training/articles/security-key-attestation.html"> - * Key Attestation</a> for the format of the certificate extension. - * <p> - * Attestation will only be successful when all of the following are true: - * 1) The device has been set up to support device identifier attestation at the factory. - * 2) The user has not permanently disabled device identifier attestation. - * 3) You have permission to access the device identifiers you are requesting attestation for. - * <p> - * For privacy reasons, you cannot distinguish between (1) and (2). If attestation is - * unsuccessful, the device may not support it in general or the user may have permanently - * disabled it. - * - * @param context the context to use for retrieving device identifiers. - * @param idTypes the types of device identifiers to attest. - * @param attestationChallenge a blob to include in the certificate alongside the device - * identifiers. - * - * @return a certificate chain containing the requested device identifiers in the first element - * - * @exception SecurityException if you are not permitted to obtain an attestation of the - * device's identifiers. - * @exception DeviceIdAttestationException if the attestation operation fails. + * Prepares Keymaster Arguments with attestation data. + * @hide should only be used by KeyChain. */ - @RequiresPermission(Manifest.permission.READ_PRIVILEGED_PHONE_STATE) - @NonNull public static X509Certificate[] attestDeviceIds(Context context, + @NonNull public static KeymasterArguments prepareAttestationArguments(Context context, @NonNull int[] idTypes, @NonNull byte[] attestationChallenge) throws DeviceIdAttestationException { // Check method arguments, retrieve requested device IDs and prepare attestation arguments. - if (idTypes == null) { - throw new NullPointerException("Missing id types"); - } if (attestationChallenge == null) { throw new NullPointerException("Missing attestation challenge"); } final KeymasterArguments attestArgs = new KeymasterArguments(); attestArgs.addBytes(KeymasterDefs.KM_TAG_ATTESTATION_CHALLENGE, attestationChallenge); + // Return early if the caller did not request any device identifiers to be included in the + // attestation record. + if (idTypes == null) { + return attestArgs; + } final Set<Integer> idTypesSet = new ArraySet<>(idTypes.length); for (int idType : idTypes) { idTypesSet.add(idType); @@ -191,6 +178,44 @@ public abstract class AttestationUtils { Build.MANUFACTURER.getBytes(StandardCharsets.UTF_8)); attestArgs.addBytes(KeymasterDefs.KM_TAG_ATTESTATION_ID_MODEL, Build.MODEL.getBytes(StandardCharsets.UTF_8)); + return attestArgs; + } + + /** + * Performs attestation of the device's identifiers. This method returns a certificate chain + * whose first element contains the requested device identifiers in an extension. The device's + * manufacturer, model, brand, device and product are always also included in the attestation. + * If the device supports attestation in secure hardware, the chain will be rooted at a + * trustworthy CA key. Otherwise, the chain will be rooted at an untrusted certificate. See + * <a href="https://developer.android.com/training/articles/security-key-attestation.html"> + * Key Attestation</a> for the format of the certificate extension. + * <p> + * Attestation will only be successful when all of the following are true: + * 1) The device has been set up to support device identifier attestation at the factory. + * 2) The user has not permanently disabled device identifier attestation. + * 3) You have permission to access the device identifiers you are requesting attestation for. + * <p> + * For privacy reasons, you cannot distinguish between (1) and (2). If attestation is + * unsuccessful, the device may not support it in general or the user may have permanently + * disabled it. + * + * @param context the context to use for retrieving device identifiers. + * @param idTypes the types of device identifiers to attest. + * @param attestationChallenge a blob to include in the certificate alongside the device + * identifiers. + * + * @return a certificate chain containing the requested device identifiers in the first element + * + * @exception SecurityException if you are not permitted to obtain an attestation of the + * device's identifiers. + * @exception DeviceIdAttestationException if the attestation operation fails. + */ + @RequiresPermission(Manifest.permission.READ_PRIVILEGED_PHONE_STATE) + @NonNull public static X509Certificate[] attestDeviceIds(Context context, + @NonNull int[] idTypes, @NonNull byte[] attestationChallenge) throws + DeviceIdAttestationException { + final KeymasterArguments attestArgs = prepareAttestationArgumentsForDeviceId( + context, idTypes, attestationChallenge); // Perform attestation. final KeymasterCertificateChain outChain = new KeymasterCertificateChain(); diff --git a/android/security/keystore/BackwardsCompat.java b/android/security/keystore/BackwardsCompat.java new file mode 100644 index 00000000..69558c4d --- /dev/null +++ b/android/security/keystore/BackwardsCompat.java @@ -0,0 +1,127 @@ +/* + * Copyright (C) 2018 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 android.security.keystore; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Function; + +/** + * Helpers for converting classes between old and new API, so we can preserve backwards + * compatibility while teamfooding. This will be removed soon. + * + * @hide + */ +class BackwardsCompat { + + + static KeychainProtectionParams toLegacyKeychainProtectionParams( + android.security.keystore.recovery.KeyChainProtectionParams keychainProtectionParams + ) { + return new KeychainProtectionParams.Builder() + .setUserSecretType(keychainProtectionParams.getUserSecretType()) + .setSecret(keychainProtectionParams.getSecret()) + .setLockScreenUiFormat(keychainProtectionParams.getLockScreenUiFormat()) + .setKeyDerivationParams( + toLegacyKeyDerivationParams( + keychainProtectionParams.getKeyDerivationParams())) + .build(); + } + + static KeyDerivationParams toLegacyKeyDerivationParams( + android.security.keystore.recovery.KeyDerivationParams keyDerivationParams + ) { + return new KeyDerivationParams( + keyDerivationParams.getAlgorithm(), keyDerivationParams.getSalt()); + } + + static WrappedApplicationKey toLegacyWrappedApplicationKey( + android.security.keystore.recovery.WrappedApplicationKey wrappedApplicationKey + ) { + return new WrappedApplicationKey.Builder() + .setAlias(wrappedApplicationKey.getAlias()) + .setEncryptedKeyMaterial(wrappedApplicationKey.getEncryptedKeyMaterial()) + .build(); + } + + static android.security.keystore.recovery.KeyDerivationParams fromLegacyKeyDerivationParams( + KeyDerivationParams keyDerivationParams + ) { + return new android.security.keystore.recovery.KeyDerivationParams( + keyDerivationParams.getAlgorithm(), keyDerivationParams.getSalt()); + } + + static android.security.keystore.recovery.WrappedApplicationKey fromLegacyWrappedApplicationKey( + WrappedApplicationKey wrappedApplicationKey + ) { + return new android.security.keystore.recovery.WrappedApplicationKey.Builder() + .setAlias(wrappedApplicationKey.getAlias()) + .setEncryptedKeyMaterial(wrappedApplicationKey.getEncryptedKeyMaterial()) + .build(); + } + + static List<android.security.keystore.recovery.WrappedApplicationKey> + fromLegacyWrappedApplicationKeys(List<WrappedApplicationKey> wrappedApplicationKeys + ) { + return map(wrappedApplicationKeys, BackwardsCompat::fromLegacyWrappedApplicationKey); + } + + static List<android.security.keystore.recovery.KeyChainProtectionParams> + fromLegacyKeychainProtectionParams( + List<KeychainProtectionParams> keychainProtectionParams) { + return map(keychainProtectionParams, BackwardsCompat::fromLegacyKeychainProtectionParam); + } + + static android.security.keystore.recovery.KeyChainProtectionParams + fromLegacyKeychainProtectionParam(KeychainProtectionParams keychainProtectionParams) { + return new android.security.keystore.recovery.KeyChainProtectionParams.Builder() + .setUserSecretType(keychainProtectionParams.getUserSecretType()) + .setSecret(keychainProtectionParams.getSecret()) + .setLockScreenUiFormat(keychainProtectionParams.getLockScreenUiFormat()) + .setKeyDerivationParams( + fromLegacyKeyDerivationParams( + keychainProtectionParams.getKeyDerivationParams())) + .build(); + } + + static KeychainSnapshot toLegacyKeychainSnapshot( + android.security.keystore.recovery.KeyChainSnapshot keychainSnapshot + ) { + return new KeychainSnapshot.Builder() + .setCounterId(keychainSnapshot.getCounterId()) + .setEncryptedRecoveryKeyBlob(keychainSnapshot.getEncryptedRecoveryKeyBlob()) + .setTrustedHardwarePublicKey(keychainSnapshot.getTrustedHardwarePublicKey()) + .setSnapshotVersion(keychainSnapshot.getSnapshotVersion()) + .setMaxAttempts(keychainSnapshot.getMaxAttempts()) + .setServerParams(keychainSnapshot.getServerParams()) + .setKeychainProtectionParams( + map(keychainSnapshot.getKeyChainProtectionParams(), + BackwardsCompat::toLegacyKeychainProtectionParams)) + .setWrappedApplicationKeys( + map(keychainSnapshot.getWrappedApplicationKeys(), + BackwardsCompat::toLegacyWrappedApplicationKey)) + .build(); + } + + static <A, B> List<B> map(List<A> as, Function<A, B> f) { + ArrayList<B> bs = new ArrayList<>(as.size()); + for (A a : as) { + bs.add(f.apply(a)); + } + return bs; + } +} diff --git a/android/security/keystore/BadCertificateFormatException.java b/android/security/keystore/BadCertificateFormatException.java new file mode 100644 index 00000000..ddc7bd23 --- /dev/null +++ b/android/security/keystore/BadCertificateFormatException.java @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2018 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 android.security.keystore; + +/** + * Error thrown when the recovery agent supplies an invalid X509 certificate. + * + * @hide + */ +public class BadCertificateFormatException extends RecoveryControllerException { + public BadCertificateFormatException(String msg) { + super(msg); + } +} diff --git a/android/security/keystore/DecryptionFailedException.java b/android/security/keystore/DecryptionFailedException.java new file mode 100644 index 00000000..945fcf6f --- /dev/null +++ b/android/security/keystore/DecryptionFailedException.java @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2018 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 android.security.keystore; + +/** + * Error thrown when decryption failed, due to an agent error. i.e., using the incorrect key, + * trying to decrypt garbage data, trying to decrypt data that has somehow been corrupted, etc. + * + * @hide + */ +public class DecryptionFailedException extends RecoveryControllerException { + + public DecryptionFailedException(String msg) { + super(msg); + } +} diff --git a/android/security/keystore/InternalRecoveryServiceException.java b/android/security/keystore/InternalRecoveryServiceException.java new file mode 100644 index 00000000..85829bed --- /dev/null +++ b/android/security/keystore/InternalRecoveryServiceException.java @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2018 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 android.security.keystore; + +/** + * An error thrown when something went wrong internally in the recovery service. + * + * <p>This is an unexpected error, and indicates a problem with the service itself, rather than the + * caller having performed some kind of illegal action. + * + * @hide + */ +public class InternalRecoveryServiceException extends RecoveryControllerException { + public InternalRecoveryServiceException(String msg) { + super(msg); + } + + public InternalRecoveryServiceException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/android/security/recoverablekeystore/KeyDerivationParameters.java b/android/security/keystore/KeyDerivationParams.java index 978e60ee..b19cee2d 100644 --- a/android/security/recoverablekeystore/KeyDerivationParameters.java +++ b/android/security/keystore/KeyDerivationParams.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package android.security.recoverablekeystore; +package android.security.keystore; import android.annotation.IntDef; import android.annotation.NonNull; @@ -28,21 +28,17 @@ import java.lang.annotation.RetentionPolicy; /** * Collection of parameters which define a key derivation function. - * Supports + * Currently only supports salted SHA-256 * - * <ul> - * <li>SHA256 - * <li>Argon2id - * </ul> * @hide */ -public final class KeyDerivationParameters implements Parcelable { +public final class KeyDerivationParams implements Parcelable { private final int mAlgorithm; private byte[] mSalt; /** @hide */ @Retention(RetentionPolicy.SOURCE) - @IntDef({ALGORITHM_SHA256, ALGORITHM_ARGON2ID}) + @IntDef(prefix = {"ALGORITHM_"}, value = {ALGORITHM_SHA256, ALGORITHM_ARGON2ID}) public @interface KeyDerivationAlgorithm { } @@ -53,6 +49,7 @@ public final class KeyDerivationParameters implements Parcelable { /** * Argon2ID + * @hide */ // TODO: add Argon2ID support. public static final int ALGORITHM_ARGON2ID = 2; @@ -60,11 +57,11 @@ public final class KeyDerivationParameters implements Parcelable { /** * Creates instance of the class to to derive key using salted SHA256 hash. */ - public static KeyDerivationParameters createSHA256Parameters(@NonNull byte[] salt) { - return new KeyDerivationParameters(ALGORITHM_SHA256, salt); + public static KeyDerivationParams createSha256Params(@NonNull byte[] salt) { + return new KeyDerivationParams(ALGORITHM_SHA256, salt); } - private KeyDerivationParameters(@KeyDerivationAlgorithm int algorithm, @NonNull byte[] salt) { + KeyDerivationParams(@KeyDerivationAlgorithm int algorithm, @NonNull byte[] salt) { mAlgorithm = algorithm; mSalt = Preconditions.checkNotNull(salt); } @@ -83,24 +80,30 @@ public final class KeyDerivationParameters implements Parcelable { return mSalt; } - public static final Parcelable.Creator<KeyDerivationParameters> CREATOR = - new Parcelable.Creator<KeyDerivationParameters>() { - public KeyDerivationParameters createFromParcel(Parcel in) { - return new KeyDerivationParameters(in); + public static final Parcelable.Creator<KeyDerivationParams> CREATOR = + new Parcelable.Creator<KeyDerivationParams>() { + public KeyDerivationParams createFromParcel(Parcel in) { + return new KeyDerivationParams(in); } - public KeyDerivationParameters[] newArray(int length) { - return new KeyDerivationParameters[length]; + public KeyDerivationParams[] newArray(int length) { + return new KeyDerivationParams[length]; } }; + /** + * @hide + */ @Override public void writeToParcel(Parcel out, int flags) { out.writeInt(mAlgorithm); out.writeByteArray(mSalt); } - protected KeyDerivationParameters(Parcel in) { + /** + * @hide + */ + protected KeyDerivationParams(Parcel in) { mAlgorithm = in.readInt(); mSalt = in.createByteArray(); } diff --git a/android/security/keystore/KeyGenParameterSpec.java b/android/security/keystore/KeyGenParameterSpec.java index 1238d877..1e2b873c 100644 --- a/android/security/keystore/KeyGenParameterSpec.java +++ b/android/security/keystore/KeyGenParameterSpec.java @@ -262,6 +262,7 @@ public final class KeyGenParameterSpec implements AlgorithmParameterSpec { private final boolean mUniqueIdIncluded; private final boolean mUserAuthenticationValidWhileOnBody; private final boolean mInvalidatedByBiometricEnrollment; + private final boolean mIsStrongBoxBacked; /** * @hide should be built with Builder @@ -289,7 +290,8 @@ public final class KeyGenParameterSpec implements AlgorithmParameterSpec { byte[] attestationChallenge, boolean uniqueIdIncluded, boolean userAuthenticationValidWhileOnBody, - boolean invalidatedByBiometricEnrollment) { + boolean invalidatedByBiometricEnrollment, + boolean isStrongBoxBacked) { if (TextUtils.isEmpty(keyStoreAlias)) { throw new IllegalArgumentException("keyStoreAlias must not be empty"); } @@ -335,6 +337,7 @@ public final class KeyGenParameterSpec implements AlgorithmParameterSpec { mUniqueIdIncluded = uniqueIdIncluded; mUserAuthenticationValidWhileOnBody = userAuthenticationValidWhileOnBody; mInvalidatedByBiometricEnrollment = invalidatedByBiometricEnrollment; + mIsStrongBoxBacked = isStrongBoxBacked; } /** @@ -625,6 +628,13 @@ public final class KeyGenParameterSpec implements AlgorithmParameterSpec { } /** + * Returns {@code true} if the key is protected by a Strongbox security chip. + */ + public boolean isStrongBoxBacked() { + return mIsStrongBoxBacked; + } + + /** * Builder of {@link KeyGenParameterSpec} instances. */ public final static class Builder { @@ -652,6 +662,7 @@ public final class KeyGenParameterSpec implements AlgorithmParameterSpec { private boolean mUniqueIdIncluded = false; private boolean mUserAuthenticationValidWhileOnBody; private boolean mInvalidatedByBiometricEnrollment = true; + private boolean mIsStrongBoxBacked = false; /** * Creates a new instance of the {@code Builder}. @@ -1177,6 +1188,15 @@ public final class KeyGenParameterSpec implements AlgorithmParameterSpec { } /** + * Sets whether this key should be protected by a StrongBox security chip. + */ + @NonNull + public Builder setIsStrongBoxBacked(boolean isStrongBoxBacked) { + mIsStrongBoxBacked = isStrongBoxBacked; + return this; + } + + /** * Builds an instance of {@code KeyGenParameterSpec}. */ @NonNull @@ -1204,7 +1224,8 @@ public final class KeyGenParameterSpec implements AlgorithmParameterSpec { mAttestationChallenge, mUniqueIdIncluded, mUserAuthenticationValidWhileOnBody, - mInvalidatedByBiometricEnrollment); + mInvalidatedByBiometricEnrollment, + mIsStrongBoxBacked); } } } diff --git a/android/security/keystore/KeyProperties.java b/android/security/keystore/KeyProperties.java index a250d1f0..f54b6dec 100644 --- a/android/security/keystore/KeyProperties.java +++ b/android/security/keystore/KeyProperties.java @@ -44,6 +44,7 @@ public abstract class KeyProperties { PURPOSE_DECRYPT, PURPOSE_SIGN, PURPOSE_VERIFY, + PURPOSE_WRAP_KEY, }) public @interface PurposeEnum {} @@ -68,6 +69,11 @@ public abstract class KeyProperties { public static final int PURPOSE_VERIFY = 1 << 3; /** + * Purpose of key: wrapping and unwrapping wrapped keys for secure import. + */ + public static final int PURPOSE_WRAP_KEY = 1 << 5; + + /** * @hide */ public static abstract class Purpose { @@ -83,6 +89,8 @@ public abstract class KeyProperties { return KeymasterDefs.KM_PURPOSE_SIGN; case PURPOSE_VERIFY: return KeymasterDefs.KM_PURPOSE_VERIFY; + case PURPOSE_WRAP_KEY: + return KeymasterDefs.KM_PURPOSE_WRAP; default: throw new IllegalArgumentException("Unknown purpose: " + purpose); } @@ -98,6 +106,8 @@ public abstract class KeyProperties { return PURPOSE_SIGN; case KeymasterDefs.KM_PURPOSE_VERIFY: return PURPOSE_VERIFY; + case KeymasterDefs.KM_PURPOSE_WRAP: + return PURPOSE_WRAP_KEY; default: throw new IllegalArgumentException("Unknown purpose: " + purpose); } @@ -146,6 +156,15 @@ public abstract class KeyProperties { /** Advanced Encryption Standard (AES) key. */ public static final String KEY_ALGORITHM_AES = "AES"; + /** + * Triple Data Encryption Algorithm (3DES) key. + * + * @deprecated Included for interoperability with legacy systems. Prefer {@link + * KeyProperties#KEY_ALGORITHM_AES} for new development. + */ + @Deprecated + public static final String KEY_ALGORITHM_3DES = "DESede"; + /** Keyed-Hash Message Authentication Code (HMAC) key using SHA-1 as the hash. */ public static final String KEY_ALGORITHM_HMAC_SHA1 = "HmacSHA1"; @@ -196,6 +215,8 @@ public abstract class KeyProperties { @NonNull @KeyAlgorithmEnum String algorithm) { if (KEY_ALGORITHM_AES.equalsIgnoreCase(algorithm)) { return KeymasterDefs.KM_ALGORITHM_AES; + } else if (KEY_ALGORITHM_3DES.equalsIgnoreCase(algorithm)) { + return KeymasterDefs.KM_ALGORITHM_3DES; } else if (algorithm.toUpperCase(Locale.US).startsWith("HMAC")) { return KeymasterDefs.KM_ALGORITHM_HMAC; } else { @@ -210,6 +231,8 @@ public abstract class KeyProperties { switch (keymasterAlgorithm) { case KeymasterDefs.KM_ALGORITHM_AES: return KEY_ALGORITHM_AES; + case KeymasterDefs.KM_ALGORITHM_3DES: + return KEY_ALGORITHM_3DES; case KeymasterDefs.KM_ALGORITHM_HMAC: switch (keymasterDigest) { case KeymasterDefs.KM_DIGEST_SHA1: @@ -666,6 +689,10 @@ public abstract class KeyProperties { */ public static final int ORIGIN_UNKNOWN = 1 << 2; + /** Key was imported into the AndroidKeyStore in an encrypted wrapper */ + public static final int ORIGIN_SECURELY_IMPORTED = 1 << 3; + + /** * @hide */ @@ -680,6 +707,8 @@ public abstract class KeyProperties { return ORIGIN_IMPORTED; case KeymasterDefs.KM_ORIGIN_UNKNOWN: return ORIGIN_UNKNOWN; + case KeymasterDefs.KM_ORIGIN_SECURELY_IMPORTED: + return ORIGIN_SECURELY_IMPORTED; default: throw new IllegalArgumentException("Unknown origin: " + origin); } diff --git a/android/security/keystore/KeyProtection.java b/android/security/keystore/KeyProtection.java index 2eb06631..dbacb9c5 100644 --- a/android/security/keystore/KeyProtection.java +++ b/android/security/keystore/KeyProtection.java @@ -488,9 +488,9 @@ public final class KeyProtection implements ProtectionParameter { private int mUserAuthenticationValidityDurationSeconds = -1; private boolean mUserAuthenticationValidWhileOnBody; private boolean mInvalidatedByBiometricEnrollment = true; - private long mBoundToSecureUserId = GateKeeper.INVALID_SECURE_USER_ID; private boolean mCriticalToDeviceEncryption = false; + /** * Creates a new instance of the {@code Builder}. * diff --git a/android/security/keystore/KeychainProtectionParams.java b/android/security/keystore/KeychainProtectionParams.java new file mode 100644 index 00000000..a940fdc7 --- /dev/null +++ b/android/security/keystore/KeychainProtectionParams.java @@ -0,0 +1,285 @@ +/* + * Copyright (C) 2017 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 android.security.keystore; + +import android.annotation.IntDef; +import android.annotation.NonNull; +import android.os.Parcel; +import android.os.Parcelable; + +import com.android.internal.util.Preconditions; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.Arrays; + +/** + * A {@link KeychainSnapshot} is protected with a key derived from the user's lock screen. This + * class wraps all the data necessary to derive the same key on a recovering device: + * + * <ul> + * <li>UI parameters for the user's lock screen - so that if e.g., the user was using a pattern, + * the recovering device can display the pattern UI to the user when asking them to enter + * the lock screen from their previous device. + * <li>The algorithm used to derive a key from the user's lock screen, e.g. SHA-256 with a salt. + * </ul> + * + * <p>As such, this data is sent along with the {@link KeychainSnapshot} when syncing the current + * version of the keychain. + * + * <p>For now, the recoverable keychain only supports a single layer of protection, which is the + * user's lock screen. In the future, the keychain will support multiple layers of protection + * (e.g. an additional keychain password, along with the lock screen). + * + * @hide + */ +public final class KeychainProtectionParams implements Parcelable { + /** @hide */ + @Retention(RetentionPolicy.SOURCE) + @IntDef({TYPE_LOCKSCREEN, TYPE_CUSTOM_PASSWORD}) + public @interface UserSecretType { + } + + /** + * Lockscreen secret is required to recover KeyStore. + */ + public static final int TYPE_LOCKSCREEN = 100; + + /** + * Custom passphrase, unrelated to lock screen, is required to recover KeyStore. + */ + public static final int TYPE_CUSTOM_PASSWORD = 101; + + /** @hide */ + @Retention(RetentionPolicy.SOURCE) + @IntDef({TYPE_PIN, TYPE_PASSWORD, TYPE_PATTERN}) + public @interface LockScreenUiFormat { + } + + /** + * Pin with digits only. + */ + public static final int TYPE_PIN = 1; + + /** + * Password. String with latin-1 characters only. + */ + public static final int TYPE_PASSWORD = 2; + + /** + * Pattern with 3 by 3 grid. + */ + public static final int TYPE_PATTERN = 3; + + @UserSecretType + private Integer mUserSecretType; + + @LockScreenUiFormat + private Integer mLockScreenUiFormat; + + /** + * Parameters of the key derivation function, including algorithm, difficulty, salt. + */ + private KeyDerivationParams mKeyDerivationParams; + private byte[] mSecret; // Derived from user secret. The field must have limited visibility. + + /** + * @param secret Constructor creates a reference to the secret. Caller must use + * @link {#clearSecret} to overwrite its value in memory. + * @hide + */ + public KeychainProtectionParams(@UserSecretType int userSecretType, + @LockScreenUiFormat int lockScreenUiFormat, + @NonNull KeyDerivationParams keyDerivationParams, + @NonNull byte[] secret) { + mUserSecretType = userSecretType; + mLockScreenUiFormat = lockScreenUiFormat; + mKeyDerivationParams = Preconditions.checkNotNull(keyDerivationParams); + mSecret = Preconditions.checkNotNull(secret); + } + + private KeychainProtectionParams() { + + } + + /** + * @see TYPE_LOCKSCREEN + * @see TYPE_CUSTOM_PASSWORD + */ + public @UserSecretType int getUserSecretType() { + return mUserSecretType; + } + + /** + * Specifies UX shown to user during recovery. + * Default value is {@code TYPE_LOCKSCREEN} + * + * @see TYPE_PIN + * @see TYPE_PASSWORD + * @see TYPE_PATTERN + */ + public @LockScreenUiFormat int getLockScreenUiFormat() { + return mLockScreenUiFormat; + } + + /** + * Specifies function used to derive symmetric key from user input + * Format is defined in separate util class. + */ + public @NonNull KeyDerivationParams getKeyDerivationParams() { + return mKeyDerivationParams; + } + + /** + * Secret derived from user input. + * Default value is empty array + * + * @return secret or empty array + */ + public @NonNull byte[] getSecret() { + return mSecret; + } + + /** + * Builder for creating {@link KeychainProtectionParams}. + */ + public static class Builder { + private KeychainProtectionParams mInstance = new KeychainProtectionParams(); + + /** + * Sets user secret type. + * + * @see TYPE_LOCKSCREEN + * @see TYPE_CUSTOM_PASSWORD + * @param userSecretType The secret type + * @return This builder. + */ + public Builder setUserSecretType(@UserSecretType int userSecretType) { + mInstance.mUserSecretType = userSecretType; + return this; + } + + /** + * Sets UI format. + * + * @see TYPE_PIN + * @see TYPE_PASSWORD + * @see TYPE_PATTERN + * @param lockScreenUiFormat The UI format + * @return This builder. + */ + public Builder setLockScreenUiFormat(@LockScreenUiFormat int lockScreenUiFormat) { + mInstance.mLockScreenUiFormat = lockScreenUiFormat; + return this; + } + + /** + * Sets parameters of the key derivation function. + * + * @param keyDerivationParams Key derivation Params + * @return This builder. + */ + public Builder setKeyDerivationParams(@NonNull KeyDerivationParams + keyDerivationParams) { + mInstance.mKeyDerivationParams = keyDerivationParams; + return this; + } + + /** + * Secret derived from user input, or empty array. + * + * @param secret The secret. + * @return This builder. + */ + public Builder setSecret(@NonNull byte[] secret) { + mInstance.mSecret = secret; + return this; + } + + + /** + * Creates a new {@link KeychainProtectionParams} instance. + * The instance will include default values, if {@link setSecret} + * or {@link setUserSecretType} were not called. + * + * @return new instance + * @throws NullPointerException if some required fields were not set. + */ + @NonNull public KeychainProtectionParams build() { + if (mInstance.mUserSecretType == null) { + mInstance.mUserSecretType = TYPE_LOCKSCREEN; + } + Preconditions.checkNotNull(mInstance.mLockScreenUiFormat); + Preconditions.checkNotNull(mInstance.mKeyDerivationParams); + if (mInstance.mSecret == null) { + mInstance.mSecret = new byte[]{}; + } + return mInstance; + } + } + + /** + * Removes secret from memory than object is no longer used. + * Since finalizer call is not reliable, please use @link {#clearSecret} directly. + */ + @Override + protected void finalize() throws Throwable { + clearSecret(); + super.finalize(); + } + + /** + * Fills mSecret with zeroes. + */ + public void clearSecret() { + Arrays.fill(mSecret, (byte) 0); + } + + public static final Parcelable.Creator<KeychainProtectionParams> CREATOR = + new Parcelable.Creator<KeychainProtectionParams>() { + public KeychainProtectionParams createFromParcel(Parcel in) { + return new KeychainProtectionParams(in); + } + + public KeychainProtectionParams[] newArray(int length) { + return new KeychainProtectionParams[length]; + } + }; + + @Override + public void writeToParcel(Parcel out, int flags) { + out.writeInt(mUserSecretType); + out.writeInt(mLockScreenUiFormat); + out.writeTypedObject(mKeyDerivationParams, flags); + out.writeByteArray(mSecret); + } + + /** + * @hide + */ + protected KeychainProtectionParams(Parcel in) { + mUserSecretType = in.readInt(); + mLockScreenUiFormat = in.readInt(); + mKeyDerivationParams = in.readTypedObject(KeyDerivationParams.CREATOR); + mSecret = in.createByteArray(); + } + + @Override + public int describeContents() { + return 0; + } +} diff --git a/android/security/keystore/KeychainSnapshot.java b/android/security/keystore/KeychainSnapshot.java new file mode 100644 index 00000000..23aec25e --- /dev/null +++ b/android/security/keystore/KeychainSnapshot.java @@ -0,0 +1,290 @@ +/* + * Copyright (C) 2017 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 android.security.keystore; + +import android.annotation.NonNull; +import android.os.Parcel; +import android.os.Parcelable; + +import com.android.internal.util.Preconditions; + +import java.util.List; + +/** + * A snapshot of a version of the keystore. Two events can trigger the generation of a new snapshot: + * + * <ul> + * <li>The user's lock screen changes. (A key derived from the user's lock screen is used to + * protected the keychain, which is why this forces a new snapshot.) + * <li>A key is added to or removed from the recoverable keychain. + * </ul> + * + * <p>The snapshot data is also encrypted with the remote trusted hardware's public key, so even + * the recovery agent itself should not be able to decipher the data. The recovery agent sends an + * instance of this to the remote trusted hardware whenever a new snapshot is generated. During a + * recovery flow, the recovery agent retrieves a snapshot from the remote trusted hardware. It then + * sends it to the framework, where it is decrypted using the user's lock screen from their previous + * device. + * + * @hide + */ +public final class KeychainSnapshot implements Parcelable { + private static final int DEFAULT_MAX_ATTEMPTS = 10; + private static final long DEFAULT_COUNTER_ID = 1L; + + private int mSnapshotVersion; + private int mMaxAttempts = DEFAULT_MAX_ATTEMPTS; + private long mCounterId = DEFAULT_COUNTER_ID; + private byte[] mServerParams; + private byte[] mPublicKey; + private List<KeychainProtectionParams> mKeychainProtectionParams; + private List<WrappedApplicationKey> mEntryRecoveryData; + private byte[] mEncryptedRecoveryKeyBlob; + + /** + * @hide + * Deprecated, consider using builder. + */ + public KeychainSnapshot( + int snapshotVersion, + @NonNull List<KeychainProtectionParams> keychainProtectionParams, + @NonNull List<WrappedApplicationKey> wrappedApplicationKeys, + @NonNull byte[] encryptedRecoveryKeyBlob) { + mSnapshotVersion = snapshotVersion; + mKeychainProtectionParams = + Preconditions.checkCollectionElementsNotNull(keychainProtectionParams, + "keychainProtectionParams"); + mEntryRecoveryData = Preconditions.checkCollectionElementsNotNull(wrappedApplicationKeys, + "wrappedApplicationKeys"); + mEncryptedRecoveryKeyBlob = Preconditions.checkNotNull(encryptedRecoveryKeyBlob); + } + + private KeychainSnapshot() { + + } + + /** + * Snapshot version for given account. It is incremented when user secret or list of application + * keys changes. + */ + public int getSnapshotVersion() { + return mSnapshotVersion; + } + + /** + * Number of user secret guesses allowed during Keychain recovery. + */ + public int getMaxAttempts() { + return mMaxAttempts; + } + + /** + * CounterId which is rotated together with user secret. + */ + public long getCounterId() { + return mCounterId; + } + + /** + * Server parameters. + */ + public @NonNull byte[] getServerParams() { + return mServerParams; + } + + /** + * Public key used to encrypt {@code encryptedRecoveryKeyBlob}. + * + * See implementation for binary key format + */ + // TODO: document key format. + public @NonNull byte[] getTrustedHardwarePublicKey() { + return mPublicKey; + } + + /** + * UI and key derivation parameters. Note that combination of secrets may be used. + */ + public @NonNull List<KeychainProtectionParams> getKeychainProtectionParams() { + return mKeychainProtectionParams; + } + + /** + * List of application keys, with key material encrypted by + * the recovery key ({@link #getEncryptedRecoveryKeyBlob}). + */ + public @NonNull List<WrappedApplicationKey> getWrappedApplicationKeys() { + return mEntryRecoveryData; + } + + /** + * Recovery key blob, encrypted by user secret and recovery service public key. + */ + public @NonNull byte[] getEncryptedRecoveryKeyBlob() { + return mEncryptedRecoveryKeyBlob; + } + + public static final Parcelable.Creator<KeychainSnapshot> CREATOR = + new Parcelable.Creator<KeychainSnapshot>() { + public KeychainSnapshot createFromParcel(Parcel in) { + return new KeychainSnapshot(in); + } + + public KeychainSnapshot[] newArray(int length) { + return new KeychainSnapshot[length]; + } + }; + + /** + * Builder for creating {@link KeychainSnapshot}. + * + * @hide + */ + public static class Builder { + private KeychainSnapshot mInstance = new KeychainSnapshot(); + + /** + * Snapshot version for given account. + * + * @param snapshotVersion The snapshot version + * @return This builder. + */ + public Builder setSnapshotVersion(int snapshotVersion) { + mInstance.mSnapshotVersion = snapshotVersion; + return this; + } + + /** + * Sets the number of user secret guesses allowed during Keychain recovery. + * + * @param maxAttempts The maximum number of guesses. + * @return This builder. + */ + public Builder setMaxAttempts(int maxAttempts) { + mInstance.mMaxAttempts = maxAttempts; + return this; + } + + /** + * Sets counter id. + * + * @param counterId The counter id. + * @return This builder. + */ + public Builder setCounterId(long counterId) { + mInstance.mCounterId = counterId; + return this; + } + + /** + * Sets server parameters. + * + * @param serverParams The server parameters + * @return This builder. + */ + public Builder setServerParams(byte[] serverParams) { + mInstance.mServerParams = serverParams; + return this; + } + + /** + * Sets public key used to encrypt recovery blob. + * + * @param publicKey The public key + * @return This builder. + */ + public Builder setTrustedHardwarePublicKey(byte[] publicKey) { + mInstance.mPublicKey = publicKey; + return this; + } + + /** + * Sets UI and key derivation parameters + * + * @param recoveryMetadata The UI and key derivation parameters + * @return This builder. + */ + public Builder setKeychainProtectionParams( + @NonNull List<KeychainProtectionParams> recoveryMetadata) { + mInstance.mKeychainProtectionParams = recoveryMetadata; + return this; + } + + /** + * List of application keys. + * + * @param entryRecoveryData List of application keys + * @return This builder. + */ + public Builder setWrappedApplicationKeys(List<WrappedApplicationKey> entryRecoveryData) { + mInstance.mEntryRecoveryData = entryRecoveryData; + return this; + } + + /** + * Sets recovery key blob + * + * @param encryptedRecoveryKeyBlob The recovery key blob. + * @return This builder. + */ + public Builder setEncryptedRecoveryKeyBlob(@NonNull byte[] encryptedRecoveryKeyBlob) { + mInstance.mEncryptedRecoveryKeyBlob = encryptedRecoveryKeyBlob; + return this; + } + + + /** + * Creates a new {@link KeychainSnapshot} instance. + * + * @return new instance + * @throws NullPointerException if some required fields were not set. + */ + @NonNull public KeychainSnapshot build() { + Preconditions.checkCollectionElementsNotNull(mInstance.mKeychainProtectionParams, + "recoveryMetadata"); + Preconditions.checkCollectionElementsNotNull(mInstance.mEntryRecoveryData, + "entryRecoveryData"); + Preconditions.checkNotNull(mInstance.mEncryptedRecoveryKeyBlob); + Preconditions.checkNotNull(mInstance.mServerParams); + Preconditions.checkNotNull(mInstance.mPublicKey); + return mInstance; + } + } + + @Override + public void writeToParcel(Parcel out, int flags) { + out.writeInt(mSnapshotVersion); + out.writeTypedList(mKeychainProtectionParams); + out.writeByteArray(mEncryptedRecoveryKeyBlob); + out.writeTypedList(mEntryRecoveryData); + } + + /** + * @hide + */ + protected KeychainSnapshot(Parcel in) { + mSnapshotVersion = in.readInt(); + mKeychainProtectionParams = in.createTypedArrayList(KeychainProtectionParams.CREATOR); + mEncryptedRecoveryKeyBlob = in.createByteArray(); + mEntryRecoveryData = in.createTypedArrayList(WrappedApplicationKey.CREATOR); + } + + @Override + public int describeContents() { + return 0; + } +} diff --git a/android/security/keystore/LockScreenRequiredException.java b/android/security/keystore/LockScreenRequiredException.java new file mode 100644 index 00000000..b07fb9cd --- /dev/null +++ b/android/security/keystore/LockScreenRequiredException.java @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2018 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 android.security.keystore; + +/** + * Error thrown when trying to generate keys for a profile that has no lock screen set. + * + * <p>A lock screen must be set, as the lock screen is used to encrypt the snapshot. + * + * @hide + */ +public class LockScreenRequiredException extends RecoveryControllerException { + public LockScreenRequiredException(String msg) { + super(msg); + } +} diff --git a/android/security/keystore/RecoveryClaim.java b/android/security/keystore/RecoveryClaim.java new file mode 100644 index 00000000..6f566af1 --- /dev/null +++ b/android/security/keystore/RecoveryClaim.java @@ -0,0 +1,54 @@ +/* + * Copyright (C) 2018 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 android.security.keystore; + +/** + * An attempt to recover a keychain protected by remote secure hardware. + * + * @hide + */ +public class RecoveryClaim { + + private final RecoverySession mRecoverySession; + private final byte[] mClaimBytes; + + RecoveryClaim(RecoverySession recoverySession, byte[] claimBytes) { + mRecoverySession = recoverySession; + mClaimBytes = claimBytes; + } + + /** + * Returns the session associated with the recovery attempt. This is used to match the symmetric + * key, which remains internal to the framework, for decrypting the claim response. + * + * @return The session data. + */ + public RecoverySession getRecoverySession() { + return mRecoverySession; + } + + /** + * Returns the encrypted claim's bytes. + * + * <p>This should be sent by the recovery agent to the remote secure hardware, which will use + * it to decrypt the keychain, before sending it re-encrypted with the session's symmetric key + * to the device. + */ + public byte[] getClaimBytes() { + return mClaimBytes; + } +} diff --git a/android/security/keystore/RecoveryController.java b/android/security/keystore/RecoveryController.java new file mode 100644 index 00000000..8be6d526 --- /dev/null +++ b/android/security/keystore/RecoveryController.java @@ -0,0 +1,515 @@ +/* + * Copyright (C) 2017 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 android.security.keystore; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.app.PendingIntent; +import android.content.pm.PackageManager.NameNotFoundException; +import android.os.RemoteException; +import android.os.ServiceManager; +import android.os.ServiceSpecificException; +import android.util.Log; + +import com.android.internal.widget.ILockSettings; + +import java.util.List; +import java.util.Map; + +/** + * An assistant for generating {@link javax.crypto.SecretKey} instances that can be recovered by + * other Android devices belonging to the user. The exported keychain is protected by the user's + * lock screen. + * + * <p>The RecoveryController must be paired with a recovery agent. The recovery agent is responsible + * for transporting the keychain to remote trusted hardware. This hardware must prevent brute force + * attempts against the user's lock screen by limiting the number of allowed guesses (to, e.g., 10). + * After that number of incorrect guesses, the trusted hardware no longer allows access to the + * key chain. + * + * <p>For now only the recovery agent itself is able to create keys, so it is expected that the + * recovery agent is itself the system app. + * + * <p>A recovery agent requires the privileged permission + * {@code android.Manifest.permission#RECOVER_KEYSTORE}. + * + * @hide + */ +public class RecoveryController { + private static final String TAG = "RecoveryController"; + + /** Key has been successfully synced. */ + public static final int RECOVERY_STATUS_SYNCED = 0; + /** Waiting for recovery agent to sync the key. */ + public static final int RECOVERY_STATUS_SYNC_IN_PROGRESS = 1; + /** Recovery account is not available. */ + public static final int RECOVERY_STATUS_MISSING_ACCOUNT = 2; + /** Key cannot be synced. */ + public static final int RECOVERY_STATUS_PERMANENT_FAILURE = 3; + + /** + * Failed because no snapshot is yet pending to be synced for the user. + * + * @hide + */ + public static final int ERROR_NO_SNAPSHOT_PENDING = 21; + + /** + * Failed due to an error internal to the recovery service. This is unexpected and indicates + * either a problem with the logic in the service, or a problem with a dependency of the + * service (such as AndroidKeyStore). + * + * @hide + */ + public static final int ERROR_SERVICE_INTERNAL_ERROR = 22; + + /** + * Failed because the user does not have a lock screen set. + * + * @hide + */ + public static final int ERROR_INSECURE_USER = 23; + + /** + * Error thrown when attempting to use a recovery session that has since been closed. + * + * @hide + */ + public static final int ERROR_SESSION_EXPIRED = 24; + + /** + * Failed because the provided certificate was not a valid X509 certificate. + * + * @hide + */ + public static final int ERROR_BAD_CERTIFICATE_FORMAT = 25; + + /** + * Error thrown if decryption failed. This might be because the tag is wrong, the key is wrong, + * the data has become corrupted, the data has been tampered with, etc. + * + * @hide + */ + public static final int ERROR_DECRYPTION_FAILED = 26; + + + private final ILockSettings mBinder; + + private RecoveryController(ILockSettings binder) { + mBinder = binder; + } + + /** + * Gets a new instance of the class. + */ + public static RecoveryController getInstance() { + ILockSettings lockSettings = + ILockSettings.Stub.asInterface(ServiceManager.getService("lock_settings")); + return new RecoveryController(lockSettings); + } + + /** + * Initializes key recovery service for the calling application. RecoveryController + * randomly chooses one of the keys from the list and keeps it to use for future key export + * operations. Collection of all keys in the list must be signed by the provided {@code + * rootCertificateAlias}, which must also be present in the list of root certificates + * preinstalled on the device. The random selection allows RecoveryController to select + * which of a set of remote recovery service devices will be used. + * + * <p>In addition, RecoveryController enforces a delay of three months between + * consecutive initialization attempts, to limit the ability of an attacker to often switch + * remote recovery devices and significantly increase number of recovery attempts. + * + * @param rootCertificateAlias alias of a root certificate preinstalled on the device + * @param signedPublicKeyList binary blob a list of X509 certificates and signature + * @throws BadCertificateFormatException if the {@code signedPublicKeyList} is in a bad format. + * @throws InternalRecoveryServiceException if an unexpected error occurred in the recovery + * service. + */ + public void initRecoveryService( + @NonNull String rootCertificateAlias, @NonNull byte[] signedPublicKeyList) + throws BadCertificateFormatException, InternalRecoveryServiceException { + try { + mBinder.initRecoveryService(rootCertificateAlias, signedPublicKeyList); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } catch (ServiceSpecificException e) { + if (e.errorCode == ERROR_BAD_CERTIFICATE_FORMAT) { + throw new BadCertificateFormatException(e.getMessage()); + } + throw wrapUnexpectedServiceSpecificException(e); + } + } + + /** + * Returns data necessary to store all recoverable keys for given account. Key material is + * encrypted with user secret and recovery public key. + * + * @param account specific to Recovery agent. + * @return Data necessary to recover keystore. + * @throws InternalRecoveryServiceException if an unexpected error occurred in the recovery + * service. + */ + public @NonNull KeychainSnapshot getRecoveryData(@NonNull byte[] account) + throws InternalRecoveryServiceException { + try { + return BackwardsCompat.toLegacyKeychainSnapshot(mBinder.getRecoveryData(account)); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } catch (ServiceSpecificException e) { + if (e.errorCode == ERROR_NO_SNAPSHOT_PENDING) { + return null; + } + throw wrapUnexpectedServiceSpecificException(e); + } + } + + /** + * Sets a listener which notifies recovery agent that new recovery snapshot is available. {@link + * #getRecoveryData} can be used to get the snapshot. Note that every recovery agent can have at + * most one registered listener at any time. + * + * @param intent triggered when new snapshot is available. Unregisters listener if the value is + * {@code null}. + * @throws InternalRecoveryServiceException if an unexpected error occurred in the recovery + * service. + */ + public void setSnapshotCreatedPendingIntent(@Nullable PendingIntent intent) + throws InternalRecoveryServiceException { + try { + mBinder.setSnapshotCreatedPendingIntent(intent); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } catch (ServiceSpecificException e) { + throw wrapUnexpectedServiceSpecificException(e); + } + } + + /** + * Returns a map from recovery agent accounts to corresponding KeyStore recovery snapshot + * version. Version zero is used, if no snapshots were created for the account. + * + * @return Map from recovery agent accounts to snapshot versions. + * @see KeychainSnapshot#getSnapshotVersion + * @throws InternalRecoveryServiceException if an unexpected error occurred in the recovery + * service. + */ + public @NonNull Map<byte[], Integer> getRecoverySnapshotVersions() + throws InternalRecoveryServiceException { + try { + // IPC doesn't support generic Maps. + @SuppressWarnings("unchecked") + Map<byte[], Integer> result = + (Map<byte[], Integer>) mBinder.getRecoverySnapshotVersions(); + return result; + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } catch (ServiceSpecificException e) { + throw wrapUnexpectedServiceSpecificException(e); + } + } + + /** + * Server parameters used to generate new recovery key blobs. This value will be included in + * {@code KeychainSnapshot.getEncryptedRecoveryKeyBlob()}. The same value must be included + * in vaultParams {@link #startRecoverySession} + * + * @param serverParams included in recovery key blob. + * @see #getRecoveryData + * @throws InternalRecoveryServiceException if an unexpected error occurred in the recovery + * service. + */ + public void setServerParams(byte[] serverParams) throws InternalRecoveryServiceException { + try { + mBinder.setServerParams(serverParams); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } catch (ServiceSpecificException e) { + throw wrapUnexpectedServiceSpecificException(e); + } + } + + /** + * Updates recovery status for given keys. It is used to notify keystore that key was + * successfully stored on the server or there were an error. Application can check this value + * using {@code getRecoveyStatus}. + * + * @param packageName Application whose recoverable keys' statuses are to be updated. + * @param aliases List of application-specific key aliases. If the array is empty, updates the + * status for all existing recoverable keys. + * @param status Status specific to recovery agent. + * @throws InternalRecoveryServiceException if an unexpected error occurred in the recovery + * service. + */ + public void setRecoveryStatus( + @NonNull String packageName, @Nullable String[] aliases, int status) + throws NameNotFoundException, InternalRecoveryServiceException { + try { + mBinder.setRecoveryStatus(packageName, aliases, status); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } catch (ServiceSpecificException e) { + throw wrapUnexpectedServiceSpecificException(e); + } + } + + /** + * Returns a {@code Map} from Application's KeyStore key aliases to their recovery status. + * Negative status values are reserved for recovery agent specific codes. List of common codes: + * + * <ul> + * <li>{@link #RECOVERY_STATUS_SYNCED} + * <li>{@link #RECOVERY_STATUS_SYNC_IN_PROGRESS} + * <li>{@link #RECOVERY_STATUS_MISSING_ACCOUNT} + * <li>{@link #RECOVERY_STATUS_PERMANENT_FAILURE} + * </ul> + * + * @return {@code Map} from KeyStore alias to recovery status. + * @see #setRecoveryStatus + * @throws InternalRecoveryServiceException if an unexpected error occurred in the recovery + * service. + */ + public Map<String, Integer> getRecoveryStatus() throws InternalRecoveryServiceException { + try { + // IPC doesn't support generic Maps. + @SuppressWarnings("unchecked") + Map<String, Integer> result = + (Map<String, Integer>) mBinder.getRecoveryStatus(/*packageName=*/ null); + return result; + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } catch (ServiceSpecificException e) { + throw wrapUnexpectedServiceSpecificException(e); + } + } + + /** + * Specifies a set of secret types used for end-to-end keystore encryption. Knowing all of them + * is necessary to recover data. + * + * @param secretTypes {@link KeychainProtectionParams#TYPE_LOCKSCREEN} or {@link + * KeychainProtectionParams#TYPE_CUSTOM_PASSWORD} + * @throws InternalRecoveryServiceException if an unexpected error occurred in the recovery + * service. + */ + public void setRecoverySecretTypes( + @NonNull @KeychainProtectionParams.UserSecretType int[] secretTypes) + throws InternalRecoveryServiceException { + try { + mBinder.setRecoverySecretTypes(secretTypes); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } catch (ServiceSpecificException e) { + throw wrapUnexpectedServiceSpecificException(e); + } + } + + /** + * Defines a set of secret types used for end-to-end keystore encryption. Knowing all of them is + * necessary to generate KeychainSnapshot. + * + * @return list of recovery secret types + * @see KeychainSnapshot + * @throws InternalRecoveryServiceException if an unexpected error occurred in the recovery + * service. + */ + public @NonNull @KeychainProtectionParams.UserSecretType int[] getRecoverySecretTypes() + throws InternalRecoveryServiceException { + try { + return mBinder.getRecoverySecretTypes(); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } catch (ServiceSpecificException e) { + throw wrapUnexpectedServiceSpecificException(e); + } + } + + /** + * Returns a list of recovery secret types, necessary to create a pending recovery snapshot. + * When user enters a secret of a pending type {@link #recoverySecretAvailable} should be + * called. + * + * @return list of recovery secret types + * @throws InternalRecoveryServiceException if an unexpected error occurred in the recovery + * service. + */ + @NonNull + public @KeychainProtectionParams.UserSecretType int[] getPendingRecoverySecretTypes() + throws InternalRecoveryServiceException { + try { + return mBinder.getPendingRecoverySecretTypes(); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } catch (ServiceSpecificException e) { + throw wrapUnexpectedServiceSpecificException(e); + } + } + + /** + * Initializes recovery session and returns a blob with proof of recovery secrets possession. + * The method generates symmetric key for a session, which trusted remote device can use to + * return recovery key. + * + * @param verifierPublicKey Encoded {@code java.security.cert.X509Certificate} with Public key + * used to create the recovery blob on the source device. + * Keystore will verify the certificate using root of trust. + * @param vaultParams Must match the parameters in the corresponding field in the recovery blob. + * Used to limit number of guesses. + * @param vaultChallenge Data passed from server for this recovery session and used to prevent + * replay attacks + * @param secrets Secrets provided by user, the method only uses type and secret fields. + * @return The recovery claim. Claim provides a b binary blob with recovery claim. It is + * encrypted with verifierPublicKey and contains a proof of user secrets, session symmetric + * key and parameters necessary to identify the counter with the number of failed recovery + * attempts. + * @throws BadCertificateFormatException if the {@code verifierPublicKey} is in an incorrect + * format. + * @throws InternalRecoveryServiceException if an unexpected error occurred in the recovery + * service. + */ + @NonNull public RecoveryClaim startRecoverySession( + @NonNull byte[] verifierPublicKey, + @NonNull byte[] vaultParams, + @NonNull byte[] vaultChallenge, + @NonNull List<KeychainProtectionParams> secrets) + throws BadCertificateFormatException, InternalRecoveryServiceException { + try { + RecoverySession recoverySession = RecoverySession.newInstance(this); + byte[] recoveryClaim = + mBinder.startRecoverySession( + recoverySession.getSessionId(), + verifierPublicKey, + vaultParams, + vaultChallenge, + BackwardsCompat.fromLegacyKeychainProtectionParams(secrets)); + return new RecoveryClaim(recoverySession, recoveryClaim); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } catch (ServiceSpecificException e) { + if (e.errorCode == ERROR_BAD_CERTIFICATE_FORMAT) { + throw new BadCertificateFormatException(e.getMessage()); + } + throw wrapUnexpectedServiceSpecificException(e); + } + } + + /** + * Imports keys. + * + * @param session Related recovery session, as originally created by invoking + * {@link #startRecoverySession(byte[], byte[], byte[], List)}. + * @param recoveryKeyBlob Recovery blob encrypted by symmetric key generated for this session. + * @param applicationKeys Application keys. Key material can be decrypted using recoveryKeyBlob + * and session. KeyStore only uses package names from the application info in {@link + * WrappedApplicationKey}. Caller is responsibility to perform certificates check. + * @return Map from alias to raw key material. + * @throws SessionExpiredException if {@code session} has since been closed. + * @throws DecryptionFailedException if unable to decrypt the snapshot. + * @throws InternalRecoveryServiceException if an error occurs internal to the recovery service. + */ + public Map<String, byte[]> recoverKeys( + @NonNull RecoverySession session, + @NonNull byte[] recoveryKeyBlob, + @NonNull List<WrappedApplicationKey> applicationKeys) + throws SessionExpiredException, DecryptionFailedException, + InternalRecoveryServiceException { + try { + return (Map<String, byte[]>) mBinder.recoverKeys( + session.getSessionId(), + recoveryKeyBlob, + BackwardsCompat.fromLegacyWrappedApplicationKeys(applicationKeys)); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } catch (ServiceSpecificException e) { + if (e.errorCode == ERROR_DECRYPTION_FAILED) { + throw new DecryptionFailedException(e.getMessage()); + } + if (e.errorCode == ERROR_SESSION_EXPIRED) { + throw new SessionExpiredException(e.getMessage()); + } + throw wrapUnexpectedServiceSpecificException(e); + } + } + + /** + * Deletes all data associated with {@code session}. Should not be invoked directly but via + * {@link RecoverySession#close()}. + * + * @hide + */ + void closeSession(RecoverySession session) { + try { + mBinder.closeSession(session.getSessionId()); + } catch (RemoteException | ServiceSpecificException e) { + Log.e(TAG, "Unexpected error trying to close session", e); + } + } + + /** + * Generates a key called {@code alias} and loads it into the recoverable key store. Returns the + * raw material of the key. + * + * @param alias The key alias. + * @throws InternalRecoveryServiceException if an unexpected error occurred in the recovery + * service. + * @throws LockScreenRequiredException if the user has not set a lock screen. This is required + * to generate recoverable keys, as the snapshots are encrypted using a key derived from the + * lock screen. + */ + public byte[] generateAndStoreKey(@NonNull String alias) + throws InternalRecoveryServiceException, LockScreenRequiredException { + try { + return mBinder.generateAndStoreKey(alias); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } catch (ServiceSpecificException e) { + if (e.errorCode == ERROR_INSECURE_USER) { + throw new LockScreenRequiredException(e.getMessage()); + } + throw wrapUnexpectedServiceSpecificException(e); + } + } + + /** + * Removes a key called {@code alias} from the recoverable key store. + * + * @param alias The key alias. + * @throws InternalRecoveryServiceException if an unexpected error occurred in the recovery + * service. + */ + public void removeKey(@NonNull String alias) throws InternalRecoveryServiceException { + try { + mBinder.removeKey(alias); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } catch (ServiceSpecificException e) { + throw wrapUnexpectedServiceSpecificException(e); + } + } + + private InternalRecoveryServiceException wrapUnexpectedServiceSpecificException( + ServiceSpecificException e) { + if (e.errorCode == ERROR_SERVICE_INTERNAL_ERROR) { + return new InternalRecoveryServiceException(e.getMessage()); + } + + // Should never happen. If it does, it's a bug, and we need to update how the method that + // called this throws its exceptions. + return new InternalRecoveryServiceException("Unexpected error code for method: " + + e.errorCode, e); + } +} diff --git a/android/security/keystore/RecoveryControllerException.java b/android/security/keystore/RecoveryControllerException.java new file mode 100644 index 00000000..5b806b75 --- /dev/null +++ b/android/security/keystore/RecoveryControllerException.java @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2018 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 android.security.keystore; + +import java.security.GeneralSecurityException; + +/** + * Base exception for errors thrown by {@link RecoveryController}. + * + * @hide + */ +public abstract class RecoveryControllerException extends GeneralSecurityException { + RecoveryControllerException() { } + + RecoveryControllerException(String msg) { + super(msg); + } + + public RecoveryControllerException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/android/security/keystore/RecoverySession.java b/android/security/keystore/RecoverySession.java new file mode 100644 index 00000000..ae8d91af --- /dev/null +++ b/android/security/keystore/RecoverySession.java @@ -0,0 +1,71 @@ +/* + * Copyright (C) 2018 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 android.security.keystore; + +import java.security.SecureRandom; + +/** + * Session to recover a {@link KeychainSnapshot} from the remote trusted hardware, initiated by a + * recovery agent. + * + * @hide + */ +public class RecoverySession implements AutoCloseable { + + private static final int SESSION_ID_LENGTH_BYTES = 16; + + private final String mSessionId; + private final RecoveryController mRecoveryController; + + private RecoverySession(RecoveryController recoveryController, String sessionId) { + mRecoveryController = recoveryController; + mSessionId = sessionId; + } + + /** + * A new session, started by {@code recoveryManager}. + */ + static RecoverySession newInstance(RecoveryController recoveryController) { + return new RecoverySession(recoveryController, newSessionId()); + } + + /** + * Returns a new random session ID. + */ + private static String newSessionId() { + SecureRandom secureRandom = new SecureRandom(); + byte[] sessionId = new byte[SESSION_ID_LENGTH_BYTES]; + secureRandom.nextBytes(sessionId); + StringBuilder sb = new StringBuilder(); + for (byte b : sessionId) { + sb.append(Byte.toHexString(b, /*upperCase=*/ false)); + } + return sb.toString(); + } + + /** + * An internal session ID, used by the framework to match recovery claims to snapshot responses. + */ + String getSessionId() { + return mSessionId; + } + + @Override + public void close() { + mRecoveryController.closeSession(this); + } +} diff --git a/android/security/keystore/SessionExpiredException.java b/android/security/keystore/SessionExpiredException.java new file mode 100644 index 00000000..f13e2060 --- /dev/null +++ b/android/security/keystore/SessionExpiredException.java @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2018 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 android.security.keystore; + +/** + * Error thrown when attempting to use a {@link RecoverySession} that has since expired. + * + * @hide + */ +public class SessionExpiredException extends RecoveryControllerException { + public SessionExpiredException(String msg) { + super(msg); + } +} diff --git a/android/security/keystore/StrongBoxUnavailableException.java b/android/security/keystore/StrongBoxUnavailableException.java new file mode 100644 index 00000000..ad41a58e --- /dev/null +++ b/android/security/keystore/StrongBoxUnavailableException.java @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2017 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 android.security.keystore; + +import java.security.ProviderException; + +/** + * Indicates that an operation could not be performed because the requested security hardware + * is not available. + */ +public class StrongBoxUnavailableException extends ProviderException { + +} + diff --git a/android/security/keystore/WrappedApplicationKey.java b/android/security/keystore/WrappedApplicationKey.java new file mode 100644 index 00000000..522bb955 --- /dev/null +++ b/android/security/keystore/WrappedApplicationKey.java @@ -0,0 +1,144 @@ +/* + * Copyright (C) 2017 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 android.security.keystore; + +import android.annotation.NonNull; +import android.os.Parcel; +import android.os.Parcelable; + +import com.android.internal.util.Preconditions; + +/** + * Helper class with data necessary recover a single application key, given a recovery key. + * + * <ul> + * <li>Alias - Keystore alias of the key. + * <li>Encrypted key material. + * </ul> + * + * Note that Application info is not included. Recovery Agent can only make its own keys + * recoverable. + * + * @hide + */ +public final class WrappedApplicationKey implements Parcelable { + private String mAlias; + // The only supported format is AES-256 symmetric key. + private byte[] mEncryptedKeyMaterial; + + /** + * Builder for creating {@link WrappedApplicationKey}. + */ + public static class Builder { + private WrappedApplicationKey mInstance = new WrappedApplicationKey(); + + /** + * Sets Application-specific alias of the key. + * + * @param alias The alias. + * @return This builder. + */ + public Builder setAlias(@NonNull String alias) { + mInstance.mAlias = alias; + return this; + } + + /** + * Sets key material encrypted by recovery key. + * + * @param encryptedKeyMaterial The key material + * @return This builder + */ + + public Builder setEncryptedKeyMaterial(@NonNull byte[] encryptedKeyMaterial) { + mInstance.mEncryptedKeyMaterial = encryptedKeyMaterial; + return this; + } + + /** + * Creates a new {@link WrappedApplicationKey} instance. + * + * @return new instance + * @throws NullPointerException if some required fields were not set. + */ + @NonNull public WrappedApplicationKey build() { + Preconditions.checkNotNull(mInstance.mAlias); + Preconditions.checkNotNull(mInstance.mEncryptedKeyMaterial); + return mInstance; + } + } + + private WrappedApplicationKey() { + + } + + /** + * Deprecated - consider using Builder. + * @hide + */ + public WrappedApplicationKey(@NonNull String alias, @NonNull byte[] encryptedKeyMaterial) { + mAlias = Preconditions.checkNotNull(alias); + mEncryptedKeyMaterial = Preconditions.checkNotNull(encryptedKeyMaterial); + } + + /** + * Application-specific alias of the key. + * + * @see java.security.KeyStore.aliases + */ + public @NonNull String getAlias() { + return mAlias; + } + + /** Key material encrypted by recovery key. */ + public @NonNull byte[] getEncryptedKeyMaterial() { + return mEncryptedKeyMaterial; + } + + public static final Parcelable.Creator<WrappedApplicationKey> CREATOR = + new Parcelable.Creator<WrappedApplicationKey>() { + public WrappedApplicationKey createFromParcel(Parcel in) { + return new WrappedApplicationKey(in); + } + + public WrappedApplicationKey[] newArray(int length) { + return new WrappedApplicationKey[length]; + } + }; + + /** + * @hide + */ + @Override + public void writeToParcel(Parcel out, int flags) { + out.writeString(mAlias); + out.writeByteArray(mEncryptedKeyMaterial); + } + + /** + * @hide + */ + protected WrappedApplicationKey(Parcel in) { + mAlias = in.readString(); + mEncryptedKeyMaterial = in.createByteArray(); + } + + @Override + public int describeContents() { + return 0; + } +} diff --git a/android/security/keystore/WrappedKeyEntry.java b/android/security/keystore/WrappedKeyEntry.java new file mode 100644 index 00000000..a8f4afe7 --- /dev/null +++ b/android/security/keystore/WrappedKeyEntry.java @@ -0,0 +1,56 @@ +/* + * Copyright (C) 2017 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 android.security.keystore; + +import java.security.KeyStore.Entry; +import java.security.spec.AlgorithmParameterSpec; + +/** + * An {@link Entry} that holds a wrapped key. + */ +public class WrappedKeyEntry implements Entry { + + private final byte[] mWrappedKeyBytes; + private final String mWrappingKeyAlias; + private final String mTransformation; + private final AlgorithmParameterSpec mAlgorithmParameterSpec; + + public WrappedKeyEntry(byte[] wrappedKeyBytes, String wrappingKeyAlias, String transformation, + AlgorithmParameterSpec algorithmParameterSpec) { + mWrappedKeyBytes = wrappedKeyBytes; + mWrappingKeyAlias = wrappingKeyAlias; + mTransformation = transformation; + mAlgorithmParameterSpec = algorithmParameterSpec; + } + + public byte[] getWrappedKeyBytes() { + return mWrappedKeyBytes; + } + + public String getWrappingKeyAlias() { + return mWrappingKeyAlias; + } + + public String getTransformation() { + return mTransformation; + } + + public AlgorithmParameterSpec getAlgorithmParameterSpec() { + return mAlgorithmParameterSpec; + } +} diff --git a/android/security/keystore/recovery/BadCertificateFormatException.java b/android/security/keystore/recovery/BadCertificateFormatException.java new file mode 100644 index 00000000..e0781a52 --- /dev/null +++ b/android/security/keystore/recovery/BadCertificateFormatException.java @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2018 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 android.security.keystore.recovery; + +/** + * Error thrown when the recovery agent supplies an invalid X509 certificate. + * + * @hide + * Deprecated + */ +public class BadCertificateFormatException extends RecoveryControllerException { + public BadCertificateFormatException(String msg) { + super(msg); + } +} diff --git a/android/security/keystore/recovery/DecryptionFailedException.java b/android/security/keystore/recovery/DecryptionFailedException.java new file mode 100644 index 00000000..af00e053 --- /dev/null +++ b/android/security/keystore/recovery/DecryptionFailedException.java @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2018 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 android.security.keystore.recovery; + +import android.annotation.SystemApi; + +import java.security.GeneralSecurityException; + +/** + * Error thrown when decryption failed, due to an agent error. i.e., using the incorrect key, + * trying to decrypt garbage data, trying to decrypt data that has somehow been corrupted, etc. + * + * @hide + */ +@SystemApi +public class DecryptionFailedException extends GeneralSecurityException { + public DecryptionFailedException(String msg) { + super(msg); + } +} diff --git a/android/security/keystore/recovery/InternalRecoveryServiceException.java b/android/security/keystore/recovery/InternalRecoveryServiceException.java new file mode 100644 index 00000000..218d26eb --- /dev/null +++ b/android/security/keystore/recovery/InternalRecoveryServiceException.java @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2018 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 android.security.keystore.recovery; + +import android.annotation.SystemApi; + +import java.security.GeneralSecurityException; +/** + * An error thrown when something went wrong internally in the recovery service. + * + * <p>This is an unexpected error, and indicates a problem with the service itself, rather than the + * caller having performed some kind of illegal action. + * + * @hide + */ +@SystemApi +public class InternalRecoveryServiceException extends GeneralSecurityException { + public InternalRecoveryServiceException(String msg) { + super(msg); + } + + public InternalRecoveryServiceException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/android/security/keystore/recovery/KeyChainProtectionParams.java b/android/security/keystore/recovery/KeyChainProtectionParams.java new file mode 100644 index 00000000..a43952a8 --- /dev/null +++ b/android/security/keystore/recovery/KeyChainProtectionParams.java @@ -0,0 +1,287 @@ +/* + * Copyright (C) 2017 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 android.security.keystore.recovery; + +import android.annotation.IntDef; +import android.annotation.NonNull; +import android.annotation.SystemApi; +import android.os.Parcel; +import android.os.Parcelable; + +import com.android.internal.util.Preconditions; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.Arrays; + +/** + * A {@link KeyChainSnapshot} is protected with a key derived from the user's lock screen. This + * class wraps all the data necessary to derive the same key on a recovering device: + * + * <ul> + * <li>UI parameters for the user's lock screen - so that if e.g., the user was using a pattern, + * the recovering device can display the pattern UI to the user when asking them to enter + * the lock screen from their previous device. + * <li>The algorithm used to derive a key from the user's lock screen, e.g. SHA-256 with a salt. + * </ul> + * + * <p>As such, this data is sent along with the {@link KeyChainSnapshot} when syncing the current + * version of the keychain. + * + * <p>For now, the recoverable keychain only supports a single layer of protection, which is the + * user's lock screen. In the future, the keychain will support multiple layers of protection + * (e.g. an additional keychain password, along with the lock screen). + * + * @hide + */ +@SystemApi +public final class KeyChainProtectionParams implements Parcelable { + /** @hide */ + @Retention(RetentionPolicy.SOURCE) + @IntDef(prefix = {"TYPE_"}, value = {TYPE_LOCKSCREEN, TYPE_CUSTOM_PASSWORD}) + public @interface UserSecretType { + } + + /** + * Lockscreen secret is required to recover KeyStore. + */ + public static final int TYPE_LOCKSCREEN = 100; + + /** + * Custom passphrase, unrelated to lock screen, is required to recover KeyStore. + */ + public static final int TYPE_CUSTOM_PASSWORD = 101; + + /** @hide */ + @Retention(RetentionPolicy.SOURCE) + @IntDef(prefix = {"UI_FORMAT_"}, value = {UI_FORMAT_PIN, UI_FORMAT_PASSWORD, UI_FORMAT_PATTERN}) + public @interface LockScreenUiFormat { + } + + /** + * Pin with digits only. + */ + public static final int UI_FORMAT_PIN = 1; + + /** + * Password. String with latin-1 characters only. + */ + public static final int UI_FORMAT_PASSWORD = 2; + + /** + * Pattern with 3 by 3 grid. + */ + public static final int UI_FORMAT_PATTERN = 3; + + @UserSecretType + private Integer mUserSecretType; + + @LockScreenUiFormat + private Integer mLockScreenUiFormat; + + /** + * Parameters of the key derivation function, including algorithm, difficulty, salt. + */ + private KeyDerivationParams mKeyDerivationParams; + private byte[] mSecret; // Derived from user secret. The field must have limited visibility. + + /** + * @param secret Constructor creates a reference to the secret. Caller must use + * @link {#clearSecret} to overwrite its value in memory. + * @hide + */ + public KeyChainProtectionParams(@UserSecretType int userSecretType, + @LockScreenUiFormat int lockScreenUiFormat, + @NonNull KeyDerivationParams keyDerivationParams, + @NonNull byte[] secret) { + mUserSecretType = userSecretType; + mLockScreenUiFormat = lockScreenUiFormat; + mKeyDerivationParams = Preconditions.checkNotNull(keyDerivationParams); + mSecret = Preconditions.checkNotNull(secret); + } + + private KeyChainProtectionParams() { + + } + + /** + * @see TYPE_LOCKSCREEN + * @see TYPE_CUSTOM_PASSWORD + */ + public @UserSecretType int getUserSecretType() { + return mUserSecretType; + } + + /** + * Specifies UX shown to user during recovery. + * Default value is {@code UI_FORMAT_LOCKSCREEN} + * + * @see UI_FORMAT_PIN + * @see UI_FORMAT_PASSWORD + * @see UI_FORMAT_PATTERN + */ + public @LockScreenUiFormat int getLockScreenUiFormat() { + return mLockScreenUiFormat; + } + + /** + * Specifies function used to derive symmetric key from user input + * Format is defined in separate util class. + */ + public @NonNull KeyDerivationParams getKeyDerivationParams() { + return mKeyDerivationParams; + } + + /** + * Secret derived from user input. + * Default value is empty array + * + * @return secret or empty array + */ + public @NonNull byte[] getSecret() { + return mSecret; + } + + /** + * Builder for creating {@link KeyChainProtectionParams}. + */ + public static class Builder { + private KeyChainProtectionParams mInstance = new KeyChainProtectionParams(); + + /** + * Sets user secret type. + * + * @see TYPE_LOCKSCREEN + * @see TYPE_CUSTOM_PASSWORD + * @param userSecretType The secret type + * @return This builder. + */ + public Builder setUserSecretType(@UserSecretType int userSecretType) { + mInstance.mUserSecretType = userSecretType; + return this; + } + + /** + * Sets UI format. + * + * @see UI_FORMAT_PIN + * @see UI_FORMAT_PASSWORD + * @see UI_FORMAT_PATTERN + * @param lockScreenUiFormat The UI format + * @return This builder. + */ + public Builder setLockScreenUiFormat(@LockScreenUiFormat int lockScreenUiFormat) { + mInstance.mLockScreenUiFormat = lockScreenUiFormat; + return this; + } + + /** + * Sets parameters of the key derivation function. + * + * @param keyDerivationParams Key derivation Params + * @return This builder. + */ + public Builder setKeyDerivationParams(@NonNull KeyDerivationParams + keyDerivationParams) { + mInstance.mKeyDerivationParams = keyDerivationParams; + return this; + } + + /** + * Secret derived from user input, or empty array. + * + * @param secret The secret. + * @return This builder. + */ + public Builder setSecret(@NonNull byte[] secret) { + mInstance.mSecret = secret; + return this; + } + + + /** + * Creates a new {@link KeyChainProtectionParams} instance. + * The instance will include default values, if {@link setSecret} + * or {@link setUserSecretType} were not called. + * + * @return new instance + * @throws NullPointerException if some required fields were not set. + */ + @NonNull public KeyChainProtectionParams build() { + if (mInstance.mUserSecretType == null) { + mInstance.mUserSecretType = TYPE_LOCKSCREEN; + } + Preconditions.checkNotNull(mInstance.mLockScreenUiFormat); + Preconditions.checkNotNull(mInstance.mKeyDerivationParams); + if (mInstance.mSecret == null) { + mInstance.mSecret = new byte[]{}; + } + return mInstance; + } + } + + /** + * Removes secret from memory than object is no longer used. + * Since finalizer call is not reliable, please use @link {#clearSecret} directly. + */ + @Override + protected void finalize() throws Throwable { + clearSecret(); + super.finalize(); + } + + /** + * Fills mSecret with zeroes. + */ + public void clearSecret() { + Arrays.fill(mSecret, (byte) 0); + } + + public static final Parcelable.Creator<KeyChainProtectionParams> CREATOR = + new Parcelable.Creator<KeyChainProtectionParams>() { + public KeyChainProtectionParams createFromParcel(Parcel in) { + return new KeyChainProtectionParams(in); + } + + public KeyChainProtectionParams[] newArray(int length) { + return new KeyChainProtectionParams[length]; + } + }; + + @Override + public void writeToParcel(Parcel out, int flags) { + out.writeInt(mUserSecretType); + out.writeInt(mLockScreenUiFormat); + out.writeTypedObject(mKeyDerivationParams, flags); + out.writeByteArray(mSecret); + } + + /** + * @hide + */ + protected KeyChainProtectionParams(Parcel in) { + mUserSecretType = in.readInt(); + mLockScreenUiFormat = in.readInt(); + mKeyDerivationParams = in.readTypedObject(KeyDerivationParams.CREATOR); + mSecret = in.createByteArray(); + } + + @Override + public int describeContents() { + return 0; + } +} diff --git a/android/security/keystore/recovery/KeyChainSnapshot.java b/android/security/keystore/recovery/KeyChainSnapshot.java new file mode 100644 index 00000000..df535ed9 --- /dev/null +++ b/android/security/keystore/recovery/KeyChainSnapshot.java @@ -0,0 +1,299 @@ +/* + * Copyright (C) 2017 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 android.security.keystore.recovery; + +import android.annotation.NonNull; +import android.annotation.SystemApi; +import android.os.Parcel; +import android.os.Parcelable; + +import com.android.internal.util.Preconditions; + +import java.util.List; + +/** + * A snapshot of a version of the keystore. Two events can trigger the generation of a new snapshot: + * + * <ul> + * <li>The user's lock screen changes. (A key derived from the user's lock screen is used to + * protected the keychain, which is why this forces a new snapshot.) + * <li>A key is added to or removed from the recoverable keychain. + * </ul> + * + * <p>The snapshot data is also encrypted with the remote trusted hardware's public key, so even + * the recovery agent itself should not be able to decipher the data. The recovery agent sends an + * instance of this to the remote trusted hardware whenever a new snapshot is generated. During a + * recovery flow, the recovery agent retrieves a snapshot from the remote trusted hardware. It then + * sends it to the framework, where it is decrypted using the user's lock screen from their previous + * device. + * + * @hide + */ +@SystemApi +public final class KeyChainSnapshot implements Parcelable { + private static final int DEFAULT_MAX_ATTEMPTS = 10; + private static final long DEFAULT_COUNTER_ID = 1L; + + private int mSnapshotVersion; + private int mMaxAttempts = DEFAULT_MAX_ATTEMPTS; + private long mCounterId = DEFAULT_COUNTER_ID; + private byte[] mServerParams; + private byte[] mPublicKey; + private List<KeyChainProtectionParams> mKeyChainProtectionParams; + private List<WrappedApplicationKey> mEntryRecoveryData; + private byte[] mEncryptedRecoveryKeyBlob; + + /** + * @hide + * Deprecated, consider using builder. + */ + public KeyChainSnapshot( + int snapshotVersion, + @NonNull List<KeyChainProtectionParams> keyChainProtectionParams, + @NonNull List<WrappedApplicationKey> wrappedApplicationKeys, + @NonNull byte[] encryptedRecoveryKeyBlob) { + mSnapshotVersion = snapshotVersion; + mKeyChainProtectionParams = + Preconditions.checkCollectionElementsNotNull(keyChainProtectionParams, + "KeyChainProtectionParams"); + mEntryRecoveryData = Preconditions.checkCollectionElementsNotNull(wrappedApplicationKeys, + "wrappedApplicationKeys"); + mEncryptedRecoveryKeyBlob = Preconditions.checkNotNull(encryptedRecoveryKeyBlob); + } + + private KeyChainSnapshot() { + + } + + /** + * Snapshot version for given account. It is incremented when user secret or list of application + * keys changes. + */ + public int getSnapshotVersion() { + return mSnapshotVersion; + } + + /** + * Number of user secret guesses allowed during Keychain recovery. + */ + public int getMaxAttempts() { + return mMaxAttempts; + } + + /** + * CounterId which is rotated together with user secret. + */ + public long getCounterId() { + return mCounterId; + } + + /** + * Server parameters. + */ + public @NonNull byte[] getServerParams() { + return mServerParams; + } + + /** + * Public key used to encrypt {@code encryptedRecoveryKeyBlob}. + * + * See implementation for binary key format + */ + // TODO: document key format. + public @NonNull byte[] getTrustedHardwarePublicKey() { + return mPublicKey; + } + + /** + * UI and key derivation parameters. Note that combination of secrets may be used. + */ + public @NonNull List<KeyChainProtectionParams> getKeyChainProtectionParams() { + return mKeyChainProtectionParams; + } + + /** + * List of application keys, with key material encrypted by + * the recovery key ({@link #getEncryptedRecoveryKeyBlob}). + */ + public @NonNull List<WrappedApplicationKey> getWrappedApplicationKeys() { + return mEntryRecoveryData; + } + + /** + * Recovery key blob, encrypted by user secret and recovery service public key. + */ + public @NonNull byte[] getEncryptedRecoveryKeyBlob() { + return mEncryptedRecoveryKeyBlob; + } + + public static final Creator<KeyChainSnapshot> CREATOR = + new Creator<KeyChainSnapshot>() { + public KeyChainSnapshot createFromParcel(Parcel in) { + return new KeyChainSnapshot(in); + } + + public KeyChainSnapshot[] newArray(int length) { + return new KeyChainSnapshot[length]; + } + }; + + /** + * Builder for creating {@link KeyChainSnapshot}. + * @hide + */ + public static class Builder { + private KeyChainSnapshot mInstance = new KeyChainSnapshot(); + + /** + * Snapshot version for given account. + * + * @param snapshotVersion The snapshot version + * @return This builder. + */ + public Builder setSnapshotVersion(int snapshotVersion) { + mInstance.mSnapshotVersion = snapshotVersion; + return this; + } + + /** + * Sets the number of user secret guesses allowed during Keychain recovery. + * + * @param maxAttempts The maximum number of guesses. + * @return This builder. + */ + public Builder setMaxAttempts(int maxAttempts) { + mInstance.mMaxAttempts = maxAttempts; + return this; + } + + /** + * Sets counter id. + * + * @param counterId The counter id. + * @return This builder. + */ + public Builder setCounterId(long counterId) { + mInstance.mCounterId = counterId; + return this; + } + + /** + * Sets server parameters. + * + * @param serverParams The server parameters + * @return This builder. + */ + public Builder setServerParams(byte[] serverParams) { + mInstance.mServerParams = serverParams; + return this; + } + + /** + * Sets public key used to encrypt recovery blob. + * + * @param publicKey The public key + * @return This builder. + */ + public Builder setTrustedHardwarePublicKey(byte[] publicKey) { + mInstance.mPublicKey = publicKey; + return this; + } + + /** + * Sets UI and key derivation parameters + * + * @param recoveryMetadata The UI and key derivation parameters + * @return This builder. + */ + public Builder setKeyChainProtectionParams( + @NonNull List<KeyChainProtectionParams> recoveryMetadata) { + mInstance.mKeyChainProtectionParams = recoveryMetadata; + return this; + } + + /** + * List of application keys. + * + * @param entryRecoveryData List of application keys + * @return This builder. + */ + public Builder setWrappedApplicationKeys(List<WrappedApplicationKey> entryRecoveryData) { + mInstance.mEntryRecoveryData = entryRecoveryData; + return this; + } + + /** + * Sets recovery key blob + * + * @param encryptedRecoveryKeyBlob The recovery key blob. + * @return This builder. + */ + public Builder setEncryptedRecoveryKeyBlob(@NonNull byte[] encryptedRecoveryKeyBlob) { + mInstance.mEncryptedRecoveryKeyBlob = encryptedRecoveryKeyBlob; + return this; + } + + + /** + * Creates a new {@link KeyChainSnapshot} instance. + * + * @return new instance + * @throws NullPointerException if some required fields were not set. + */ + @NonNull public KeyChainSnapshot build() { + Preconditions.checkCollectionElementsNotNull(mInstance.mKeyChainProtectionParams, + "recoveryMetadata"); + Preconditions.checkCollectionElementsNotNull(mInstance.mEntryRecoveryData, + "entryRecoveryData"); + Preconditions.checkNotNull(mInstance.mEncryptedRecoveryKeyBlob); + Preconditions.checkNotNull(mInstance.mServerParams); + Preconditions.checkNotNull(mInstance.mPublicKey); + return mInstance; + } + } + + @Override + public void writeToParcel(Parcel out, int flags) { + out.writeInt(mSnapshotVersion); + out.writeTypedList(mKeyChainProtectionParams); + out.writeByteArray(mEncryptedRecoveryKeyBlob); + out.writeTypedList(mEntryRecoveryData); + out.writeInt(mMaxAttempts); + out.writeLong(mCounterId); + out.writeByteArray(mServerParams); + out.writeByteArray(mPublicKey); + } + + /** + * @hide + */ + protected KeyChainSnapshot(Parcel in) { + mSnapshotVersion = in.readInt(); + mKeyChainProtectionParams = in.createTypedArrayList(KeyChainProtectionParams.CREATOR); + mEncryptedRecoveryKeyBlob = in.createByteArray(); + mEntryRecoveryData = in.createTypedArrayList(WrappedApplicationKey.CREATOR); + mMaxAttempts = in.readInt(); + mCounterId = in.readLong(); + mServerParams = in.createByteArray(); + mPublicKey = in.createByteArray(); + } + + @Override + public int describeContents() { + return 0; + } +} diff --git a/android/security/keystore/recovery/KeyDerivationParams.java b/android/security/keystore/recovery/KeyDerivationParams.java new file mode 100644 index 00000000..fc909a0a --- /dev/null +++ b/android/security/keystore/recovery/KeyDerivationParams.java @@ -0,0 +1,119 @@ +/* + * Copyright (C) 2017 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 android.security.keystore.recovery; + +import android.annotation.IntDef; +import android.annotation.NonNull; +import android.annotation.SystemApi; +import android.os.Parcel; +import android.os.Parcelable; + + +import com.android.internal.util.Preconditions; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * Collection of parameters which define a key derivation function. + * Currently only supports salted SHA-256 + * + * @hide + */ +@SystemApi +public final class KeyDerivationParams implements Parcelable { + private final int mAlgorithm; + private byte[] mSalt; + + /** @hide */ + @Retention(RetentionPolicy.SOURCE) + @IntDef(prefix = {"ALGORITHM_"}, value = {ALGORITHM_SHA256, ALGORITHM_ARGON2ID}) + public @interface KeyDerivationAlgorithm { + } + + /** + * Salted SHA256 + */ + public static final int ALGORITHM_SHA256 = 1; + + /** + * Argon2ID + * @hide + */ + // TODO: add Argon2ID support. + public static final int ALGORITHM_ARGON2ID = 2; + + /** + * Creates instance of the class to to derive key using salted SHA256 hash. + */ + public static KeyDerivationParams createSha256Params(@NonNull byte[] salt) { + return new KeyDerivationParams(ALGORITHM_SHA256, salt); + } + + /** + * @hide + */ + // TODO: Make private once legacy API is removed + public KeyDerivationParams(@KeyDerivationAlgorithm int algorithm, @NonNull byte[] salt) { + mAlgorithm = algorithm; + mSalt = Preconditions.checkNotNull(salt); + } + + /** + * Gets algorithm. + */ + public @KeyDerivationAlgorithm int getAlgorithm() { + return mAlgorithm; + } + + /** + * Gets salt. + */ + public @NonNull byte[] getSalt() { + return mSalt; + } + + public static final Parcelable.Creator<KeyDerivationParams> CREATOR = + new Parcelable.Creator<KeyDerivationParams>() { + public KeyDerivationParams createFromParcel(Parcel in) { + return new KeyDerivationParams(in); + } + + public KeyDerivationParams[] newArray(int length) { + return new KeyDerivationParams[length]; + } + }; + + @Override + public void writeToParcel(Parcel out, int flags) { + out.writeInt(mAlgorithm); + out.writeByteArray(mSalt); + } + + /** + * @hide + */ + protected KeyDerivationParams(Parcel in) { + mAlgorithm = in.readInt(); + mSalt = in.createByteArray(); + } + + @Override + public int describeContents() { + return 0; + } +} diff --git a/android/security/keystore/recovery/LockScreenRequiredException.java b/android/security/keystore/recovery/LockScreenRequiredException.java new file mode 100644 index 00000000..0062d290 --- /dev/null +++ b/android/security/keystore/recovery/LockScreenRequiredException.java @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2018 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 android.security.keystore.recovery; + +import android.annotation.SystemApi; + +import java.security.GeneralSecurityException; + +/** + * Error thrown when trying to generate keys for a profile that has no lock screen set. + * + * <p>A lock screen must be set, as the lock screen is used to encrypt the snapshot. + * + * @hide + */ +@SystemApi +public class LockScreenRequiredException extends GeneralSecurityException { + public LockScreenRequiredException(String msg) { + super(msg); + } +} diff --git a/android/security/keystore/recovery/RecoveryClaim.java b/android/security/keystore/recovery/RecoveryClaim.java new file mode 100644 index 00000000..45c6b4ff --- /dev/null +++ b/android/security/keystore/recovery/RecoveryClaim.java @@ -0,0 +1,55 @@ +/* + * Copyright (C) 2018 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 android.security.keystore.recovery; + +/** + * An attempt to recover a keychain protected by remote secure hardware. + * + * @hide + * Deprecated + */ +public class RecoveryClaim { + + private final RecoverySession mRecoverySession; + private final byte[] mClaimBytes; + + RecoveryClaim(RecoverySession recoverySession, byte[] claimBytes) { + mRecoverySession = recoverySession; + mClaimBytes = claimBytes; + } + + /** + * Returns the session associated with the recovery attempt. This is used to match the symmetric + * key, which remains internal to the framework, for decrypting the claim response. + * + * @return The session data. + */ + public RecoverySession getRecoverySession() { + return mRecoverySession; + } + + /** + * Returns the encrypted claim's bytes. + * + * <p>This should be sent by the recovery agent to the remote secure hardware, which will use + * it to decrypt the keychain, before sending it re-encrypted with the session's symmetric key + * to the device. + */ + public byte[] getClaimBytes() { + return mClaimBytes; + } +} diff --git a/android/security/keystore/recovery/RecoveryController.java b/android/security/keystore/recovery/RecoveryController.java new file mode 100644 index 00000000..71a36f19 --- /dev/null +++ b/android/security/keystore/recovery/RecoveryController.java @@ -0,0 +1,460 @@ +/* + * Copyright (C) 2017 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 android.security.keystore.recovery; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.RequiresPermission; +import android.annotation.SystemApi; +import android.app.PendingIntent; +import android.content.Context; +import android.content.pm.PackageManager.NameNotFoundException; +import android.os.RemoteException; +import android.os.ServiceManager; +import android.os.ServiceSpecificException; + +import com.android.internal.widget.ILockSettings; + +import java.security.cert.CertificateException; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +/** + * An assistant for generating {@link javax.crypto.SecretKey} instances that can be recovered by + * other Android devices belonging to the user. The exported keychain is protected by the user's + * lock screen. + * + * <p>The RecoveryController must be paired with a recovery agent. The recovery agent is responsible + * for transporting the keychain to remote trusted hardware. This hardware must prevent brute force + * attempts against the user's lock screen by limiting the number of allowed guesses (to, e.g., 10). + * After that number of incorrect guesses, the trusted hardware no longer allows access to the + * key chain. + * + * <p>For now only the recovery agent itself is able to create keys, so it is expected that the + * recovery agent is itself the system app. + * + * <p>A recovery agent requires the privileged permission + * {@code android.Manifest.permission#RECOVER_KEYSTORE}. + * + * @hide + */ +@SystemApi +public class RecoveryController { + private static final String TAG = "RecoveryController"; + + /** Key has been successfully synced. */ + public static final int RECOVERY_STATUS_SYNCED = 0; + /** Waiting for recovery agent to sync the key. */ + public static final int RECOVERY_STATUS_SYNC_IN_PROGRESS = 1; + /** Recovery account is not available. */ + public static final int RECOVERY_STATUS_MISSING_ACCOUNT = 2; + /** Key cannot be synced. */ + public static final int RECOVERY_STATUS_PERMANENT_FAILURE = 3; + + /** + * Failed because no snapshot is yet pending to be synced for the user. + * + * @hide + */ + public static final int ERROR_NO_SNAPSHOT_PENDING = 21; + + /** + * Failed due to an error internal to the recovery service. This is unexpected and indicates + * either a problem with the logic in the service, or a problem with a dependency of the + * service (such as AndroidKeyStore). + * + * @hide + */ + public static final int ERROR_SERVICE_INTERNAL_ERROR = 22; + + /** + * Failed because the user does not have a lock screen set. + * + * @hide + */ + public static final int ERROR_INSECURE_USER = 23; + + /** + * Error thrown when attempting to use a recovery session that has since been closed. + * + * @hide + */ + public static final int ERROR_SESSION_EXPIRED = 24; + + /** + * Failed because the provided certificate was not a valid X509 certificate. + * + * @hide + */ + public static final int ERROR_BAD_CERTIFICATE_FORMAT = 25; + + /** + * Error thrown if decryption failed. This might be because the tag is wrong, the key is wrong, + * the data has become corrupted, the data has been tampered with, etc. + * + * @hide + */ + public static final int ERROR_DECRYPTION_FAILED = 26; + + + private final ILockSettings mBinder; + + private RecoveryController(ILockSettings binder) { + mBinder = binder; + } + + /** + * Internal method used by {@code RecoverySession}. + * + * @hide + */ + ILockSettings getBinder() { + return mBinder; + } + + /** + * Gets a new instance of the class. + */ + public static RecoveryController getInstance(Context context) { + ILockSettings lockSettings = + ILockSettings.Stub.asInterface(ServiceManager.getService("lock_settings")); + return new RecoveryController(lockSettings); + } + + /** + * Initializes key recovery service for the calling application. RecoveryController + * randomly chooses one of the keys from the list and keeps it to use for future key export + * operations. Collection of all keys in the list must be signed by the provided {@code + * rootCertificateAlias}, which must also be present in the list of root certificates + * preinstalled on the device. The random selection allows RecoveryController to select + * which of a set of remote recovery service devices will be used. + * + * <p>In addition, RecoveryController enforces a delay of three months between + * consecutive initialization attempts, to limit the ability of an attacker to often switch + * remote recovery devices and significantly increase number of recovery attempts. + * + * @param rootCertificateAlias alias of a root certificate preinstalled on the device + * @param signedPublicKeyList binary blob a list of X509 certificates and signature + * @throws CertificateException if the {@code signedPublicKeyList} is in a bad format. + * @throws InternalRecoveryServiceException if an unexpected error occurred in the recovery + * service. + */ + @RequiresPermission(android.Manifest.permission.RECOVER_KEYSTORE) + public void initRecoveryService( + @NonNull String rootCertificateAlias, @NonNull byte[] signedPublicKeyList) + throws CertificateException, InternalRecoveryServiceException { + try { + mBinder.initRecoveryService(rootCertificateAlias, signedPublicKeyList); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } catch (ServiceSpecificException e) { + if (e.errorCode == ERROR_BAD_CERTIFICATE_FORMAT) { + throw new CertificateException(e.getMessage()); + } + throw wrapUnexpectedServiceSpecificException(e); + } + } + + /** + * Returns data necessary to store all recoverable keys. Key material is + * encrypted with user secret and recovery public key. + * + * @return Data necessary to recover keystore. + * @throws InternalRecoveryServiceException if an unexpected error occurred in the recovery + * service. + */ + @RequiresPermission(android.Manifest.permission.RECOVER_KEYSTORE) + public @NonNull KeyChainSnapshot getRecoveryData() + throws InternalRecoveryServiceException { + try { + return mBinder.getRecoveryData(/*account=*/ new byte[]{}); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } catch (ServiceSpecificException e) { + if (e.errorCode == ERROR_NO_SNAPSHOT_PENDING) { + return null; + } + throw wrapUnexpectedServiceSpecificException(e); + } + } + + /** + * Sets a listener which notifies recovery agent that new recovery snapshot is available. {@link + * #getRecoveryData} can be used to get the snapshot. Note that every recovery agent can have at + * most one registered listener at any time. + * + * @param intent triggered when new snapshot is available. Unregisters listener if the value is + * {@code null}. + * @throws InternalRecoveryServiceException if an unexpected error occurred in the recovery + * service. + */ + @RequiresPermission(android.Manifest.permission.RECOVER_KEYSTORE) + public void setSnapshotCreatedPendingIntent(@Nullable PendingIntent intent) + throws InternalRecoveryServiceException { + try { + mBinder.setSnapshotCreatedPendingIntent(intent); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } catch (ServiceSpecificException e) { + throw wrapUnexpectedServiceSpecificException(e); + } + } + + /** + * Server parameters used to generate new recovery key blobs. This value will be included in + * {@code KeyChainSnapshot.getEncryptedRecoveryKeyBlob()}. The same value must be included + * in vaultParams {@link #startRecoverySession} + * + * @param serverParams included in recovery key blob. + * @see #getRecoveryData + * @throws InternalRecoveryServiceException if an unexpected error occurred in the recovery + * service. + */ + @RequiresPermission(android.Manifest.permission.RECOVER_KEYSTORE) + public void setServerParams(byte[] serverParams) throws InternalRecoveryServiceException { + try { + mBinder.setServerParams(serverParams); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } catch (ServiceSpecificException e) { + throw wrapUnexpectedServiceSpecificException(e); + } + } + + /** + * Gets aliases of recoverable keys for the application. + * + * @param packageName which recoverable keys' aliases will be returned. + * + * @return {@code List} of all aliases. + */ + public List<String> getAliases(@Nullable String packageName) + throws InternalRecoveryServiceException { + try { + // TODO: update aidl + Map<String, Integer> allStatuses = mBinder.getRecoveryStatus(packageName); + return new ArrayList<>(allStatuses.keySet()); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } catch (ServiceSpecificException e) { + throw wrapUnexpectedServiceSpecificException(e); + } + } + + /** + * Updates recovery status for given key. It is used to notify keystore that key was + * successfully stored on the server or there were an error. Application can check this value + * using {@code getRecoveyStatus}. + * + * @param packageName Application whose recoverable key's status are to be updated. + * @param alias Application-specific key alias. + * @param status Status specific to recovery agent. + * @throws InternalRecoveryServiceException if an unexpected error occurred in the recovery + * service. + */ + @RequiresPermission(android.Manifest.permission.RECOVER_KEYSTORE) + public void setRecoveryStatus( + @NonNull String packageName, String alias, int status) + throws NameNotFoundException, InternalRecoveryServiceException { + try { + // TODO: update aidl + String[] aliases = alias == null ? null : new String[]{alias}; + mBinder.setRecoveryStatus(packageName, aliases, status); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } catch (ServiceSpecificException e) { + throw wrapUnexpectedServiceSpecificException(e); + } + } + + /** + * Returns recovery status for Application's KeyStore key. + * Negative status values are reserved for recovery agent specific codes. List of common codes: + * + * <ul> + * <li>{@link #RECOVERY_STATUS_SYNCED} + * <li>{@link #RECOVERY_STATUS_SYNC_IN_PROGRESS} + * <li>{@link #RECOVERY_STATUS_MISSING_ACCOUNT} + * <li>{@link #RECOVERY_STATUS_PERMANENT_FAILURE} + * </ul> + * + * @param packageName Application whose recoverable key status is returned. + * @param alias Application-specific key alias. + * @return Recovery status. + * @see #setRecoveryStatus + * @throws InternalRecoveryServiceException if an unexpected error occurred in the recovery + * service. + */ + public int getRecoveryStatus(String packageName, String alias) + throws InternalRecoveryServiceException { + try { + // TODO: update aidl + Map<String, Integer> allStatuses = mBinder.getRecoveryStatus(packageName); + Integer status = allStatuses.get(alias); + if (status == null) { + return RecoveryController.RECOVERY_STATUS_PERMANENT_FAILURE; + } else { + return status; + } + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } catch (ServiceSpecificException e) { + throw wrapUnexpectedServiceSpecificException(e); + } + } + + /** + * Specifies a set of secret types used for end-to-end keystore encryption. Knowing all of them + * is necessary to recover data. + * + * @param secretTypes {@link KeyChainProtectionParams#TYPE_LOCKSCREEN} or {@link + * KeyChainProtectionParams#TYPE_CUSTOM_PASSWORD} + * @throws InternalRecoveryServiceException if an unexpected error occurred in the recovery + * service. + */ + public void setRecoverySecretTypes( + @NonNull @KeyChainProtectionParams.UserSecretType int[] secretTypes) + throws InternalRecoveryServiceException { + try { + mBinder.setRecoverySecretTypes(secretTypes); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } catch (ServiceSpecificException e) { + throw wrapUnexpectedServiceSpecificException(e); + } + } + + /** + * Defines a set of secret types used for end-to-end keystore encryption. Knowing all of them is + * necessary to generate KeyChainSnapshot. + * + * @return list of recovery secret types + * @see KeyChainSnapshot + * @throws InternalRecoveryServiceException if an unexpected error occurred in the recovery + * service. + */ + public @NonNull @KeyChainProtectionParams.UserSecretType int[] getRecoverySecretTypes() + throws InternalRecoveryServiceException { + try { + return mBinder.getRecoverySecretTypes(); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } catch (ServiceSpecificException e) { + throw wrapUnexpectedServiceSpecificException(e); + } + } + + /** + * Returns a list of recovery secret types, necessary to create a pending recovery snapshot. + * When user enters a secret of a pending type {@link #recoverySecretAvailable} should be + * called. + * + * @return list of recovery secret types + * @throws InternalRecoveryServiceException if an unexpected error occurred in the recovery + * service. + */ + @NonNull + public @KeyChainProtectionParams.UserSecretType int[] getPendingRecoverySecretTypes() + throws InternalRecoveryServiceException { + try { + return mBinder.getPendingRecoverySecretTypes(); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } catch (ServiceSpecificException e) { + throw wrapUnexpectedServiceSpecificException(e); + } + } + + /** + * Method notifies KeyStore that a user-generated secret is available. This method generates a + * symmetric session key which a trusted remote device can use to return a recovery key. Caller + * should use {@link KeyChainProtectionParams#clearSecret} to override the secret value in + * memory. + * + * @param recoverySecret user generated secret together with parameters necessary to regenerate + * it on a new device. + * @throws InternalRecoveryServiceException if an unexpected error occurred in the recovery + * service. + */ + public void recoverySecretAvailable(@NonNull KeyChainProtectionParams recoverySecret) + throws InternalRecoveryServiceException { + try { + mBinder.recoverySecretAvailable(recoverySecret); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } catch (ServiceSpecificException e) { + throw wrapUnexpectedServiceSpecificException(e); + } + } + + /** + * Generates a AES256/GCM/NoPADDING key called {@code alias} and loads it into the recoverable + * key store. Returns the raw material of the key. + * + * @param alias The key alias. + * @param account The account associated with the key + * @throws InternalRecoveryServiceException if an unexpected error occurred in the recovery + * service. + * @throws LockScreenRequiredException if the user has not set a lock screen. This is required + * to generate recoverable keys, as the snapshots are encrypted using a key derived from the + * lock screen. + */ + public byte[] generateAndStoreKey(@NonNull String alias, byte[] account) + throws InternalRecoveryServiceException, LockScreenRequiredException { + try { + // TODO: add account + return mBinder.generateAndStoreKey(alias); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } catch (ServiceSpecificException e) { + if (e.errorCode == ERROR_INSECURE_USER) { + throw new LockScreenRequiredException(e.getMessage()); + } + throw wrapUnexpectedServiceSpecificException(e); + } + } + + /** + * Removes a key called {@code alias} from the recoverable key store. + * + * @param alias The key alias. + * @throws InternalRecoveryServiceException if an unexpected error occurred in the recovery + * service. + */ + public void removeKey(@NonNull String alias) throws InternalRecoveryServiceException { + try { + mBinder.removeKey(alias); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } catch (ServiceSpecificException e) { + throw wrapUnexpectedServiceSpecificException(e); + } + } + + InternalRecoveryServiceException wrapUnexpectedServiceSpecificException( + ServiceSpecificException e) { + if (e.errorCode == ERROR_SERVICE_INTERNAL_ERROR) { + return new InternalRecoveryServiceException(e.getMessage()); + } + + // Should never happen. If it does, it's a bug, and we need to update how the method that + // called this throws its exceptions. + return new InternalRecoveryServiceException("Unexpected error code for method: " + + e.errorCode, e); + } +} diff --git a/android/security/keystore/recovery/RecoveryControllerException.java b/android/security/keystore/recovery/RecoveryControllerException.java new file mode 100644 index 00000000..2733acab --- /dev/null +++ b/android/security/keystore/recovery/RecoveryControllerException.java @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2018 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 android.security.keystore.recovery; + +import java.security.GeneralSecurityException; + +/** + * Base exception for errors thrown by {@link RecoveryController}. + * + * @hide + * Deprecated + */ +public abstract class RecoveryControllerException extends GeneralSecurityException { + RecoveryControllerException() { } + + RecoveryControllerException(String msg) { + super(msg); + } + + public RecoveryControllerException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/android/security/keystore/recovery/RecoverySession.java b/android/security/keystore/recovery/RecoverySession.java new file mode 100644 index 00000000..4db5d6e0 --- /dev/null +++ b/android/security/keystore/recovery/RecoverySession.java @@ -0,0 +1,177 @@ +/* + * Copyright (C) 2018 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 android.security.keystore.recovery; + +import android.annotation.NonNull; +import android.annotation.RequiresPermission; +import android.annotation.SystemApi; +import android.os.RemoteException; +import android.os.ServiceSpecificException; +import android.util.Log; + +import java.security.SecureRandom; +import java.security.cert.CertificateException; +import java.util.List; +import java.util.Map; + +/** + * Session to recover a {@link KeyChainSnapshot} from the remote trusted hardware, initiated by a + * recovery agent. + * + * @hide + */ +@SystemApi +public class RecoverySession implements AutoCloseable { + private static final String TAG = "RecoverySession"; + + private static final int SESSION_ID_LENGTH_BYTES = 16; + + private final String mSessionId; + private final RecoveryController mRecoveryController; + + private RecoverySession(RecoveryController recoveryController, String sessionId) { + mRecoveryController = recoveryController; + mSessionId = sessionId; + } + + /** + * A new session, started by {@code recoveryManager}. + */ + @RequiresPermission(android.Manifest.permission.RECOVER_KEYSTORE) + static RecoverySession newInstance(RecoveryController recoveryController) { + return new RecoverySession(recoveryController, newSessionId()); + } + + /** + * Returns a new random session ID. + */ + private static String newSessionId() { + SecureRandom secureRandom = new SecureRandom(); + byte[] sessionId = new byte[SESSION_ID_LENGTH_BYTES]; + secureRandom.nextBytes(sessionId); + StringBuilder sb = new StringBuilder(); + for (byte b : sessionId) { + sb.append(Byte.toHexString(b, /*upperCase=*/ false)); + } + return sb.toString(); + } + + /** + * Starts a recovery session and returns a blob with proof of recovery secret possession. + * The method generates a symmetric key for a session, which trusted remote device can use to + * return recovery key. + * + * @param verifierPublicKey Encoded {@code java.security.cert.X509Certificate} with Public key + * used to create the recovery blob on the source device. + * Keystore will verify the certificate using root of trust. + * @param vaultParams Must match the parameters in the corresponding field in the recovery blob. + * Used to limit number of guesses. + * @param vaultChallenge Data passed from server for this recovery session and used to prevent + * replay attacks + * @param secrets Secrets provided by user, the method only uses type and secret fields. + * @return The recovery claim. Claim provides a b binary blob with recovery claim. It is + * encrypted with verifierPublicKey and contains a proof of user secrets, session symmetric + * key and parameters necessary to identify the counter with the number of failed recovery + * attempts. + * @throws CertificateException if the {@code verifierPublicKey} is in an incorrect + * format. + * @throws InternalRecoveryServiceException if an unexpected error occurred in the recovery + * service. + */ + @RequiresPermission(android.Manifest.permission.RECOVER_KEYSTORE) + @NonNull public byte[] start( + @NonNull byte[] verifierPublicKey, + @NonNull byte[] vaultParams, + @NonNull byte[] vaultChallenge, + @NonNull List<KeyChainProtectionParams> secrets) + throws CertificateException, InternalRecoveryServiceException { + try { + byte[] recoveryClaim = + mRecoveryController.getBinder().startRecoverySession( + mSessionId, + verifierPublicKey, + vaultParams, + vaultChallenge, + secrets); + return recoveryClaim; + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } catch (ServiceSpecificException e) { + if (e.errorCode == RecoveryController.ERROR_BAD_CERTIFICATE_FORMAT) { + throw new CertificateException(e.getMessage()); + } + throw mRecoveryController.wrapUnexpectedServiceSpecificException(e); + } + } + + /** + * Imports keys. + * + * @param recoveryKeyBlob Recovery blob encrypted by symmetric key generated for this session. + * @param applicationKeys Application keys. Key material can be decrypted using recoveryKeyBlob + * and session. KeyStore only uses package names from the application info in {@link + * WrappedApplicationKey}. Caller is responsibility to perform certificates check. + * @return Map from alias to raw key material. + * @throws SessionExpiredException if {@code session} has since been closed. + * @throws DecryptionFailedException if unable to decrypt the snapshot. + * @throws InternalRecoveryServiceException if an error occurs internal to the recovery service. + */ + @RequiresPermission(android.Manifest.permission.RECOVER_KEYSTORE) + public Map<String, byte[]> recoverKeys( + @NonNull byte[] recoveryKeyBlob, + @NonNull List<WrappedApplicationKey> applicationKeys) + throws SessionExpiredException, DecryptionFailedException, + InternalRecoveryServiceException { + try { + return (Map<String, byte[]>) mRecoveryController.getBinder().recoverKeys( + mSessionId, recoveryKeyBlob, applicationKeys); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } catch (ServiceSpecificException e) { + if (e.errorCode == RecoveryController.ERROR_DECRYPTION_FAILED) { + throw new DecryptionFailedException(e.getMessage()); + } + if (e.errorCode == RecoveryController.ERROR_SESSION_EXPIRED) { + throw new SessionExpiredException(e.getMessage()); + } + throw mRecoveryController.wrapUnexpectedServiceSpecificException(e); + } + } + + /** + * An internal session ID, used by the framework to match recovery claims to snapshot responses. + * + * @hide + */ + String getSessionId() { + return mSessionId; + } + + /** + * Deletes all data associated with {@code session}. Should not be invoked directly but via + * {@link RecoverySession#close()}. + */ + @RequiresPermission(android.Manifest.permission.RECOVER_KEYSTORE) + @Override + public void close() { + try { + mRecoveryController.getBinder().closeSession(mSessionId); + } catch (RemoteException | ServiceSpecificException e) { + Log.e(TAG, "Unexpected error trying to close session", e); + } + } +} diff --git a/android/security/keystore/recovery/SessionExpiredException.java b/android/security/keystore/recovery/SessionExpiredException.java new file mode 100644 index 00000000..8c18e419 --- /dev/null +++ b/android/security/keystore/recovery/SessionExpiredException.java @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2018 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 android.security.keystore.recovery; + +import android.annotation.SystemApi; + +import java.security.GeneralSecurityException; + +/** + * Error thrown when attempting to use a {@link RecoverySession} that has since expired. + * + * @hide + */ +@SystemApi +public class SessionExpiredException extends GeneralSecurityException { + public SessionExpiredException(String msg) { + super(msg); + } +} diff --git a/android/security/keystore/recovery/WrappedApplicationKey.java b/android/security/keystore/recovery/WrappedApplicationKey.java new file mode 100644 index 00000000..f360bbe9 --- /dev/null +++ b/android/security/keystore/recovery/WrappedApplicationKey.java @@ -0,0 +1,169 @@ +/* + * Copyright (C) 2017 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 android.security.keystore.recovery; + +import android.annotation.NonNull; +import android.annotation.SystemApi; + +import android.os.Parcel; +import android.os.Parcelable; + +import com.android.internal.util.Preconditions; + +/** + * Helper class with data necessary recover a single application key, given a recovery key. + * + * <ul> + * <li>Alias - Keystore alias of the key. + * <li>Account Recovery Agent specific account associated with the key. + * <li>Encrypted key material. + * </ul> + * + * Note that Application info is not included. Recovery Agent can only make its own keys + * recoverable. + * + * @hide + */ +@SystemApi +public final class WrappedApplicationKey implements Parcelable { + private String mAlias; + // The only supported format is AES-256 symmetric key. + private byte[] mEncryptedKeyMaterial; + private byte[] mAccount; + + /** + * Builder for creating {@link WrappedApplicationKey}. + */ + public static class Builder { + private WrappedApplicationKey mInstance = new WrappedApplicationKey(); + + /** + * Sets Application-specific alias of the key. + * + * @param alias The alias. + * @return This builder. + */ + public Builder setAlias(@NonNull String alias) { + mInstance.mAlias = alias; + return this; + } + + /** + * Sets Recovery agent specific account. + * + * @param account The account. + * @return This builder. + */ + public Builder setAccount(@NonNull byte[] account) { + mInstance.mAccount = account; + return this; + } + + /** + * Sets key material encrypted by recovery key. + * + * @param encryptedKeyMaterial The key material + * @return This builder + */ + + public Builder setEncryptedKeyMaterial(@NonNull byte[] encryptedKeyMaterial) { + mInstance.mEncryptedKeyMaterial = encryptedKeyMaterial; + return this; + } + + /** + * Creates a new {@link WrappedApplicationKey} instance. + * + * @return new instance + * @throws NullPointerException if some required fields were not set. + */ + @NonNull public WrappedApplicationKey build() { + Preconditions.checkNotNull(mInstance.mAlias); + Preconditions.checkNotNull(mInstance.mEncryptedKeyMaterial); + if (mInstance.mAccount == null) { + mInstance.mAccount = new byte[]{}; + } + return mInstance; + } + } + + private WrappedApplicationKey() { + } + + /** + * Deprecated - consider using Builder. + * @hide + */ + public WrappedApplicationKey(@NonNull String alias, @NonNull byte[] encryptedKeyMaterial) { + mAlias = Preconditions.checkNotNull(alias); + mEncryptedKeyMaterial = Preconditions.checkNotNull(encryptedKeyMaterial); + } + + /** + * Application-specific alias of the key. + * + * @see java.security.KeyStore.aliases + */ + public @NonNull String getAlias() { + return mAlias; + } + + /** Key material encrypted by recovery key. */ + public @NonNull byte[] getEncryptedKeyMaterial() { + return mEncryptedKeyMaterial; + } + + /** Account, default value is empty array */ + public @NonNull byte[] getAccount() { + if (mAccount == null) { + return new byte[]{}; + } + return mAccount; + } + + public static final Parcelable.Creator<WrappedApplicationKey> CREATOR = + new Parcelable.Creator<WrappedApplicationKey>() { + public WrappedApplicationKey createFromParcel(Parcel in) { + return new WrappedApplicationKey(in); + } + + public WrappedApplicationKey[] newArray(int length) { + return new WrappedApplicationKey[length]; + } + }; + + @Override + public void writeToParcel(Parcel out, int flags) { + out.writeString(mAlias); + out.writeByteArray(mEncryptedKeyMaterial); + out.writeByteArray(mAccount); + } + + /** + * @hide + */ + protected WrappedApplicationKey(Parcel in) { + mAlias = in.readString(); + mEncryptedKeyMaterial = in.createByteArray(); + mAccount = in.createByteArray(); + } + + @Override + public int describeContents() { + return 0; + } +} diff --git a/android/security/recoverablekeystore/KeyEntryRecoveryData.java b/android/security/recoverablekeystore/KeyEntryRecoveryData.java deleted file mode 100644 index 80f5aa71..00000000 --- a/android/security/recoverablekeystore/KeyEntryRecoveryData.java +++ /dev/null @@ -1,90 +0,0 @@ -/* - * Copyright (C) 2017 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 android.security.recoverablekeystore; - -import android.annotation.NonNull; -import android.os.Parcel; -import android.os.Parcelable; - -import com.android.internal.util.Preconditions; - - -/** - * Helper class with data necessary recover a single application key, given a recovery key. - * - * <ul> - * <li>Alias - Keystore alias of the key. - * <li>Encrypted key material. - * </ul> - * - * Note that Application info is not included. Recovery Agent can only make its own keys - * recoverable. - * - * @hide - */ -public final class KeyEntryRecoveryData implements Parcelable { - private final byte[] mAlias; - // The only supported format is AES-256 symmetric key. - private final byte[] mEncryptedKeyMaterial; - - public KeyEntryRecoveryData(@NonNull byte[] alias, @NonNull byte[] encryptedKeyMaterial) { - mAlias = Preconditions.checkNotNull(alias); - mEncryptedKeyMaterial = Preconditions.checkNotNull(encryptedKeyMaterial); - } - - /** - * Application-specific alias of the key. - * @see java.security.KeyStore.aliases - */ - public @NonNull byte[] getAlias() { - return mAlias; - } - - /** - * Encrypted key material encrypted by recovery key. - */ - public @NonNull byte[] getEncryptedKeyMaterial() { - return mEncryptedKeyMaterial; - } - - public static final Parcelable.Creator<KeyEntryRecoveryData> CREATOR = - new Parcelable.Creator<KeyEntryRecoveryData>() { - public KeyEntryRecoveryData createFromParcel(Parcel in) { - return new KeyEntryRecoveryData(in); - } - - public KeyEntryRecoveryData[] newArray(int length) { - return new KeyEntryRecoveryData[length]; - } - }; - - @Override - public void writeToParcel(Parcel out, int flags) { - out.writeByteArray(mAlias); - out.writeByteArray(mEncryptedKeyMaterial); - } - - protected KeyEntryRecoveryData(Parcel in) { - mAlias = in.createByteArray(); - mEncryptedKeyMaterial = in.createByteArray(); - } - - @Override - public int describeContents() { - return 0; - } -} diff --git a/android/security/recoverablekeystore/KeyStoreRecoveryData.java b/android/security/recoverablekeystore/KeyStoreRecoveryData.java deleted file mode 100644 index 087f7a25..00000000 --- a/android/security/recoverablekeystore/KeyStoreRecoveryData.java +++ /dev/null @@ -1,115 +0,0 @@ -/* - * Copyright (C) 2017 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 android.security.recoverablekeystore; - -import android.annotation.NonNull; -import android.os.Parcel; -import android.os.Parcelable; - -import com.android.internal.util.Preconditions; - -import java.util.List; - -/** - * Helper class which returns data necessary to recover keys. - * Contains - * - * <ul> - * <li>Snapshot version. - * <li>Recovery metadata with UI and key derivation parameters. - * <li>List of application keys encrypted by recovery key. - * <li>Encrypted recovery key. - * </ul> - * - * @hide - */ -public final class KeyStoreRecoveryData implements Parcelable { - private final int mSnapshotVersion; - private final List<KeyStoreRecoveryMetadata> mRecoveryMetadata; - private final List<KeyEntryRecoveryData> mApplicationKeyBlobs; - private final byte[] mEncryptedRecoveryKeyBlob; - - public KeyStoreRecoveryData(int snapshotVersion, @NonNull List<KeyStoreRecoveryMetadata> - recoveryMetadata, @NonNull List<KeyEntryRecoveryData> applicationKeyBlobs, - @NonNull byte[] encryptedRecoveryKeyBlob) { - mSnapshotVersion = snapshotVersion; - mRecoveryMetadata = Preconditions.checkNotNull(recoveryMetadata); - mApplicationKeyBlobs = Preconditions.checkNotNull(applicationKeyBlobs); - mEncryptedRecoveryKeyBlob = Preconditions.checkNotNull(encryptedRecoveryKeyBlob); - } - - /** - * Snapshot version for given account. It is incremented when user secret or list of application - * keys changes. - */ - public int getSnapshotVersion() { - return mSnapshotVersion; - } - - /** - * UI and key derivation parameters. Note that combination of secrets may be used. - */ - public @NonNull List<KeyStoreRecoveryMetadata> getRecoveryMetadata() { - return mRecoveryMetadata; - } - - /** - * List of application keys, with key material encrypted by - * the recovery key ({@link #getEncryptedRecoveryKeyBlob}). - */ - public @NonNull List<KeyEntryRecoveryData> getApplicationKeyBlobs() { - return mApplicationKeyBlobs; - } - - /** - * Recovery key blob, encrypted by user secret and recovery service public key. - */ - public @NonNull byte[] getEncryptedRecoveryKeyBlob() { - return mEncryptedRecoveryKeyBlob; - } - - public static final Parcelable.Creator<KeyStoreRecoveryData> CREATOR = - new Parcelable.Creator<KeyStoreRecoveryData>() { - public KeyStoreRecoveryData createFromParcel(Parcel in) { - return new KeyStoreRecoveryData(in); - } - - public KeyStoreRecoveryData[] newArray(int length) { - return new KeyStoreRecoveryData[length]; - } - }; - - @Override - public void writeToParcel(Parcel out, int flags) { - out.writeInt(mSnapshotVersion); - out.writeTypedList(mRecoveryMetadata); - out.writeByteArray(mEncryptedRecoveryKeyBlob); - out.writeTypedList(mApplicationKeyBlobs); - } - - protected KeyStoreRecoveryData(Parcel in) { - mSnapshotVersion = in.readInt(); - mRecoveryMetadata = in.createTypedArrayList(KeyStoreRecoveryMetadata.CREATOR); - mEncryptedRecoveryKeyBlob = in.createByteArray(); - mApplicationKeyBlobs = in.createTypedArrayList(KeyEntryRecoveryData.CREATOR); - } - - @Override - public int describeContents() { - return 0; - } -} diff --git a/android/security/recoverablekeystore/KeyStoreRecoveryMetadata.java b/android/security/recoverablekeystore/KeyStoreRecoveryMetadata.java deleted file mode 100644 index 43f9c805..00000000 --- a/android/security/recoverablekeystore/KeyStoreRecoveryMetadata.java +++ /dev/null @@ -1,180 +0,0 @@ -/* - * Copyright (C) 2017 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 android.security.recoverablekeystore; - -import android.annotation.IntDef; -import android.annotation.NonNull; -import android.os.Parcel; -import android.os.Parcelable; - -import com.android.internal.util.Preconditions; - -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.util.Arrays; - -/** - * Helper class with data necessary to recover Keystore on a new device. - * It defines UI shown to the user and a way to derive a cryptographic key from user output. - * - * @hide - */ -public final class KeyStoreRecoveryMetadata implements Parcelable { - /** @hide */ - @Retention(RetentionPolicy.SOURCE) - @IntDef({TYPE_LOCKSCREEN, TYPE_CUSTOM_PASSWORD}) - public @interface UserSecretType { - } - - /** - * Lockscreen secret is required to recover KeyStore. - */ - public static final int TYPE_LOCKSCREEN = 1; - - /** - * Custom passphrase, unrelated to lock screen, is required to recover KeyStore. - */ - public static final int TYPE_CUSTOM_PASSWORD = 2; - - /** @hide */ - @Retention(RetentionPolicy.SOURCE) - @IntDef({TYPE_PIN, TYPE_PASSWORD, TYPE_PATTERN}) - public @interface LockScreenUiFormat { - } - - /** - * Pin with digits only. - */ - public static final int TYPE_PIN = 1; - - /** - * Password. String with latin-1 characters only. - */ - public static final int TYPE_PASSWORD = 2; - - /** - * Pattern with 3 by 3 grid. - */ - public static final int TYPE_PATTERN = 3; - - @UserSecretType - private final int mUserSecretType; - - @LockScreenUiFormat - private final int mLockScreenUiFormat; - - /** - * Parameters of key derivation function, including algorithm, difficulty, salt. - */ - private KeyDerivationParameters mKeyDerivationParameters; - private byte[] mSecret; // Derived from user secret. The field must have limited visibility. - - /** - * @param secret Constructor creates a reference to the secret. Caller must use - * @link {#clearSecret} to overwrite its value in memory. - */ - public KeyStoreRecoveryMetadata(@UserSecretType int userSecretType, - @LockScreenUiFormat int lockScreenUiFormat, - @NonNull KeyDerivationParameters keyDerivationParameters, @NonNull byte[] secret) { - mUserSecretType = userSecretType; - mLockScreenUiFormat = lockScreenUiFormat; - mKeyDerivationParameters = Preconditions.checkNotNull(keyDerivationParameters); - mSecret = Preconditions.checkNotNull(secret); - } - - /** - * Specifies UX shown to user during recovery. - * - * @see KeyStore.TYPE_PIN - * @see KeyStore.TYPE_PASSWORD - * @see KeyStore.TYPE_PATTERN - */ - public @LockScreenUiFormat int getLockScreenUiFormat() { - return mLockScreenUiFormat; - } - - /** - * Specifies function used to derive symmetric key from user input - * Format is defined in separate util class. - */ - public @NonNull KeyDerivationParameters getKeyDerivationParameters() { - return mKeyDerivationParameters; - } - - /** - * Secret string derived from user input. - */ - public @NonNull byte[] getSecret() { - return mSecret; - } - - /** - * @see KeyStore.TYPE_LOCKSCREEN - * @see KeyStore.TYPE_CUSTOM_PASSWORD - */ - public @UserSecretType int getUserSecretType() { - return mUserSecretType; - } - - /** - * Removes secret from memory than object is no longer used. - * Since finalizer call is not reliable, please use @link {#clearSecret} directly. - */ - @Override - protected void finalize() throws Throwable { - clearSecret(); - super.finalize(); - } - - /** - * Fills mSecret with zeroes. - */ - public void clearSecret() { - Arrays.fill(mSecret, (byte) 0); - } - - public static final Parcelable.Creator<KeyStoreRecoveryMetadata> CREATOR = - new Parcelable.Creator<KeyStoreRecoveryMetadata>() { - public KeyStoreRecoveryMetadata createFromParcel(Parcel in) { - return new KeyStoreRecoveryMetadata(in); - } - - public KeyStoreRecoveryMetadata[] newArray(int length) { - return new KeyStoreRecoveryMetadata[length]; - } - }; - - @Override - public void writeToParcel(Parcel out, int flags) { - out.writeInt(mUserSecretType); - out.writeInt(mLockScreenUiFormat); - out.writeTypedObject(mKeyDerivationParameters, flags); - out.writeByteArray(mSecret); - } - - protected KeyStoreRecoveryMetadata(Parcel in) { - mUserSecretType = in.readInt(); - mLockScreenUiFormat = in.readInt(); - mKeyDerivationParameters = in.readTypedObject(KeyDerivationParameters.CREATOR); - mSecret = in.createByteArray(); - } - - @Override - public int describeContents() { - return 0; - } -} diff --git a/android/security/recoverablekeystore/RecoverableKeyStoreLoader.java b/android/security/recoverablekeystore/RecoverableKeyStoreLoader.java deleted file mode 100644 index 72a138a6..00000000 --- a/android/security/recoverablekeystore/RecoverableKeyStoreLoader.java +++ /dev/null @@ -1,467 +0,0 @@ -/* - * Copyright (C) 2017 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 android.security.recoverablekeystore; - -import android.annotation.NonNull; -import android.annotation.Nullable; -import android.app.PendingIntent; -import android.content.pm.PackageManager.NameNotFoundException; -import android.os.RemoteException; -import android.os.ServiceManager; -import android.os.ServiceSpecificException; -import android.os.UserHandle; -import android.security.KeyStore; -import android.util.AndroidException; - -import com.android.internal.widget.ILockSettings; - -import java.util.List; -import java.util.Map; - -/** - * A wrapper around KeyStore which lets key be exported to trusted hardware on server side and - * recovered later. - * - * @hide - */ -public class RecoverableKeyStoreLoader { - - public static final String PERMISSION_RECOVER_KEYSTORE = "android.permission.RECOVER_KEYSTORE"; - - public static final int NO_ERROR = KeyStore.NO_ERROR; - public static final int SYSTEM_ERROR = KeyStore.SYSTEM_ERROR; - public static final int UNINITIALIZED_RECOVERY_PUBLIC_KEY = 20; - public static final int NO_SNAPSHOT_PENDING_ERROR = 21; - - /** - * Rate limit is enforced to prevent using too many trusted remote devices, since each device - * can have its own number of user secret guesses allowed. - * - * @hide - */ - public static final int RATE_LIMIT_EXCEEDED = 21; - - /** Key has been successfully synced. */ - public static final int RECOVERY_STATUS_SYNCED = 0; - /** Waiting for recovery agent to sync the key. */ - public static final int RECOVERY_STATUS_SYNC_IN_PROGRESS = 1; - /** Recovery account is not available. */ - public static final int RECOVERY_STATUS_MISSING_ACCOUNT = 2; - /** Key cannot be synced. */ - public static final int RECOVERY_STATUS_PERMANENT_FAILURE = 3; - - private final ILockSettings mBinder; - - private RecoverableKeyStoreLoader(ILockSettings binder) { - mBinder = binder; - } - - /** @hide */ - public static RecoverableKeyStoreLoader getInstance() { - ILockSettings lockSettings = - ILockSettings.Stub.asInterface(ServiceManager.getService("lock_settings")); - return new RecoverableKeyStoreLoader(lockSettings); - } - - /** - * Exceptions returned by {@link RecoverableKeyStoreLoader}. - * - * @hide - */ - public static class RecoverableKeyStoreLoaderException extends AndroidException { - private int mErrorCode; - - /** - * Creates new {@link #RecoverableKeyStoreLoaderException} instance from the error code. - * - * @param errorCode - * @hide - */ - public static RecoverableKeyStoreLoaderException fromErrorCode(int errorCode) { - return new RecoverableKeyStoreLoaderException( - errorCode, getMessageFromErrorCode(errorCode)); - } - - /** - * Creates new {@link #RecoverableKeyStoreLoaderException} from {@link - * ServiceSpecificException}. - * - * @param e exception thrown on service side. - * @hide - */ - static RecoverableKeyStoreLoaderException fromServiceSpecificException( - ServiceSpecificException e) throws RecoverableKeyStoreLoaderException { - throw RecoverableKeyStoreLoaderException.fromErrorCode(e.errorCode); - } - - private RecoverableKeyStoreLoaderException(int errorCode, String message) { - super(message); - } - - /** Returns errorCode. */ - public int getErrorCode() { - return mErrorCode; - } - - /** @hide */ - private static String getMessageFromErrorCode(int errorCode) { - switch (errorCode) { - case NO_ERROR: - return "OK"; - case SYSTEM_ERROR: - return "System error"; - case UNINITIALIZED_RECOVERY_PUBLIC_KEY: - return "Recovery service is not initialized"; - case RATE_LIMIT_EXCEEDED: - return "Rate limit exceeded"; - default: - return String.valueOf("Unknown error code " + errorCode); - } - } - } - - /** - * Initializes key recovery service for the calling application. RecoverableKeyStoreLoader - * randomly chooses one of the keys from the list and keeps it to use for future key export - * operations. Collection of all keys in the list must be signed by the provided {@code - * rootCertificateAlias}, which must also be present in the list of root certificates - * preinstalled on the device. The random selection allows RecoverableKeyStoreLoader to select - * which of a set of remote recovery service devices will be used. - * - * <p>In addition, RecoverableKeyStoreLoader enforces a delay of three months between - * consecutive initialization attempts, to limit the ability of an attacker to often switch - * remote recovery devices and significantly increase number of recovery attempts. - * - * @param rootCertificateAlias alias of a root certificate preinstalled on the device - * @param signedPublicKeyList binary blob a list of X509 certificates and signature - * @throws RecoverableKeyStoreLoaderException if signature is invalid, or key rotation was rate - * limited. - * @hide - */ - public void initRecoveryService( - @NonNull String rootCertificateAlias, @NonNull byte[] signedPublicKeyList) - throws RecoverableKeyStoreLoaderException { - try { - mBinder.initRecoveryService( - rootCertificateAlias, signedPublicKeyList, UserHandle.getCallingUserId()); - } catch (RemoteException e) { - throw e.rethrowFromSystemServer(); - } catch (ServiceSpecificException e) { - throw RecoverableKeyStoreLoaderException.fromServiceSpecificException(e); - } - } - - /** - * Returns data necessary to store all recoverable keys for given account. Key material is - * encrypted with user secret and recovery public key. - * - * @param account specific to Recovery agent. - * @return Data necessary to recover keystore. - * @hide - */ - public @NonNull KeyStoreRecoveryData getRecoveryData(@NonNull byte[] account) - throws RecoverableKeyStoreLoaderException { - try { - KeyStoreRecoveryData recoveryData = - mBinder.getRecoveryData(account, UserHandle.getCallingUserId()); - return recoveryData; - } catch (RemoteException e) { - throw e.rethrowFromSystemServer(); - } catch (ServiceSpecificException e) { - throw RecoverableKeyStoreLoaderException.fromServiceSpecificException(e); - } - } - - /** - * Sets a listener which notifies recovery agent that new recovery snapshot is available. {@link - * #getRecoveryData} can be used to get the snapshot. Note that every recovery agent can have at - * most one registered listener at any time. - * - * @param intent triggered when new snapshot is available. Unregisters listener if the value is - * {@code null}. - * @hide - */ - public void setSnapshotCreatedPendingIntent(@Nullable PendingIntent intent) - throws RecoverableKeyStoreLoaderException { - try { - mBinder.setSnapshotCreatedPendingIntent(intent, UserHandle.getCallingUserId()); - } catch (RemoteException e) { - throw e.rethrowFromSystemServer(); - } catch (ServiceSpecificException e) { - throw RecoverableKeyStoreLoaderException.fromServiceSpecificException(e); - } - } - - /** - * Returns a map from recovery agent accounts to corresponding KeyStore recovery snapshot - * version. Version zero is used, if no snapshots were created for the account. - * - * @return Map from recovery agent accounts to snapshot versions. - * @see KeyStoreRecoveryData#getSnapshotVersion - * @hide - */ - public @NonNull Map<byte[], Integer> getRecoverySnapshotVersions() - throws RecoverableKeyStoreLoaderException { - try { - // IPC doesn't support generic Maps. - @SuppressWarnings("unchecked") - Map<byte[], Integer> result = - (Map<byte[], Integer>) - mBinder.getRecoverySnapshotVersions(UserHandle.getCallingUserId()); - return result; - } catch (RemoteException e) { - throw e.rethrowFromSystemServer(); - } catch (ServiceSpecificException e) { - throw RecoverableKeyStoreLoaderException.fromServiceSpecificException(e); - } - } - - /** - * Server parameters used to generate new recovery key blobs. This value will be included in - * {@code KeyStoreRecoveryData.getEncryptedRecoveryKeyBlob()}. The same value must be included - * in vaultParams {@link #startRecoverySession} - * - * @param serverParameters included in recovery key blob. - * @see #getRecoveryData - * @throws RecoverableKeyStoreLoaderException If parameters rotation is rate limited. - * @hide - */ - public void setServerParameters(long serverParameters) - throws RecoverableKeyStoreLoaderException { - try { - mBinder.setServerParameters(serverParameters, UserHandle.getCallingUserId()); - } catch (RemoteException e) { - throw e.rethrowFromSystemServer(); - } catch (ServiceSpecificException e) { - throw RecoverableKeyStoreLoaderException.fromServiceSpecificException(e); - } - } - - /** - * Updates recovery status for given keys. It is used to notify keystore that key was - * successfully stored on the server or there were an error. Application can check this value - * using {@code getRecoveyStatus}. - * - * @param packageName Application whose recoverable keys' statuses are to be updated. - * @param aliases List of application-specific key aliases. If the array is empty, updates the - * status for all existing recoverable keys. - * @param status Status specific to recovery agent. - */ - public void setRecoveryStatus( - @NonNull String packageName, @Nullable String[] aliases, int status) - throws NameNotFoundException, RecoverableKeyStoreLoaderException { - try { - mBinder.setRecoveryStatus(packageName, aliases, status, UserHandle.getCallingUserId()); - } catch (RemoteException e) { - throw e.rethrowFromSystemServer(); - } catch (ServiceSpecificException e) { - throw RecoverableKeyStoreLoaderException.fromServiceSpecificException(e); - } - } - - /** - * Returns a {@code Map} from Application's KeyStore key aliases to their recovery status. - * Negative status values are reserved for recovery agent specific codes. List of common codes: - * - * <ul> - * <li>{@link #RECOVERY_STATUS_SYNCED} - * <li>{@link #RECOVERY_STATUS_SYNC_IN_PROGRESS} - * <li>{@link #RECOVERY_STATUS_MISSING_ACCOUNT} - * <li>{@link #RECOVERY_STATUS_PERMANENT_FAILURE} - * </ul> - * - * @param packageName Application whose recoverable keys' statuses are to be retrieved. if - * {@code null} caller's package will be used. - * @return {@code Map} from KeyStore alias to recovery status. - * @see #setRecoveryStatus - * @hide - */ - public Map<String, Integer> getRecoveryStatus(@Nullable String packageName) - throws RecoverableKeyStoreLoaderException { - try { - // IPC doesn't support generic Maps. - @SuppressWarnings("unchecked") - Map<String, Integer> result = - (Map<String, Integer>) - mBinder.getRecoveryStatus(packageName, UserHandle.getCallingUserId()); - return result; - } catch (RemoteException e) { - throw e.rethrowFromSystemServer(); - } catch (ServiceSpecificException e) { - throw RecoverableKeyStoreLoaderException.fromServiceSpecificException(e); - } - } - - /** - * Specifies a set of secret types used for end-to-end keystore encryption. Knowing all of them - * is necessary to recover data. - * - * @param secretTypes {@link KeyStoreRecoveryMetadata#TYPE_LOCKSCREEN} or {@link - * KeyStoreRecoveryMetadata#TYPE_CUSTOM_PASSWORD} - */ - public void setRecoverySecretTypes( - @NonNull @KeyStoreRecoveryMetadata.UserSecretType int[] secretTypes) - throws RecoverableKeyStoreLoaderException { - try { - mBinder.setRecoverySecretTypes(secretTypes, UserHandle.getCallingUserId()); - } catch (RemoteException e) { - throw e.rethrowFromSystemServer(); - } catch (ServiceSpecificException e) { - throw RecoverableKeyStoreLoaderException.fromServiceSpecificException(e); - } - } - - /** - * Defines a set of secret types used for end-to-end keystore encryption. Knowing all of them is - * necessary to generate KeyStoreRecoveryData. - * - * @return list of recovery secret types - * @see KeyStoreRecoveryData - */ - public @NonNull @KeyStoreRecoveryMetadata.UserSecretType int[] getRecoverySecretTypes() - throws RecoverableKeyStoreLoaderException { - try { - return mBinder.getRecoverySecretTypes(UserHandle.getCallingUserId()); - } catch (RemoteException e) { - throw e.rethrowFromSystemServer(); - } catch (ServiceSpecificException e) { - throw RecoverableKeyStoreLoaderException.fromServiceSpecificException(e); - } - } - - /** - * Returns a list of recovery secret types, necessary to create a pending recovery snapshot. - * When user enters a secret of a pending type {@link #recoverySecretAvailable} should be - * called. - * - * @return list of recovery secret types - */ - public @NonNull @KeyStoreRecoveryMetadata.UserSecretType int[] getPendingRecoverySecretTypes() - throws RecoverableKeyStoreLoaderException { - try { - return mBinder.getPendingRecoverySecretTypes(UserHandle.getCallingUserId()); - } catch (RemoteException e) { - throw e.rethrowFromSystemServer(); - } catch (ServiceSpecificException e) { - throw RecoverableKeyStoreLoaderException.fromServiceSpecificException(e); - } - } - - /** - * Method notifies KeyStore that a user-generated secret is available. This method generates a - * symmetric session key which a trusted remote device can use to return a recovery key. Caller - * should use {@link KeyStoreRecoveryMetadata#clearSecret} to override the secret value in - * memory. - * - * @param recoverySecret user generated secret together with parameters necessary to regenerate - * it on a new device. - */ - public void recoverySecretAvailable(@NonNull KeyStoreRecoveryMetadata recoverySecret) - throws RecoverableKeyStoreLoaderException { - try { - mBinder.recoverySecretAvailable(recoverySecret, UserHandle.getCallingUserId()); - } catch (RemoteException e) { - throw e.rethrowFromSystemServer(); - } catch (ServiceSpecificException e) { - throw RecoverableKeyStoreLoaderException.fromServiceSpecificException(e); - } - } - - /** - * Initializes recovery session and returns a blob with proof of recovery secrets possession. - * The method generates symmetric key for a session, which trusted remote device can use to - * return recovery key. - * - * @param sessionId ID for recovery session. - * @param verifierPublicKey Certificate with Public key used to create the recovery blob on the - * source device. Keystore will verify the certificate using root of trust. - * @param vaultParams Must match the parameters in the corresponding field in the recovery blob. - * Used to limit number of guesses. - * @param vaultChallenge Data passed from server for this recovery session and used to prevent - * replay attacks - * @param secrets Secrets provided by user, the method only uses type and secret fields. - * @return Binary blob with recovery claim. It is encrypted with verifierPublicKey and contains - * a proof of user secrets, session symmetric key and parameters necessary to identify the - * counter with the number of failed recovery attempts. - */ - public @NonNull byte[] startRecoverySession( - @NonNull String sessionId, - @NonNull byte[] verifierPublicKey, - @NonNull byte[] vaultParams, - @NonNull byte[] vaultChallenge, - @NonNull List<KeyStoreRecoveryMetadata> secrets) - throws RecoverableKeyStoreLoaderException { - try { - byte[] recoveryClaim = - mBinder.startRecoverySession( - sessionId, - verifierPublicKey, - vaultParams, - vaultChallenge, - secrets, - UserHandle.getCallingUserId()); - return recoveryClaim; - } catch (RemoteException e) { - throw e.rethrowFromSystemServer(); - } catch (ServiceSpecificException e) { - throw RecoverableKeyStoreLoaderException.fromServiceSpecificException(e); - } - } - - /** - * Imports keys. - * - * @param sessionId Id for recovery session, same as in - * {@link #startRecoverySession(String, byte[], byte[], byte[], List)} on}. - * @param recoveryKeyBlob Recovery blob encrypted by symmetric key generated for this session. - * @param applicationKeys Application keys. Key material can be decrypted using recoveryKeyBlob - * and session. KeyStore only uses package names from the application info in {@link - * KeyEntryRecoveryData}. Caller is responsibility to perform certificates check. - * @return Map from alias to raw key material. - */ - public Map<String, byte[]> recoverKeys( - @NonNull String sessionId, - @NonNull byte[] recoveryKeyBlob, - @NonNull List<KeyEntryRecoveryData> applicationKeys) - throws RecoverableKeyStoreLoaderException { - try { - return (Map<String, byte[]>) mBinder.recoverKeys( - sessionId, recoveryKeyBlob, applicationKeys, UserHandle.getCallingUserId()); - } catch (RemoteException e) { - throw e.rethrowFromSystemServer(); - } catch (ServiceSpecificException e) { - throw RecoverableKeyStoreLoaderException.fromServiceSpecificException(e); - } - } - - /** - * Generates a key called {@code alias} and loads it into the recoverable key store. Returns the - * raw material of the key. - * - * @throws RecoverableKeyStoreLoaderException if an error occurred generating and storing the - * key. - */ - public byte[] generateAndStoreKey(String alias) throws RecoverableKeyStoreLoaderException { - try { - return mBinder.generateAndStoreKey(alias); - } catch (RemoteException e) { - throw e.rethrowFromSystemServer(); - } catch (ServiceSpecificException e) { - throw RecoverableKeyStoreLoaderException.fromServiceSpecificException(e); - } - } -} |