diff options
author | Treehugger Robot <treehugger-gerrit@google.com> | 2020-11-16 22:51:03 +0000 |
---|---|---|
committer | Gerrit Code Review <noreply-gerritcodereview@google.com> | 2020-11-16 22:51:03 +0000 |
commit | aeb15e8592b9a74937afbb3fa232d2ced5971f3c (patch) | |
tree | 1d60d433579554151324eb04664f6205ba426d53 | |
parent | febaec9f3e70de992cc6cc72789aa95c0b80a66f (diff) | |
parent | 4392c6977ce935a084ab30baeed511f170a606d5 (diff) |
Merge changes I9731d978,I9e325782,I441a4d4d,I86a85e48,I9268fd66, ...
* changes:
Keystore 2.0 SPI: Install legacy Keystore provider as AndroidKeyStoreLegacy
Keystore 2.0 SPI: Zygote install Keystore2 provider conditionally
Keystore 2.0 SPI: Evolve the generator SPI.
Keystore 2.0 SPI: Evolve Factory SPI
Keystore 2.0 SPI: AndroidKeyStoreProvider loads keys from Keystore 2.0
Keystore 2.0 SPI: Evolve the Crypto SPI.
Keystore 2.0 SPI: KeyParameter utilities.
Keystore 2.0 SPI: Update the chunked streamer.
Keystore 2.0 SPI: KeyStoreCryptoOperationUtils
Keystore 2.0 SPI: KeyStoreKeys adopt Keystore 2.0
Keystore 2.0: Shim around the basic functionality of Keystore 2.0
Keystore 2.0 SPI: Duplicate Keystore SPI to android.security.keystore2 package
39 files changed, 9232 insertions, 7 deletions
diff --git a/api/current.txt b/api/current.txt index 69f76451a5c4..6632db3d7acf 100644 --- a/api/current.txt +++ b/api/current.txt @@ -42742,6 +42742,12 @@ package android.security.identity { package android.security.keystore { + public class BackendBusyException extends java.security.ProviderException { + ctor public BackendBusyException(); + ctor public BackendBusyException(@NonNull String); + ctor public BackendBusyException(@NonNull String, @NonNull Throwable); + } + public class KeyExpiredException extends java.security.InvalidKeyException { ctor public KeyExpiredException(); ctor public KeyExpiredException(String); diff --git a/core/api/current.txt b/core/api/current.txt index 055f909077f7..02652b2c103c 100644 --- a/core/api/current.txt +++ b/core/api/current.txt @@ -40910,6 +40910,12 @@ package android.security.identity { package android.security.keystore { + public class BackendBusyException extends java.security.ProviderException { + ctor public BackendBusyException(); + ctor public BackendBusyException(@NonNull String); + ctor public BackendBusyException(@NonNull String, @NonNull Throwable); + } + public class KeyExpiredException extends java.security.InvalidKeyException { ctor public KeyExpiredException(); ctor public KeyExpiredException(String); diff --git a/core/java/com/android/internal/os/ZygoteInit.java b/core/java/com/android/internal/os/ZygoteInit.java index aa37334b2c54..6a67670d8160 100644 --- a/core/java/com/android/internal/os/ZygoteInit.java +++ b/core/java/com/android/internal/os/ZygoteInit.java @@ -73,6 +73,7 @@ import java.io.InputStream; import java.io.InputStreamReader; import java.security.Provider; import java.security.Security; +import java.util.Optional; /** * Startup class for the zygote process. @@ -225,7 +226,17 @@ public class ZygoteInit { // AndroidKeyStoreProvider.install() manipulates the list of JCA providers to insert // preferred providers. Note this is not done via security.properties as the JCA providers // are not on the classpath in the case of, for example, raw dalvikvm runtimes. - AndroidKeyStoreProvider.install(); + // TODO b/171305684 This code is used to conditionally enable the installation of the + // Keystore 2.0 provider to enable teams adjusting to Keystore 2.0 at their own + // pace. This code will be removed when all calling code was adjusted to + // Keystore 2.0. + Optional<Boolean> keystore2_enabled = + android.sysprop.Keystore2Properties.keystore2_enabled(); + if (keystore2_enabled.isPresent() && keystore2_enabled.get()) { + android.security.keystore2.AndroidKeyStoreProvider.install(); + } else { + AndroidKeyStoreProvider.install(); + } Log.i(TAG, "Installed AndroidKeyStoreProvider in " + (SystemClock.uptimeMillis() - startTime) + "ms."); Trace.traceEnd(Trace.TRACE_TAG_DALVIK); diff --git a/keystore/java/android/security/CheckedRemoteRequest.java b/keystore/java/android/security/CheckedRemoteRequest.java new file mode 100644 index 000000000000..b6e7c1fa61b9 --- /dev/null +++ b/keystore/java/android/security/CheckedRemoteRequest.java @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2020 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; + +import android.os.RemoteException; + +/** + * This is a Producer of {@code R} that is expected to throw a {@link RemoteException}. + * + * It is used by Keystore2 service wrappers to handle and convert {@link RemoteException} + * and {@link android.os.ServiceSpecificException} into {@link KeyStoreException}. + * + * @hide + * @param <R> + */ +@FunctionalInterface +interface CheckedRemoteRequest<R> { + R execute() throws RemoteException; +} diff --git a/keystore/java/android/security/KeyStore2.java b/keystore/java/android/security/KeyStore2.java new file mode 100644 index 000000000000..92d87aa0fed6 --- /dev/null +++ b/keystore/java/android/security/KeyStore2.java @@ -0,0 +1,277 @@ +/* + * Copyright (C) 2020 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; + +import android.annotation.NonNull; +import android.compat.annotation.ChangeId; +import android.compat.annotation.EnabledAfter; +import android.os.Build; +import android.os.RemoteException; +import android.os.ServiceManager; +import android.os.ServiceSpecificException; +import android.system.keystore2.IKeystoreService; +import android.system.keystore2.KeyDescriptor; +import android.system.keystore2.KeyEntryResponse; +import android.system.keystore2.ResponseCode; +import android.util.Log; + +import java.util.Calendar; + +/** + * @hide This should not be made public in its present form because it + * assumes that private and secret key bytes are available and would + * preclude the use of hardware crypto. + */ +public class KeyStore2 { + private static final String TAG = "KeyStore"; + + private static final int RECOVERY_GRACE_PERIOD_MS = 50; + + /** + * Keystore operation creation may fail + * + * Keystore used to work under the assumption that the creation of cryptographic operations + * always succeeds. However, the KeyMint backend has only a limited number of operation slots. + * In order to keep up the appearance of "infinite" operation slots, the Keystore daemon + * would prune least recently used operations if there is no available operation slot. + * As a result, good operations could be terminated prematurely. + * + * This opens AndroidKeystore up to denial-of-service and unintended livelock situations. + * E.g.: if multiple apps wake up at the same time, e.g., due to power management optimizations, + * and attempt to perform crypto operations, they start terminating each others operations + * without making any progress. + * + * To break out of livelocks and to discourage DoS attempts we have changed the pruning + * strategy such that it prefers clients that use few operation slots and only briefly. + * As a result we can, almost, guarantee that single operations that don't linger inactive + * for more than 5 seconds will conclude unhampered by the pruning strategy. "Almost", + * because there are operations related to file system encryption that can prune even + * these operations, but those are extremely rare. + * + * As a side effect of this new pruning strategy operation creation can now fail if the + * client has a lower pruning power than all of the existing operations. + * + * Pruning strategy + * + * To find a suitable candidate we compute the malus for the caller and each existing + * operation. The malus is the inverse of the pruning power (caller) or pruning + * resistance (existing operation). For the caller to be able to prune an operation it must + * find an operation with a malus higher than its own. + * + * For more detail on the pruning strategy consult the implementation at + * https://android.googlesource.com/platform/system/security/+/refs/heads/master/keystore2/src/operation.rs + * + * For older SDK version, KeyStore2 will poll the Keystore daemon for a free operation + * slot. So to applications, targeting earlier SDK versions, it will still look like cipher and + * signature object initialization always succeeds, however, it may take longer to get an + * operation. + * + * All SDK version benefit from fairer operation slot scheduling and a better chance to + * successfully conclude an operation. + */ + @ChangeId + @EnabledAfter(targetSdkVersion = Build.VERSION_CODES.R) + static final long KEYSTORE_OPERATION_CREATION_MAY_FAIL = 169897160L; + + // Never use mBinder directly, use KeyStore2.getService() instead or better yet + // handleRemoteExceptionWithRetry which retries connecting to Keystore once in case + // of a remote exception. + private IKeystoreService mBinder; + + + @FunctionalInterface + interface CheckedRemoteRequest<R> { + R execute(IKeystoreService service) throws RemoteException; + } + + private <R> R handleRemoteExceptionWithRetry(@NonNull CheckedRemoteRequest<R> request) + throws KeyStoreException { + IKeystoreService service = getService(false /* retryLookup */); + boolean firstTry = true; + while (true) { + try { + return request.execute(service); + } catch (ServiceSpecificException e) { + Log.e(TAG, "KeyStore exception", e); + throw new KeyStoreException(e.errorCode, ""); + } catch (RemoteException e) { + if (firstTry) { + Log.w(TAG, "Looks like we may have lost connection to the Keystore " + + "daemon."); + Log.w(TAG, "Retrying after giving Keystore " + + RECOVERY_GRACE_PERIOD_MS + "ms to recover."); + interruptedPreservingSleep(RECOVERY_GRACE_PERIOD_MS); + service = getService(true /* retry Lookup */); + firstTry = false; + } else { + Log.e(TAG, "Cannot connect to Keystore daemon.", e); + throw new KeyStoreException(ResponseCode.SYSTEM_ERROR, ""); + } + } + } + } + + + private KeyStore2() { + mBinder = null; + } + + public static KeyStore2 getInstance() { + return new KeyStore2(); + } + + private synchronized IKeystoreService getService(boolean retryLookup) { + if (mBinder == null || retryLookup) { + mBinder = IKeystoreService.Stub.asInterface(ServiceManager + .getService("android.system.keystore2")); + } + return mBinder; + } + + void delete(KeyDescriptor descriptor) throws KeyStoreException { + handleRemoteExceptionWithRetry((service) -> { + service.deleteKey(descriptor); + return 0; + }); + } + + /** + * List all entries in the keystore for in the given namespace. + */ + public KeyDescriptor[] list(int domain, long namespace) throws KeyStoreException { + return handleRemoteExceptionWithRetry((service) -> service.listEntries(domain, namespace)); + } + + /** + * Create a grant that allows the grantee identified by {@code granteeUid} to use + * the key specified by {@code descriptor} withint the restrictions given by + * {@code accessVectore}. + * @see IKeystoreService#grant(KeyDescriptor, int, int) for more details. + * @param descriptor + * @param granteeUid + * @param accessVector + * @return + * @throws KeyStoreException + * @hide + */ + public KeyDescriptor grant(KeyDescriptor descriptor, int granteeUid, int accessVector) + throws KeyStoreException { + return handleRemoteExceptionWithRetry( + (service) -> service.grant(descriptor, granteeUid, accessVector) + ); + } + + /** + * Destroys a grant. + * @see IKeystoreService#ungrant(KeyDescriptor, int) for more details. + * @param descriptor + * @param granteeUid + * @throws KeyStoreException + * @hide + */ + public void ungrant(KeyDescriptor descriptor, int granteeUid) + throws KeyStoreException { + handleRemoteExceptionWithRetry((service) -> { + service.ungrant(descriptor, granteeUid); + return 0; + }); + } + + /** + * Retrieves a key entry from the keystore backend. + * @see IKeystoreService#getKeyEntry(KeyDescriptor) for more details. + * @param descriptor + * @return + * @throws KeyStoreException + * @hide + */ + public KeyEntryResponse getKeyEntry(@NonNull KeyDescriptor descriptor) + throws KeyStoreException { + return handleRemoteExceptionWithRetry((service) -> service.getKeyEntry(descriptor)); + } + + /** + * Get the security level specific keystore interface from the keystore daemon. + * @see IKeystoreService#getSecurityLevel(int) for more details. + * @param securityLevel + * @return + * @throws KeyStoreException + * @hide + */ + public KeyStoreSecurityLevel getSecurityLevel(int securityLevel) + throws KeyStoreException { + return handleRemoteExceptionWithRetry((service) -> + new KeyStoreSecurityLevel( + service.getSecurityLevel(securityLevel) + ) + ); + } + + /** + * Update the subcomponents of a key entry designated by the key descriptor. + * @see IKeystoreService#updateSubcomponent(KeyDescriptor, byte[], byte[]) for more details. + * @param key + * @param publicCert + * @param publicCertChain + * @throws KeyStoreException + * @hide + */ + public void updateSubcomponents(@NonNull KeyDescriptor key, byte[] publicCert, + byte[] publicCertChain) throws KeyStoreException { + handleRemoteExceptionWithRetry((service) -> { + service.updateSubcomponent(key, publicCert, publicCertChain); + return 0; + }); + } + + /** + * Delete the key designed by the key descriptor. + * @see IKeystoreService#deleteKey(KeyDescriptor) for more details. + * @param descriptor + * @throws KeyStoreException + * @hide + */ + public void deleteKey(@NonNull KeyDescriptor descriptor) + throws KeyStoreException { + handleRemoteExceptionWithRetry((service) -> { + service.deleteKey(descriptor); + return 0; + }); + } + + protected static void interruptedPreservingSleep(long millis) { + boolean wasInterrupted = false; + Calendar calendar = Calendar.getInstance(); + long target = calendar.getTimeInMillis() + millis; + while (true) { + try { + Thread.sleep(target - calendar.getTimeInMillis()); + break; + } catch (InterruptedException e) { + wasInterrupted = true; + } catch (IllegalArgumentException e) { + // This means that the argument to sleep was negative. + // So we are done sleeping. + break; + } + } + if (wasInterrupted) { + Thread.currentThread().interrupt(); + } + } + +} diff --git a/keystore/java/android/security/KeyStoreOperation.java b/keystore/java/android/security/KeyStoreOperation.java new file mode 100644 index 000000000000..9af15a5f4a16 --- /dev/null +++ b/keystore/java/android/security/KeyStoreOperation.java @@ -0,0 +1,141 @@ +/* + * Copyright (C) 2020 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; + +import android.annotation.NonNull; +import android.os.RemoteException; +import android.os.ServiceSpecificException; +import android.security.keymaster.KeymasterDefs; +import android.system.keystore2.IKeystoreOperation; +import android.system.keystore2.KeyParameter; +import android.system.keystore2.ResponseCode; +import android.util.Log; + +/** + * @hide + */ +public class KeyStoreOperation { + static final String TAG = "KeyStoreOperation"; + private final IKeystoreOperation mOperation; + private final Long mChallenge; + private final KeyParameter[] mParameters; + + public KeyStoreOperation( + @NonNull IKeystoreOperation operation, + Long challenge, + KeyParameter[] parameters + ) { + this.mOperation = operation; + this.mChallenge = challenge; + this.mParameters = parameters; + } + + /** + * Gets the challenge associated with this operation. + * @return null if the operation does not required authorization. A 64bit operation + * challenge otherwise. + */ + public Long getChallenge() { + return mChallenge; + } + + /** + * Gets the parameters associated with this operation. + * @return + */ + public KeyParameter[] getParameters() { + return mParameters; + } + + private <R> R handleExceptions(@NonNull CheckedRemoteRequest<R> request) + throws KeyStoreException { + try { + return request.execute(); + } catch (ServiceSpecificException e) { + switch(e.errorCode) { + case ResponseCode.OPERATION_BUSY: { + throw new IllegalThreadStateException( + "Cannot update the same operation concurrently." + ); + } + default: + // TODO Human readable string. Use something like KeyStore.getKeyStoreException + throw new KeyStoreException(e.errorCode, ""); + } + } catch (RemoteException e) { + // Log exception and report invalid operation handle. + // This should prompt the caller drop the reference to this operation and retry. + Log.e( + TAG, + "Remote exception while advancing a KeyStoreOperation.", + e + ); + throw new KeyStoreException(KeymasterDefs.KM_ERROR_INVALID_OPERATION_HANDLE, ""); + } + } + + /** + * Updates the Keystore operation represented by this object with more associated data. + * @see IKeystoreOperation#updateAad(byte[]) for more details. + * @param input + * @throws KeyStoreException + */ + public void updateAad(@NonNull byte[] input) throws KeyStoreException { + handleExceptions(() -> { + mOperation.updateAad(input); + return 0; + }); + } + + /** + * Updates the Keystore operation represented by this object. + * @see IKeystoreOperation#update(byte[]) for more details. + * @param input + * @return + * @throws KeyStoreException + * @hide + */ + public byte[] update(@NonNull byte[] input) throws KeyStoreException { + return handleExceptions(() -> mOperation.update(input)); + } + + /** + * Finalizes the Keystore operation represented by this object. + * @see IKeystoreOperation#finish(byte[], byte[]) for more details. + * @param input + * @param signature + * @return + * @throws KeyStoreException + * @hide + */ + public byte[] finish(byte[] input, byte[] signature) throws KeyStoreException { + return handleExceptions(() -> mOperation.finish(input, signature)); + } + + /** + * Aborts the Keystore operation represented by this object. + * @see IKeystoreOperation#abort() for more details. + * @throws KeyStoreException + * @hide + */ + public void abort() throws KeyStoreException { + handleExceptions(() -> { + mOperation.abort(); + return 0; + }); + } +} diff --git a/keystore/java/android/security/KeyStoreSecurityLevel.java b/keystore/java/android/security/KeyStoreSecurityLevel.java new file mode 100644 index 000000000000..9d3b62278ba0 --- /dev/null +++ b/keystore/java/android/security/KeyStoreSecurityLevel.java @@ -0,0 +1,217 @@ +/* + * Copyright (C) 2020 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; + +import android.annotation.NonNull; +import android.app.compat.CompatChanges; +import android.os.RemoteException; +import android.os.ServiceSpecificException; +import android.security.keystore.BackendBusyException; +import android.security.keystore.KeyStoreConnectException; +import android.system.keystore2.AuthenticatorSpec; +import android.system.keystore2.CreateOperationResponse; +import android.system.keystore2.IKeystoreSecurityLevel; +import android.system.keystore2.KeyDescriptor; +import android.system.keystore2.KeyMetadata; +import android.system.keystore2.KeyParameter; +import android.system.keystore2.ResponseCode; +import android.util.Log; + +import java.util.Calendar; +import java.util.Collection; + +/** + * This is a shim around the security level specific interface of Keystore 2.0. Services with + * this interface are instantiated per KeyMint backend, each having there own security level. + * Thus this object representation of a security level. + * @hide + */ +public class KeyStoreSecurityLevel { + private static final String TAG = "KeyStoreSecurityLevel"; + private final IKeystoreSecurityLevel mSecurityLevel; + + public KeyStoreSecurityLevel(IKeystoreSecurityLevel securityLevel) { + this.mSecurityLevel = securityLevel; + } + + private <R> R handleExceptions(CheckedRemoteRequest<R> request) throws KeyStoreException { + try { + return request.execute(); + } catch (ServiceSpecificException e) { + throw new KeyStoreException(e.errorCode, ""); + } catch (RemoteException e) { + // Log exception and report invalid operation handle. + // This should prompt the caller drop the reference to this operation and retry. + Log.e(TAG, "Could not connect to Keystore.", e); + throw new KeyStoreException(ResponseCode.SYSTEM_ERROR, ""); + } + } + + /** + * Creates a new keystore operation. + * @see IKeystoreSecurityLevel#createOperation(KeyDescriptor, KeyParameter[], boolean) for more + * details. + * @param keyDescriptor + * @param args + * @return + * @throws KeyStoreException + * @hide + */ + public KeyStoreOperation createOperation(@NonNull KeyDescriptor keyDescriptor, + Collection<KeyParameter> args) throws KeyStoreException { + while (true) { + try { + CreateOperationResponse createOperationResponse = + mSecurityLevel.createOperation( + keyDescriptor, + args.toArray(new KeyParameter[args.size()]), + false /* forced */ + ); + Long challenge = null; + if (createOperationResponse.operationChallenge != null) { + challenge = createOperationResponse.operationChallenge.challenge; + } + KeyParameter[] parameters = null; + if (createOperationResponse.parameters != null) { + parameters = createOperationResponse.parameters.keyParameter; + } + return new KeyStoreOperation( + createOperationResponse.iOperation, + challenge, + parameters); + } catch (ServiceSpecificException e) { + switch (e.errorCode) { + case ResponseCode.BACKEND_BUSY: { + if (CompatChanges.isChangeEnabled( + KeyStore2.KEYSTORE_OPERATION_CREATION_MAY_FAIL)) { + // Starting with Android S we inform the caller about the + // backend being busy. + throw new BackendBusyException(); + } else { + // Before Android S operation creation must always succeed. So we + // just have to retry. We do so with a randomized back-off between + // 50 and 250ms. + // It is a little awkward that we cannot break out of this loop + // by interrupting this thread. But that is the expected behavior. + // There is some comfort in the fact that interrupting a thread + // also does not unblock a thread waiting for a binder transaction. + interruptedPreservingSleep((long) (Math.random() * 200 + 50)); + } + break; + } + default: + throw new KeyStoreException(e.errorCode, ""); + } + } catch (RemoteException e) { + Log.w(TAG, "Cannot connect to keystore", e); + throw new KeyStoreConnectException(); + } + } + } + + /** + * Generates a new key in Keystore. + * @see IKeystoreSecurityLevel#generateKey(KeyDescriptor, KeyDescriptor, KeyParameter[], int, + * byte[]) for more details. + * @param descriptor + * @param attestationKey + * @param args + * @param flags + * @param entropy + * @return + * @throws KeyStoreException + * @hide + */ + public KeyMetadata generateKey(@NonNull KeyDescriptor descriptor, KeyDescriptor attestationKey, + Collection<KeyParameter> args, int flags, byte[] entropy) + throws KeyStoreException { + return handleExceptions(() -> mSecurityLevel.generateKey( + descriptor, attestationKey, args.toArray(new KeyParameter[args.size()]), + flags, entropy)); + } + + /** + * Imports a key into Keystore. + * @see IKeystoreSecurityLevel#importKey(KeyDescriptor, KeyDescriptor, KeyParameter[], int, + * byte[]) for more details. + * @param descriptor + * @param attestationKey + * @param args + * @param flags + * @param keyData + * @return + * @throws KeyStoreException + * @hide + */ + public KeyMetadata importKey(KeyDescriptor descriptor, KeyDescriptor attestationKey, + Collection<KeyParameter> args, int flags, byte[] keyData) + throws KeyStoreException { + return handleExceptions(() -> mSecurityLevel.importKey(descriptor, attestationKey, + args.toArray(new KeyParameter[args.size()]), flags, keyData)); + } + + /** + * Imports a wrapped key into Keystore. + * @see IKeystoreSecurityLevel#importWrappedKey(KeyDescriptor, KeyDescriptor, byte[], + * KeyParameter[], AuthenticatorSpec[]) for more details. + * @param wrappedKeyDescriptor + * @param wrappingKeyDescriptor + * @param wrappedKey + * @param maskingKey + * @param args + * @param authenticatorSpecs + * @return + * @throws KeyStoreException + * @hide + */ + public KeyMetadata importWrappedKey(@NonNull KeyDescriptor wrappedKeyDescriptor, + @NonNull KeyDescriptor wrappingKeyDescriptor, + @NonNull byte[] wrappedKey, byte[] maskingKey, + Collection<KeyParameter> args, @NonNull AuthenticatorSpec[] authenticatorSpecs) + throws KeyStoreException { + KeyDescriptor keyDescriptor = new KeyDescriptor(); + keyDescriptor.alias = wrappedKeyDescriptor.alias; + keyDescriptor.nspace = wrappedKeyDescriptor.nspace; + keyDescriptor.blob = wrappedKey; + keyDescriptor.domain = wrappedKeyDescriptor.domain; + + return handleExceptions(() -> mSecurityLevel.importWrappedKey(wrappedKeyDescriptor, + wrappingKeyDescriptor, maskingKey, + args.toArray(new KeyParameter[args.size()]), authenticatorSpecs)); + } + + protected static void interruptedPreservingSleep(long millis) { + boolean wasInterrupted = false; + Calendar calendar = Calendar.getInstance(); + long target = calendar.getTimeInMillis() + millis; + while (true) { + try { + Thread.sleep(target - calendar.getTimeInMillis()); + break; + } catch (InterruptedException e) { + wasInterrupted = true; + } catch (IllegalArgumentException e) { + // This means that the argument to sleep was negative. + // So we are done sleeping. + break; + } + } + if (wasInterrupted) { + Thread.currentThread().interrupt(); + } + } +} diff --git a/keystore/java/android/security/keystore/AndroidKeyStoreBCWorkaroundProvider.java b/keystore/java/android/security/keystore/AndroidKeyStoreBCWorkaroundProvider.java index 624321cbf5ea..5730234184ab 100644 --- a/keystore/java/android/security/keystore/AndroidKeyStoreBCWorkaroundProvider.java +++ b/keystore/java/android/security/keystore/AndroidKeyStoreBCWorkaroundProvider.java @@ -34,7 +34,7 @@ import java.security.Provider; * * @hide */ -class AndroidKeyStoreBCWorkaroundProvider extends Provider { +public class AndroidKeyStoreBCWorkaroundProvider extends Provider { // IMPLEMENTATION NOTE: Class names are hard-coded in this provider to avoid loading these // classes when this provider is instantiated and installed early on during each app's @@ -50,8 +50,14 @@ class AndroidKeyStoreBCWorkaroundProvider extends Provider { private static final String DESEDE_SYSTEM_PROPERTY = "ro.hardware.keystore_desede"; - AndroidKeyStoreBCWorkaroundProvider() { - super("AndroidKeyStoreBCWorkaround", + /** @hide */ + public AndroidKeyStoreBCWorkaroundProvider() { + this("AndroidKeyStoreBCWorkaround"); + } + + /** @hide **/ + public AndroidKeyStoreBCWorkaroundProvider(String providerName) { + super(providerName, 1.0, "Android KeyStore security provider to work around Bouncy Castle"); diff --git a/keystore/java/android/security/keystore/AndroidKeyStoreProvider.java b/keystore/java/android/security/keystore/AndroidKeyStoreProvider.java index d1b4464c1aed..3ac9d68d5a9f 100644 --- a/keystore/java/android/security/keystore/AndroidKeyStoreProvider.java +++ b/keystore/java/android/security/keystore/AndroidKeyStoreProvider.java @@ -71,14 +71,20 @@ public class AndroidKeyStoreProvider extends Provider { private static final String DESEDE_SYSTEM_PROPERTY = "ro.hardware.keystore_desede"; - /** @hide **/ + /** @hide */ public AndroidKeyStoreProvider() { - super(PROVIDER_NAME, 1.0, "Android KeyStore security provider"); + this(PROVIDER_NAME); + } + + /** @hide **/ + public AndroidKeyStoreProvider(String providerName) { + super(providerName, 1.0, "Android KeyStore security provider"); boolean supports3DES = "true".equals(android.os.SystemProperties.get(DESEDE_SYSTEM_PROPERTY)); // java.security.KeyStore put("KeyStore.AndroidKeyStore", PACKAGE_NAME + ".AndroidKeyStoreSpi"); + put("alg.alias.KeyStore.AndroidKeyStoreLegacy", "AndroidKeyStore"); // java.security.KeyPairGenerator put("KeyPairGenerator.EC", PACKAGE_NAME + ".AndroidKeyStoreKeyPairGeneratorSpi$EC"); @@ -438,8 +444,12 @@ public class AndroidKeyStoreProvider extends Provider { @NonNull public static java.security.KeyStore getKeyStoreForUid(int uid) throws KeyStoreException, NoSuchProviderException { + String providerName = PROVIDER_NAME; + if (android.security.keystore2.AndroidKeyStoreProvider.isInstalled()) { + providerName = "AndroidKeyStoreLegacy"; + } java.security.KeyStore result = - java.security.KeyStore.getInstance("AndroidKeyStore", PROVIDER_NAME); + java.security.KeyStore.getInstance(providerName); try { result.load(new AndroidKeyStoreLoadStoreParameter(uid)); } catch (NoSuchAlgorithmException | CertificateException | IOException e) { diff --git a/keystore/java/android/security/keystore/BackendBusyException.java b/keystore/java/android/security/keystore/BackendBusyException.java new file mode 100644 index 000000000000..1a88469d7e54 --- /dev/null +++ b/keystore/java/android/security/keystore/BackendBusyException.java @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2020 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 java.security.ProviderException; + +/** + * Indicates a transient error that prevented a key operation from being created. + * Callers should try again with a back-off period of 10-30 milliseconds. + */ +public class BackendBusyException extends ProviderException { + + /** + * Constructs a new {@code BackendBusyException} without detail message and cause. + */ + public BackendBusyException() { + super("The keystore backend has no operation slots available. Retry later."); + } + + /** + * Constructs a new {@code BackendBusyException} with the provided detail message and + * no cause. + */ + public BackendBusyException(@NonNull String message) { + super(message); + } + + /** + * Constructs a new {@code BackendBusyException} with the provided detail message and + * cause. + */ + public BackendBusyException(@NonNull String message, @NonNull Throwable cause) { + super(message, cause); + } + +} diff --git a/keystore/java/android/security/keystore2/AndroidKeyStore3DESCipherSpi.java b/keystore/java/android/security/keystore2/AndroidKeyStore3DESCipherSpi.java new file mode 100644 index 000000000000..70713a47ad6d --- /dev/null +++ b/keystore/java/android/security/keystore2/AndroidKeyStore3DESCipherSpi.java @@ -0,0 +1,317 @@ +/* + * 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.keystore2; + +import android.annotation.NonNull; +import android.security.keymaster.KeymasterDefs; +import android.security.keystore.ArrayUtils; +import android.security.keystore.KeyProperties; +import android.system.keystore2.KeyParameter; + +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 java.util.List; + +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(@NonNull List<KeyParameter> parameters) { + 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."); + } + + parameters.add(KeyStore2ParameterUtils.makeEnum( + KeymasterDefs.KM_TAG_ALGORITHM, + KeymasterDefs.KM_ALGORITHM_3DES + )); + parameters.add(KeyStore2ParameterUtils.makeEnum( + KeymasterDefs.KM_TAG_BLOCK_MODE, + mKeymasterBlockMode + )); + parameters.add(KeyStore2ParameterUtils.makeEnum( + KeymasterDefs.KM_TAG_PADDING, + mKeymasterPadding + )); + + if (mIvRequired && (mIv != null)) { + parameters.add(KeyStore2ParameterUtils.makeBytes(KeymasterDefs.KM_TAG_NONCE, mIv)); + } + } + + @Override + protected void loadAlgorithmSpecificParametersFromBeginResult( + KeyParameter[] parameters) { + mIvHasBeenUsed = true; + + // NOTE: Keymaster doesn't always return an IV, even if it's used. + byte[] returnedIv = null; + if (parameters != null) { + for (KeyParameter p : parameters) { + if (p.tag == KeymasterDefs.KM_TAG_NONCE) { + returnedIv = p.blob; + break; + } + } + } + + 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/keystore/java/android/security/keystore2/AndroidKeyStoreAuthenticatedAESCipherSpi.java b/keystore/java/android/security/keystore2/AndroidKeyStoreAuthenticatedAESCipherSpi.java new file mode 100644 index 000000000000..dd094b7a5fd0 --- /dev/null +++ b/keystore/java/android/security/keystore2/AndroidKeyStoreAuthenticatedAESCipherSpi.java @@ -0,0 +1,439 @@ +/* + * Copyright (C) 2015 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.keystore2; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.security.KeyStoreException; +import android.security.KeyStoreOperation; +import android.security.keymaster.KeymasterDefs; +import android.security.keystore.ArrayUtils; +import android.security.keystore.KeyProperties; +import android.security.keystore2.KeyStoreCryptoOperationChunkedStreamer.Stream; +import android.system.keystore2.KeyParameter; + +import libcore.util.EmptyArray; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +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 java.util.List; + +import javax.crypto.CipherSpi; +import javax.crypto.spec.GCMParameterSpec; + +/** + * Base class for Android Keystore authenticated AES {@link CipherSpi} implementations. + * + * @hide + */ +abstract class AndroidKeyStoreAuthenticatedAESCipherSpi extends AndroidKeyStoreCipherSpiBase { + + abstract static class GCM extends AndroidKeyStoreAuthenticatedAESCipherSpi { + static final int MIN_SUPPORTED_TAG_LENGTH_BITS = 96; + private static final int MAX_SUPPORTED_TAG_LENGTH_BITS = 128; + private static final int DEFAULT_TAG_LENGTH_BITS = 128; + private static final int IV_LENGTH_BYTES = 12; + + private int mTagLengthBits = DEFAULT_TAG_LENGTH_BITS; + + GCM(int keymasterPadding) { + super(KeymasterDefs.KM_MODE_GCM, keymasterPadding); + } + + @Override + protected final void resetAll() { + mTagLengthBits = DEFAULT_TAG_LENGTH_BITS; + super.resetAll(); + } + + @Override + protected final void resetWhilePreservingInitState() { + super.resetWhilePreservingInitState(); + } + + @Override + protected final void initAlgorithmSpecificParameters() throws InvalidKeyException { + if (!isEncrypting()) { + throw new InvalidKeyException("IV required when decrypting" + + ". Use IvParameterSpec or AlgorithmParameters to provide it."); + } + } + + @Override + protected final void initAlgorithmSpecificParameters(AlgorithmParameterSpec params) + throws InvalidAlgorithmParameterException { + // IV is used + if (params == null) { + if (!isEncrypting()) { + // IV must be provided by the caller + throw new InvalidAlgorithmParameterException( + "GCMParameterSpec must be provided when decrypting"); + } + return; + } + if (!(params instanceof GCMParameterSpec)) { + throw new InvalidAlgorithmParameterException("Only GCMParameterSpec supported"); + } + GCMParameterSpec spec = (GCMParameterSpec) params; + byte[] iv = spec.getIV(); + if (iv == null) { + throw new InvalidAlgorithmParameterException("Null IV in GCMParameterSpec"); + } else if (iv.length != IV_LENGTH_BYTES) { + throw new InvalidAlgorithmParameterException("Unsupported IV length: " + + iv.length + " bytes. Only " + IV_LENGTH_BYTES + + " bytes long IV supported"); + } + int tagLengthBits = spec.getTLen(); + if ((tagLengthBits < MIN_SUPPORTED_TAG_LENGTH_BITS) + || (tagLengthBits > MAX_SUPPORTED_TAG_LENGTH_BITS) + || ((tagLengthBits % 8) != 0)) { + throw new InvalidAlgorithmParameterException( + "Unsupported tag length: " + tagLengthBits + " bits" + + ". Supported lengths: 96, 104, 112, 120, 128"); + } + setIv(iv); + mTagLengthBits = tagLengthBits; + } + + @Override + protected final void initAlgorithmSpecificParameters(AlgorithmParameters params) + throws InvalidAlgorithmParameterException { + if (params == null) { + if (!isEncrypting()) { + // IV must be provided by the caller + throw new InvalidAlgorithmParameterException("IV required when decrypting" + + ". Use GCMParameterSpec or GCM AlgorithmParameters to provide it."); + } + return; + } + + if (!"GCM".equalsIgnoreCase(params.getAlgorithm())) { + throw new InvalidAlgorithmParameterException( + "Unsupported AlgorithmParameters algorithm: " + params.getAlgorithm() + + ". Supported: GCM"); + } + + GCMParameterSpec spec; + try { + spec = params.getParameterSpec(GCMParameterSpec.class); + } catch (InvalidParameterSpecException e) { + if (!isEncrypting()) { + // IV must be provided by the caller + throw new InvalidAlgorithmParameterException("IV and tag length required when" + + " decrypting, but not found in parameters: " + params, e); + } + setIv(null); + return; + } + initAlgorithmSpecificParameters(spec); + } + + @Nullable + @Override + protected final AlgorithmParameters engineGetParameters() { + byte[] iv = getIv(); + if ((iv != null) && (iv.length > 0)) { + try { + AlgorithmParameters params = AlgorithmParameters.getInstance("GCM"); + params.init(new GCMParameterSpec(mTagLengthBits, iv)); + return params; + } catch (NoSuchAlgorithmException e) { + throw new ProviderException( + "Failed to obtain GCM AlgorithmParameters", e); + } catch (InvalidParameterSpecException e) { + throw new ProviderException( + "Failed to initialize GCM AlgorithmParameters", e); + } + } + return null; + } + + @NonNull + @Override + protected KeyStoreCryptoOperationStreamer createMainDataStreamer( + KeyStoreOperation operation) { + KeyStoreCryptoOperationStreamer streamer = new KeyStoreCryptoOperationChunkedStreamer( + new KeyStoreCryptoOperationChunkedStreamer.MainDataStream( + operation), 0); + if (isEncrypting()) { + return streamer; + } else { + // When decrypting, to avoid leaking unauthenticated plaintext, do not return any + // plaintext before ciphertext is authenticated by KeyStore.finish. + return new BufferAllOutputUntilDoFinalStreamer(streamer); + } + } + + @NonNull + @Override + protected final KeyStoreCryptoOperationStreamer createAdditionalAuthenticationDataStreamer( + KeyStoreOperation operation) { + return new KeyStoreCryptoOperationChunkedStreamer( + new AdditionalAuthenticationDataStream(operation), 0); + } + + @Override + protected final int getAdditionalEntropyAmountForBegin() { + if ((getIv() == null) && (isEncrypting())) { + // IV will need to be generated + return IV_LENGTH_BYTES; + } + + return 0; + } + + @Override + protected final int getAdditionalEntropyAmountForFinish() { + return 0; + } + + @Override + protected final void addAlgorithmSpecificParametersToBegin( + @NonNull List<KeyParameter> parameters) { + super.addAlgorithmSpecificParametersToBegin(parameters); + parameters.add(KeyStore2ParameterUtils.makeInt( + KeymasterDefs.KM_TAG_MAC_LENGTH, + mTagLengthBits + )); + } + + protected final int getTagLengthBits() { + return mTagLengthBits; + } + + public static final class NoPadding extends GCM { + public NoPadding() { + super(KeymasterDefs.KM_PAD_NONE); + } + + @Override + protected final int engineGetOutputSize(int inputLen) { + int tagLengthBytes = (getTagLengthBits() + 7) / 8; + long result; + if (isEncrypting()) { + result = getConsumedInputSizeBytes() - getProducedOutputSizeBytes() + inputLen + + tagLengthBytes; + } else { + result = getConsumedInputSizeBytes() - getProducedOutputSizeBytes() + inputLen + - tagLengthBytes; + } + if (result < 0) { + return 0; + } else if (result > Integer.MAX_VALUE) { + return Integer.MAX_VALUE; + } + return (int) result; + } + } + } + + private static final int BLOCK_SIZE_BYTES = 16; + + private final int mKeymasterBlockMode; + private final int mKeymasterPadding; + + private byte[] mIv; + + /** Whether the current {@code #mIv} has been used by the underlying crypto operation. */ + private boolean mIvHasBeenUsed; + + AndroidKeyStoreAuthenticatedAESCipherSpi( + int keymasterBlockMode, + int keymasterPadding) { + mKeymasterBlockMode = keymasterBlockMode; + mKeymasterPadding = keymasterPadding; + } + + @Override + protected void resetAll() { + mIv = null; + mIvHasBeenUsed = false; + super.resetAll(); + } + + @Override + protected final void initKey(int opmode, Key key) throws InvalidKeyException { + if (!(key instanceof AndroidKeyStoreSecretKey)) { + throw new InvalidKeyException( + "Unsupported key: " + ((key != null) ? key.getClass().getName() : "null")); + } + if (!KeyProperties.KEY_ALGORITHM_AES.equalsIgnoreCase(key.getAlgorithm())) { + throw new InvalidKeyException( + "Unsupported key algorithm: " + key.getAlgorithm() + ". Only " + + KeyProperties.KEY_ALGORITHM_AES + " supported"); + } + setKey((AndroidKeyStoreSecretKey) key); + } + + @Override + protected void addAlgorithmSpecificParametersToBegin( + @NonNull List<KeyParameter> parameters) { + if ((isEncrypting()) && (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."); + } + parameters.add(KeyStore2ParameterUtils.makeEnum( + KeymasterDefs.KM_TAG_ALGORITHM, + KeymasterDefs.KM_ALGORITHM_AES + )); + parameters.add(KeyStore2ParameterUtils.makeEnum( + KeymasterDefs.KM_TAG_BLOCK_MODE, + mKeymasterBlockMode + )); + parameters.add(KeyStore2ParameterUtils.makeEnum( + KeymasterDefs.KM_TAG_PADDING, + mKeymasterPadding + )); + + if (mIv != null) { + parameters.add(KeyStore2ParameterUtils.makeBytes(KeymasterDefs.KM_TAG_NONCE, mIv)); + } + } + + @Override + protected final void loadAlgorithmSpecificParametersFromBeginResult( + KeyParameter[] parameters) { + mIvHasBeenUsed = true; + + // NOTE: Keymaster doesn't always return an IV, even if it's used. + byte[] returnedIv = null; + if (parameters != null) { + for (KeyParameter p : parameters) { + if (p.tag == KeymasterDefs.KM_TAG_NONCE) { + returnedIv = p.blob; + break; + } + } + } + + if (mIv == null) { + mIv = returnedIv; + } else if ((returnedIv != null) && (!Arrays.equals(returnedIv, mIv))) { + throw new ProviderException("IV in use differs from provided IV"); + } + } + + @Override + protected final int engineGetBlockSize() { + return BLOCK_SIZE_BYTES; + } + + @Override + protected final byte[] engineGetIV() { + return ArrayUtils.cloneIfNotEmpty(mIv); + } + + protected void setIv(byte[] iv) { + mIv = iv; + } + + protected byte[] getIv() { + return mIv; + } + + /** + * {@link KeyStoreCryptoOperationStreamer} which buffers all output until {@code doFinal} from + * which it returns all output in one go, provided {@code doFinal} succeeds. + */ + private static class BufferAllOutputUntilDoFinalStreamer + implements KeyStoreCryptoOperationStreamer { + + private final KeyStoreCryptoOperationStreamer mDelegate; + private ByteArrayOutputStream mBufferedOutput = new ByteArrayOutputStream(); + private long mProducedOutputSizeBytes; + + private BufferAllOutputUntilDoFinalStreamer(KeyStoreCryptoOperationStreamer delegate) { + mDelegate = delegate; + } + + @Override + public byte[] update(byte[] input, int inputOffset, int inputLength) + throws KeyStoreException { + byte[] output = mDelegate.update(input, inputOffset, inputLength); + if (output != null) { + try { + mBufferedOutput.write(output); + } catch (IOException e) { + throw new ProviderException("Failed to buffer output", e); + } + } + return EmptyArray.BYTE; + } + + @Override + public byte[] doFinal(byte[] input, int inputOffset, int inputLength, + byte[] signature) throws KeyStoreException { + byte[] output = mDelegate.doFinal(input, inputOffset, inputLength, signature); + if (output != null) { + try { + mBufferedOutput.write(output); + } catch (IOException e) { + throw new ProviderException("Failed to buffer output", e); + } + } + byte[] result = mBufferedOutput.toByteArray(); + mBufferedOutput.reset(); + mProducedOutputSizeBytes += result.length; + return result; + } + + @Override + public long getConsumedInputSizeBytes() { + return mDelegate.getConsumedInputSizeBytes(); + } + + @Override + public long getProducedOutputSizeBytes() { + return mProducedOutputSizeBytes; + } + } + + /** + * Additional Authentication Data (AAD) stream via a KeyStore streaming operation. This stream + * sends AAD into the KeyStore. + */ + private static class AdditionalAuthenticationDataStream implements Stream { + + private final KeyStoreOperation mOperation; + + private AdditionalAuthenticationDataStream(KeyStoreOperation operation) { + mOperation = operation; + } + + @Override + public byte[] update(byte[] input) throws KeyStoreException { + mOperation.updateAad(input); + return null; + } + + @Override + public byte[] finish(byte[] input, byte[] signature) { + return null; + } + } +}
\ No newline at end of file diff --git a/keystore/java/android/security/keystore2/AndroidKeyStoreBCWorkaroundProvider.java b/keystore/java/android/security/keystore2/AndroidKeyStoreBCWorkaroundProvider.java new file mode 100644 index 000000000000..dd943d422e62 --- /dev/null +++ b/keystore/java/android/security/keystore2/AndroidKeyStoreBCWorkaroundProvider.java @@ -0,0 +1,273 @@ +/* + * Copyright (C) 2015 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.keystore2; + +import java.security.Provider; + +/** + * {@link Provider} of JCA crypto operations operating on Android KeyStore keys. + * + * <p>This provider was separated out of {@link AndroidKeyStoreProvider} to work around the issue + * that Bouncy Castle provider incorrectly declares that it accepts arbitrary keys (incl. Android + * KeyStore ones). This causes JCA to select the Bouncy Castle's implementation of JCA crypto + * operations for Android KeyStore keys unless Android KeyStore's own implementations are installed + * as higher-priority than Bouncy Castle ones. The purpose of this provider is to do just that: to + * offer crypto operations operating on Android KeyStore keys and to be installed at higher priority + * than the Bouncy Castle provider. + * + * <p>Once Bouncy Castle provider is fixed, this provider can be merged into the + * {@code AndroidKeyStoreProvider}. + * + * @hide + */ +class AndroidKeyStoreBCWorkaroundProvider extends Provider { + + // IMPLEMENTATION NOTE: Class names are hard-coded in this provider to avoid loading these + // classes when this provider is instantiated and installed early on during each app's + // initialization process. + + private static final String PACKAGE_NAME = "android.security.keystore2"; + private static final String KEYSTORE_SECRET_KEY_CLASS_NAME = + PACKAGE_NAME + ".AndroidKeyStoreSecretKey"; + private static final String KEYSTORE_PRIVATE_KEY_CLASS_NAME = + PACKAGE_NAME + ".AndroidKeyStorePrivateKey"; + private static final String KEYSTORE_PUBLIC_KEY_CLASS_NAME = + PACKAGE_NAME + ".AndroidKeyStorePublicKey"; + + private static final String DESEDE_SYSTEM_PROPERTY = "ro.hardware.keystore_desede"; + + AndroidKeyStoreBCWorkaroundProvider() { + super("AndroidKeyStoreBCWorkaround", + 1.0, + "Android KeyStore security provider to work around Bouncy Castle"); + + // --------------------- javax.crypto.Mac + putMacImpl("HmacSHA1", PACKAGE_NAME + ".AndroidKeyStoreHmacSpi$HmacSHA1"); + put("Alg.Alias.Mac.1.2.840.113549.2.7", "HmacSHA1"); + put("Alg.Alias.Mac.HMAC-SHA1", "HmacSHA1"); + put("Alg.Alias.Mac.HMAC/SHA1", "HmacSHA1"); + + putMacImpl("HmacSHA224", PACKAGE_NAME + ".AndroidKeyStoreHmacSpi$HmacSHA224"); + put("Alg.Alias.Mac.1.2.840.113549.2.9", "HmacSHA224"); + put("Alg.Alias.Mac.HMAC-SHA224", "HmacSHA224"); + put("Alg.Alias.Mac.HMAC/SHA224", "HmacSHA224"); + + putMacImpl("HmacSHA256", PACKAGE_NAME + ".AndroidKeyStoreHmacSpi$HmacSHA256"); + put("Alg.Alias.Mac.1.2.840.113549.2.9", "HmacSHA256"); + put("Alg.Alias.Mac.HMAC-SHA256", "HmacSHA256"); + put("Alg.Alias.Mac.HMAC/SHA256", "HmacSHA256"); + + putMacImpl("HmacSHA384", PACKAGE_NAME + ".AndroidKeyStoreHmacSpi$HmacSHA384"); + put("Alg.Alias.Mac.1.2.840.113549.2.10", "HmacSHA384"); + put("Alg.Alias.Mac.HMAC-SHA384", "HmacSHA384"); + put("Alg.Alias.Mac.HMAC/SHA384", "HmacSHA384"); + + putMacImpl("HmacSHA512", PACKAGE_NAME + ".AndroidKeyStoreHmacSpi$HmacSHA512"); + put("Alg.Alias.Mac.1.2.840.113549.2.11", "HmacSHA512"); + put("Alg.Alias.Mac.HMAC-SHA512", "HmacSHA512"); + put("Alg.Alias.Mac.HMAC/SHA512", "HmacSHA512"); + + // --------------------- javax.crypto.Cipher + putSymmetricCipherImpl("AES/ECB/NoPadding", + PACKAGE_NAME + ".AndroidKeyStoreUnauthenticatedAESCipherSpi$ECB$NoPadding"); + putSymmetricCipherImpl("AES/ECB/PKCS7Padding", + PACKAGE_NAME + ".AndroidKeyStoreUnauthenticatedAESCipherSpi$ECB$PKCS7Padding"); + + putSymmetricCipherImpl("AES/CBC/NoPadding", + PACKAGE_NAME + ".AndroidKeyStoreUnauthenticatedAESCipherSpi$CBC$NoPadding"); + putSymmetricCipherImpl("AES/CBC/PKCS7Padding", + PACKAGE_NAME + ".AndroidKeyStoreUnauthenticatedAESCipherSpi$CBC$PKCS7Padding"); + + putSymmetricCipherImpl("AES/CTR/NoPadding", + PACKAGE_NAME + ".AndroidKeyStoreUnauthenticatedAESCipherSpi$CTR$NoPadding"); + + if ("true".equals(android.os.SystemProperties.get(DESEDE_SYSTEM_PROPERTY))) { + 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"); + + putAsymmetricCipherImpl("RSA/ECB/NoPadding", + PACKAGE_NAME + ".AndroidKeyStoreRSACipherSpi$NoPadding"); + put("Alg.Alias.Cipher.RSA/None/NoPadding", "RSA/ECB/NoPadding"); + putAsymmetricCipherImpl("RSA/ECB/PKCS1Padding", + PACKAGE_NAME + ".AndroidKeyStoreRSACipherSpi$PKCS1Padding"); + put("Alg.Alias.Cipher.RSA/None/PKCS1Padding", "RSA/ECB/PKCS1Padding"); + putAsymmetricCipherImpl("RSA/ECB/OAEPPadding", + PACKAGE_NAME + ".AndroidKeyStoreRSACipherSpi$OAEPWithSHA1AndMGF1Padding"); + put("Alg.Alias.Cipher.RSA/None/OAEPPadding", "RSA/ECB/OAEPPadding"); + putAsymmetricCipherImpl("RSA/ECB/OAEPWithSHA-1AndMGF1Padding", + PACKAGE_NAME + ".AndroidKeyStoreRSACipherSpi$OAEPWithSHA1AndMGF1Padding"); + put("Alg.Alias.Cipher.RSA/None/OAEPWithSHA-1AndMGF1Padding", + "RSA/ECB/OAEPWithSHA-1AndMGF1Padding"); + putAsymmetricCipherImpl("RSA/ECB/OAEPWithSHA-224AndMGF1Padding", + PACKAGE_NAME + ".AndroidKeyStoreRSACipherSpi$OAEPWithSHA224AndMGF1Padding"); + put("Alg.Alias.Cipher.RSA/None/OAEPWithSHA-224AndMGF1Padding", + "RSA/ECB/OAEPWithSHA-256AndMGF1Padding"); + putAsymmetricCipherImpl("RSA/ECB/OAEPWithSHA-256AndMGF1Padding", + PACKAGE_NAME + ".AndroidKeyStoreRSACipherSpi$OAEPWithSHA256AndMGF1Padding"); + put("Alg.Alias.Cipher.RSA/None/OAEPWithSHA-256AndMGF1Padding", + "RSA/ECB/OAEPWithSHA-256AndMGF1Padding"); + putAsymmetricCipherImpl("RSA/ECB/OAEPWithSHA-384AndMGF1Padding", + PACKAGE_NAME + ".AndroidKeyStoreRSACipherSpi$OAEPWithSHA384AndMGF1Padding"); + put("Alg.Alias.Cipher.RSA/None/OAEPWithSHA-384AndMGF1Padding", + "RSA/ECB/OAEPWithSHA-384AndMGF1Padding"); + putAsymmetricCipherImpl("RSA/ECB/OAEPWithSHA-512AndMGF1Padding", + PACKAGE_NAME + ".AndroidKeyStoreRSACipherSpi$OAEPWithSHA512AndMGF1Padding"); + put("Alg.Alias.Cipher.RSA/None/OAEPWithSHA-512AndMGF1Padding", + "RSA/ECB/OAEPWithSHA-512AndMGF1Padding"); + + // --------------------- java.security.Signature + putSignatureImpl("NONEwithRSA", + PACKAGE_NAME + ".AndroidKeyStoreRSASignatureSpi$NONEWithPKCS1Padding"); + + putSignatureImpl("MD5withRSA", + PACKAGE_NAME + ".AndroidKeyStoreRSASignatureSpi$MD5WithPKCS1Padding"); + put("Alg.Alias.Signature.MD5WithRSAEncryption", "MD5withRSA"); + put("Alg.Alias.Signature.MD5/RSA", "MD5withRSA"); + put("Alg.Alias.Signature.1.2.840.113549.1.1.4", "MD5withRSA"); + put("Alg.Alias.Signature.1.2.840.113549.2.5with1.2.840.113549.1.1.1", "MD5withRSA"); + + putSignatureImpl("SHA1withRSA", + PACKAGE_NAME + ".AndroidKeyStoreRSASignatureSpi$SHA1WithPKCS1Padding"); + put("Alg.Alias.Signature.SHA1WithRSAEncryption", "SHA1withRSA"); + put("Alg.Alias.Signature.SHA1/RSA", "SHA1withRSA"); + put("Alg.Alias.Signature.SHA-1/RSA", "SHA1withRSA"); + put("Alg.Alias.Signature.1.2.840.113549.1.1.5", "SHA1withRSA"); + put("Alg.Alias.Signature.1.3.14.3.2.26with1.2.840.113549.1.1.1", "SHA1withRSA"); + put("Alg.Alias.Signature.1.3.14.3.2.26with1.2.840.113549.1.1.5", "SHA1withRSA"); + put("Alg.Alias.Signature.1.3.14.3.2.29", "SHA1withRSA"); + + putSignatureImpl("SHA224withRSA", + PACKAGE_NAME + ".AndroidKeyStoreRSASignatureSpi$SHA224WithPKCS1Padding"); + put("Alg.Alias.Signature.SHA224WithRSAEncryption", "SHA224withRSA"); + put("Alg.Alias.Signature.1.2.840.113549.1.1.11", "SHA224withRSA"); + put("Alg.Alias.Signature.2.16.840.1.101.3.4.2.4with1.2.840.113549.1.1.1", + "SHA224withRSA"); + put("Alg.Alias.Signature.2.16.840.1.101.3.4.2.4with1.2.840.113549.1.1.11", + "SHA224withRSA"); + + putSignatureImpl("SHA256withRSA", + PACKAGE_NAME + ".AndroidKeyStoreRSASignatureSpi$SHA256WithPKCS1Padding"); + put("Alg.Alias.Signature.SHA256WithRSAEncryption", "SHA256withRSA"); + put("Alg.Alias.Signature.1.2.840.113549.1.1.11", "SHA256withRSA"); + put("Alg.Alias.Signature.2.16.840.1.101.3.4.2.1with1.2.840.113549.1.1.1", + "SHA256withRSA"); + put("Alg.Alias.Signature.2.16.840.1.101.3.4.2.1with1.2.840.113549.1.1.11", + "SHA256withRSA"); + + putSignatureImpl("SHA384withRSA", + PACKAGE_NAME + ".AndroidKeyStoreRSASignatureSpi$SHA384WithPKCS1Padding"); + put("Alg.Alias.Signature.SHA384WithRSAEncryption", "SHA384withRSA"); + put("Alg.Alias.Signature.1.2.840.113549.1.1.12", "SHA384withRSA"); + put("Alg.Alias.Signature.2.16.840.1.101.3.4.2.2with1.2.840.113549.1.1.1", + "SHA384withRSA"); + + putSignatureImpl("SHA512withRSA", + PACKAGE_NAME + ".AndroidKeyStoreRSASignatureSpi$SHA512WithPKCS1Padding"); + put("Alg.Alias.Signature.SHA512WithRSAEncryption", "SHA512withRSA"); + put("Alg.Alias.Signature.1.2.840.113549.1.1.13", "SHA512withRSA"); + put("Alg.Alias.Signature.2.16.840.1.101.3.4.2.3with1.2.840.113549.1.1.1", + "SHA512withRSA"); + + putSignatureImpl("SHA1withRSA/PSS", + PACKAGE_NAME + ".AndroidKeyStoreRSASignatureSpi$SHA1WithPSSPadding"); + putSignatureImpl("SHA224withRSA/PSS", + PACKAGE_NAME + ".AndroidKeyStoreRSASignatureSpi$SHA224WithPSSPadding"); + putSignatureImpl("SHA256withRSA/PSS", + PACKAGE_NAME + ".AndroidKeyStoreRSASignatureSpi$SHA256WithPSSPadding"); + putSignatureImpl("SHA384withRSA/PSS", + PACKAGE_NAME + ".AndroidKeyStoreRSASignatureSpi$SHA384WithPSSPadding"); + putSignatureImpl("SHA512withRSA/PSS", + PACKAGE_NAME + ".AndroidKeyStoreRSASignatureSpi$SHA512WithPSSPadding"); + + putSignatureImpl("NONEwithECDSA", + PACKAGE_NAME + ".AndroidKeyStoreECDSASignatureSpi$NONE"); + + putSignatureImpl("SHA1withECDSA", PACKAGE_NAME + ".AndroidKeyStoreECDSASignatureSpi$SHA1"); + put("Alg.Alias.Signature.ECDSA", "SHA1withECDSA"); + put("Alg.Alias.Signature.ECDSAwithSHA1", "SHA1withECDSA"); + // iso(1) member-body(2) us(840) ansi-x962(10045) signatures(4) ecdsa-with-SHA1(1) + put("Alg.Alias.Signature.1.2.840.10045.4.1", "SHA1withECDSA"); + put("Alg.Alias.Signature.1.3.14.3.2.26with1.2.840.10045.2.1", "SHA1withECDSA"); + + // iso(1) member-body(2) us(840) ansi-x962(10045) signatures(4) ecdsa-with-SHA2(3) + putSignatureImpl("SHA224withECDSA", + PACKAGE_NAME + ".AndroidKeyStoreECDSASignatureSpi$SHA224"); + // ecdsa-with-SHA224(1) + put("Alg.Alias.Signature.1.2.840.10045.4.3.1", "SHA224withECDSA"); + put("Alg.Alias.Signature.2.16.840.1.101.3.4.2.4with1.2.840.10045.2.1", "SHA224withECDSA"); + + // iso(1) member-body(2) us(840) ansi-x962(10045) signatures(4) ecdsa-with-SHA2(3) + putSignatureImpl("SHA256withECDSA", + PACKAGE_NAME + ".AndroidKeyStoreECDSASignatureSpi$SHA256"); + // ecdsa-with-SHA256(2) + put("Alg.Alias.Signature.1.2.840.10045.4.3.2", "SHA256withECDSA"); + put("Alg.Alias.Signature.2.16.840.1.101.3.4.2.1with1.2.840.10045.2.1", "SHA256withECDSA"); + + putSignatureImpl("SHA384withECDSA", + PACKAGE_NAME + ".AndroidKeyStoreECDSASignatureSpi$SHA384"); + // ecdsa-with-SHA384(3) + put("Alg.Alias.Signature.1.2.840.10045.4.3.3", "SHA384withECDSA"); + put("Alg.Alias.Signature.2.16.840.1.101.3.4.2.2with1.2.840.10045.2.1", "SHA384withECDSA"); + + putSignatureImpl("SHA512withECDSA", + PACKAGE_NAME + ".AndroidKeyStoreECDSASignatureSpi$SHA512"); + // ecdsa-with-SHA512(4) + put("Alg.Alias.Signature.1.2.840.10045.4.3.4", "SHA512withECDSA"); + put("Alg.Alias.Signature.2.16.840.1.101.3.4.2.3with1.2.840.10045.2.1", "SHA512withECDSA"); + } + + private void putMacImpl(String algorithm, String implClass) { + put("Mac." + algorithm, implClass); + put("Mac." + algorithm + " SupportedKeyClasses", KEYSTORE_SECRET_KEY_CLASS_NAME); + } + + private void putSymmetricCipherImpl(String transformation, String implClass) { + put("Cipher." + transformation, implClass); + put("Cipher." + transformation + " SupportedKeyClasses", KEYSTORE_SECRET_KEY_CLASS_NAME); + } + + private void putAsymmetricCipherImpl(String transformation, String implClass) { + put("Cipher." + transformation, implClass); + put("Cipher." + transformation + " SupportedKeyClasses", + KEYSTORE_PRIVATE_KEY_CLASS_NAME + "|" + KEYSTORE_PUBLIC_KEY_CLASS_NAME); + } + + private void putSignatureImpl(String algorithm, String implClass) { + put("Signature." + algorithm, implClass); + put("Signature." + algorithm + " SupportedKeyClasses", + KEYSTORE_PRIVATE_KEY_CLASS_NAME + "|" + KEYSTORE_PUBLIC_KEY_CLASS_NAME); + } + + public static String[] getSupportedEcdsaSignatureDigests() { + return new String[] {"NONE", "SHA-1", "SHA-224", "SHA-256", "SHA-384", "SHA-512"}; + } + + public static String[] getSupportedRsaSignatureWithPkcs1PaddingDigests() { + return new String[] {"NONE", "MD5", "SHA-1", "SHA-224", "SHA-256", "SHA-384", "SHA-512"}; + } +} diff --git a/keystore/java/android/security/keystore2/AndroidKeyStoreCipherSpiBase.java b/keystore/java/android/security/keystore2/AndroidKeyStoreCipherSpiBase.java new file mode 100644 index 000000000000..b785ee5c6966 --- /dev/null +++ b/keystore/java/android/security/keystore2/AndroidKeyStoreCipherSpiBase.java @@ -0,0 +1,905 @@ +/* + * Copyright (C) 2015 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.keystore2; + +import android.annotation.CallSuper; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.security.KeyStoreException; +import android.security.KeyStoreOperation; +import android.security.keymaster.KeymasterDefs; +import android.security.keystore.KeyStoreCryptoOperation; +import android.system.keystore2.KeyParameter; + +import libcore.util.EmptyArray; + +import java.nio.BufferOverflowException; +import java.nio.ByteBuffer; +import java.security.AlgorithmParameters; +import java.security.GeneralSecurityException; +import java.security.InvalidAlgorithmParameterException; +import java.security.InvalidKeyException; +import java.security.InvalidParameterException; +import java.security.Key; +import java.security.KeyFactory; +import java.security.NoSuchAlgorithmException; +import java.security.PrivateKey; +import java.security.ProviderException; +import java.security.PublicKey; +import java.security.SecureRandom; +import java.security.spec.AlgorithmParameterSpec; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.PKCS8EncodedKeySpec; +import java.security.spec.X509EncodedKeySpec; +import java.util.ArrayList; +import java.util.List; + +import javax.crypto.AEADBadTagException; +import javax.crypto.BadPaddingException; +import javax.crypto.Cipher; +import javax.crypto.CipherSpi; +import javax.crypto.IllegalBlockSizeException; +import javax.crypto.NoSuchPaddingException; +import javax.crypto.SecretKey; +import javax.crypto.SecretKeyFactory; +import javax.crypto.ShortBufferException; +import javax.crypto.spec.SecretKeySpec; + +/** + * Base class for {@link CipherSpi} implementations of Android KeyStore backed ciphers. + * + * @hide + */ +abstract class AndroidKeyStoreCipherSpiBase extends CipherSpi implements KeyStoreCryptoOperation { + private static final String TAG = "AndroidKeyStoreCipherSpiBase"; + + // Fields below are populated by Cipher.init and KeyStore.begin and should be preserved after + // doFinal finishes. + private boolean mEncrypting; + private int mKeymasterPurposeOverride = -1; + private AndroidKeyStoreKey mKey; + private SecureRandom mRng; + + /** + * Object representing this operation inside keystore service. It is initialized + * by {@code engineInit} and is invalidated when {@code engineDoFinal} succeeds and on some + * error conditions in between. + */ + private KeyStoreOperation mOperation; + /** + * The operation challenge is required when an operation needs user authorization. + * The challenge is subjected to an authenticator, e.g., Gatekeeper or a biometric + * authenticator, and included in the authentication token minted by this authenticator. + * It may be null, if the operation does not require authorization. + */ + private long mOperationChallenge; + private KeyStoreCryptoOperationStreamer mMainDataStreamer; + private KeyStoreCryptoOperationStreamer mAdditionalAuthenticationDataStreamer; + private boolean mAdditionalAuthenticationDataStreamerClosed; + + /** + * Encountered exception which could not be immediately thrown because it was encountered inside + * a method that does not throw checked exception. This exception will be thrown from + * {@code engineDoFinal}. Once such an exception is encountered, {@code engineUpdate} and + * {@code engineDoFinal} start ignoring input data. + */ + private Exception mCachedException; + + AndroidKeyStoreCipherSpiBase() { + mOperation = null; + mEncrypting = false; + mKeymasterPurposeOverride = -1; + mKey = null; + mRng = null; + mOperationChallenge = 0; + mMainDataStreamer = null; + mAdditionalAuthenticationDataStreamer = null; + mAdditionalAuthenticationDataStreamerClosed = false; + mCachedException = null; + } + + @Override + protected final void engineInit(int opmode, Key key, SecureRandom random) + throws InvalidKeyException { + resetAll(); + + boolean success = false; + try { + init(opmode, key, random); + initAlgorithmSpecificParameters(); + try { + ensureKeystoreOperationInitialized(); + } catch (InvalidAlgorithmParameterException e) { + throw new InvalidKeyException(e); + } + success = true; + } finally { + if (!success) { + resetAll(); + } + } + } + + @Override + protected final void engineInit(int opmode, Key key, AlgorithmParameters params, + SecureRandom random) throws InvalidKeyException, InvalidAlgorithmParameterException { + resetAll(); + + boolean success = false; + try { + init(opmode, key, random); + initAlgorithmSpecificParameters(params); + ensureKeystoreOperationInitialized(); + success = true; + } finally { + if (!success) { + resetAll(); + } + } + } + + @Override + protected final void engineInit(int opmode, Key key, AlgorithmParameterSpec params, + SecureRandom random) throws InvalidKeyException, InvalidAlgorithmParameterException { + resetAll(); + + boolean success = false; + try { + init(opmode, key, random); + initAlgorithmSpecificParameters(params); + ensureKeystoreOperationInitialized(); + success = true; + } finally { + if (!success) { + resetAll(); + } + } + } + + private void init(int opmode, Key key, SecureRandom random) throws InvalidKeyException { + switch (opmode) { + case Cipher.ENCRYPT_MODE: + case Cipher.WRAP_MODE: + mEncrypting = true; + break; + case Cipher.DECRYPT_MODE: + case Cipher.UNWRAP_MODE: + mEncrypting = false; + break; + default: + throw new InvalidParameterException("Unsupported opmode: " + opmode); + } + initKey(opmode, key); + if (mKey == null) { + throw new ProviderException("initKey did not initialize the key"); + } + mRng = random; + } + + private void abortOperation() { + KeyStoreCryptoOperationUtils.abortOperation(mOperation); + mOperation = null; + } + + /** + * Resets this cipher to its pristine pre-init state. This must be equivalent to obtaining a new + * cipher instance. + * + * <p>Subclasses storing additional state should override this method, reset the additional + * state, and then chain to superclass. + */ + @CallSuper + protected void resetAll() { + abortOperation(); + mEncrypting = false; + mKeymasterPurposeOverride = -1; + mKey = null; + mRng = null; + mOperationChallenge = 0; + mMainDataStreamer = null; + mAdditionalAuthenticationDataStreamer = null; + mAdditionalAuthenticationDataStreamerClosed = false; + mCachedException = null; + } + + /** + * Resets this cipher while preserving the initialized state. This must be equivalent to + * rolling back the cipher's state to just after the most recent {@code engineInit} completed + * successfully. + * + * <p>Subclasses storing additional post-init state should override this method, reset the + * additional state, and then chain to superclass. + */ + @CallSuper + protected void resetWhilePreservingInitState() { + abortOperation(); + mOperationChallenge = 0; + mMainDataStreamer = null; + mAdditionalAuthenticationDataStreamer = null; + mAdditionalAuthenticationDataStreamerClosed = false; + mCachedException = null; + } + + private void ensureKeystoreOperationInitialized() throws InvalidKeyException, + InvalidAlgorithmParameterException { + if (mMainDataStreamer != null) { + return; + } + if (mCachedException != null) { + return; + } + if (mKey == null) { + throw new IllegalStateException("Not initialized"); + } + + List<KeyParameter> parameters = new ArrayList<>(); + addAlgorithmSpecificParametersToBegin(parameters); + + int purpose; + if (mKeymasterPurposeOverride != -1) { + purpose = mKeymasterPurposeOverride; + } else { + purpose = mEncrypting + ? KeymasterDefs.KM_PURPOSE_ENCRYPT : KeymasterDefs.KM_PURPOSE_DECRYPT; + } + + parameters.add(KeyStore2ParameterUtils.makeEnum(KeymasterDefs.KM_TAG_PURPOSE, purpose)); + + try { + mOperation = mKey.getSecurityLevel().createOperation( + mKey.getKeyIdDescriptor(), + parameters + ); + } catch (KeyStoreException keyStoreException) { + GeneralSecurityException e = KeyStoreCryptoOperationUtils.getExceptionForCipherInit( + mKey, keyStoreException); + if (e != null) { + if (e instanceof InvalidKeyException) { + throw (InvalidKeyException) e; + } else if (e instanceof InvalidAlgorithmParameterException) { + throw (InvalidAlgorithmParameterException) e; + } else { + throw new ProviderException("Unexpected exception type", e); + } + } + } + + // Now we check if we got an operation challenge. This indicates that user authorization + // is required. And if we got a challenge we check if the authorization can possibly + // succeed. + mOperationChallenge = KeyStoreCryptoOperationUtils.getOrMakeOperationChallenge( + mOperation, mKey); + + loadAlgorithmSpecificParametersFromBeginResult(mOperation.getParameters()); + mMainDataStreamer = createMainDataStreamer(mOperation); + mAdditionalAuthenticationDataStreamer = + createAdditionalAuthenticationDataStreamer(mOperation); + mAdditionalAuthenticationDataStreamerClosed = false; + } + + /** + * Creates a streamer which sends plaintext/ciphertext into the provided KeyStore and receives + * the corresponding ciphertext/plaintext from the KeyStore. + * + * <p>This implementation returns a working streamer. + */ + @NonNull + protected KeyStoreCryptoOperationStreamer createMainDataStreamer( + KeyStoreOperation operation) { + return new KeyStoreCryptoOperationChunkedStreamer( + new KeyStoreCryptoOperationChunkedStreamer.MainDataStream( + operation), 0); + } + + /** + * Creates a streamer which sends Additional Authentication Data (AAD) into the KeyStore. + * + * <p>This implementation returns {@code null}. + * + * @return stream or {@code null} if AAD is not supported by this cipher. + */ + @Nullable + protected KeyStoreCryptoOperationStreamer createAdditionalAuthenticationDataStreamer( + @SuppressWarnings("unused") KeyStoreOperation operation) { + return null; + } + + @Override + protected final byte[] engineUpdate(byte[] input, int inputOffset, int inputLen) { + if (mCachedException != null) { + return null; + } + try { + ensureKeystoreOperationInitialized(); + } catch (InvalidKeyException | InvalidAlgorithmParameterException e) { + mCachedException = e; + return null; + } + + if (inputLen == 0) { + return null; + } + + byte[] output; + try { + flushAAD(); + output = mMainDataStreamer.update(input, inputOffset, inputLen); + } catch (KeyStoreException e) { + mCachedException = e; + return null; + } + + if (output.length == 0) { + return null; + } + + return output; + } + + private void flushAAD() throws KeyStoreException { + if ((mAdditionalAuthenticationDataStreamer != null) + && (!mAdditionalAuthenticationDataStreamerClosed)) { + byte[] output; + try { + output = mAdditionalAuthenticationDataStreamer.doFinal( + EmptyArray.BYTE, 0, 0, + null); // no signature + } finally { + mAdditionalAuthenticationDataStreamerClosed = true; + } + if ((output != null) && (output.length > 0)) { + throw new ProviderException( + "AAD update unexpectedly returned data: " + output.length + " bytes"); + } + } + } + + @Override + protected final int engineUpdate(byte[] input, int inputOffset, int inputLen, byte[] output, + int outputOffset) throws ShortBufferException { + byte[] outputCopy = engineUpdate(input, inputOffset, inputLen); + if (outputCopy == null) { + return 0; + } + int outputAvailable = output.length - outputOffset; + if (outputCopy.length > outputAvailable) { + throw new ShortBufferException("Output buffer too short. Produced: " + + outputCopy.length + ", available: " + outputAvailable); + } + System.arraycopy(outputCopy, 0, output, outputOffset, outputCopy.length); + return outputCopy.length; + } + + @Override + protected final int engineUpdate(ByteBuffer input, ByteBuffer output) + throws ShortBufferException { + if (input == null) { + throw new NullPointerException("input == null"); + } + if (output == null) { + throw new NullPointerException("output == null"); + } + + int inputSize = input.remaining(); + byte[] outputArray; + if (input.hasArray()) { + outputArray = + engineUpdate( + input.array(), input.arrayOffset() + input.position(), inputSize); + input.position(input.position() + inputSize); + } else { + byte[] inputArray = new byte[inputSize]; + input.get(inputArray); + outputArray = engineUpdate(inputArray, 0, inputSize); + } + + int outputSize = (outputArray != null) ? outputArray.length : 0; + if (outputSize > 0) { + int outputBufferAvailable = output.remaining(); + try { + output.put(outputArray); + } catch (BufferOverflowException e) { + throw new ShortBufferException( + "Output buffer too small. Produced: " + outputSize + ", available: " + + outputBufferAvailable); + } + } + return outputSize; + } + + @Override + protected final void engineUpdateAAD(byte[] input, int inputOffset, int inputLen) { + if (mCachedException != null) { + return; + } + + try { + ensureKeystoreOperationInitialized(); + } catch (InvalidKeyException | InvalidAlgorithmParameterException e) { + mCachedException = e; + return; + } + + if (mAdditionalAuthenticationDataStreamerClosed) { + throw new IllegalStateException( + "AAD can only be provided before Cipher.update is invoked"); + } + + if (mAdditionalAuthenticationDataStreamer == null) { + throw new IllegalStateException("This cipher does not support AAD"); + } + + byte[] output; + try { + output = mAdditionalAuthenticationDataStreamer.update(input, inputOffset, inputLen); + } catch (KeyStoreException e) { + mCachedException = e; + return; + } + + if ((output != null) && (output.length > 0)) { + throw new ProviderException("AAD update unexpectedly produced output: " + + output.length + " bytes"); + } + } + + @Override + protected final void engineUpdateAAD(ByteBuffer src) { + if (src == null) { + throw new IllegalArgumentException("src == null"); + } + if (!src.hasRemaining()) { + return; + } + + byte[] input; + int inputOffset; + int inputLen; + if (src.hasArray()) { + input = src.array(); + inputOffset = src.arrayOffset() + src.position(); + inputLen = src.remaining(); + src.position(src.limit()); + } else { + input = new byte[src.remaining()]; + inputOffset = 0; + inputLen = input.length; + src.get(input); + } + engineUpdateAAD(input, inputOffset, inputLen); + } + + @Override + protected final byte[] engineDoFinal(byte[] input, int inputOffset, int inputLen) + throws IllegalBlockSizeException, BadPaddingException { + if (mCachedException != null) { + throw (IllegalBlockSizeException) + new IllegalBlockSizeException().initCause(mCachedException); + } + + try { + ensureKeystoreOperationInitialized(); + } catch (InvalidKeyException | InvalidAlgorithmParameterException e) { + throw (IllegalBlockSizeException) new IllegalBlockSizeException().initCause(e); + } + + byte[] output; + try { + flushAAD(); + output = mMainDataStreamer.doFinal( + input, inputOffset, inputLen, + null); // no signature involved + } catch (KeyStoreException e) { + switch (e.getErrorCode()) { + case KeymasterDefs.KM_ERROR_INVALID_ARGUMENT: + throw (BadPaddingException) new BadPaddingException().initCause(e); + case KeymasterDefs.KM_ERROR_VERIFICATION_FAILED: + throw (AEADBadTagException) new AEADBadTagException().initCause(e); + default: + throw (IllegalBlockSizeException) new IllegalBlockSizeException().initCause(e); + } + } + + resetWhilePreservingInitState(); + return output; + } + + @Override + protected final int engineDoFinal(byte[] input, int inputOffset, int inputLen, byte[] output, + int outputOffset) throws ShortBufferException, IllegalBlockSizeException, + BadPaddingException { + byte[] outputCopy = engineDoFinal(input, inputOffset, inputLen); + if (outputCopy == null) { + return 0; + } + int outputAvailable = output.length - outputOffset; + if (outputCopy.length > outputAvailable) { + throw new ShortBufferException("Output buffer too short. Produced: " + + outputCopy.length + ", available: " + outputAvailable); + } + System.arraycopy(outputCopy, 0, output, outputOffset, outputCopy.length); + return outputCopy.length; + } + + @Override + protected final int engineDoFinal(ByteBuffer input, ByteBuffer output) + throws ShortBufferException, IllegalBlockSizeException, BadPaddingException { + if (input == null) { + throw new NullPointerException("input == null"); + } + if (output == null) { + throw new NullPointerException("output == null"); + } + + int inputSize = input.remaining(); + byte[] outputArray; + if (input.hasArray()) { + outputArray = + engineDoFinal( + input.array(), input.arrayOffset() + input.position(), inputSize); + input.position(input.position() + inputSize); + } else { + byte[] inputArray = new byte[inputSize]; + input.get(inputArray); + outputArray = engineDoFinal(inputArray, 0, inputSize); + } + + int outputSize = (outputArray != null) ? outputArray.length : 0; + if (outputSize > 0) { + int outputBufferAvailable = output.remaining(); + try { + output.put(outputArray); + } catch (BufferOverflowException e) { + throw new ShortBufferException( + "Output buffer too small. Produced: " + outputSize + ", available: " + + outputBufferAvailable); + } + } + return outputSize; + } + + @Override + protected final byte[] engineWrap(Key key) + throws IllegalBlockSizeException, InvalidKeyException { + if (mKey == null) { + throw new IllegalStateException("Not initilized"); + } + + if (!isEncrypting()) { + throw new IllegalStateException( + "Cipher must be initialized in Cipher.WRAP_MODE to wrap keys"); + } + + if (key == null) { + throw new NullPointerException("key == null"); + } + byte[] encoded = null; + if (key instanceof SecretKey) { + if ("RAW".equalsIgnoreCase(key.getFormat())) { + encoded = key.getEncoded(); + } + if (encoded == null) { + try { + SecretKeyFactory keyFactory = SecretKeyFactory.getInstance(key.getAlgorithm()); + SecretKeySpec spec = + (SecretKeySpec) keyFactory.getKeySpec( + (SecretKey) key, SecretKeySpec.class); + encoded = spec.getEncoded(); + } catch (NoSuchAlgorithmException | InvalidKeySpecException e) { + throw new InvalidKeyException( + "Failed to wrap key because it does not export its key material", + e); + } + } + } else if (key instanceof PrivateKey) { + if ("PKCS8".equalsIgnoreCase(key.getFormat())) { + encoded = key.getEncoded(); + } + if (encoded == null) { + try { + KeyFactory keyFactory = KeyFactory.getInstance(key.getAlgorithm()); + PKCS8EncodedKeySpec spec = + keyFactory.getKeySpec(key, PKCS8EncodedKeySpec.class); + encoded = spec.getEncoded(); + } catch (NoSuchAlgorithmException | InvalidKeySpecException e) { + throw new InvalidKeyException( + "Failed to wrap key because it does not export its key material", + e); + } + } + } else if (key instanceof PublicKey) { + if ("X.509".equalsIgnoreCase(key.getFormat())) { + encoded = key.getEncoded(); + } + if (encoded == null) { + try { + KeyFactory keyFactory = KeyFactory.getInstance(key.getAlgorithm()); + X509EncodedKeySpec spec = + keyFactory.getKeySpec(key, X509EncodedKeySpec.class); + encoded = spec.getEncoded(); + } catch (NoSuchAlgorithmException | InvalidKeySpecException e) { + throw new InvalidKeyException( + "Failed to wrap key because it does not export its key material", + e); + } + } + } else { + throw new InvalidKeyException("Unsupported key type: " + key.getClass().getName()); + } + + if (encoded == null) { + throw new InvalidKeyException( + "Failed to wrap key because it does not export its key material"); + } + + try { + return engineDoFinal(encoded, 0, encoded.length); + } catch (BadPaddingException e) { + throw (IllegalBlockSizeException) new IllegalBlockSizeException().initCause(e); + } + } + + @Override + protected final Key engineUnwrap(byte[] wrappedKey, String wrappedKeyAlgorithm, + int wrappedKeyType) throws InvalidKeyException, NoSuchAlgorithmException { + if (mKey == null) { + throw new IllegalStateException("Not initilized"); + } + + if (isEncrypting()) { + throw new IllegalStateException( + "Cipher must be initialized in Cipher.WRAP_MODE to wrap keys"); + } + + if (wrappedKey == null) { + throw new NullPointerException("wrappedKey == null"); + } + + byte[] encoded; + try { + encoded = engineDoFinal(wrappedKey, 0, wrappedKey.length); + } catch (IllegalBlockSizeException | BadPaddingException e) { + throw new InvalidKeyException("Failed to unwrap key", e); + } + + switch (wrappedKeyType) { + case Cipher.SECRET_KEY: + { + return new SecretKeySpec(encoded, wrappedKeyAlgorithm); + // break; + } + case Cipher.PRIVATE_KEY: + { + KeyFactory keyFactory = KeyFactory.getInstance(wrappedKeyAlgorithm); + try { + return keyFactory.generatePrivate(new PKCS8EncodedKeySpec(encoded)); + } catch (InvalidKeySpecException e) { + throw new InvalidKeyException( + "Failed to create private key from its PKCS#8 encoded form", e); + } + // break; + } + case Cipher.PUBLIC_KEY: + { + KeyFactory keyFactory = KeyFactory.getInstance(wrappedKeyAlgorithm); + try { + return keyFactory.generatePublic(new X509EncodedKeySpec(encoded)); + } catch (InvalidKeySpecException e) { + throw new InvalidKeyException( + "Failed to create public key from its X.509 encoded form", e); + } + // break; + } + default: + throw new InvalidParameterException( + "Unsupported wrappedKeyType: " + wrappedKeyType); + } + } + + @Override + protected final void engineSetMode(String mode) throws NoSuchAlgorithmException { + // This should never be invoked because all algorithms registered with the AndroidKeyStore + // provide explicitly specify block mode. + throw new UnsupportedOperationException(); + } + + @Override + protected final void engineSetPadding(String arg0) throws NoSuchPaddingException { + // This should never be invoked because all algorithms registered with the AndroidKeyStore + // provide explicitly specify padding mode. + throw new UnsupportedOperationException(); + } + + @Override + protected final int engineGetKeySize(Key key) throws InvalidKeyException { + throw new UnsupportedOperationException(); + } + + @CallSuper + @Override + public void finalize() throws Throwable { + try { + abortOperation(); + } finally { + super.finalize(); + } + } + + @Override + public final long getOperationHandle() { + return mOperationChallenge; + } + + protected final void setKey(@NonNull AndroidKeyStoreKey key) { + mKey = key; + } + + /** + * Overrides the default purpose/type of the crypto operation. + */ + protected final void setKeymasterPurposeOverride(int keymasterPurpose) { + mKeymasterPurposeOverride = keymasterPurpose; + } + + protected final int getKeymasterPurposeOverride() { + return mKeymasterPurposeOverride; + } + + /** + * Returns {@code true} if this cipher is initialized for encryption, {@code false} if this + * cipher is initialized for decryption. + */ + protected final boolean isEncrypting() { + return mEncrypting; + } + + protected final long getConsumedInputSizeBytes() { + if (mMainDataStreamer == null) { + throw new IllegalStateException("Not initialized"); + } + return mMainDataStreamer.getConsumedInputSizeBytes(); + } + + protected final long getProducedOutputSizeBytes() { + if (mMainDataStreamer == null) { + throw new IllegalStateException("Not initialized"); + } + return mMainDataStreamer.getProducedOutputSizeBytes(); + } + + static String opmodeToString(int opmode) { + switch (opmode) { + case Cipher.ENCRYPT_MODE: + return "ENCRYPT_MODE"; + case Cipher.DECRYPT_MODE: + return "DECRYPT_MODE"; + case Cipher.WRAP_MODE: + return "WRAP_MODE"; + case Cipher.UNWRAP_MODE: + return "UNWRAP_MODE"; + default: + return String.valueOf(opmode); + } + } + + // The methods below need to be implemented by subclasses. + + /** + * Initializes this cipher with the provided key. + * + * @throws InvalidKeyException if the {@code key} is not suitable for this cipher in the + * specified {@code opmode}. + * + * @see #setKey(AndroidKeyStoreKey) + */ + protected abstract void initKey(int opmode, @Nullable Key key) throws InvalidKeyException; + + /** + * Returns algorithm-specific parameters used by this cipher or {@code null} if no + * algorithm-specific parameters are used. + */ + @Nullable + @Override + protected abstract AlgorithmParameters engineGetParameters(); + + /** + * Invoked by {@code engineInit} to initialize algorithm-specific parameters when no additional + * initialization parameters were provided. + * + * @throws InvalidKeyException if this cipher cannot be configured based purely on the provided + * key and needs additional parameters to be provided to {@code Cipher.init}. + */ + protected abstract void initAlgorithmSpecificParameters() throws InvalidKeyException; + + /** + * Invoked by {@code engineInit} to initialize algorithm-specific parameters when additional + * parameters were provided. + * + * @param params additional algorithm parameters or {@code null} if not specified. + * + * @throws InvalidAlgorithmParameterException if there is insufficient information to configure + * this cipher or if the provided parameters are not suitable for this cipher. + */ + protected abstract void initAlgorithmSpecificParameters( + @Nullable AlgorithmParameterSpec params) throws InvalidAlgorithmParameterException; + + /** + * Invoked by {@code engineInit} to initialize algorithm-specific parameters when additional + * parameters were provided. + * + * @param params additional algorithm parameters or {@code null} if not specified. + * + * @throws InvalidAlgorithmParameterException if there is insufficient information to configure + * this cipher or if the provided parameters are not suitable for this cipher. + */ + protected abstract void initAlgorithmSpecificParameters(@Nullable AlgorithmParameters params) + throws InvalidAlgorithmParameterException; + + /** + * Returns the amount of additional entropy (in bytes) to be provided to the KeyStore's + * {@code begin} operation. This amount of entropy is typically what's consumed to generate + * random parameters, such as IV. + * + * <p>For decryption, the return value should be {@code 0} because decryption should not be + * consuming any entropy. For encryption, the value combined with + * {@link #getAdditionalEntropyAmountForFinish()} should match (or exceed) the amount of Shannon + * entropy of the ciphertext produced by this cipher assuming the key, the plaintext, and all + * explicitly provided parameters to {@code Cipher.init} are known. For example, for AES CBC + * encryption with an explicitly provided IV the return value should be {@code 0}, whereas for + * the case where IV is generated by the KeyStore's {@code begin} operation it should be + * {@code 16}. + */ + protected abstract int getAdditionalEntropyAmountForBegin(); + + /** + * Returns the amount of additional entropy (in bytes) to be provided to the KeyStore's + * {@code finish} operation. This amount of entropy is typically what's consumed by encryption + * padding scheme. + * + * <p>For decryption, the return value should be {@code 0} because decryption should not be + * consuming any entropy. For encryption, the value combined with + * {@link #getAdditionalEntropyAmountForBegin()} should match (or exceed) the amount of Shannon + * entropy of the ciphertext produced by this cipher assuming the key, the plaintext, and all + * explicitly provided parameters to {@code Cipher.init} are known. For example, for RSA with + * OAEP the return value should be the size of the OAEP hash output. For RSA with PKCS#1 padding + * the return value should be the size of the padding string or could be raised (for simplicity) + * to the size of the modulus. + */ + protected abstract int getAdditionalEntropyAmountForFinish(); + + /** + * Invoked to add algorithm-specific parameters for the KeyStore's {@code begin} operation. + * + * @param parameters keystore/keymaster arguments to be populated with algorithm-specific + * parameters. + */ + protected abstract void addAlgorithmSpecificParametersToBegin( + @NonNull List<KeyParameter> parameters); + + /** + * Invoked to obtain algorithm-specific parameters from the result of the KeyStore's + * {@code begin} operation. + * + * <p>Some parameters, such as IV, are not required to be provided to {@code Cipher.init}. Such + * parameters, if not provided, must be generated by KeyStore and returned to the user of + * {@code Cipher} and potentially reused after {@code doFinal}. + * + * @param parameters keystore/keymaster arguments returned by KeyStore {@code createOperation}. + */ + protected abstract void loadAlgorithmSpecificParametersFromBeginResult( + KeyParameter[] parameters); +} diff --git a/keystore/java/android/security/keystore2/AndroidKeyStoreECDSASignatureSpi.java b/keystore/java/android/security/keystore2/AndroidKeyStoreECDSASignatureSpi.java new file mode 100644 index 000000000000..9f7f2383a416 --- /dev/null +++ b/keystore/java/android/security/keystore2/AndroidKeyStoreECDSASignatureSpi.java @@ -0,0 +1,207 @@ +/* + * Copyright (C) 2015 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.keystore2; + +import android.annotation.NonNull; +import android.security.KeyStoreException; +import android.security.KeyStoreOperation; +import android.security.keymaster.KeymasterDefs; +import android.security.keystore.KeyProperties; +import android.system.keystore2.Authorization; +import android.system.keystore2.KeyParameter; + +import libcore.util.EmptyArray; + +import java.io.ByteArrayOutputStream; +import java.security.InvalidKeyException; +import java.security.SignatureSpi; +import java.util.List; + +/** + * Base class for {@link SignatureSpi} providing Android KeyStore backed ECDSA signatures. + * + * @hide + */ +abstract class AndroidKeyStoreECDSASignatureSpi extends AndroidKeyStoreSignatureSpiBase { + + public final static class NONE extends AndroidKeyStoreECDSASignatureSpi { + public NONE() { + super(KeymasterDefs.KM_DIGEST_NONE); + } + + @Override + protected KeyStoreCryptoOperationStreamer createMainDataStreamer( + KeyStoreOperation operation) { + return new TruncateToFieldSizeMessageStreamer( + super.createMainDataStreamer(operation), + getGroupSizeBits()); + } + + /** + * Streamer which buffers all input, then truncates it to field size, and then sends it into + * KeyStore via the provided delegate streamer. + */ + private static class TruncateToFieldSizeMessageStreamer + implements KeyStoreCryptoOperationStreamer { + + private final KeyStoreCryptoOperationStreamer mDelegate; + private final int mGroupSizeBits; + private final ByteArrayOutputStream mInputBuffer = new ByteArrayOutputStream(); + private long mConsumedInputSizeBytes; + + private TruncateToFieldSizeMessageStreamer( + KeyStoreCryptoOperationStreamer delegate, + int groupSizeBits) { + mDelegate = delegate; + mGroupSizeBits = groupSizeBits; + } + + @Override + public byte[] update(byte[] input, int inputOffset, int inputLength) + throws KeyStoreException { + if (inputLength > 0) { + mInputBuffer.write(input, inputOffset, inputLength); + mConsumedInputSizeBytes += inputLength; + } + return EmptyArray.BYTE; + } + + @Override + public byte[] doFinal(byte[] input, int inputOffset, int inputLength, byte[] signature) + throws KeyStoreException { + if (inputLength > 0) { + mConsumedInputSizeBytes += inputLength; + mInputBuffer.write(input, inputOffset, inputLength); + } + + byte[] bufferedInput = mInputBuffer.toByteArray(); + mInputBuffer.reset(); + // Truncate input at field size (bytes) + return mDelegate.doFinal(bufferedInput, + 0, + Math.min(bufferedInput.length, ((mGroupSizeBits + 7) / 8)), + signature); + } + + @Override + public long getConsumedInputSizeBytes() { + return mConsumedInputSizeBytes; + } + + @Override + public long getProducedOutputSizeBytes() { + return mDelegate.getProducedOutputSizeBytes(); + } + } + } + + public final static class SHA1 extends AndroidKeyStoreECDSASignatureSpi { + public SHA1() { + super(KeymasterDefs.KM_DIGEST_SHA1); + } + } + + public final static class SHA224 extends AndroidKeyStoreECDSASignatureSpi { + public SHA224() { + super(KeymasterDefs.KM_DIGEST_SHA_2_224); + } + } + + public final static class SHA256 extends AndroidKeyStoreECDSASignatureSpi { + public SHA256() { + super(KeymasterDefs.KM_DIGEST_SHA_2_256); + } + } + + public final static class SHA384 extends AndroidKeyStoreECDSASignatureSpi { + public SHA384() { + super(KeymasterDefs.KM_DIGEST_SHA_2_384); + } + } + + public final static class SHA512 extends AndroidKeyStoreECDSASignatureSpi { + public SHA512() { + super(KeymasterDefs.KM_DIGEST_SHA_2_512); + } + } + + private final int mKeymasterDigest; + + private int mGroupSizeBits = -1; + + AndroidKeyStoreECDSASignatureSpi(int keymasterDigest) { + mKeymasterDigest = keymasterDigest; + } + + @Override + protected final void initKey(AndroidKeyStoreKey key) throws InvalidKeyException { + if (!KeyProperties.KEY_ALGORITHM_EC.equalsIgnoreCase(key.getAlgorithm())) { + throw new InvalidKeyException("Unsupported key algorithm: " + key.getAlgorithm() + + ". Only" + KeyProperties.KEY_ALGORITHM_EC + " supported"); + } + + long keySizeBits = -1; + for (Authorization a : key.getAuthorizations()) { + if (a.keyParameter.tag == KeymasterDefs.KM_TAG_KEY_SIZE) { + keySizeBits = KeyStore2ParameterUtils.getUnsignedInt(a); + } + } + + if (keySizeBits == -1) { + throw new InvalidKeyException("Size of key not known"); + } else if (keySizeBits > Integer.MAX_VALUE) { + throw new InvalidKeyException("Key too large: " + keySizeBits + " bits"); + } + mGroupSizeBits = (int) keySizeBits; + + super.initKey(key); + } + + @Override + protected final void resetAll() { + mGroupSizeBits = -1; + super.resetAll(); + } + + @Override + protected final void resetWhilePreservingInitState() { + super.resetWhilePreservingInitState(); + } + + @Override + protected final void addAlgorithmSpecificParametersToBegin( + @NonNull List<KeyParameter> parameters) { + parameters.add(KeyStore2ParameterUtils.makeEnum( + KeymasterDefs.KM_TAG_ALGORITHM, KeymasterDefs.KM_ALGORITHM_EC + )); + parameters.add(KeyStore2ParameterUtils.makeEnum( + KeymasterDefs.KM_TAG_DIGEST, mKeymasterDigest + )); + } + + @Override + protected final int getAdditionalEntropyAmountForSign() { + return (mGroupSizeBits + 7) / 8; + } + + protected final int getGroupSizeBits() { + if (mGroupSizeBits == -1) { + throw new IllegalStateException("Not initialized"); + } + return mGroupSizeBits; + } +} diff --git a/keystore/java/android/security/keystore2/AndroidKeyStoreECPrivateKey.java b/keystore/java/android/security/keystore2/AndroidKeyStoreECPrivateKey.java new file mode 100644 index 000000000000..35effde6234b --- /dev/null +++ b/keystore/java/android/security/keystore2/AndroidKeyStoreECPrivateKey.java @@ -0,0 +1,50 @@ +/* + * Copyright (C) 2015 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.keystore2; + +import android.annotation.NonNull; +import android.security.KeyStoreSecurityLevel; +import android.security.keystore.KeyProperties; +import android.system.keystore2.Authorization; +import android.system.keystore2.KeyDescriptor; + +import java.security.PrivateKey; +import java.security.interfaces.ECKey; +import java.security.spec.ECParameterSpec; + +/** + * EC private key (instance of {@link PrivateKey} and {@link ECKey}) backed by keystore. + * + * @hide + */ +public class AndroidKeyStoreECPrivateKey extends AndroidKeyStorePrivateKey implements ECKey { + private final ECParameterSpec mParams; + + public AndroidKeyStoreECPrivateKey(@NonNull KeyDescriptor descriptor, + long keyId, + Authorization[] authorizations, + @NonNull KeyStoreSecurityLevel securityLevel, + @NonNull ECParameterSpec params) { + super(descriptor, keyId, authorizations, KeyProperties.KEY_ALGORITHM_EC, securityLevel); + mParams = params; + } + + @Override + public ECParameterSpec getParams() { + return mParams; + } +} diff --git a/keystore/java/android/security/keystore2/AndroidKeyStoreECPublicKey.java b/keystore/java/android/security/keystore2/AndroidKeyStoreECPublicKey.java new file mode 100644 index 000000000000..6ddaa704afa8 --- /dev/null +++ b/keystore/java/android/security/keystore2/AndroidKeyStoreECPublicKey.java @@ -0,0 +1,74 @@ +/* + * Copyright (C) 2015 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.keystore2; + +import android.annotation.NonNull; +import android.security.KeyStoreSecurityLevel; +import android.security.keystore.KeyProperties; +import android.system.keystore2.KeyDescriptor; +import android.system.keystore2.KeyMetadata; + +import java.security.interfaces.ECPublicKey; +import java.security.spec.ECParameterSpec; +import java.security.spec.ECPoint; + +/** + * {@link ECPublicKey} backed by keystore. + * + * @hide + */ +public class AndroidKeyStoreECPublicKey extends AndroidKeyStorePublicKey implements ECPublicKey { + + private final ECParameterSpec mParams; + private final ECPoint mW; + + public AndroidKeyStoreECPublicKey(@NonNull KeyDescriptor descriptor, + @NonNull KeyMetadata metadata, + @NonNull KeyStoreSecurityLevel securityLevel, + @NonNull ECParameterSpec params, @NonNull ECPoint w) { + super(descriptor, metadata, KeyProperties.KEY_ALGORITHM_EC, securityLevel); + mParams = params; + mW = w; + } + + public AndroidKeyStoreECPublicKey(@NonNull KeyDescriptor descriptor, + @NonNull KeyMetadata metadata, + @NonNull KeyStoreSecurityLevel securityLevel, @NonNull ECPublicKey info) { + this(descriptor, metadata, securityLevel, info.getParams(), info.getW()); + if (!"X.509".equalsIgnoreCase(info.getFormat())) { + throw new IllegalArgumentException( + "Unsupported key export format: " + info.getFormat()); + } + } + + @Override + public AndroidKeyStorePrivateKey getPrivateKey() { + return new AndroidKeyStoreECPrivateKey( + getUserKeyDescriptor(), getKeyIdDescriptor().nspace, getAuthorizations(), + getSecurityLevel(), mParams); + } + + @Override + public ECParameterSpec getParams() { + return mParams; + } + + @Override + public ECPoint getW() { + return mW; + } +} diff --git a/keystore/java/android/security/keystore2/AndroidKeyStoreHmacSpi.java b/keystore/java/android/security/keystore2/AndroidKeyStoreHmacSpi.java new file mode 100644 index 000000000000..3dde2e592259 --- /dev/null +++ b/keystore/java/android/security/keystore2/AndroidKeyStoreHmacSpi.java @@ -0,0 +1,259 @@ +/* + * Copyright (C) 2015 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.keystore2; + +import android.security.KeyStoreException; +import android.security.KeyStoreOperation; +import android.security.keymaster.KeymasterDefs; +import android.security.keystore.KeyStoreCryptoOperation; +import android.security.keystore.KeymasterUtils; +import android.system.keystore2.KeyParameter; + +import java.security.InvalidAlgorithmParameterException; +import java.security.InvalidKeyException; +import java.security.Key; +import java.security.ProviderException; +import java.security.spec.AlgorithmParameterSpec; +import java.util.ArrayList; +import java.util.List; + +import javax.crypto.MacSpi; + +/** + * {@link MacSpi} which provides HMAC implementations backed by Android KeyStore. + * + * @hide + */ +public abstract class AndroidKeyStoreHmacSpi extends MacSpi implements KeyStoreCryptoOperation { + + private static final String TAG = "AndroidKeyStoreHmacSpi"; + + public static class HmacSHA1 extends AndroidKeyStoreHmacSpi { + public HmacSHA1() { + super(KeymasterDefs.KM_DIGEST_SHA1); + } + } + + public static class HmacSHA224 extends AndroidKeyStoreHmacSpi { + public HmacSHA224() { + super(KeymasterDefs.KM_DIGEST_SHA_2_224); + } + } + + public static class HmacSHA256 extends AndroidKeyStoreHmacSpi { + public HmacSHA256() { + super(KeymasterDefs.KM_DIGEST_SHA_2_256); + } + } + + public static class HmacSHA384 extends AndroidKeyStoreHmacSpi { + public HmacSHA384() { + super(KeymasterDefs.KM_DIGEST_SHA_2_384); + } + } + + public static class HmacSHA512 extends AndroidKeyStoreHmacSpi { + public HmacSHA512() { + super(KeymasterDefs.KM_DIGEST_SHA_2_512); + } + } + + private final int mKeymasterDigest; + private final int mMacSizeBits; + + // Fields below are populated by engineInit and should be preserved after engineDoFinal. + private AndroidKeyStoreSecretKey mKey; + + // Fields below are reset when engineDoFinal succeeds. + private KeyStoreCryptoOperationChunkedStreamer mChunkedStreamer; + private KeyStoreOperation mOperation; + private long mOperationChallenge; + + protected AndroidKeyStoreHmacSpi(int keymasterDigest) { + mKeymasterDigest = keymasterDigest; + mMacSizeBits = KeymasterUtils.getDigestOutputSizeBits(keymasterDigest); + mOperation = null; + mOperationChallenge = 0; + mKey = null; + mChunkedStreamer = null; + } + + @Override + protected int engineGetMacLength() { + return (mMacSizeBits + 7) / 8; + } + + @Override + protected void engineInit(Key key, AlgorithmParameterSpec params) throws InvalidKeyException, + InvalidAlgorithmParameterException { + resetAll(); + + boolean success = false; + try { + init(key, params); + ensureKeystoreOperationInitialized(); + success = true; + } finally { + if (!success) { + resetAll(); + } + } + } + + private void init(Key key, AlgorithmParameterSpec params) throws InvalidKeyException, + InvalidAlgorithmParameterException { + if (key == null) { + throw new InvalidKeyException("key == null"); + } else if (!(key instanceof AndroidKeyStoreSecretKey)) { + throw new InvalidKeyException( + "Only Android KeyStore secret keys supported. Key: " + key); + } + mKey = (AndroidKeyStoreSecretKey) key; + + if (params != null) { + throw new InvalidAlgorithmParameterException( + "Unsupported algorithm parameters: " + params); + } + + } + + private void abortOperation() { + KeyStoreCryptoOperationUtils.abortOperation(mOperation); + mOperation = null; + } + + private void resetAll() { + abortOperation(); + mOperationChallenge = 0; + mKey = null; + mChunkedStreamer = null; + } + + private void resetWhilePreservingInitState() { + abortOperation(); + mOperationChallenge = 0; + mChunkedStreamer = null; + } + + @Override + protected void engineReset() { + resetWhilePreservingInitState(); + } + + private void ensureKeystoreOperationInitialized() throws InvalidKeyException { + if (mChunkedStreamer != null) { + return; + } + if (mKey == null) { + throw new IllegalStateException("Not initialized"); + } + + List<KeyParameter> parameters = new ArrayList<>(); + parameters.add(KeyStore2ParameterUtils.makeEnum( + KeymasterDefs.KM_TAG_ALGORITHM, KeymasterDefs.KM_ALGORITHM_HMAC + )); + parameters.add(KeyStore2ParameterUtils.makeEnum( + KeymasterDefs.KM_TAG_DIGEST, mKeymasterDigest + )); + parameters.add(KeyStore2ParameterUtils.makeInt( + KeymasterDefs.KM_TAG_MAC_LENGTH, mMacSizeBits + )); + + try { + mOperation = mKey.getSecurityLevel().createOperation( + mKey.getKeyIdDescriptor(), + parameters + ); + } catch (KeyStoreException keyStoreException) { + // If necessary, throw an exception due to KeyStore operation having failed. + InvalidKeyException e = KeyStoreCryptoOperationUtils.getInvalidKeyException( + mKey, keyStoreException); + if (e != null) { + throw e; + } + } + + // Now we check if we got an operation challenge. This indicates that user authorization + // is required. And if we got a challenge we check if the authorization can possibly + // succeed. + mOperationChallenge = KeyStoreCryptoOperationUtils.getOrMakeOperationChallenge( + mOperation, mKey); + + mChunkedStreamer = new KeyStoreCryptoOperationChunkedStreamer( + new KeyStoreCryptoOperationChunkedStreamer.MainDataStream( + mOperation)); + } + + @Override + protected void engineUpdate(byte input) { + engineUpdate(new byte[] {input}, 0, 1); + } + + @Override + protected void engineUpdate(byte[] input, int offset, int len) { + try { + ensureKeystoreOperationInitialized(); + } catch (InvalidKeyException e) { + throw new ProviderException("Failed to reinitialize MAC", e); + } + + byte[] output; + try { + output = mChunkedStreamer.update(input, offset, len); + } catch (KeyStoreException e) { + throw new ProviderException("Keystore operation failed", e); + } + if ((output != null) && (output.length != 0)) { + throw new ProviderException("Update operation unexpectedly produced output"); + } + } + + @Override + protected byte[] engineDoFinal() { + try { + ensureKeystoreOperationInitialized(); + } catch (InvalidKeyException e) { + throw new ProviderException("Failed to reinitialize MAC", e); + } + + byte[] result; + try { + result = mChunkedStreamer.doFinal( + null, 0, 0, + null); // no signature provided -- this invocation will generate one + } catch (KeyStoreException e) { + throw new ProviderException("Keystore operation failed", e); + } + + resetWhilePreservingInitState(); + return result; + } + + @Override + public void finalize() throws Throwable { + try { + abortOperation(); + } finally { + super.finalize(); + } + } + + @Override + public long getOperationHandle() { + return mOperationChallenge; + } +} diff --git a/keystore/java/android/security/keystore2/AndroidKeyStoreKey.java b/keystore/java/android/security/keystore2/AndroidKeyStoreKey.java new file mode 100644 index 000000000000..32650aeda1b1 --- /dev/null +++ b/keystore/java/android/security/keystore2/AndroidKeyStoreKey.java @@ -0,0 +1,141 @@ +/* + * Copyright (C) 2015 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.keystore2; + +import android.annotation.NonNull; +import android.security.KeyStoreSecurityLevel; +import android.system.keystore2.Authorization; +import android.system.keystore2.Domain; +import android.system.keystore2.KeyDescriptor; +import android.util.Log; + +import java.security.Key; + +/** + * {@link Key} backed by Android Keystore. + * + * @hide + */ +public class AndroidKeyStoreKey implements Key { + // This is the original KeyDescriptor by which the key was loaded from + // with alias and domain. + private final KeyDescriptor mDescriptor; + // The key id can be used make certain manipulations to the keystore database + // assuring that the manipulation is made to the exact key that was loaded + // from the database. Alias based manipulations can not assure this, because + // aliases can be rebound to other keys at any time. + private final long mKeyId; + private final Authorization[] mAuthorizations; + // TODO extract algorithm string from metadata. + private final String mAlgorithm; + + // This is the security level interface, that this key is associated with. + // We do not include this member in comparisons. + private final KeyStoreSecurityLevel mSecurityLevel; + + AndroidKeyStoreKey(@NonNull KeyDescriptor descriptor, + long keyId, + @NonNull Authorization[] authorizations, + @NonNull String algorithm, + @NonNull KeyStoreSecurityLevel securityLevel) { + mDescriptor = descriptor; + mKeyId = keyId; + mAuthorizations = authorizations; + mAlgorithm = algorithm; + mSecurityLevel = securityLevel; + } + + KeyDescriptor getUserKeyDescriptor() { + return mDescriptor; + } + + KeyDescriptor getKeyIdDescriptor() { + KeyDescriptor descriptor = new KeyDescriptor(); + descriptor.nspace = mKeyId; + descriptor.domain = Domain.KEY_ID; + descriptor.alias = null; + descriptor.blob = null; + return descriptor; + } + + Authorization[] getAuthorizations() { + return mAuthorizations; + } + + KeyStoreSecurityLevel getSecurityLevel() { + return mSecurityLevel; + } + + + @Override + public String getAlgorithm() { + return mAlgorithm; + } + + @Override + public String getFormat() { + // This key does not export its key material + return null; + } + + @Override + public byte[] getEncoded() { + // This key does not export its key material + return null; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + + result = prime * result + ((mDescriptor == null) ? 0 : mDescriptor.hashCode()); + result = prime * result + (int) (mKeyId >>> 32); + result = prime * result + (int) (mKeyId & 0xffffffff); + result = prime * result + ((mAuthorizations == null) ? 0 : mAuthorizations.hashCode()); + result = prime * result + ((mAlgorithm == null) ? 0 : mAlgorithm.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + AndroidKeyStoreKey other = (AndroidKeyStoreKey) obj; + if (mKeyId != other.mKeyId) { + return false; + } + + // If the key ids are equal and the class matches all the other fields cannot differ + // unless we have a bug. + if (!mAlgorithm.equals(other.mAlgorithm) + || !mAuthorizations.equals(other.mAuthorizations) + || !mDescriptor.equals(other.mDescriptor)) { + Log.e("AndroidKeyStoreKey", "Bug: key ids are identical, but key metadata" + + "differs."); + return false; + } + return true; + } +} diff --git a/keystore/java/android/security/keystore2/AndroidKeyStoreKeyFactorySpi.java b/keystore/java/android/security/keystore2/AndroidKeyStoreKeyFactorySpi.java new file mode 100644 index 000000000000..a8dd7f3f8b14 --- /dev/null +++ b/keystore/java/android/security/keystore2/AndroidKeyStoreKeyFactorySpi.java @@ -0,0 +1,144 @@ +/* + * Copyright (C) 2015 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.keystore2; + +import android.security.KeyStore; +import android.security.keystore.KeyGenParameterSpec; +import android.security.keystore.KeyInfo; + +import java.security.InvalidKeyException; +import java.security.Key; +import java.security.KeyFactorySpi; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.spec.ECPublicKeySpec; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.KeySpec; +import java.security.spec.PKCS8EncodedKeySpec; +import java.security.spec.RSAPublicKeySpec; +import java.security.spec.X509EncodedKeySpec; + +/** + * {@link KeyFactorySpi} backed by Android KeyStore. + * + * @hide + */ +public class AndroidKeyStoreKeyFactorySpi extends KeyFactorySpi { + + private final KeyStore mKeyStore = KeyStore.getInstance(); + + @Override + protected <T extends KeySpec> T engineGetKeySpec(Key key, Class<T> keySpecClass) + throws InvalidKeySpecException { + if (key == null) { + throw new InvalidKeySpecException("key == null"); + } else if ((!(key instanceof AndroidKeyStorePrivateKey)) + && (!(key instanceof AndroidKeyStorePublicKey))) { + throw new InvalidKeySpecException( + "Unsupported key type: " + key.getClass().getName() + + ". This KeyFactory supports only Android Keystore asymmetric keys"); + } + + // key is an Android Keystore private or public key + + if (keySpecClass == null) { + throw new InvalidKeySpecException("keySpecClass == null"); + } else if (KeyInfo.class.equals(keySpecClass)) { + if (!(key instanceof AndroidKeyStorePrivateKey)) { + throw new InvalidKeySpecException( + "Unsupported key type: " + key.getClass().getName() + + ". KeyInfo can be obtained only for Android Keystore private keys"); + } + AndroidKeyStorePrivateKey keystorePrivateKey = (AndroidKeyStorePrivateKey) key; + @SuppressWarnings("unchecked") + T result = (T) AndroidKeyStoreSecretKeyFactorySpi.getKeyInfo(keystorePrivateKey); + return result; + } else if (X509EncodedKeySpec.class.equals(keySpecClass)) { + if (!(key instanceof AndroidKeyStorePublicKey)) { + throw new InvalidKeySpecException( + "Unsupported key type: " + key.getClass().getName() + + ". X509EncodedKeySpec can be obtained only for Android Keystore public" + + " keys"); + } + @SuppressWarnings("unchecked") + T result = (T) new X509EncodedKeySpec(((AndroidKeyStorePublicKey) key).getEncoded()); + return result; + } else if (PKCS8EncodedKeySpec.class.equals(keySpecClass)) { + if (key instanceof AndroidKeyStorePrivateKey) { + throw new InvalidKeySpecException( + "Key material export of Android Keystore private keys is not supported"); + } else { + throw new InvalidKeySpecException( + "Cannot export key material of public key in PKCS#8 format." + + " Only X.509 format (X509EncodedKeySpec) supported for public keys."); + } + } else if (RSAPublicKeySpec.class.equals(keySpecClass)) { + if (key instanceof AndroidKeyStoreRSAPublicKey) { + AndroidKeyStoreRSAPublicKey rsaKey = (AndroidKeyStoreRSAPublicKey) key; + @SuppressWarnings("unchecked") + T result = + (T) new RSAPublicKeySpec(rsaKey.getModulus(), rsaKey.getPublicExponent()); + return result; + } else { + throw new InvalidKeySpecException( + "Obtaining RSAPublicKeySpec not supported for " + key.getAlgorithm() + " " + + ((key instanceof AndroidKeyStorePrivateKey) ? "private" : "public") + + " key"); + } + } else if (ECPublicKeySpec.class.equals(keySpecClass)) { + if (key instanceof AndroidKeyStoreECPublicKey) { + AndroidKeyStoreECPublicKey ecKey = (AndroidKeyStoreECPublicKey) key; + @SuppressWarnings("unchecked") + T result = (T) new ECPublicKeySpec(ecKey.getW(), ecKey.getParams()); + return result; + } else { + throw new InvalidKeySpecException( + "Obtaining ECPublicKeySpec not supported for " + key.getAlgorithm() + " " + + ((key instanceof AndroidKeyStorePrivateKey) ? "private" : "public") + + " key"); + } + } else { + throw new InvalidKeySpecException("Unsupported key spec: " + keySpecClass.getName()); + } + } + + @Override + protected PrivateKey engineGeneratePrivate(KeySpec spec) throws InvalidKeySpecException { + throw new InvalidKeySpecException( + "To generate a key pair in Android Keystore, use KeyPairGenerator initialized with" + + " " + KeyGenParameterSpec.class.getName()); + } + + @Override + protected PublicKey engineGeneratePublic(KeySpec spec) throws InvalidKeySpecException { + throw new InvalidKeySpecException( + "To generate a key pair in Android Keystore, use KeyPairGenerator initialized with" + + " " + KeyGenParameterSpec.class.getName()); + } + + @Override + protected Key engineTranslateKey(Key key) throws InvalidKeyException { + if (key == null) { + throw new InvalidKeyException("key == null"); + } else if ((!(key instanceof AndroidKeyStorePrivateKey)) + && (!(key instanceof AndroidKeyStorePublicKey))) { + throw new InvalidKeyException( + "To import a key into Android Keystore, use KeyStore.setEntry"); + } + return key; + } +} diff --git a/keystore/java/android/security/keystore2/AndroidKeyStoreKeyGeneratorSpi.java b/keystore/java/android/security/keystore2/AndroidKeyStoreKeyGeneratorSpi.java new file mode 100644 index 000000000000..ccd0a4bf92ff --- /dev/null +++ b/keystore/java/android/security/keystore2/AndroidKeyStoreKeyGeneratorSpi.java @@ -0,0 +1,428 @@ +/* + * Copyright (C) 2015 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.keystore2; + +import android.security.KeyStore2; +import android.security.KeyStoreSecurityLevel; +import android.security.keymaster.KeymasterArguments; +import android.security.keymaster.KeymasterDefs; +import android.security.keystore.ArrayUtils; +import android.security.keystore.KeyGenParameterSpec; +import android.security.keystore.KeyProperties; +import android.security.keystore.KeymasterUtils; +import android.security.keystore.StrongBoxUnavailableException; +import android.system.keystore2.Domain; +import android.system.keystore2.IKeystoreSecurityLevel; +import android.system.keystore2.KeyDescriptor; +import android.system.keystore2.KeyMetadata; +import android.system.keystore2.KeyParameter; +import android.system.keystore2.SecurityLevel; +import android.util.Log; + +import libcore.util.EmptyArray; + +import java.security.InvalidAlgorithmParameterException; +import java.security.ProviderException; +import java.security.SecureRandom; +import java.security.spec.AlgorithmParameterSpec; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import javax.crypto.KeyGeneratorSpi; +import javax.crypto.SecretKey; + +/** + * {@link KeyGeneratorSpi} backed by Android KeyStore. + * + * @hide + */ +public abstract class AndroidKeyStoreKeyGeneratorSpi extends KeyGeneratorSpi { + private static final String TAG = "AndroidKeyStoreKeyGeneratorSpi"; + + public static class AES extends AndroidKeyStoreKeyGeneratorSpi { + public AES() { + super(KeymasterDefs.KM_ALGORITHM_AES, 128); + } + + @Override + protected void engineInit(AlgorithmParameterSpec params, SecureRandom random) + throws InvalidAlgorithmParameterException { + super.engineInit(params, random); + if ((mKeySizeBits != 128) && (mKeySizeBits != 192) && (mKeySizeBits != 256)) { + throw new InvalidAlgorithmParameterException( + "Unsupported key size: " + mKeySizeBits + + ". Supported: 128, 192, 256."); + } + } + } + + 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, + keymasterDigest, + KeymasterUtils.getDigestOutputSizeBits(keymasterDigest)); + } + } + + public static class HmacSHA1 extends HmacBase { + public HmacSHA1() { + super(KeymasterDefs.KM_DIGEST_SHA1); + } + } + + public static class HmacSHA224 extends HmacBase { + public HmacSHA224() { + super(KeymasterDefs.KM_DIGEST_SHA_2_224); + } + } + + public static class HmacSHA256 extends HmacBase { + public HmacSHA256() { + super(KeymasterDefs.KM_DIGEST_SHA_2_256); + } + } + + public static class HmacSHA384 extends HmacBase { + public HmacSHA384() { + super(KeymasterDefs.KM_DIGEST_SHA_2_384); + } + } + + public static class HmacSHA512 extends HmacBase { + public HmacSHA512() { + super(KeymasterDefs.KM_DIGEST_SHA_2_512); + } + } + + private final KeyStore2 mKeyStore = KeyStore2.getInstance(); + private final int mKeymasterAlgorithm; + private final int mKeymasterDigest; + private final int mDefaultKeySizeBits; + + private KeyGenParameterSpec mSpec; + private SecureRandom mRng; + + protected int mKeySizeBits; + private int[] mKeymasterPurposes; + private int[] mKeymasterBlockModes; + private int[] mKeymasterPaddings; + private int[] mKeymasterDigests; + + protected AndroidKeyStoreKeyGeneratorSpi( + int keymasterAlgorithm, + int defaultKeySizeBits) { + this(keymasterAlgorithm, -1, defaultKeySizeBits); + } + + protected AndroidKeyStoreKeyGeneratorSpi( + int keymasterAlgorithm, + int keymasterDigest, + int defaultKeySizeBits) { + mKeymasterAlgorithm = keymasterAlgorithm; + mKeymasterDigest = keymasterDigest; + mDefaultKeySizeBits = defaultKeySizeBits; + if (mDefaultKeySizeBits <= 0) { + throw new IllegalArgumentException("Default key size must be positive"); + } + + if ((mKeymasterAlgorithm == KeymasterDefs.KM_ALGORITHM_HMAC) && (mKeymasterDigest == -1)) { + throw new IllegalArgumentException( + "Digest algorithm must be specified for HMAC key"); + } + } + + @Override + protected void engineInit(SecureRandom random) { + throw new UnsupportedOperationException("Cannot initialize without a " + + KeyGenParameterSpec.class.getName() + " parameter"); + } + + @Override + protected void engineInit(int keySize, SecureRandom random) { + throw new UnsupportedOperationException("Cannot initialize without a " + + KeyGenParameterSpec.class.getName() + " parameter"); + } + + @Override + protected void engineInit(AlgorithmParameterSpec params, SecureRandom random) + throws InvalidAlgorithmParameterException { + resetAll(); + + boolean success = false; + try { + if ((params == null) || (!(params instanceof KeyGenParameterSpec))) { + throw new InvalidAlgorithmParameterException("Cannot initialize without a " + + KeyGenParameterSpec.class.getName() + " parameter"); + } + KeyGenParameterSpec spec = (KeyGenParameterSpec) params; + if (spec.getKeystoreAlias() == null) { + throw new InvalidAlgorithmParameterException("KeyStore entry alias not provided"); + } + + mRng = random; + mSpec = spec; + + mKeySizeBits = (spec.getKeySize() != -1) ? spec.getKeySize() : mDefaultKeySizeBits; + if (mKeySizeBits <= 0) { + throw new InvalidAlgorithmParameterException( + "Key size must be positive: " + mKeySizeBits); + } else if ((mKeySizeBits % 8) != 0) { + throw new InvalidAlgorithmParameterException( + "Key size must be a multiple of 8: " + mKeySizeBits); + } + + try { + mKeymasterPurposes = KeyProperties.Purpose.allToKeymaster(spec.getPurposes()); + mKeymasterPaddings = KeyProperties.EncryptionPadding.allToKeymaster( + spec.getEncryptionPaddings()); + if (spec.getSignaturePaddings().length > 0) { + throw new InvalidAlgorithmParameterException( + "Signature paddings not supported for symmetric key algorithms"); + } + mKeymasterBlockModes = KeyProperties.BlockMode.allToKeymaster(spec.getBlockModes()); + if (((spec.getPurposes() & KeyProperties.PURPOSE_ENCRYPT) != 0) + && (spec.isRandomizedEncryptionRequired())) { + for (int keymasterBlockMode : mKeymasterBlockModes) { + if (!KeymasterUtils.isKeymasterBlockModeIndCpaCompatibleWithSymmetricCrypto( + keymasterBlockMode)) { + throw new InvalidAlgorithmParameterException( + "Randomized encryption (IND-CPA) required but may be violated" + + " by block mode: " + + KeyProperties.BlockMode.fromKeymaster(keymasterBlockMode) + + ". See " + KeyGenParameterSpec.class.getName() + + " documentation."); + } + } + } + if (mKeymasterAlgorithm == KeymasterDefs.KM_ALGORITHM_3DES) { + if (mKeySizeBits != 168) { + throw new InvalidAlgorithmParameterException( + "3DES key size must be 168 bits."); + } + } + if (mKeymasterAlgorithm == KeymasterDefs.KM_ALGORITHM_HMAC) { + if (mKeySizeBits < 64 || mKeySizeBits > 512) { + throw new InvalidAlgorithmParameterException( + "HMAC key sizes must be within 64-512 bits, inclusive."); + } + + // JCA HMAC key algorithm implies a digest (e.g., HmacSHA256 key algorithm + // implies SHA-256 digest). Because keymaster HMAC key is authorized only for + // one digest, we don't let algorithm parameter spec override the digest implied + // by the key. If the spec specifies digests at all, it must specify only one + // digest, the only implied by key algorithm. + mKeymasterDigests = new int[] {mKeymasterDigest}; + if (spec.isDigestsSpecified()) { + // Digest(s) explicitly specified in the spec. Check that the list + // consists of exactly one digest, the one implied by key algorithm. + int[] keymasterDigestsFromSpec = + KeyProperties.Digest.allToKeymaster(spec.getDigests()); + if ((keymasterDigestsFromSpec.length != 1) + || (keymasterDigestsFromSpec[0] != mKeymasterDigest)) { + throw new InvalidAlgorithmParameterException( + "Unsupported digests specification: " + + Arrays.asList(spec.getDigests()) + ". Only " + + KeyProperties.Digest.fromKeymaster(mKeymasterDigest) + + " supported for this HMAC key algorithm"); + } + } + } else { + // Key algorithm does not imply a digest. + if (spec.isDigestsSpecified()) { + mKeymasterDigests = KeyProperties.Digest.allToKeymaster(spec.getDigests()); + } else { + mKeymasterDigests = EmptyArray.INT; + } + } + + // Check that user authentication related parameters are acceptable. This method + // will throw an IllegalStateException if there are issues (e.g., secure lock screen + // not set up). + KeymasterUtils.addUserAuthArgs(new KeymasterArguments(), spec); + } catch (IllegalStateException | IllegalArgumentException e) { + throw new InvalidAlgorithmParameterException(e); + } + + success = true; + } finally { + if (!success) { + resetAll(); + } + } + } + + private void resetAll() { + mSpec = null; + mRng = null; + mKeySizeBits = -1; + mKeymasterPurposes = null; + mKeymasterPaddings = null; + mKeymasterBlockModes = null; + } + + @Override + protected SecretKey engineGenerateKey() { + KeyGenParameterSpec spec = mSpec; + if (spec == null) { + throw new IllegalStateException("Not initialized"); + } + + List<KeyParameter> params = new ArrayList<>(); + + params.add(KeyStore2ParameterUtils.makeInt( + KeymasterDefs.KM_TAG_KEY_SIZE, mKeySizeBits + )); + params.add(KeyStore2ParameterUtils.makeEnum( + KeymasterDefs.KM_TAG_ALGORITHM, mKeymasterAlgorithm + )); + ArrayUtils.forEach(mKeymasterPurposes, (purpose) -> { + params.add(KeyStore2ParameterUtils.makeEnum( + KeymasterDefs.KM_TAG_PURPOSE, purpose + )); + }); + ArrayUtils.forEach(mKeymasterBlockModes, (blockMode) -> { + if (blockMode == KeymasterDefs.KM_MODE_GCM + && mKeymasterAlgorithm == KeymasterDefs.KM_ALGORITHM_AES) { + params.add(KeyStore2ParameterUtils.makeInt( + KeymasterDefs.KM_TAG_MIN_MAC_LENGTH, + AndroidKeyStoreAuthenticatedAESCipherSpi.GCM + .MIN_SUPPORTED_TAG_LENGTH_BITS + )); + } + params.add(KeyStore2ParameterUtils.makeEnum( + KeymasterDefs.KM_TAG_BLOCK_MODE, blockMode + )); + }); + ArrayUtils.forEach(mKeymasterPaddings, (padding) -> { + params.add(KeyStore2ParameterUtils.makeEnum( + KeymasterDefs.KM_TAG_PADDING, padding + )); + }); + ArrayUtils.forEach(mKeymasterDigests, (digest) -> { + params.add(KeyStore2ParameterUtils.makeEnum( + KeymasterDefs.KM_TAG_DIGEST, digest + )); + }); + + if (mKeymasterAlgorithm == KeymasterDefs.KM_ALGORITHM_HMAC + && mKeymasterDigests.length != 0) { + int digestOutputSizeBits = KeymasterUtils.getDigestOutputSizeBits(mKeymasterDigests[0]); + if (digestOutputSizeBits == -1) { + throw new ProviderException( + "HMAC key authorized for unsupported digest: " + + KeyProperties.Digest.fromKeymaster(mKeymasterDigests[0])); + } + params.add(KeyStore2ParameterUtils.makeInt( + KeymasterDefs.KM_TAG_MIN_MAC_LENGTH, digestOutputSizeBits + )); + } + + KeyStore2ParameterUtils.addUserAuthArgs(params, spec); + + if (spec.getKeyValidityStart() != null) { + params.add(KeyStore2ParameterUtils.makeDate( + KeymasterDefs.KM_TAG_ACTIVE_DATETIME, spec.getKeyValidityStart() + )); + } + if (spec.getKeyValidityForOriginationEnd() != null) { + params.add(KeyStore2ParameterUtils.makeDate( + KeymasterDefs.KM_TAG_ORIGINATION_EXPIRE_DATETIME, + spec.getKeyValidityForOriginationEnd() + )); + } + if (spec.getKeyValidityForConsumptionEnd() != null) { + params.add(KeyStore2ParameterUtils.makeDate( + KeymasterDefs.KM_TAG_USAGE_EXPIRE_DATETIME, + spec.getKeyValidityForConsumptionEnd() + )); + } + + if (((spec.getPurposes() & KeyProperties.PURPOSE_ENCRYPT) != 0) + && (!spec.isRandomizedEncryptionRequired())) { + // Permit caller-provided IV when encrypting with this key + params.add(KeyStore2ParameterUtils.makeBool( + KeymasterDefs.KM_TAG_CALLER_NONCE + )); + } + + byte[] additionalEntropy = + KeyStoreCryptoOperationUtils.getRandomBytesToMixIntoKeystoreRng( + mRng, (mKeySizeBits + 7) / 8); + + @SecurityLevel int securityLevel = SecurityLevel.TRUSTED_ENVIRONMENT; + if (spec.isStrongBoxBacked()) { + securityLevel = SecurityLevel.STRONGBOX; + } + + int flags = 0; + if (spec.isCriticalToDeviceEncryption()) { + flags |= IKeystoreSecurityLevel.KEY_FLAG_AUTH_BOUND_WITHOUT_CRYPTOGRAPHIC_LSKF_BINDING; + } + + KeyDescriptor descriptor = new KeyDescriptor(); + descriptor.alias = spec.getKeystoreAlias(); + descriptor.nspace = spec.getNamespace(); + descriptor.domain = descriptor.nspace == KeyProperties.NAMESPACE_APPLICATION + ? Domain.APP + : Domain.SELINUX; + descriptor.blob = null; + + KeyMetadata metadata = null; + KeyStoreSecurityLevel iSecurityLevel = null; + try { + iSecurityLevel = mKeyStore.getSecurityLevel(securityLevel); + metadata = iSecurityLevel.generateKey( + descriptor, + null, /* Attestation key not applicable to symmetric keys. */ + params, + flags, + additionalEntropy); + } catch (android.security.KeyStoreException e) { + switch (e.getErrorCode()) { + // TODO replace with ErrorCode.HARDWARE_TYPE_UNAVAILABLE when KeyMint spec + // becomes available. + case KeymasterDefs.KM_ERROR_HARDWARE_TYPE_UNAVAILABLE: + throw new StrongBoxUnavailableException("Failed to generate key"); + default: + throw new ProviderException("Keystore key generation failed", e); + } + } + @KeyProperties.KeyAlgorithmEnum String keyAlgorithmJCA; + try { + keyAlgorithmJCA = KeyProperties.KeyAlgorithm.fromKeymasterSecretKeyAlgorithm( + mKeymasterAlgorithm, mKeymasterDigest); + } catch (IllegalArgumentException e) { + try { + mKeyStore.deleteKey(descriptor); + } catch (android.security.KeyStoreException kse) { + Log.e(TAG, "Failed to delete key after generating successfully but" + + " failed to get the algorithm string.", kse); + } + throw new ProviderException("Failed to obtain JCA secret key algorithm name", e); + } + SecretKey result = new AndroidKeyStoreSecretKey(descriptor, metadata, keyAlgorithmJCA, + iSecurityLevel); + return result; + } +} diff --git a/keystore/java/android/security/keystore2/AndroidKeyStoreKeyPairGeneratorSpi.java b/keystore/java/android/security/keystore2/AndroidKeyStoreKeyPairGeneratorSpi.java new file mode 100644 index 000000000000..a747a0e727d8 --- /dev/null +++ b/keystore/java/android/security/keystore2/AndroidKeyStoreKeyPairGeneratorSpi.java @@ -0,0 +1,804 @@ +/* + * Copyright (C) 2012 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.keystore2; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.os.Build; +import android.security.KeyPairGeneratorSpec; +import android.security.KeyStore2; +import android.security.KeyStoreException; +import android.security.KeyStoreSecurityLevel; +import android.security.keymaster.KeymasterArguments; +import android.security.keymaster.KeymasterDefs; +import android.security.keystore.ArrayUtils; +import android.security.keystore.KeyGenParameterSpec; +import android.security.keystore.KeyProperties; +import android.security.keystore.KeymasterUtils; +import android.security.keystore.SecureKeyImportUnavailableException; +import android.security.keystore.StrongBoxUnavailableException; +import android.system.keystore2.Domain; +import android.system.keystore2.IKeystoreSecurityLevel; +import android.system.keystore2.KeyDescriptor; +import android.system.keystore2.KeyMetadata; +import android.system.keystore2.KeyParameter; +import android.system.keystore2.ResponseCode; +import android.system.keystore2.SecurityLevel; +import android.util.Log; + +import libcore.util.EmptyArray; + +import java.math.BigInteger; +import java.nio.charset.StandardCharsets; +import java.security.InvalidAlgorithmParameterException; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.KeyPairGeneratorSpi; +import java.security.ProviderException; +import java.security.SecureRandom; +import java.security.UnrecoverableKeyException; +import java.security.spec.AlgorithmParameterSpec; +import java.security.spec.ECGenParameterSpec; +import java.security.spec.RSAKeyGenParameterSpec; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Set; + +/** + * Provides a way to create instances of a KeyPair which will be placed in the + * Android keystore service usable only by the application that called it. This + * can be used in conjunction with + * {@link java.security.KeyStore#getInstance(String)} using the + * {@code "AndroidKeyStore"} type. + * <p> + * This class can not be directly instantiated and must instead be used via the + * {@link KeyPairGenerator#getInstance(String) + * KeyPairGenerator.getInstance("AndroidKeyStore")} API. + * + * @hide + */ +public abstract class AndroidKeyStoreKeyPairGeneratorSpi extends KeyPairGeneratorSpi { + private static final String TAG = "AndroidKeyStoreKeyPairGeneratorSpi"; + + public static class RSA extends AndroidKeyStoreKeyPairGeneratorSpi { + public RSA() { + super(KeymasterDefs.KM_ALGORITHM_RSA); + } + } + + public static class EC extends AndroidKeyStoreKeyPairGeneratorSpi { + public EC() { + super(KeymasterDefs.KM_ALGORITHM_EC); + } + } + + /* + * These must be kept in sync with system/security/keystore/defaults.h + */ + + /* EC */ + private static final int EC_DEFAULT_KEY_SIZE = 256; + + /* RSA */ + private static final int RSA_DEFAULT_KEY_SIZE = 2048; + private static final int RSA_MIN_KEY_SIZE = 512; + private static final int RSA_MAX_KEY_SIZE = 8192; + + private static final Map<String, Integer> SUPPORTED_EC_NIST_CURVE_NAME_TO_SIZE = + new HashMap<String, Integer>(); + private static final List<String> SUPPORTED_EC_NIST_CURVE_NAMES = new ArrayList<String>(); + private static final List<Integer> SUPPORTED_EC_NIST_CURVE_SIZES = new ArrayList<Integer>(); + static { + // Aliases for NIST P-224 + SUPPORTED_EC_NIST_CURVE_NAME_TO_SIZE.put("p-224", 224); + SUPPORTED_EC_NIST_CURVE_NAME_TO_SIZE.put("secp224r1", 224); + + + // Aliases for NIST P-256 + SUPPORTED_EC_NIST_CURVE_NAME_TO_SIZE.put("p-256", 256); + SUPPORTED_EC_NIST_CURVE_NAME_TO_SIZE.put("secp256r1", 256); + SUPPORTED_EC_NIST_CURVE_NAME_TO_SIZE.put("prime256v1", 256); + + // Aliases for NIST P-384 + SUPPORTED_EC_NIST_CURVE_NAME_TO_SIZE.put("p-384", 384); + SUPPORTED_EC_NIST_CURVE_NAME_TO_SIZE.put("secp384r1", 384); + + // Aliases for NIST P-521 + SUPPORTED_EC_NIST_CURVE_NAME_TO_SIZE.put("p-521", 521); + SUPPORTED_EC_NIST_CURVE_NAME_TO_SIZE.put("secp521r1", 521); + + SUPPORTED_EC_NIST_CURVE_NAMES.addAll(SUPPORTED_EC_NIST_CURVE_NAME_TO_SIZE.keySet()); + Collections.sort(SUPPORTED_EC_NIST_CURVE_NAMES); + + SUPPORTED_EC_NIST_CURVE_SIZES.addAll( + new HashSet<Integer>(SUPPORTED_EC_NIST_CURVE_NAME_TO_SIZE.values())); + Collections.sort(SUPPORTED_EC_NIST_CURVE_SIZES); + } + + private final int mOriginalKeymasterAlgorithm; + + private KeyStore2 mKeyStore; + + private KeyGenParameterSpec mSpec; + + private String mEntryAlias; + private int mEntryUid; + private @KeyProperties.KeyAlgorithmEnum String mJcaKeyAlgorithm; + private int mKeymasterAlgorithm = -1; + private int mKeySizeBits; + private SecureRandom mRng; + + private int[] mKeymasterPurposes; + private int[] mKeymasterBlockModes; + private int[] mKeymasterEncryptionPaddings; + private int[] mKeymasterSignaturePaddings; + private int[] mKeymasterDigests; + + private Long mRSAPublicExponent; + + protected AndroidKeyStoreKeyPairGeneratorSpi(int keymasterAlgorithm) { + mOriginalKeymasterAlgorithm = keymasterAlgorithm; + } + + @SuppressWarnings("deprecation") + @Override + public void initialize(int keysize, SecureRandom random) { + throw new IllegalArgumentException( + KeyGenParameterSpec.class.getName() + " or " + KeyPairGeneratorSpec.class.getName() + + " required to initialize this KeyPairGenerator"); + } + + @SuppressWarnings("deprecation") + @Override + public void initialize(AlgorithmParameterSpec params, SecureRandom random) + throws InvalidAlgorithmParameterException { + resetAll(); + + boolean success = false; + try { + if (params == null) { + throw new InvalidAlgorithmParameterException( + "Must supply params of type " + KeyGenParameterSpec.class.getName() + + " or " + KeyPairGeneratorSpec.class.getName()); + } + + KeyGenParameterSpec spec; + boolean encryptionAtRestRequired = false; + int keymasterAlgorithm = mOriginalKeymasterAlgorithm; + if (params instanceof KeyGenParameterSpec) { + spec = (KeyGenParameterSpec) params; + } else if (params instanceof KeyPairGeneratorSpec) { + // Legacy/deprecated spec + KeyPairGeneratorSpec legacySpec = (KeyPairGeneratorSpec) params; + try { + KeyGenParameterSpec.Builder specBuilder; + String specKeyAlgorithm = legacySpec.getKeyType(); + if (specKeyAlgorithm != null) { + // Spec overrides the generator's default key algorithm + try { + keymasterAlgorithm = + KeyProperties.KeyAlgorithm.toKeymasterAsymmetricKeyAlgorithm( + specKeyAlgorithm); + } catch (IllegalArgumentException e) { + throw new InvalidAlgorithmParameterException( + "Invalid key type in parameters", e); + } + } + switch (keymasterAlgorithm) { + case KeymasterDefs.KM_ALGORITHM_EC: + specBuilder = new KeyGenParameterSpec.Builder( + legacySpec.getKeystoreAlias(), + KeyProperties.PURPOSE_SIGN + | KeyProperties.PURPOSE_VERIFY); + // Authorized to be used with any digest (including no digest). + // MD5 was never offered for Android Keystore for ECDSA. + specBuilder.setDigests( + KeyProperties.DIGEST_NONE, + KeyProperties.DIGEST_SHA1, + KeyProperties.DIGEST_SHA224, + KeyProperties.DIGEST_SHA256, + KeyProperties.DIGEST_SHA384, + KeyProperties.DIGEST_SHA512); + break; + case KeymasterDefs.KM_ALGORITHM_RSA: + specBuilder = new KeyGenParameterSpec.Builder( + legacySpec.getKeystoreAlias(), + KeyProperties.PURPOSE_ENCRYPT + | KeyProperties.PURPOSE_DECRYPT + | KeyProperties.PURPOSE_SIGN + | KeyProperties.PURPOSE_VERIFY); + // Authorized to be used with any digest (including no digest). + specBuilder.setDigests( + KeyProperties.DIGEST_NONE, + KeyProperties.DIGEST_MD5, + KeyProperties.DIGEST_SHA1, + KeyProperties.DIGEST_SHA224, + KeyProperties.DIGEST_SHA256, + KeyProperties.DIGEST_SHA384, + KeyProperties.DIGEST_SHA512); + // Authorized to be used with any encryption and signature padding + // schemes (including no padding). + specBuilder.setEncryptionPaddings( + KeyProperties.ENCRYPTION_PADDING_NONE, + KeyProperties.ENCRYPTION_PADDING_RSA_PKCS1, + KeyProperties.ENCRYPTION_PADDING_RSA_OAEP); + specBuilder.setSignaturePaddings( + KeyProperties.SIGNATURE_PADDING_RSA_PKCS1, + KeyProperties.SIGNATURE_PADDING_RSA_PSS); + // Disable randomized encryption requirement to support encryption + // padding NONE above. + specBuilder.setRandomizedEncryptionRequired(false); + break; + default: + throw new ProviderException( + "Unsupported algorithm: " + mKeymasterAlgorithm); + } + + if (legacySpec.getKeySize() != -1) { + specBuilder.setKeySize(legacySpec.getKeySize()); + } + if (legacySpec.getAlgorithmParameterSpec() != null) { + specBuilder.setAlgorithmParameterSpec( + legacySpec.getAlgorithmParameterSpec()); + } + specBuilder.setCertificateSubject(legacySpec.getSubjectDN()); + specBuilder.setCertificateSerialNumber(legacySpec.getSerialNumber()); + specBuilder.setCertificateNotBefore(legacySpec.getStartDate()); + specBuilder.setCertificateNotAfter(legacySpec.getEndDate()); + specBuilder.setUserAuthenticationRequired(false); + + spec = specBuilder.build(); + } catch (NullPointerException | IllegalArgumentException e) { + throw new InvalidAlgorithmParameterException(e); + } + } else { + throw new InvalidAlgorithmParameterException( + "Unsupported params class: " + params.getClass().getName() + + ". Supported: " + KeyGenParameterSpec.class.getName() + + ", " + KeyPairGeneratorSpec.class.getName()); + } + + mEntryAlias = spec.getKeystoreAlias(); + mEntryUid = spec.getUid(); + mSpec = spec; + mKeymasterAlgorithm = keymasterAlgorithm; + mKeySizeBits = spec.getKeySize(); + initAlgorithmSpecificParameters(); + if (mKeySizeBits == -1) { + mKeySizeBits = getDefaultKeySize(keymasterAlgorithm); + } + checkValidKeySize(keymasterAlgorithm, mKeySizeBits, mSpec.isStrongBoxBacked()); + + if (spec.getKeystoreAlias() == null) { + throw new InvalidAlgorithmParameterException("KeyStore entry alias not provided"); + } + + String jcaKeyAlgorithm; + try { + jcaKeyAlgorithm = KeyProperties.KeyAlgorithm.fromKeymasterAsymmetricKeyAlgorithm( + keymasterAlgorithm); + mKeymasterPurposes = KeyProperties.Purpose.allToKeymaster(spec.getPurposes()); + mKeymasterBlockModes = KeyProperties.BlockMode.allToKeymaster(spec.getBlockModes()); + mKeymasterEncryptionPaddings = KeyProperties.EncryptionPadding.allToKeymaster( + spec.getEncryptionPaddings()); + if (((spec.getPurposes() & KeyProperties.PURPOSE_ENCRYPT) != 0) + && (spec.isRandomizedEncryptionRequired())) { + for (int keymasterPadding : mKeymasterEncryptionPaddings) { + if (!KeymasterUtils + .isKeymasterPaddingSchemeIndCpaCompatibleWithAsymmetricCrypto( + keymasterPadding)) { + throw new InvalidAlgorithmParameterException( + "Randomized encryption (IND-CPA) required but may be violated" + + " by padding scheme: " + + KeyProperties.EncryptionPadding.fromKeymaster( + keymasterPadding) + + ". See " + KeyGenParameterSpec.class.getName() + + " documentation."); + } + } + } + mKeymasterSignaturePaddings = KeyProperties.SignaturePadding.allToKeymaster( + spec.getSignaturePaddings()); + if (spec.isDigestsSpecified()) { + mKeymasterDigests = KeyProperties.Digest.allToKeymaster(spec.getDigests()); + } else { + mKeymasterDigests = EmptyArray.INT; + } + + // Check that user authentication related parameters are acceptable. This method + // will throw an IllegalStateException if there are issues (e.g., secure lock screen + // not set up). + KeymasterUtils.addUserAuthArgs(new KeymasterArguments(), mSpec); + } catch (IllegalArgumentException | IllegalStateException e) { + throw new InvalidAlgorithmParameterException(e); + } + + mJcaKeyAlgorithm = jcaKeyAlgorithm; + mRng = random; + mKeyStore = KeyStore2.getInstance(); + success = true; + } finally { + if (!success) { + resetAll(); + } + } + } + + private void resetAll() { + mEntryAlias = null; + mEntryUid = KeyProperties.NAMESPACE_APPLICATION; + mJcaKeyAlgorithm = null; + mKeymasterAlgorithm = -1; + mKeymasterPurposes = null; + mKeymasterBlockModes = null; + mKeymasterEncryptionPaddings = null; + mKeymasterSignaturePaddings = null; + mKeymasterDigests = null; + mKeySizeBits = 0; + mSpec = null; + mRSAPublicExponent = null; + mRng = null; + mKeyStore = null; + } + + private void initAlgorithmSpecificParameters() throws InvalidAlgorithmParameterException { + AlgorithmParameterSpec algSpecificSpec = mSpec.getAlgorithmParameterSpec(); + switch (mKeymasterAlgorithm) { + case KeymasterDefs.KM_ALGORITHM_RSA: + { + BigInteger publicExponent = null; + if (algSpecificSpec instanceof RSAKeyGenParameterSpec) { + RSAKeyGenParameterSpec rsaSpec = (RSAKeyGenParameterSpec) algSpecificSpec; + if (mKeySizeBits == -1) { + mKeySizeBits = rsaSpec.getKeysize(); + } else if (mKeySizeBits != rsaSpec.getKeysize()) { + throw new InvalidAlgorithmParameterException("RSA key size must match " + + " between " + mSpec + " and " + algSpecificSpec + + ": " + mKeySizeBits + " vs " + rsaSpec.getKeysize()); + } + publicExponent = rsaSpec.getPublicExponent(); + } else if (algSpecificSpec != null) { + throw new InvalidAlgorithmParameterException( + "RSA may only use RSAKeyGenParameterSpec"); + } + if (publicExponent == null) { + publicExponent = RSAKeyGenParameterSpec.F4; + } + if (publicExponent.compareTo(BigInteger.ZERO) < 1) { + throw new InvalidAlgorithmParameterException( + "RSA public exponent must be positive: " + publicExponent); + } + if ((publicExponent.signum() == -1) + || (publicExponent.compareTo(KeymasterArguments.UINT64_MAX_VALUE) > 0)) { + throw new InvalidAlgorithmParameterException( + "Unsupported RSA public exponent: " + publicExponent + + ". Maximum supported value: " + KeymasterArguments.UINT64_MAX_VALUE); + } + mRSAPublicExponent = publicExponent.longValue(); + break; + } + case KeymasterDefs.KM_ALGORITHM_EC: + if (algSpecificSpec instanceof ECGenParameterSpec) { + ECGenParameterSpec ecSpec = (ECGenParameterSpec) algSpecificSpec; + String curveName = ecSpec.getName(); + Integer ecSpecKeySizeBits = SUPPORTED_EC_NIST_CURVE_NAME_TO_SIZE.get( + curveName.toLowerCase(Locale.US)); + if (ecSpecKeySizeBits == null) { + throw new InvalidAlgorithmParameterException( + "Unsupported EC curve name: " + curveName + + ". Supported: " + SUPPORTED_EC_NIST_CURVE_NAMES); + } + if (mKeySizeBits == -1) { + mKeySizeBits = ecSpecKeySizeBits; + } else if (mKeySizeBits != ecSpecKeySizeBits) { + throw new InvalidAlgorithmParameterException("EC key size must match " + + " between " + mSpec + " and " + algSpecificSpec + + ": " + mKeySizeBits + " vs " + ecSpecKeySizeBits); + } + } else if (algSpecificSpec != null) { + throw new InvalidAlgorithmParameterException( + "EC may only use ECGenParameterSpec"); + } + break; + default: + throw new ProviderException("Unsupported algorithm: " + mKeymasterAlgorithm); + } + } + + @Override + public KeyPair generateKeyPair() { + if (mKeyStore == null || mSpec == null) { + throw new IllegalStateException("Not initialized"); + } + + final @SecurityLevel int securityLevel = + mSpec.isStrongBoxBacked() + ? SecurityLevel.STRONGBOX + : SecurityLevel.TRUSTED_ENVIRONMENT; + + final int flags = + mSpec.isCriticalToDeviceEncryption() + ? IKeystoreSecurityLevel + .KEY_FLAG_AUTH_BOUND_WITHOUT_CRYPTOGRAPHIC_LSKF_BINDING + : 0; + + byte[] additionalEntropy = + KeyStoreCryptoOperationUtils.getRandomBytesToMixIntoKeystoreRng( + mRng, (mKeySizeBits + 7) / 8); + + KeyDescriptor descriptor = new KeyDescriptor(); + descriptor.alias = mEntryAlias; + descriptor.domain = mEntryUid == KeyProperties.NAMESPACE_APPLICATION + ? Domain.APP + : Domain.SELINUX; + descriptor.nspace = mEntryUid; + descriptor.blob = null; + + boolean success = false; + try { + KeyStoreSecurityLevel iSecurityLevel = mKeyStore.getSecurityLevel(securityLevel); + + KeyMetadata metadata = iSecurityLevel.generateKey(descriptor, null, + constructKeyGenerationArguments(), flags, additionalEntropy); + + AndroidKeyStorePublicKey publicKey = + AndroidKeyStoreProvider.makeAndroidKeyStorePublicKeyFromKeyEntryResponse( + descriptor, metadata, iSecurityLevel, mKeymasterAlgorithm); + + success = true; + return new KeyPair(publicKey, publicKey.getPrivateKey()); + } catch (android.security.KeyStoreException e) { + switch(e.getErrorCode()) { + case KeymasterDefs.KM_ERROR_HARDWARE_TYPE_UNAVAILABLE: + throw new StrongBoxUnavailableException("Failed to generated key pair.", e); + default: + ProviderException p = new ProviderException("Failed to generate key pair.", e); + if ((mSpec.getPurposes() & KeyProperties.PURPOSE_WRAP_KEY) != 0) { + throw new SecureKeyImportUnavailableException(p); + } + throw p; + } + } catch (UnrecoverableKeyException e) { + throw new ProviderException( + "Failed to construct key object from newly generated key pair.", e); + } finally { + if (!success) { + try { + mKeyStore.deleteKey(descriptor); + } catch (KeyStoreException e) { + if (e.getErrorCode() != ResponseCode.KEY_NOT_FOUND) { + Log.e(TAG, "Failed to delete newly generated key after " + + "generation failed unexpectedly.", e); + } + } + } + } + } + + private void addAttestationParameters(@NonNull List<KeyParameter> params) + throws ProviderException { + byte[] challenge = mSpec.getAttestationChallenge(); + + if (challenge != null) { + params.add(KeyStore2ParameterUtils.makeBytes( + KeymasterDefs.KM_TAG_ATTESTATION_CHALLENGE, challenge + )); + + if (mSpec.isDevicePropertiesAttestationIncluded()) { + params.add(KeyStore2ParameterUtils.makeBytes( + KeymasterDefs.KM_TAG_ATTESTATION_ID_BRAND, + Build.BRAND.getBytes(StandardCharsets.UTF_8) + )); + params.add(KeyStore2ParameterUtils.makeBytes( + KeymasterDefs.KM_TAG_ATTESTATION_ID_DEVICE, + Build.DEVICE.getBytes(StandardCharsets.UTF_8) + )); + params.add(KeyStore2ParameterUtils.makeBytes( + KeymasterDefs.KM_TAG_ATTESTATION_ID_PRODUCT, + Build.PRODUCT.getBytes(StandardCharsets.UTF_8) + )); + params.add(KeyStore2ParameterUtils.makeBytes( + KeymasterDefs.KM_TAG_ATTESTATION_ID_MANUFACTURER, + Build.MANUFACTURER.getBytes(StandardCharsets.UTF_8) + )); + params.add(KeyStore2ParameterUtils.makeBytes( + KeymasterDefs.KM_TAG_ATTESTATION_ID_MODEL, + Build.MODEL.getBytes(StandardCharsets.UTF_8) + )); + } + } else { + if (mSpec.isDevicePropertiesAttestationIncluded()) { + throw new ProviderException("An attestation challenge must be provided when " + + "requesting device properties attestation."); + } + } + } + + private Collection<KeyParameter> constructKeyGenerationArguments() { + List<KeyParameter> params = new ArrayList<>(); + params.add(KeyStore2ParameterUtils.makeInt(KeymasterDefs.KM_TAG_KEY_SIZE, mKeySizeBits)); + params.add(KeyStore2ParameterUtils.makeEnum( + KeymasterDefs.KM_TAG_ALGORITHM, mKeymasterAlgorithm + )); + ArrayUtils.forEach(mKeymasterPurposes, (purpose) -> { + params.add(KeyStore2ParameterUtils.makeEnum( + KeymasterDefs.KM_TAG_PURPOSE, purpose + )); + }); + ArrayUtils.forEach(mKeymasterBlockModes, (blockMode) -> { + params.add(KeyStore2ParameterUtils.makeEnum( + KeymasterDefs.KM_TAG_BLOCK_MODE, blockMode + )); + }); + ArrayUtils.forEach(mKeymasterEncryptionPaddings, (padding) -> { + params.add(KeyStore2ParameterUtils.makeEnum( + KeymasterDefs.KM_TAG_PADDING, padding + )); + }); + ArrayUtils.forEach(mKeymasterSignaturePaddings, (padding) -> { + params.add(KeyStore2ParameterUtils.makeEnum( + KeymasterDefs.KM_TAG_PADDING, padding + )); + }); + ArrayUtils.forEach(mKeymasterDigests, (digest) -> { + params.add(KeyStore2ParameterUtils.makeEnum( + KeymasterDefs.KM_TAG_DIGEST, digest + )); + }); + + KeyStore2ParameterUtils.addUserAuthArgs(params, mSpec); + + if (mSpec.getKeyValidityStart() != null) { + params.add(KeyStore2ParameterUtils.makeDate( + KeymasterDefs.KM_TAG_ACTIVE_DATETIME, mSpec.getKeyValidityStart() + )); + } + if (mSpec.getKeyValidityForOriginationEnd() != null) { + params.add(KeyStore2ParameterUtils.makeDate( + KeymasterDefs.KM_TAG_ORIGINATION_EXPIRE_DATETIME, + mSpec.getKeyValidityForOriginationEnd() + )); + } + if (mSpec.getKeyValidityForConsumptionEnd() != null) { + params.add(KeyStore2ParameterUtils.makeDate( + KeymasterDefs.KM_TAG_USAGE_EXPIRE_DATETIME, + mSpec.getKeyValidityForConsumptionEnd() + )); + } + + addAlgorithmSpecificParameters(params); + + if (mSpec.isUniqueIdIncluded()) { + params.add(KeyStore2ParameterUtils.makeBool(KeymasterDefs.KM_TAG_INCLUDE_UNIQUE_ID)); + } + + addAttestationParameters(params); + + return params; + } + + private void addAlgorithmSpecificParameters(List<KeyParameter> params) { + switch (mKeymasterAlgorithm) { + case KeymasterDefs.KM_ALGORITHM_RSA: + params.add(KeyStore2ParameterUtils.makeLong( + KeymasterDefs.KM_TAG_RSA_PUBLIC_EXPONENT, mRSAPublicExponent + )); + break; + case KeymasterDefs.KM_ALGORITHM_EC: + break; + default: + throw new ProviderException("Unsupported algorithm: " + mKeymasterAlgorithm); + } + } + + private static int getDefaultKeySize(int keymasterAlgorithm) { + switch (keymasterAlgorithm) { + case KeymasterDefs.KM_ALGORITHM_EC: + return EC_DEFAULT_KEY_SIZE; + case KeymasterDefs.KM_ALGORITHM_RSA: + return RSA_DEFAULT_KEY_SIZE; + default: + throw new ProviderException("Unsupported algorithm: " + keymasterAlgorithm); + } + } + + private static void checkValidKeySize( + int keymasterAlgorithm, + int keySize, + boolean isStrongBoxBacked) + throws InvalidAlgorithmParameterException { + switch (keymasterAlgorithm) { + case KeymasterDefs.KM_ALGORITHM_EC: + if (isStrongBoxBacked && keySize != 256) { + throw new InvalidAlgorithmParameterException( + "Unsupported StrongBox EC key size: " + + keySize + " bits. Supported: 256"); + } + if (!SUPPORTED_EC_NIST_CURVE_SIZES.contains(keySize)) { + throw new InvalidAlgorithmParameterException("Unsupported EC key size: " + + keySize + " bits. Supported: " + SUPPORTED_EC_NIST_CURVE_SIZES); + } + break; + case KeymasterDefs.KM_ALGORITHM_RSA: + if (keySize < RSA_MIN_KEY_SIZE || keySize > RSA_MAX_KEY_SIZE) { + throw new InvalidAlgorithmParameterException("RSA key size must be >= " + + RSA_MIN_KEY_SIZE + " and <= " + RSA_MAX_KEY_SIZE); + } + break; + default: + throw new ProviderException("Unsupported algorithm: " + keymasterAlgorithm); + } + } + + /** + * Returns the {@code Signature} algorithm to be used for signing a certificate using the + * specified key or {@code null} if the key cannot be used for signing a certificate. + */ + @Nullable + private static String getCertificateSignatureAlgorithm( + int keymasterAlgorithm, + int keySizeBits, + KeyGenParameterSpec spec) { + // Constraints: + // 1. Key must be authorized for signing without user authentication. + // 2. Signature digest must be one of key's authorized digests. + // 3. For RSA keys, the digest output size must not exceed modulus size minus space overhead + // of RSA PKCS#1 signature padding scheme (about 30 bytes). + // 4. For EC keys, the there is no point in using a digest whose output size is longer than + // key/field size because the digest will be truncated to that size. + + if ((spec.getPurposes() & KeyProperties.PURPOSE_SIGN) == 0) { + // Key not authorized for signing + return null; + } + if (spec.isUserAuthenticationRequired()) { + // Key not authorized for use without user authentication + return null; + } + if (!spec.isDigestsSpecified()) { + // Key not authorized for any digests -- can't sign + return null; + } + switch (keymasterAlgorithm) { + case KeymasterDefs.KM_ALGORITHM_EC: + { + Set<Integer> availableKeymasterDigests = getAvailableKeymasterSignatureDigests( + spec.getDigests(), + AndroidKeyStoreBCWorkaroundProvider.getSupportedEcdsaSignatureDigests()); + + int bestKeymasterDigest = -1; + int bestDigestOutputSizeBits = -1; + for (int keymasterDigest : availableKeymasterDigests) { + int outputSizeBits = KeymasterUtils.getDigestOutputSizeBits(keymasterDigest); + if (outputSizeBits == keySizeBits) { + // Perfect match -- use this digest + bestKeymasterDigest = keymasterDigest; + bestDigestOutputSizeBits = outputSizeBits; + break; + } + // Not a perfect match -- check against the best digest so far + if (bestKeymasterDigest == -1) { + // First digest tested -- definitely the best so far + bestKeymasterDigest = keymasterDigest; + bestDigestOutputSizeBits = outputSizeBits; + } else { + // Prefer output size to be as close to key size as possible, with output + // sizes larger than key size preferred to those smaller than key size. + if (bestDigestOutputSizeBits < keySizeBits) { + // Output size of the best digest so far is smaller than key size. + // Anything larger is a win. + if (outputSizeBits > bestDigestOutputSizeBits) { + bestKeymasterDigest = keymasterDigest; + bestDigestOutputSizeBits = outputSizeBits; + } + } else { + // Output size of the best digest so far is larger than key size. + // Anything smaller is a win, as long as it's not smaller than key size. + if ((outputSizeBits < bestDigestOutputSizeBits) + && (outputSizeBits >= keySizeBits)) { + bestKeymasterDigest = keymasterDigest; + bestDigestOutputSizeBits = outputSizeBits; + } + } + } + } + if (bestKeymasterDigest == -1) { + return null; + } + return KeyProperties.Digest.fromKeymasterToSignatureAlgorithmDigest( + bestKeymasterDigest) + "WithECDSA"; + } + case KeymasterDefs.KM_ALGORITHM_RSA: + { + // Check whether this key is authorized for PKCS#1 signature padding. + // We use Bouncy Castle to generate self-signed RSA certificates. Bouncy Castle + // only supports RSA certificates signed using PKCS#1 padding scheme. The key needs + // to be authorized for PKCS#1 padding or padding NONE which means any padding. + boolean pkcs1SignaturePaddingSupported = + com.android.internal.util.ArrayUtils.contains( + KeyProperties.SignaturePadding.allToKeymaster( + spec.getSignaturePaddings()), + KeymasterDefs.KM_PAD_RSA_PKCS1_1_5_SIGN); + if (!pkcs1SignaturePaddingSupported) { + // Key not authorized for PKCS#1 signature padding -- can't sign + return null; + } + + Set<Integer> availableKeymasterDigests = getAvailableKeymasterSignatureDigests( + spec.getDigests(), + AndroidKeyStoreBCWorkaroundProvider.getSupportedEcdsaSignatureDigests()); + + // The amount of space available for the digest is less than modulus size by about + // 30 bytes because padding must be at least 11 bytes long (00 || 01 || PS || 00, + // where PS must be at least 8 bytes long), and then there's also the 15--19 bytes + // overhead (depending the on chosen digest) for encoding digest OID and digest + // value in DER. + int maxDigestOutputSizeBits = keySizeBits - 30 * 8; + int bestKeymasterDigest = -1; + int bestDigestOutputSizeBits = -1; + for (int keymasterDigest : availableKeymasterDigests) { + int outputSizeBits = KeymasterUtils.getDigestOutputSizeBits(keymasterDigest); + if (outputSizeBits > maxDigestOutputSizeBits) { + // Digest too long (signature generation will fail) -- skip + continue; + } + if (bestKeymasterDigest == -1) { + // First digest tested -- definitely the best so far + bestKeymasterDigest = keymasterDigest; + bestDigestOutputSizeBits = outputSizeBits; + } else { + // The longer the better + if (outputSizeBits > bestDigestOutputSizeBits) { + bestKeymasterDigest = keymasterDigest; + bestDigestOutputSizeBits = outputSizeBits; + } + } + } + if (bestKeymasterDigest == -1) { + return null; + } + return KeyProperties.Digest.fromKeymasterToSignatureAlgorithmDigest( + bestKeymasterDigest) + "WithRSA"; + } + default: + throw new ProviderException("Unsupported algorithm: " + keymasterAlgorithm); + } + } + + private static Set<Integer> getAvailableKeymasterSignatureDigests( + @KeyProperties.DigestEnum String[] authorizedKeyDigests, + @KeyProperties.DigestEnum String[] supportedSignatureDigests) { + Set<Integer> authorizedKeymasterKeyDigests = new HashSet<Integer>(); + for (int keymasterDigest : KeyProperties.Digest.allToKeymaster(authorizedKeyDigests)) { + authorizedKeymasterKeyDigests.add(keymasterDigest); + } + Set<Integer> supportedKeymasterSignatureDigests = new HashSet<Integer>(); + for (int keymasterDigest + : KeyProperties.Digest.allToKeymaster(supportedSignatureDigests)) { + supportedKeymasterSignatureDigests.add(keymasterDigest); + } + Set<Integer> result = new HashSet<Integer>(supportedKeymasterSignatureDigests); + result.retainAll(authorizedKeymasterKeyDigests); + return result; + } +} diff --git a/keystore/java/android/security/keystore2/AndroidKeyStoreLoadStoreParameter.java b/keystore/java/android/security/keystore2/AndroidKeyStoreLoadStoreParameter.java new file mode 100644 index 000000000000..afb10547411b --- /dev/null +++ b/keystore/java/android/security/keystore2/AndroidKeyStoreLoadStoreParameter.java @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2015 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.keystore2; + +import java.security.KeyStore; +import java.security.KeyStore.ProtectionParameter; + +/** + * @hide + */ +class AndroidKeyStoreLoadStoreParameter implements KeyStore.LoadStoreParameter { + + private final int mNamespace; + + AndroidKeyStoreLoadStoreParameter(int namespace) { + mNamespace = namespace; + } + + @Override + public ProtectionParameter getProtectionParameter() { + return null; + } + + int getNamespace() { + return mNamespace; + } +} diff --git a/keystore/java/android/security/keystore2/AndroidKeyStorePrivateKey.java b/keystore/java/android/security/keystore2/AndroidKeyStorePrivateKey.java new file mode 100644 index 000000000000..8b331ee3b880 --- /dev/null +++ b/keystore/java/android/security/keystore2/AndroidKeyStorePrivateKey.java @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2015 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.keystore2; + +import android.annotation.NonNull; +import android.security.KeyStoreSecurityLevel; +import android.system.keystore2.Authorization; +import android.system.keystore2.KeyDescriptor; + +import java.security.PrivateKey; + +/** + * {@link PrivateKey} backed by Android Keystore. + * + * @hide + */ +public class AndroidKeyStorePrivateKey extends AndroidKeyStoreKey implements PrivateKey { + + public AndroidKeyStorePrivateKey(@NonNull KeyDescriptor descriptor, + long keyId, @NonNull Authorization[] authorizations, @NonNull String algorithm, + @NonNull KeyStoreSecurityLevel securityLevel) { + super(descriptor, keyId, authorizations, algorithm, securityLevel); + } + +} diff --git a/keystore/java/android/security/keystore2/AndroidKeyStoreProvider.java b/keystore/java/android/security/keystore2/AndroidKeyStoreProvider.java new file mode 100644 index 000000000000..b2e32a3175e3 --- /dev/null +++ b/keystore/java/android/security/keystore2/AndroidKeyStoreProvider.java @@ -0,0 +1,415 @@ +/* + * Copyright (C) 2012 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.keystore2; + +import android.annotation.NonNull; +import android.security.KeyStore; +import android.security.KeyStore2; +import android.security.KeyStoreSecurityLevel; +import android.security.keymaster.KeymasterDefs; +import android.security.keystore.KeyPermanentlyInvalidatedException; +import android.security.keystore.KeyProperties; +import android.security.keystore.KeyStoreCryptoOperation; +import android.system.keystore2.Authorization; +import android.system.keystore2.Domain; +import android.system.keystore2.KeyDescriptor; +import android.system.keystore2.KeyEntryResponse; +import android.system.keystore2.KeyMetadata; +import android.system.keystore2.ResponseCode; + +import java.security.KeyFactory; +import java.security.KeyPair; +import java.security.NoSuchAlgorithmException; +import java.security.Provider; +import java.security.ProviderException; +import java.security.PublicKey; +import java.security.Security; +import java.security.Signature; +import java.security.UnrecoverableKeyException; +import java.security.interfaces.ECPublicKey; +import java.security.interfaces.RSAPublicKey; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.X509EncodedKeySpec; + +import javax.crypto.Cipher; +import javax.crypto.Mac; + +/** + * A provider focused on providing JCA interfaces for the Android KeyStore. + * + * @hide + */ +public class AndroidKeyStoreProvider extends Provider { + private static final String PROVIDER_NAME = "AndroidKeyStore"; + + // IMPLEMENTATION NOTE: Class names are hard-coded in this provider to avoid loading these + // classes when this provider is instantiated and installed early on during each app's + // initialization process. + // + // Crypto operations operating on the AndroidKeyStore keys must not be offered by this provider. + // Instead, they need to be offered by AndroidKeyStoreBCWorkaroundProvider. See its Javadoc + // for details. + + private static final String PACKAGE_NAME = "android.security.keystore2"; + + private static final String DESEDE_SYSTEM_PROPERTY = + "ro.hardware.keystore_desede"; + + /** @hide **/ + public AndroidKeyStoreProvider() { + super(PROVIDER_NAME, 1.0, "Android KeyStore security provider"); + + boolean supports3DES = "true".equals(android.os.SystemProperties.get(DESEDE_SYSTEM_PROPERTY)); + + // java.security.KeyStore + put("KeyStore.AndroidKeyStore", PACKAGE_NAME + ".AndroidKeyStoreSpi"); + + // java.security.KeyPairGenerator + put("KeyPairGenerator.EC", PACKAGE_NAME + ".AndroidKeyStoreKeyPairGeneratorSpi$EC"); + put("KeyPairGenerator.RSA", PACKAGE_NAME + ".AndroidKeyStoreKeyPairGeneratorSpi$RSA"); + + // java.security.KeyFactory + putKeyFactoryImpl("EC"); + putKeyFactoryImpl("RSA"); + + // javax.crypto.KeyGenerator + put("KeyGenerator.AES", PACKAGE_NAME + ".AndroidKeyStoreKeyGeneratorSpi$AES"); + put("KeyGenerator.HmacSHA1", PACKAGE_NAME + ".AndroidKeyStoreKeyGeneratorSpi$HmacSHA1"); + put("KeyGenerator.HmacSHA224", PACKAGE_NAME + ".AndroidKeyStoreKeyGeneratorSpi$HmacSHA224"); + put("KeyGenerator.HmacSHA256", PACKAGE_NAME + ".AndroidKeyStoreKeyGeneratorSpi$HmacSHA256"); + put("KeyGenerator.HmacSHA384", PACKAGE_NAME + ".AndroidKeyStoreKeyGeneratorSpi$HmacSHA384"); + put("KeyGenerator.HmacSHA512", PACKAGE_NAME + ".AndroidKeyStoreKeyGeneratorSpi$HmacSHA512"); + + if (supports3DES) { + put("KeyGenerator.DESede", PACKAGE_NAME + ".AndroidKeyStoreKeyGeneratorSpi$DESede"); + } + + // java.security.SecretKeyFactory + putSecretKeyFactoryImpl("AES"); + if (supports3DES) { + putSecretKeyFactoryImpl("DESede"); + } + putSecretKeyFactoryImpl("HmacSHA1"); + putSecretKeyFactoryImpl("HmacSHA224"); + putSecretKeyFactoryImpl("HmacSHA256"); + putSecretKeyFactoryImpl("HmacSHA384"); + putSecretKeyFactoryImpl("HmacSHA512"); + } + + private static boolean sInstalled = false; + + /** + * This function indicates whether or not this provider was installed. This is manly used + * as indicator for + * {@link android.security.keystore.AndroidKeyStoreProvider#getKeyStoreForUid(int)} + * to whether or not to retrieve the Keystore provider by "AndroidKeyStoreLegacy". + * This function can be removed once the transition to Keystore 2.0 is complete. + * b/171305684 + * + * @return true if this provider was installed. + * @hide + */ + public static boolean isInstalled() { + return sInstalled; + } + + /** + * Installs a new instance of this provider (and the + * {@link AndroidKeyStoreBCWorkaroundProvider}). + * @hide + */ + public static void install() { + Provider[] providers = Security.getProviders(); + int bcProviderIndex = -1; + for (int i = 0; i < providers.length; i++) { + Provider provider = providers[i]; + if ("BC".equals(provider.getName())) { + bcProviderIndex = i; + break; + } + } + sInstalled = true; + + Security.addProvider(new AndroidKeyStoreProvider()); + Security.addProvider( + new android.security.keystore.AndroidKeyStoreProvider( + "AndroidKeyStoreLegacy")); + Provider workaroundProvider = new AndroidKeyStoreBCWorkaroundProvider(); + Provider legacyWorkaroundProvider = + new android.security.keystore.AndroidKeyStoreBCWorkaroundProvider( + "AndroidKeyStoreBCWorkaroundLegacy"); + if (bcProviderIndex != -1) { + // Bouncy Castle provider found -- install the workaround provider above it. + // insertProviderAt uses 1-based positions. + Security.insertProviderAt(legacyWorkaroundProvider, bcProviderIndex + 1); + Security.insertProviderAt(workaroundProvider, bcProviderIndex + 1); + } else { + // Bouncy Castle provider not found -- install the workaround provider at lowest + // priority. + Security.addProvider(workaroundProvider); + Security.addProvider(legacyWorkaroundProvider); + } + } + + private void putSecretKeyFactoryImpl(String algorithm) { + put("SecretKeyFactory." + algorithm, PACKAGE_NAME + ".AndroidKeyStoreSecretKeyFactorySpi"); + } + + private void putKeyFactoryImpl(String algorithm) { + put("KeyFactory." + algorithm, PACKAGE_NAME + ".AndroidKeyStoreKeyFactorySpi"); + } + + /** + * Gets the {@link KeyStore} operation handle corresponding to the provided JCA crypto + * primitive. + * + * <p>The following primitives are supported: {@link Cipher} and {@link Mac}. + * + * @return KeyStore operation handle or {@code 0} if the provided primitive's KeyStore operation + * is not in progress. + * + * @throws IllegalArgumentException if the provided primitive is not supported or is not backed + * by AndroidKeyStore provider. + * @throws IllegalStateException if the provided primitive is not initialized. + * @hide + */ + public static long getKeyStoreOperationHandle(Object cryptoPrimitive) { + if (cryptoPrimitive == null) { + throw new NullPointerException(); + } + Object spi; + if (cryptoPrimitive instanceof Signature) { + spi = ((Signature) cryptoPrimitive).getCurrentSpi(); + } else if (cryptoPrimitive instanceof Mac) { + spi = ((Mac) cryptoPrimitive).getCurrentSpi(); + } else if (cryptoPrimitive instanceof Cipher) { + spi = ((Cipher) cryptoPrimitive).getCurrentSpi(); + } else { + throw new IllegalArgumentException("Unsupported crypto primitive: " + cryptoPrimitive + + ". Supported: Signature, Mac, Cipher"); + } + if (spi == null) { + throw new IllegalStateException("Crypto primitive not initialized"); + } else if (!(spi instanceof KeyStoreCryptoOperation)) { + throw new IllegalArgumentException( + "Crypto primitive not backed by AndroidKeyStore provider: " + cryptoPrimitive + + ", spi: " + spi); + } + return ((KeyStoreCryptoOperation) spi).getOperationHandle(); + } + + /** + * This helper function gets called if the key loaded from the keystore daemon + * is for an asymmetric algorithm. It constructs an instance of {@link AndroidKeyStorePublicKey} + * which implements {@link PublicKey}. + * + * @param descriptor The original key descriptor that was used to load the key. + * + * @param metadata The key metadata which includes the public key material, a reference to the + * stored private key material, the key characteristics. + * @param iSecurityLevel A binder interface that allows using the private key. + * @param algorithm Must indicate EC or RSA. + * @return AndroidKeyStorePublicKey + * @throws UnrecoverableKeyException + * @hide + */ + @NonNull + static AndroidKeyStorePublicKey makeAndroidKeyStorePublicKeyFromKeyEntryResponse( + @NonNull KeyDescriptor descriptor, + @NonNull KeyMetadata metadata, + @NonNull KeyStoreSecurityLevel iSecurityLevel, int algorithm) + throws UnrecoverableKeyException { + if (metadata.certificate == null) { + throw new UnrecoverableKeyException("Failed to obtain X.509 form of public key." + + " Keystore has no public certificate stored."); + } + final byte[] x509EncodedPublicKey = metadata.certificate; + + String jcaKeyAlgorithm; + try { + jcaKeyAlgorithm = KeyProperties.KeyAlgorithm.fromKeymasterAsymmetricKeyAlgorithm( + algorithm); + } catch (IllegalArgumentException e) { + throw (UnrecoverableKeyException) + new UnrecoverableKeyException("Failed to load private key") + .initCause(e); + } + + PublicKey publicKey; + try { + KeyFactory keyFactory = KeyFactory.getInstance(jcaKeyAlgorithm); + publicKey = keyFactory.generatePublic(new X509EncodedKeySpec(x509EncodedPublicKey)); + } catch (NoSuchAlgorithmException e) { + throw new ProviderException( + "Failed to obtain " + jcaKeyAlgorithm + " KeyFactory", e); + } catch (InvalidKeySpecException e) { + throw new ProviderException("Invalid X.509 encoding of public key", e); + } + + KeyStoreSecurityLevel securityLevel = iSecurityLevel; + if (KeyProperties.KEY_ALGORITHM_EC.equalsIgnoreCase(jcaKeyAlgorithm)) { + + return new AndroidKeyStoreECPublicKey(descriptor, metadata, + iSecurityLevel, (ECPublicKey) publicKey); + } else if (KeyProperties.KEY_ALGORITHM_RSA.equalsIgnoreCase(jcaKeyAlgorithm)) { + return new AndroidKeyStoreRSAPublicKey(descriptor, metadata, + iSecurityLevel, (RSAPublicKey) publicKey); + } else { + throw new ProviderException("Unsupported Android Keystore public key algorithm: " + + jcaKeyAlgorithm); + } + } + + /** @hide **/ + @NonNull + public static AndroidKeyStorePublicKey loadAndroidKeyStorePublicKeyFromKeystore( + @NonNull KeyStore2 keyStore, @NonNull String privateKeyAlias, int namespace) + throws UnrecoverableKeyException, KeyPermanentlyInvalidatedException { + AndroidKeyStoreKey key = + loadAndroidKeyStoreKeyFromKeystore(keyStore, privateKeyAlias, namespace); + if (key instanceof AndroidKeyStorePublicKey) { + return (AndroidKeyStorePublicKey) key; + } else { + throw new UnrecoverableKeyException("No asymmetric key found by the given alias."); + } + } + + /** @hide **/ + @NonNull + public static KeyPair loadAndroidKeyStoreKeyPairFromKeystore( + @NonNull KeyStore2 keyStore, @NonNull String privateKeyAlias, int namespace) + throws UnrecoverableKeyException, KeyPermanentlyInvalidatedException { + AndroidKeyStoreKey key = + loadAndroidKeyStoreKeyFromKeystore(keyStore, privateKeyAlias, namespace); + if (key instanceof AndroidKeyStorePublicKey) { + AndroidKeyStorePublicKey publicKey = (AndroidKeyStorePublicKey) key; + return new KeyPair(publicKey, publicKey.getPrivateKey()); + } else { + throw new UnrecoverableKeyException("No asymmetric key found by the given alias."); + } + } + + /** @hide **/ + @NonNull + public static AndroidKeyStorePrivateKey loadAndroidKeyStorePrivateKeyFromKeystore( + @NonNull KeyStore2 keyStore, @NonNull String privateKeyAlias, int namespace) + throws UnrecoverableKeyException, KeyPermanentlyInvalidatedException { + AndroidKeyStoreKey key = + loadAndroidKeyStoreKeyFromKeystore(keyStore, privateKeyAlias, namespace); + if (key instanceof AndroidKeyStorePublicKey) { + return ((AndroidKeyStorePublicKey) key).getPrivateKey(); + } else { + throw new UnrecoverableKeyException("No asymmetric key found by the given alias."); + } + } + + + @NonNull + private static AndroidKeyStoreSecretKey makeAndroidKeyStoreSecretKeyFromKeyEntryResponse( + @NonNull KeyDescriptor descriptor, + @NonNull KeyEntryResponse response, int algorithm, int digest) + throws UnrecoverableKeyException { + + @KeyProperties.KeyAlgorithmEnum String keyAlgorithmString; + try { + keyAlgorithmString = KeyProperties.KeyAlgorithm.fromKeymasterSecretKeyAlgorithm( + algorithm, digest); + } catch (IllegalArgumentException e) { + throw (UnrecoverableKeyException) + new UnrecoverableKeyException("Unsupported secret key type").initCause(e); + } + + return new AndroidKeyStoreSecretKey(descriptor, + response.metadata, keyAlgorithmString, + new KeyStoreSecurityLevel(response.iSecurityLevel)); + } + + /** + * Loads an an AndroidKeyStoreKey from the AndroidKeyStore backend. + * + * @param keyStore The keystore2 backend. + * @param alias The alias of the key in the Keystore database. + * @param namespace The a Keystore namespace. This is used by system api only to request + * Android system specific keystore namespace, which can be configured + * in the device's SEPolicy. Third party apps and most system components + * set this parameter to -1 to indicate their application specific namespace. + * TODO b/171806779 link to public Keystore 2.0 documentation. + * See bug for more details for now. + * @hide + **/ + @NonNull + public static AndroidKeyStoreKey loadAndroidKeyStoreKeyFromKeystore( + @NonNull KeyStore2 keyStore, @NonNull String alias, int namespace) + throws UnrecoverableKeyException, KeyPermanentlyInvalidatedException { + + KeyDescriptor descriptor = new KeyDescriptor(); + if (namespace == KeyProperties.NAMESPACE_APPLICATION) { + descriptor.nspace = 0; // ignored; + descriptor.domain = Domain.APP; + } else { + descriptor.nspace = namespace; + descriptor.domain = Domain.SELINUX; + } + descriptor.alias = alias; + descriptor.blob = null; + KeyEntryResponse response = null; + try { + response = keyStore.getKeyEntry(descriptor); + } catch (android.security.KeyStoreException e) { + if (e.getErrorCode() == ResponseCode.KEY_PERMANENTLY_INVALIDATED) { + throw new KeyPermanentlyInvalidatedException( + "User changed or deleted their auth credentials", + e); + } else { + throw (UnrecoverableKeyException) + new UnrecoverableKeyException("Failed to obtain information about key") + .initCause(e); + } + } + + Integer keymasterAlgorithm = null; + // We just need one digest for the algorithm name + int keymasterDigest = -1; + for (Authorization a : response.metadata.authorizations) { + switch (a.keyParameter.tag) { + case KeymasterDefs.KM_TAG_ALGORITHM: + keymasterAlgorithm = a.keyParameter.integer; + break; + case KeymasterDefs.KM_TAG_DIGEST: + if (keymasterDigest == -1) keymasterDigest = a.keyParameter.integer; + break; + } + } + if (keymasterAlgorithm == null) { + throw new UnrecoverableKeyException("Key algorithm unknown"); + } + + if (keymasterAlgorithm == KeymasterDefs.KM_ALGORITHM_HMAC || + keymasterAlgorithm == KeymasterDefs.KM_ALGORITHM_AES || + keymasterAlgorithm == KeymasterDefs.KM_ALGORITHM_3DES) { + return makeAndroidKeyStoreSecretKeyFromKeyEntryResponse(descriptor, response, + keymasterAlgorithm, keymasterDigest); + } else if (keymasterAlgorithm == KeymasterDefs.KM_ALGORITHM_RSA || + keymasterAlgorithm == KeymasterDefs.KM_ALGORITHM_EC) { + return makeAndroidKeyStorePublicKeyFromKeyEntryResponse(descriptor, response.metadata, + new KeyStoreSecurityLevel(response.iSecurityLevel), + keymasterAlgorithm); + } else { + throw new UnrecoverableKeyException("Key algorithm unknown"); + } + } +} diff --git a/keystore/java/android/security/keystore2/AndroidKeyStorePublicKey.java b/keystore/java/android/security/keystore2/AndroidKeyStorePublicKey.java new file mode 100644 index 000000000000..49dd77e3a3db --- /dev/null +++ b/keystore/java/android/security/keystore2/AndroidKeyStorePublicKey.java @@ -0,0 +1,81 @@ +/* + * Copyright (C) 2015 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.keystore2; + +import android.annotation.NonNull; +import android.security.KeyStoreSecurityLevel; +import android.security.keystore.ArrayUtils; +import android.system.keystore2.KeyDescriptor; +import android.system.keystore2.KeyMetadata; + +import java.security.PublicKey; + +/** + * {@link PublicKey} backed by Android Keystore. + * + * @hide + */ +public abstract class AndroidKeyStorePublicKey extends AndroidKeyStoreKey implements PublicKey { + private final byte[] mCertificate; + private final byte[] mCertificateChain; + + public AndroidKeyStorePublicKey(@NonNull KeyDescriptor descriptor, + @NonNull KeyMetadata metadata, @NonNull String algorithm, + @NonNull KeyStoreSecurityLevel securityLevel) { + super(descriptor, metadata.key.nspace, metadata.authorizations, algorithm, securityLevel); + mCertificate = metadata.certificate; + mCertificateChain = metadata.certificateChain; + } + + abstract AndroidKeyStorePrivateKey getPrivateKey(); + + @Override + public String getFormat() { + return "X.509"; + } + + @Override + public byte[] getEncoded() { + return ArrayUtils.cloneIfNotEmpty(mCertificate); + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + + result = prime * result + super.hashCode(); + result = prime * result + ((mCertificate == null) ? 0 : mCertificate.hashCode()); + result = prime * result + ((mCertificateChain == null) ? 0 : mCertificateChain.hashCode()); + + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (!super.equals(obj)) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + return true; + } +} diff --git a/keystore/java/android/security/keystore2/AndroidKeyStoreRSACipherSpi.java b/keystore/java/android/security/keystore2/AndroidKeyStoreRSACipherSpi.java new file mode 100644 index 000000000000..a6ea9723db24 --- /dev/null +++ b/keystore/java/android/security/keystore2/AndroidKeyStoreRSACipherSpi.java @@ -0,0 +1,525 @@ +/* + * Copyright (C) 2015 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.keystore2; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.security.keymaster.KeymasterDefs; +import android.security.keystore.KeyProperties; +import android.security.keystore.KeymasterUtils; +import android.system.keystore2.Authorization; +import android.system.keystore2.KeyParameter; + +import java.security.AlgorithmParameters; +import java.security.InvalidAlgorithmParameterException; +import java.security.InvalidKeyException; +import java.security.Key; +import java.security.NoSuchAlgorithmException; +import java.security.PrivateKey; +import java.security.ProviderException; +import java.security.spec.AlgorithmParameterSpec; +import java.security.spec.InvalidParameterSpecException; +import java.security.spec.MGF1ParameterSpec; +import java.util.List; + +import javax.crypto.Cipher; +import javax.crypto.CipherSpi; +import javax.crypto.spec.OAEPParameterSpec; +import javax.crypto.spec.PSource; + +/** + * Base class for {@link CipherSpi} providing Android KeyStore backed RSA encryption/decryption. + * + * @hide + */ +abstract class AndroidKeyStoreRSACipherSpi extends AndroidKeyStoreCipherSpiBase { + + /** + * Raw RSA cipher without any padding. + */ + public static final class NoPadding extends AndroidKeyStoreRSACipherSpi { + public NoPadding() { + super(KeymasterDefs.KM_PAD_NONE); + } + + @Override + protected boolean adjustConfigForEncryptingWithPrivateKey() { + // RSA encryption with no padding using private key is a way to implement raw RSA + // signatures which JCA does not expose via Signature. We thus have to support this. + setKeymasterPurposeOverride(KeymasterDefs.KM_PURPOSE_SIGN); + return true; + } + + @Override + protected void initAlgorithmSpecificParameters() throws InvalidKeyException {} + + @Override + protected void initAlgorithmSpecificParameters(@Nullable AlgorithmParameterSpec params) + throws InvalidAlgorithmParameterException { + if (params != null) { + throw new InvalidAlgorithmParameterException( + "Unexpected parameters: " + params + ". No parameters supported"); + } + } + + @Override + protected void initAlgorithmSpecificParameters(@Nullable AlgorithmParameters params) + throws InvalidAlgorithmParameterException { + + if (params != null) { + throw new InvalidAlgorithmParameterException( + "Unexpected parameters: " + params + ". No parameters supported"); + } + } + + @Override + protected AlgorithmParameters engineGetParameters() { + return null; + } + + @Override + protected final int getAdditionalEntropyAmountForBegin() { + return 0; + } + + @Override + protected final int getAdditionalEntropyAmountForFinish() { + return 0; + } + } + + /** + * RSA cipher with PKCS#1 v1.5 encryption padding. + */ + public static final class PKCS1Padding extends AndroidKeyStoreRSACipherSpi { + public PKCS1Padding() { + super(KeymasterDefs.KM_PAD_RSA_PKCS1_1_5_ENCRYPT); + } + + @Override + protected boolean adjustConfigForEncryptingWithPrivateKey() { + // RSA encryption with PCKS#1 padding using private key is a way to implement RSA + // signatures with PKCS#1 padding. We have to support this for legacy reasons. + setKeymasterPurposeOverride(KeymasterDefs.KM_PURPOSE_SIGN); + setKeymasterPaddingOverride(KeymasterDefs.KM_PAD_RSA_PKCS1_1_5_SIGN); + return true; + } + + @Override + protected void initAlgorithmSpecificParameters() throws InvalidKeyException {} + + @Override + protected void initAlgorithmSpecificParameters(@Nullable AlgorithmParameterSpec params) + throws InvalidAlgorithmParameterException { + if (params != null) { + throw new InvalidAlgorithmParameterException( + "Unexpected parameters: " + params + ". No parameters supported"); + } + } + + @Override + protected void initAlgorithmSpecificParameters(@Nullable AlgorithmParameters params) + throws InvalidAlgorithmParameterException { + + if (params != null) { + throw new InvalidAlgorithmParameterException( + "Unexpected parameters: " + params + ". No parameters supported"); + } + } + + @Override + protected AlgorithmParameters engineGetParameters() { + return null; + } + + @Override + protected final int getAdditionalEntropyAmountForBegin() { + return 0; + } + + @Override + protected final int getAdditionalEntropyAmountForFinish() { + return (isEncrypting()) ? getModulusSizeBytes() : 0; + } + } + + /** + * RSA cipher with OAEP encryption padding. Only SHA-1 based MGF1 is supported as MGF. + */ + abstract static class OAEPWithMGF1Padding extends AndroidKeyStoreRSACipherSpi { + + private static final String MGF_ALGORITGM_MGF1 = "MGF1"; + + private int mKeymasterDigest = -1; + private int mDigestOutputSizeBytes; + + OAEPWithMGF1Padding(int keymasterDigest) { + super(KeymasterDefs.KM_PAD_RSA_OAEP); + mKeymasterDigest = keymasterDigest; + mDigestOutputSizeBytes = + (KeymasterUtils.getDigestOutputSizeBits(keymasterDigest) + 7) / 8; + } + + @Override + protected final void initAlgorithmSpecificParameters() throws InvalidKeyException {} + + @Override + protected final void initAlgorithmSpecificParameters( + @Nullable AlgorithmParameterSpec params) throws InvalidAlgorithmParameterException { + if (params == null) { + return; + } + + if (!(params instanceof OAEPParameterSpec)) { + throw new InvalidAlgorithmParameterException( + "Unsupported parameter spec: " + params + + ". Only OAEPParameterSpec supported"); + } + OAEPParameterSpec spec = (OAEPParameterSpec) params; + if (!MGF_ALGORITGM_MGF1.equalsIgnoreCase(spec.getMGFAlgorithm())) { + throw new InvalidAlgorithmParameterException( + "Unsupported MGF: " + spec.getMGFAlgorithm() + + ". Only " + MGF_ALGORITGM_MGF1 + " supported"); + } + String jcaDigest = spec.getDigestAlgorithm(); + int keymasterDigest; + try { + keymasterDigest = KeyProperties.Digest.toKeymaster(jcaDigest); + } catch (IllegalArgumentException e) { + throw new InvalidAlgorithmParameterException( + "Unsupported digest: " + jcaDigest, e); + } + switch (keymasterDigest) { + case KeymasterDefs.KM_DIGEST_SHA1: + case KeymasterDefs.KM_DIGEST_SHA_2_224: + case KeymasterDefs.KM_DIGEST_SHA_2_256: + case KeymasterDefs.KM_DIGEST_SHA_2_384: + case KeymasterDefs.KM_DIGEST_SHA_2_512: + // Permitted. + break; + default: + throw new InvalidAlgorithmParameterException( + "Unsupported digest: " + jcaDigest); + } + AlgorithmParameterSpec mgfParams = spec.getMGFParameters(); + if (mgfParams == null) { + throw new InvalidAlgorithmParameterException("MGF parameters must be provided"); + } + // Check whether MGF parameters match the OAEPParameterSpec + if (!(mgfParams instanceof MGF1ParameterSpec)) { + throw new InvalidAlgorithmParameterException("Unsupported MGF parameters" + + ": " + mgfParams + ". Only MGF1ParameterSpec supported"); + } + MGF1ParameterSpec mgfSpec = (MGF1ParameterSpec) mgfParams; + String mgf1JcaDigest = mgfSpec.getDigestAlgorithm(); + if (!KeyProperties.DIGEST_SHA1.equalsIgnoreCase(mgf1JcaDigest)) { + throw new InvalidAlgorithmParameterException( + "Unsupported MGF1 digest: " + mgf1JcaDigest + + ". Only " + KeyProperties.DIGEST_SHA1 + " supported"); + } + PSource pSource = spec.getPSource(); + if (!(pSource instanceof PSource.PSpecified)) { + throw new InvalidAlgorithmParameterException( + "Unsupported source of encoding input P: " + pSource + + ". Only pSpecifiedEmpty (PSource.PSpecified.DEFAULT) supported"); + } + PSource.PSpecified pSourceSpecified = (PSource.PSpecified) pSource; + byte[] pSourceValue = pSourceSpecified.getValue(); + if ((pSourceValue != null) && (pSourceValue.length > 0)) { + throw new InvalidAlgorithmParameterException( + "Unsupported source of encoding input P: " + pSource + + ". Only pSpecifiedEmpty (PSource.PSpecified.DEFAULT) supported"); + } + mKeymasterDigest = keymasterDigest; + mDigestOutputSizeBytes = + (KeymasterUtils.getDigestOutputSizeBits(keymasterDigest) + 7) / 8; + } + + @Override + protected final void initAlgorithmSpecificParameters(@Nullable AlgorithmParameters params) + throws InvalidAlgorithmParameterException { + if (params == null) { + return; + } + + OAEPParameterSpec spec; + try { + spec = params.getParameterSpec(OAEPParameterSpec.class); + } catch (InvalidParameterSpecException e) { + throw new InvalidAlgorithmParameterException("OAEP parameters required" + + ", but not found in parameters: " + params, e); + } + if (spec == null) { + throw new InvalidAlgorithmParameterException("OAEP parameters required" + + ", but not provided in parameters: " + params); + } + initAlgorithmSpecificParameters(spec); + } + + @Override + protected final AlgorithmParameters engineGetParameters() { + OAEPParameterSpec spec = + new OAEPParameterSpec( + KeyProperties.Digest.fromKeymaster(mKeymasterDigest), + MGF_ALGORITGM_MGF1, + MGF1ParameterSpec.SHA1, + PSource.PSpecified.DEFAULT); + try { + AlgorithmParameters params = AlgorithmParameters.getInstance("OAEP"); + params.init(spec); + return params; + } catch (NoSuchAlgorithmException e) { + throw new ProviderException( + "Failed to obtain OAEP AlgorithmParameters", e); + } catch (InvalidParameterSpecException e) { + throw new ProviderException( + "Failed to initialize OAEP AlgorithmParameters with an IV", + e); + } + } + + @Override + protected final void addAlgorithmSpecificParametersToBegin( + @NonNull List<KeyParameter> parameters) { + super.addAlgorithmSpecificParametersToBegin(parameters); + parameters.add(KeyStore2ParameterUtils.makeEnum( + KeymasterDefs.KM_TAG_DIGEST, mKeymasterDigest + )); + } + + @Override + protected final void loadAlgorithmSpecificParametersFromBeginResult( + KeyParameter[] parameters) { + super.loadAlgorithmSpecificParametersFromBeginResult(parameters); + } + + @Override + protected final int getAdditionalEntropyAmountForBegin() { + return 0; + } + + @Override + protected final int getAdditionalEntropyAmountForFinish() { + return (isEncrypting()) ? mDigestOutputSizeBytes : 0; + } + } + + public static class OAEPWithSHA1AndMGF1Padding extends OAEPWithMGF1Padding { + public OAEPWithSHA1AndMGF1Padding() { + super(KeymasterDefs.KM_DIGEST_SHA1); + } + } + + public static class OAEPWithSHA224AndMGF1Padding extends OAEPWithMGF1Padding { + public OAEPWithSHA224AndMGF1Padding() { + super(KeymasterDefs.KM_DIGEST_SHA_2_224); + } + } + + public static class OAEPWithSHA256AndMGF1Padding extends OAEPWithMGF1Padding { + public OAEPWithSHA256AndMGF1Padding() { + super(KeymasterDefs.KM_DIGEST_SHA_2_256); + } + } + + public static class OAEPWithSHA384AndMGF1Padding extends OAEPWithMGF1Padding { + public OAEPWithSHA384AndMGF1Padding() { + super(KeymasterDefs.KM_DIGEST_SHA_2_384); + } + } + + public static class OAEPWithSHA512AndMGF1Padding extends OAEPWithMGF1Padding { + public OAEPWithSHA512AndMGF1Padding() { + super(KeymasterDefs.KM_DIGEST_SHA_2_512); + } + } + + private final int mKeymasterPadding; + private int mKeymasterPaddingOverride; + + private int mModulusSizeBytes = -1; + + AndroidKeyStoreRSACipherSpi(int keymasterPadding) { + mKeymasterPadding = keymasterPadding; + } + + @Override + protected final void initKey(int opmode, Key key) throws InvalidKeyException { + if (key == null) { + throw new InvalidKeyException("Unsupported key: null"); + } + if (!KeyProperties.KEY_ALGORITHM_RSA.equalsIgnoreCase(key.getAlgorithm())) { + throw new InvalidKeyException("Unsupported key algorithm: " + key.getAlgorithm() + + ". Only " + KeyProperties.KEY_ALGORITHM_RSA + " supported"); + } + AndroidKeyStoreKey keystoreKey; + if (key instanceof AndroidKeyStorePrivateKey) { + keystoreKey = (AndroidKeyStoreKey) key; + } else if (key instanceof AndroidKeyStorePublicKey) { + keystoreKey = (AndroidKeyStoreKey) key; + } else { + throw new InvalidKeyException("Unsupported key type: " + key); + } + + if (keystoreKey instanceof PrivateKey) { + // Private key + switch (opmode) { + case Cipher.DECRYPT_MODE: + case Cipher.UNWRAP_MODE: + // Permitted + break; + case Cipher.ENCRYPT_MODE: + case Cipher.WRAP_MODE: + if (!adjustConfigForEncryptingWithPrivateKey()) { + throw new InvalidKeyException( + "RSA private keys cannot be used with " + opmodeToString(opmode) + + " and padding " + + KeyProperties.EncryptionPadding.fromKeymaster(mKeymasterPadding) + + ". Only RSA public keys supported for this mode"); + } + break; + default: + throw new InvalidKeyException( + "RSA private keys cannot be used with opmode: " + opmode); + } + } else { + // Public key + switch (opmode) { + case Cipher.ENCRYPT_MODE: + case Cipher.WRAP_MODE: + // Permitted + break; + case Cipher.DECRYPT_MODE: + case Cipher.UNWRAP_MODE: + throw new InvalidKeyException( + "RSA public keys cannot be used with " + opmodeToString(opmode) + + " and padding " + + KeyProperties.EncryptionPadding.fromKeymaster(mKeymasterPadding) + + ". Only RSA private keys supported for this opmode."); + // break; + default: + throw new InvalidKeyException( + "RSA public keys cannot be used with " + opmodeToString(opmode)); + } + } + + long keySizeBits = -1; + for (Authorization a : keystoreKey.getAuthorizations()) { + if (a.keyParameter.tag == KeymasterDefs.KM_TAG_KEY_SIZE) { + keySizeBits = KeyStore2ParameterUtils.getUnsignedInt(a); + } + } + + if (keySizeBits == -1) { + throw new InvalidKeyException("Size of key not known"); + } else if (keySizeBits > Integer.MAX_VALUE) { + throw new InvalidKeyException("Key too large: " + keySizeBits + " bits"); + } + mModulusSizeBytes = (int) ((keySizeBits + 7) / 8); + + setKey(keystoreKey); + } + + /** + * Adjusts the configuration of this cipher for encrypting using the private key. + * + * <p>The default implementation does nothing and refuses to adjust the configuration. + * + * @return {@code true} if the configuration has been adjusted, {@code false} if encrypting + * using private key is not permitted for this cipher. + */ + protected boolean adjustConfigForEncryptingWithPrivateKey() { + return false; + } + + @Override + protected final void resetAll() { + mModulusSizeBytes = -1; + mKeymasterPaddingOverride = -1; + super.resetAll(); + } + + @Override + protected final void resetWhilePreservingInitState() { + super.resetWhilePreservingInitState(); + } + + @Override + protected void addAlgorithmSpecificParametersToBegin( + @NonNull List<KeyParameter> parameters) { + parameters.add(KeyStore2ParameterUtils.makeEnum( + KeymasterDefs.KM_TAG_ALGORITHM, KeymasterDefs.KM_ALGORITHM_RSA + )); + int keymasterPadding = getKeymasterPaddingOverride(); + if (keymasterPadding == -1) { + keymasterPadding = mKeymasterPadding; + } + parameters.add(KeyStore2ParameterUtils.makeEnum( + KeymasterDefs.KM_TAG_PADDING, keymasterPadding + )); + int purposeOverride = getKeymasterPurposeOverride(); + if ((purposeOverride != -1) + && ((purposeOverride == KeymasterDefs.KM_PURPOSE_SIGN) + || (purposeOverride == KeymasterDefs.KM_PURPOSE_VERIFY))) { + // Keymaster sign/verify requires digest to be specified. + // For raw sign/verify it's NONE. + parameters.add(KeyStore2ParameterUtils.makeEnum( + KeymasterDefs.KM_TAG_DIGEST, KeymasterDefs.KM_DIGEST_NONE + )); + } + } + + @Override + protected void loadAlgorithmSpecificParametersFromBeginResult( + KeyParameter[] parameters) { + } + + @Override + protected final int engineGetBlockSize() { + // Not a block cipher, according to the RI + return 0; + } + + @Override + protected final byte[] engineGetIV() { + // IV never used + return null; + } + + @Override + protected final int engineGetOutputSize(int inputLen) { + return getModulusSizeBytes(); + } + + protected final int getModulusSizeBytes() { + if (mModulusSizeBytes == -1) { + throw new IllegalStateException("Not initialized"); + } + return mModulusSizeBytes; + } + + /** + * Overrides the default padding of the crypto operation. + */ + protected final void setKeymasterPaddingOverride(int keymasterPadding) { + mKeymasterPaddingOverride = keymasterPadding; + } + + protected final int getKeymasterPaddingOverride() { + return mKeymasterPaddingOverride; + } +} diff --git a/keystore/java/android/security/keystore2/AndroidKeyStoreRSAPrivateKey.java b/keystore/java/android/security/keystore2/AndroidKeyStoreRSAPrivateKey.java new file mode 100644 index 000000000000..ef0d3bc4d46a --- /dev/null +++ b/keystore/java/android/security/keystore2/AndroidKeyStoreRSAPrivateKey.java @@ -0,0 +1,51 @@ +/* + * Copyright (C) 2015 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.keystore2; + +import android.annotation.NonNull; +import android.security.KeyStoreSecurityLevel; +import android.security.keystore.KeyProperties; +import android.system.keystore2.Authorization; +import android.system.keystore2.KeyDescriptor; + +import java.math.BigInteger; +import java.security.PrivateKey; +import java.security.interfaces.RSAKey; + +/** + * RSA private key (instance of {@link PrivateKey} and {@link RSAKey}) backed by keystore. + * + * @hide + */ +public class AndroidKeyStoreRSAPrivateKey extends AndroidKeyStorePrivateKey implements RSAKey { + + private final BigInteger mModulus; + + + public AndroidKeyStoreRSAPrivateKey(@NonNull KeyDescriptor descriptor, + long keyId, + @NonNull Authorization[] authorizations, + @NonNull KeyStoreSecurityLevel securityLevel, @NonNull BigInteger modulus) { + super(descriptor, keyId, authorizations, KeyProperties.KEY_ALGORITHM_RSA, securityLevel); + mModulus = modulus; + } + + @Override + public BigInteger getModulus() { + return mModulus; + } +} diff --git a/keystore/java/android/security/keystore2/AndroidKeyStoreRSAPublicKey.java b/keystore/java/android/security/keystore2/AndroidKeyStoreRSAPublicKey.java new file mode 100644 index 000000000000..b578ea9baa06 --- /dev/null +++ b/keystore/java/android/security/keystore2/AndroidKeyStoreRSAPublicKey.java @@ -0,0 +1,71 @@ +/* + * Copyright (C) 2015 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.keystore2; + +import android.annotation.NonNull; +import android.security.KeyStoreSecurityLevel; +import android.security.keystore.KeyProperties; +import android.system.keystore2.KeyDescriptor; +import android.system.keystore2.KeyMetadata; + +import java.math.BigInteger; +import java.security.interfaces.RSAPublicKey; + +/** + * {@link RSAPublicKey} backed by Android Keystore. + * + * @hide + */ +public class AndroidKeyStoreRSAPublicKey extends AndroidKeyStorePublicKey implements RSAPublicKey { + private final BigInteger mModulus; + private final BigInteger mPublicExponent; + + public AndroidKeyStoreRSAPublicKey(@NonNull KeyDescriptor descriptor, + @NonNull KeyMetadata metadata, + @NonNull KeyStoreSecurityLevel securityLevel, @NonNull BigInteger modulus, + @NonNull BigInteger publicExponent) { + super(descriptor, metadata, KeyProperties.KEY_ALGORITHM_RSA, securityLevel); + mModulus = modulus; + mPublicExponent = publicExponent; + } + + public AndroidKeyStoreRSAPublicKey(@NonNull KeyDescriptor descriptor, + @NonNull KeyMetadata metadata, + @NonNull KeyStoreSecurityLevel securityLevel, @NonNull RSAPublicKey info) { + this(descriptor, metadata, securityLevel, info.getModulus(), info.getPublicExponent()); + if (!"X.509".equalsIgnoreCase(info.getFormat())) { + throw new IllegalArgumentException( + "Unsupported key export format: " + info.getFormat()); + } + } + + @Override + public AndroidKeyStorePrivateKey getPrivateKey() { + return new AndroidKeyStoreRSAPrivateKey(getUserKeyDescriptor(), getKeyIdDescriptor().nspace, + getAuthorizations(), getSecurityLevel(), mModulus); + } + + @Override + public BigInteger getModulus() { + return mModulus; + } + + @Override + public BigInteger getPublicExponent() { + return mPublicExponent; + } +} diff --git a/keystore/java/android/security/keystore2/AndroidKeyStoreRSASignatureSpi.java b/keystore/java/android/security/keystore2/AndroidKeyStoreRSASignatureSpi.java new file mode 100644 index 000000000000..5f1b9c0586a1 --- /dev/null +++ b/keystore/java/android/security/keystore2/AndroidKeyStoreRSASignatureSpi.java @@ -0,0 +1,172 @@ +/* + * Copyright (C) 2015 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.keystore2; + +import android.annotation.NonNull; +import android.security.keymaster.KeymasterDefs; +import android.security.keystore.KeyProperties; +import android.system.keystore2.KeyParameter; + +import java.security.InvalidKeyException; +import java.security.SignatureSpi; +import java.util.List; + +/** + * Base class for {@link SignatureSpi} providing Android KeyStore backed RSA signatures. + * + * @hide + */ +abstract class AndroidKeyStoreRSASignatureSpi extends AndroidKeyStoreSignatureSpiBase { + + abstract static class PKCS1Padding extends AndroidKeyStoreRSASignatureSpi { + PKCS1Padding(int keymasterDigest) { + super(keymasterDigest, KeymasterDefs.KM_PAD_RSA_PKCS1_1_5_SIGN); + } + + @Override + protected final int getAdditionalEntropyAmountForSign() { + // No entropy required for this deterministic signature scheme. + return 0; + } + } + + public static final class NONEWithPKCS1Padding extends PKCS1Padding { + public NONEWithPKCS1Padding() { + super(KeymasterDefs.KM_DIGEST_NONE); + } + } + + public static final class MD5WithPKCS1Padding extends PKCS1Padding { + public MD5WithPKCS1Padding() { + super(KeymasterDefs.KM_DIGEST_MD5); + } + } + + public static final class SHA1WithPKCS1Padding extends PKCS1Padding { + public SHA1WithPKCS1Padding() { + super(KeymasterDefs.KM_DIGEST_SHA1); + } + } + + public static final class SHA224WithPKCS1Padding extends PKCS1Padding { + public SHA224WithPKCS1Padding() { + super(KeymasterDefs.KM_DIGEST_SHA_2_224); + } + } + + public static final class SHA256WithPKCS1Padding extends PKCS1Padding { + public SHA256WithPKCS1Padding() { + super(KeymasterDefs.KM_DIGEST_SHA_2_256); + } + } + + public static final class SHA384WithPKCS1Padding extends PKCS1Padding { + public SHA384WithPKCS1Padding() { + super(KeymasterDefs.KM_DIGEST_SHA_2_384); + } + } + + public static final class SHA512WithPKCS1Padding extends PKCS1Padding { + public SHA512WithPKCS1Padding() { + super(KeymasterDefs.KM_DIGEST_SHA_2_512); + } + } + + abstract static class PSSPadding extends AndroidKeyStoreRSASignatureSpi { + private static final int SALT_LENGTH_BYTES = 20; + + PSSPadding(int keymasterDigest) { + super(keymasterDigest, KeymasterDefs.KM_PAD_RSA_PSS); + } + + @Override + protected final int getAdditionalEntropyAmountForSign() { + return SALT_LENGTH_BYTES; + } + } + + public static final class SHA1WithPSSPadding extends PSSPadding { + public SHA1WithPSSPadding() { + super(KeymasterDefs.KM_DIGEST_SHA1); + } + } + + public static final class SHA224WithPSSPadding extends PSSPadding { + public SHA224WithPSSPadding() { + super(KeymasterDefs.KM_DIGEST_SHA_2_224); + } + } + + public static final class SHA256WithPSSPadding extends PSSPadding { + public SHA256WithPSSPadding() { + super(KeymasterDefs.KM_DIGEST_SHA_2_256); + } + } + + public static final class SHA384WithPSSPadding extends PSSPadding { + public SHA384WithPSSPadding() { + super(KeymasterDefs.KM_DIGEST_SHA_2_384); + } + } + + public static final class SHA512WithPSSPadding extends PSSPadding { + public SHA512WithPSSPadding() { + super(KeymasterDefs.KM_DIGEST_SHA_2_512); + } + } + + private final int mKeymasterDigest; + private final int mKeymasterPadding; + + AndroidKeyStoreRSASignatureSpi(int keymasterDigest, int keymasterPadding) { + mKeymasterDigest = keymasterDigest; + mKeymasterPadding = keymasterPadding; + } + + @Override + protected final void initKey(AndroidKeyStoreKey key) throws InvalidKeyException { + if (!KeyProperties.KEY_ALGORITHM_RSA.equalsIgnoreCase(key.getAlgorithm())) { + throw new InvalidKeyException("Unsupported key algorithm: " + key.getAlgorithm() + + ". Only" + KeyProperties.KEY_ALGORITHM_RSA + " supported"); + } + super.initKey(key); + } + + @Override + protected final void resetAll() { + super.resetAll(); + } + + @Override + protected final void resetWhilePreservingInitState() { + super.resetWhilePreservingInitState(); + } + + @Override + protected final void addAlgorithmSpecificParametersToBegin( + @NonNull List<KeyParameter> parameters) { + parameters.add(KeyStore2ParameterUtils.makeEnum( + KeymasterDefs.KM_TAG_ALGORITHM, KeymasterDefs.KM_ALGORITHM_RSA + )); + parameters.add(KeyStore2ParameterUtils.makeEnum( + KeymasterDefs.KM_TAG_DIGEST, mKeymasterDigest + )); + parameters.add(KeyStore2ParameterUtils.makeEnum( + KeymasterDefs.KM_TAG_PADDING, mKeymasterPadding + )); + } +} diff --git a/keystore/java/android/security/keystore2/AndroidKeyStoreSecretKey.java b/keystore/java/android/security/keystore2/AndroidKeyStoreSecretKey.java new file mode 100644 index 000000000000..4e459137e875 --- /dev/null +++ b/keystore/java/android/security/keystore2/AndroidKeyStoreSecretKey.java @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2015 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.keystore2; + +import android.annotation.NonNull; +import android.security.KeyStoreSecurityLevel; +import android.system.keystore2.KeyDescriptor; +import android.system.keystore2.KeyMetadata; + +import javax.crypto.SecretKey; + +/** + * {@link SecretKey} backed by Android Keystore. + * + * @hide + */ +public class AndroidKeyStoreSecretKey extends AndroidKeyStoreKey implements SecretKey { + + public AndroidKeyStoreSecretKey(@NonNull KeyDescriptor descriptor, + @NonNull KeyMetadata metadata, @NonNull String algorithm, + @NonNull KeyStoreSecurityLevel securityLevel) { + super(descriptor, metadata.key.nspace, metadata.authorizations, algorithm, securityLevel); + } +} diff --git a/keystore/java/android/security/keystore2/AndroidKeyStoreSecretKeyFactorySpi.java b/keystore/java/android/security/keystore2/AndroidKeyStoreSecretKeyFactorySpi.java new file mode 100644 index 000000000000..9d3b9704d711 --- /dev/null +++ b/keystore/java/android/security/keystore2/AndroidKeyStoreSecretKeyFactorySpi.java @@ -0,0 +1,272 @@ +/* + * Copyright (C) 2015 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.keystore2; + +import android.annotation.NonNull; +import android.security.GateKeeper; +import android.security.KeyStore; +import android.security.keymaster.KeymasterArguments; +import android.security.keymaster.KeymasterDefs; +import android.security.keystore.KeyGenParameterSpec; +import android.security.keystore.KeyInfo; +import android.security.keystore.KeyProperties; +import android.system.keystore2.Authorization; + +import java.math.BigInteger; +import java.security.InvalidKeyException; +import java.security.ProviderException; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.KeySpec; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; + +import javax.crypto.SecretKey; +import javax.crypto.SecretKeyFactorySpi; +import javax.crypto.spec.SecretKeySpec; + +/** + * {@link SecretKeyFactorySpi} backed by Android Keystore. + * + * @hide + */ +public class AndroidKeyStoreSecretKeyFactorySpi extends SecretKeyFactorySpi { + + private final KeyStore mKeyStore = KeyStore.getInstance(); + + @Override + protected KeySpec engineGetKeySpec(SecretKey key, + @SuppressWarnings("rawtypes") Class keySpecClass) throws InvalidKeySpecException { + if (keySpecClass == null) { + throw new InvalidKeySpecException("keySpecClass == null"); + } + if (!(key instanceof AndroidKeyStoreSecretKey)) { + throw new InvalidKeySpecException("Only Android KeyStore secret keys supported: " + + ((key != null) ? key.getClass().getName() : "null")); + } + if (SecretKeySpec.class.isAssignableFrom(keySpecClass)) { + throw new InvalidKeySpecException( + "Key material export of Android KeyStore keys is not supported"); + } + if (!KeyInfo.class.equals(keySpecClass)) { + throw new InvalidKeySpecException("Unsupported key spec: " + keySpecClass.getName()); + } + AndroidKeyStoreKey keystoreKey = (AndroidKeyStoreKey) key; + + return getKeyInfo(keystoreKey); + } + + static @NonNull KeyInfo getKeyInfo(@NonNull AndroidKeyStoreKey key) { + + @KeyProperties.SecurityLevelEnum int securityLevel = + KeyProperties.SECURITY_LEVEL_SOFTWARE; + boolean insideSecureHardware = false; + @KeyProperties.OriginEnum int origin = -1; + int keySize = -1; + @KeyProperties.PurposeEnum int purposes = 0; + String[] encryptionPaddings; + String[] signaturePaddings; + List<String> digestsList = new ArrayList<>(); + List<String> blockModesList = new ArrayList<>(); + int keymasterSwEnforcedUserAuthenticators = 0; + int keymasterHwEnforcedUserAuthenticators = 0; + List<BigInteger> keymasterSecureUserIds = new ArrayList<BigInteger>(); + List<String> encryptionPaddingsList = new ArrayList<String>(); + List<String> signaturePaddingsList = new ArrayList<String>(); + Date keyValidityStart = null; + Date keyValidityForOriginationEnd = null; + Date keyValidityForConsumptionEnd = null; + long userAuthenticationValidityDurationSeconds = 0; + boolean userAuthenticationRequired = true; + boolean userAuthenticationValidWhileOnBody = false; + boolean trustedUserPresenceRequired = false; + boolean trustedUserConfirmationRequired = false; + try { + for (Authorization a : key.getAuthorizations()) { + switch (a.keyParameter.tag) { + case KeymasterDefs.KM_TAG_ORIGIN: + insideSecureHardware = + KeyStore2ParameterUtils.isSecureHardware(a.securityLevel); + securityLevel = a.securityLevel; + origin = KeyProperties.Origin.fromKeymaster(a.keyParameter.integer); + break; + case KeymasterDefs.KM_TAG_KEY_SIZE: + long keySizeUnsigned = KeyStore2ParameterUtils.getUnsignedInt(a); + if (keySizeUnsigned > Integer.MAX_VALUE) { + throw new ProviderException( + "Key too large: " + keySizeUnsigned + " bits"); + } + keySize = (int) keySizeUnsigned; + break; + case KeymasterDefs.KM_TAG_PURPOSE: + purposes |= KeyProperties.Purpose.fromKeymaster(a.keyParameter.integer); + break; + case KeymasterDefs.KM_TAG_PADDING: + try { + if (a.keyParameter.integer == KeymasterDefs.KM_PAD_RSA_PKCS1_1_5_SIGN + || a.keyParameter.integer == KeymasterDefs.KM_PAD_RSA_PSS) { + @KeyProperties.SignaturePaddingEnum String padding = + KeyProperties.SignaturePadding.fromKeymaster( + a.keyParameter.integer); + signaturePaddingsList.add(padding); + } else { + @KeyProperties.EncryptionPaddingEnum String jcaPadding = + KeyProperties.EncryptionPadding.fromKeymaster( + a.keyParameter.integer); + encryptionPaddingsList.add(jcaPadding); + } + } catch (IllegalArgumentException e) { + throw new ProviderException("Unsupported padding: " + + a.keyParameter.integer); + } + break; + case KeymasterDefs.KM_TAG_DIGEST: + digestsList.add(KeyProperties.Digest.fromKeymaster(a.keyParameter.integer)); + break; + case KeymasterDefs.KM_TAG_BLOCK_MODE: + blockModesList.add( + KeyProperties.BlockMode.fromKeymaster(a.keyParameter.integer) + ); + break; + case KeymasterDefs.KM_TAG_USER_AUTH_TYPE: + if (KeyStore2ParameterUtils.isSecureHardware(a.securityLevel)) { + keymasterHwEnforcedUserAuthenticators = a.keyParameter.integer; + } else { + keymasterSwEnforcedUserAuthenticators = a.keyParameter.integer; + } + break; + case KeymasterDefs.KM_TAG_USER_SECURE_ID: + keymasterSecureUserIds.add( + KeymasterArguments.toUint64(a.keyParameter.longInteger)); + break; + case KeymasterDefs.KM_TAG_ACTIVE_DATETIME: + keyValidityStart = KeyStore2ParameterUtils.getDate(a); + break; + case KeymasterDefs.KM_TAG_ORIGINATION_EXPIRE_DATETIME: + keyValidityForOriginationEnd = + KeyStore2ParameterUtils.getDate(a); + break; + case KeymasterDefs.KM_TAG_USAGE_EXPIRE_DATETIME: + keyValidityForConsumptionEnd = + KeyStore2ParameterUtils.getDate(a); + break; + case KeymasterDefs.KM_TAG_NO_AUTH_REQUIRED: + userAuthenticationRequired = false; + break; + case KeymasterDefs.KM_TAG_AUTH_TIMEOUT: + userAuthenticationValidityDurationSeconds = + KeyStore2ParameterUtils.getUnsignedInt(a); + if (userAuthenticationValidityDurationSeconds > Integer.MAX_VALUE) { + throw new ProviderException( + "User authentication timeout validity too long: " + + userAuthenticationValidityDurationSeconds + " seconds"); + } + break; + case KeymasterDefs.KM_TAG_ALLOW_WHILE_ON_BODY: + userAuthenticationValidWhileOnBody = + KeyStore2ParameterUtils.isSecureHardware(a.securityLevel); + break; + case KeymasterDefs.KM_TAG_TRUSTED_USER_PRESENCE_REQUIRED: + trustedUserPresenceRequired = + KeyStore2ParameterUtils.isSecureHardware(a.securityLevel); + break; + case KeymasterDefs.KM_TAG_TRUSTED_CONFIRMATION_REQUIRED: + trustedUserConfirmationRequired = + KeyStore2ParameterUtils.isSecureHardware(a.securityLevel); + break; + } + } + } catch (IllegalArgumentException e) { + throw new ProviderException("Unsupported key characteristic", e); + } + if (keySize == -1) { + throw new ProviderException("Key size not available"); + } + if (origin == -1) { + throw new ProviderException("Key origin not available"); + } + + encryptionPaddings = + encryptionPaddingsList.toArray(new String[0]); + signaturePaddings = + signaturePaddingsList.toArray(new String[0]); + + boolean userAuthenticationRequirementEnforcedBySecureHardware = (userAuthenticationRequired) + && (keymasterHwEnforcedUserAuthenticators != 0) + && (keymasterSwEnforcedUserAuthenticators == 0); + + String[] digests = digestsList.toArray(new String[0]); + String[] blockModes = blockModesList.toArray(new String[0]); + + boolean invalidatedByBiometricEnrollment = false; + if (keymasterSwEnforcedUserAuthenticators == KeymasterDefs.HW_AUTH_BIOMETRIC + || keymasterHwEnforcedUserAuthenticators == KeymasterDefs.HW_AUTH_BIOMETRIC) { + // Fingerprint-only key; will be invalidated if the root SID isn't in the SID list. + invalidatedByBiometricEnrollment = !keymasterSecureUserIds.isEmpty() + && !keymasterSecureUserIds.contains(getGateKeeperSecureUserId()); + } + + return new KeyInfo(key.getUserKeyDescriptor().alias, + insideSecureHardware, + origin, + keySize, + keyValidityStart, + keyValidityForOriginationEnd, + keyValidityForConsumptionEnd, + purposes, + encryptionPaddings, + signaturePaddings, + digests, + blockModes, + userAuthenticationRequired, + (int) userAuthenticationValidityDurationSeconds, + keymasterHwEnforcedUserAuthenticators, + userAuthenticationRequirementEnforcedBySecureHardware, + userAuthenticationValidWhileOnBody, + trustedUserPresenceRequired, + invalidatedByBiometricEnrollment, + trustedUserConfirmationRequired, + securityLevel); + } + + private static BigInteger getGateKeeperSecureUserId() throws ProviderException { + try { + return BigInteger.valueOf(GateKeeper.getSecureUserId()); + } catch (IllegalStateException e) { + throw new ProviderException("Failed to get GateKeeper secure user ID", e); + } + } + + @Override + protected SecretKey engineGenerateSecret(KeySpec keySpec) throws InvalidKeySpecException { + throw new InvalidKeySpecException( + "To generate secret key in Android Keystore, use KeyGenerator initialized with " + + KeyGenParameterSpec.class.getName()); + } + + @Override + protected SecretKey engineTranslateKey(SecretKey key) throws InvalidKeyException { + if (key == null) { + throw new InvalidKeyException("key == null"); + } else if (!(key instanceof AndroidKeyStoreSecretKey)) { + throw new InvalidKeyException( + "To import a secret key into Android Keystore, use KeyStore.setEntry"); + } + + return key; + } +} diff --git a/keystore/java/android/security/keystore2/AndroidKeyStoreSignatureSpiBase.java b/keystore/java/android/security/keystore2/AndroidKeyStoreSignatureSpiBase.java new file mode 100644 index 000000000000..55414b70d403 --- /dev/null +++ b/keystore/java/android/security/keystore2/AndroidKeyStoreSignatureSpiBase.java @@ -0,0 +1,423 @@ +/* + * Copyright (C) 2015 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.keystore2; + +import android.annotation.CallSuper; +import android.annotation.NonNull; +import android.security.KeyStoreException; +import android.security.KeyStoreOperation; +import android.security.keymaster.KeymasterDefs; +import android.security.keystore.ArrayUtils; +import android.security.keystore.KeyStoreCryptoOperation; +import android.system.keystore2.KeyParameter; + +import libcore.util.EmptyArray; + +import java.nio.ByteBuffer; +import java.security.InvalidKeyException; +import java.security.InvalidParameterException; +import java.security.PrivateKey; +import java.security.ProviderException; +import java.security.PublicKey; +import java.security.SecureRandom; +import java.security.SignatureException; +import java.security.SignatureSpi; +import java.util.ArrayList; +import java.util.List; + +/** + * Base class for {@link SignatureSpi} implementations of Android KeyStore backed ciphers. + * + * @hide + */ +abstract class AndroidKeyStoreSignatureSpiBase extends SignatureSpi + implements KeyStoreCryptoOperation { + private static final String TAG = "AndroidKeyStoreSignatureSpiBase"; + + // Fields below are populated by SignatureSpi.engineInitSign/engineInitVerify and KeyStore.begin + // and should be preserved after SignatureSpi.engineSign/engineVerify finishes. + private boolean mSigning; + private AndroidKeyStoreKey mKey; + + /** + * Object representing this operation inside keystore service. It is initialized + * by {@code engineInit} and is invalidated when {@code engineDoFinal} succeeds and on some + * error conditions in between. + */ + private KeyStoreOperation mOperation; + /** + * The operation challenge is required when an operation needs user authorization. + * The challenge is subjected to an authenticator, e.g., Gatekeeper or a biometric + * authenticator, and included in the authentication token minted by this authenticator. + * It may be null, if the operation does not require authorization. + */ + private long mOperationChallenge; + private KeyStoreCryptoOperationStreamer mMessageStreamer; + + /** + * Encountered exception which could not be immediately thrown because it was encountered inside + * a method that does not throw checked exception. This exception will be thrown from + * {@code engineSign} or {@code engineVerify}. Once such an exception is encountered, + * {@code engineUpdate} starts ignoring input data. + */ + private Exception mCachedException; + + AndroidKeyStoreSignatureSpiBase() { + mOperation = null; + mOperationChallenge = 0; + mSigning = false; + mKey = null; + appRandom = null; + mMessageStreamer = null; + mCachedException = null; + } + + @Override + protected final void engineInitSign(PrivateKey key) throws InvalidKeyException { + engineInitSign(key, null); + } + + @Override + protected final void engineInitSign(PrivateKey privateKey, SecureRandom random) + throws InvalidKeyException { + resetAll(); + + boolean success = false; + try { + if (privateKey == null) { + throw new InvalidKeyException("Unsupported key: null"); + } + AndroidKeyStoreKey keystoreKey; + if (privateKey instanceof AndroidKeyStorePrivateKey) { + keystoreKey = (AndroidKeyStoreKey) privateKey; + } else { + throw new InvalidKeyException("Unsupported private key type: " + privateKey); + } + mSigning = true; + initKey(keystoreKey); + appRandom = random; + ensureKeystoreOperationInitialized(); + success = true; + } finally { + if (!success) { + resetAll(); + } + } + } + + @Override + protected final void engineInitVerify(PublicKey publicKey) throws InvalidKeyException { + resetAll(); + + boolean success = false; + try { + if (publicKey == null) { + throw new InvalidKeyException("Unsupported key: null"); + } + AndroidKeyStoreKey keystoreKey; + if (publicKey instanceof AndroidKeyStorePublicKey) { + keystoreKey = (AndroidKeyStorePublicKey) publicKey; + } else { + throw new InvalidKeyException("Unsupported public key type: " + publicKey); + } + mSigning = false; + initKey(keystoreKey); + appRandom = null; + ensureKeystoreOperationInitialized(); + success = true; + } finally { + if (!success) { + resetAll(); + } + } + } + + /** + * Configures this signature instance to use the provided key. + * + * @throws InvalidKeyException if the {@code key} is not suitable. + */ + @CallSuper + protected void initKey(AndroidKeyStoreKey key) throws InvalidKeyException { + mKey = key; + } + + private void abortOperation() { + KeyStoreCryptoOperationUtils.abortOperation(mOperation); + mOperation = null; + } + + /** + * Resets this cipher to its pristine pre-init state. This must be equivalent to obtaining a new + * cipher instance. + * + * <p>Subclasses storing additional state should override this method, reset the additional + * state, and then chain to superclass. + */ + @CallSuper + protected void resetAll() { + abortOperation(); + mOperationChallenge = 0; + mSigning = false; + mKey = null; + appRandom = null; + mMessageStreamer = null; + mCachedException = null; + } + + /** + * Resets this cipher while preserving the initialized state. This must be equivalent to + * rolling back the cipher's state to just after the most recent {@code engineInit} completed + * successfully. + * + * <p>Subclasses storing additional post-init state should override this method, reset the + * additional state, and then chain to superclass. + */ + @CallSuper + protected void resetWhilePreservingInitState() { + abortOperation(); + mOperationChallenge = 0; + mMessageStreamer = null; + mCachedException = null; + } + + private void ensureKeystoreOperationInitialized() throws InvalidKeyException { + if (mMessageStreamer != null) { + return; + } + if (mCachedException != null) { + return; + } + if (mKey == null) { + throw new IllegalStateException("Not initialized"); + } + + List<KeyParameter> parameters = new ArrayList<>(); + addAlgorithmSpecificParametersToBegin(parameters); + + int purpose = mSigning ? KeymasterDefs.KM_PURPOSE_SIGN : KeymasterDefs.KM_PURPOSE_VERIFY; + + parameters.add(KeyStore2ParameterUtils.makeEnum(KeymasterDefs.KM_TAG_PURPOSE, purpose)); + + try { + mOperation = mKey.getSecurityLevel().createOperation( + mKey.getKeyIdDescriptor(), + parameters); + } catch (KeyStoreException keyStoreException) { + throw KeyStoreCryptoOperationUtils.getInvalidKeyException( + mKey, keyStoreException); + } + + // Now we check if we got an operation challenge. This indicates that user authorization + // is required. And if we got a challenge we check if the authorization can possibly + // succeed. + mOperationChallenge = KeyStoreCryptoOperationUtils.getOrMakeOperationChallenge( + mOperation, mKey); + + mMessageStreamer = createMainDataStreamer(mOperation); + } + + /** + * Creates a streamer which sends the message to be signed/verified into the provided KeyStore + * + * <p>This implementation returns a working streamer. + */ + @NonNull + protected KeyStoreCryptoOperationStreamer createMainDataStreamer( + @NonNull KeyStoreOperation operation) { + return new KeyStoreCryptoOperationChunkedStreamer( + new KeyStoreCryptoOperationChunkedStreamer.MainDataStream( + operation)); + } + + @Override + public final long getOperationHandle() { + return mOperationChallenge; + } + + @Override + protected final void engineUpdate(byte[] b, int off, int len) throws SignatureException { + if (mCachedException != null) { + throw new SignatureException(mCachedException); + } + + try { + ensureKeystoreOperationInitialized(); + } catch (InvalidKeyException e) { + throw new SignatureException(e); + } + + if (len == 0) { + return; + } + + byte[] output; + try { + output = mMessageStreamer.update(b, off, len); + } catch (KeyStoreException e) { + throw new SignatureException(e); + } + + if (output.length != 0) { + throw new ProviderException( + "Update operation unexpectedly produced output: " + output.length + " bytes"); + } + } + + @Override + protected final void engineUpdate(byte b) throws SignatureException { + engineUpdate(new byte[] {b}, 0, 1); + } + + @Override + protected final void engineUpdate(ByteBuffer input) { + byte[] b; + int off; + int len = input.remaining(); + if (input.hasArray()) { + b = input.array(); + off = input.arrayOffset() + input.position(); + input.position(input.limit()); + } else { + b = new byte[len]; + off = 0; + input.get(b); + } + + try { + engineUpdate(b, off, len); + } catch (SignatureException e) { + mCachedException = e; + } + } + + @Override + protected final int engineSign(byte[] out, int outOffset, int outLen) + throws SignatureException { + return super.engineSign(out, outOffset, outLen); + } + + @Override + protected final byte[] engineSign() throws SignatureException { + if (mCachedException != null) { + throw new SignatureException(mCachedException); + } + + byte[] signature; + try { + ensureKeystoreOperationInitialized(); + + byte[] additionalEntropy = + KeyStoreCryptoOperationUtils.getRandomBytesToMixIntoKeystoreRng( + appRandom, getAdditionalEntropyAmountForSign()); + signature = mMessageStreamer.doFinal( + EmptyArray.BYTE, 0, 0, + null); // no signature provided -- it'll be generated by this invocation + } catch (InvalidKeyException | KeyStoreException e) { + throw new SignatureException(e); + } + + resetWhilePreservingInitState(); + return signature; + } + + @Override + protected final boolean engineVerify(byte[] signature) throws SignatureException { + if (mCachedException != null) { + throw new SignatureException(mCachedException); + } + + try { + ensureKeystoreOperationInitialized(); + } catch (InvalidKeyException e) { + throw new SignatureException(e); + } + + boolean verified; + try { + byte[] output = mMessageStreamer.doFinal( + EmptyArray.BYTE, 0, 0, + signature); + if (output.length != 0) { + throw new ProviderException( + "Signature verification unexpected produced output: " + output.length + + " bytes"); + } + verified = true; + } catch (KeyStoreException e) { + switch (e.getErrorCode()) { + case KeymasterDefs.KM_ERROR_VERIFICATION_FAILED: + verified = false; + break; + default: + throw new SignatureException(e); + } + } + + resetWhilePreservingInitState(); + return verified; + } + + @Override + protected final boolean engineVerify(byte[] sigBytes, int offset, int len) + throws SignatureException { + return engineVerify(ArrayUtils.subarray(sigBytes, offset, len)); + } + + @Deprecated + @Override + protected final Object engineGetParameter(String param) throws InvalidParameterException { + throw new InvalidParameterException(); + } + + @Deprecated + @Override + protected final void engineSetParameter(String param, Object value) + throws InvalidParameterException { + throw new InvalidParameterException(); + } + + /** + * Returns {@code true} if this signature is initialized for signing, {@code false} if this + * signature is initialized for verification. + */ + protected final boolean isSigning() { + return mSigning; + } + + // The methods below need to be implemented by subclasses. + + /** + * Returns the amount of additional entropy (in bytes) to be provided to the KeyStore's + * {@code finish} operation when generating a signature. + * + * <p>This value should match (or exceed) the amount of Shannon entropy of the produced + * signature assuming the key and the message are known. For example, for ECDSA signature this + * should be the size of {@code R}, whereas for the RSA signature with PKCS#1 padding this + * should be {@code 0}. + */ + protected abstract int getAdditionalEntropyAmountForSign(); + + /** + * Invoked to add algorithm-specific parameters for the KeyStore's {@code begin} operation. + * + * @param parameters keystore/keymaster arguments to be populated with algorithm-specific + * parameters. + */ + protected abstract void addAlgorithmSpecificParametersToBegin( + @NonNull List<KeyParameter> parameters); +} diff --git a/keystore/java/android/security/keystore2/AndroidKeyStoreSpi.java b/keystore/java/android/security/keystore2/AndroidKeyStoreSpi.java new file mode 100644 index 000000000000..4c26864cb02b --- /dev/null +++ b/keystore/java/android/security/keystore2/AndroidKeyStoreSpi.java @@ -0,0 +1,1167 @@ +/* + * Copyright (C) 2012 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.keystore2; + +import android.annotation.NonNull; +import android.hardware.biometrics.BiometricManager; +import android.security.GateKeeper; +import android.security.KeyStore2; +import android.security.KeyStoreParameter; +import android.security.KeyStoreSecurityLevel; +import android.security.keymaster.KeymasterDefs; +import android.security.keystore.KeyGenParameterSpec; +import android.security.keystore.KeyPermanentlyInvalidatedException; +import android.security.keystore.KeyProperties; +import android.security.keystore.KeyProtection; +import android.security.keystore.KeymasterUtils; +import android.security.keystore.SecureKeyImportUnavailableException; +import android.security.keystore.WrappedKeyEntry; +import android.system.keystore2.AuthenticatorSpec; +import android.system.keystore2.Domain; +import android.system.keystore2.IKeystoreSecurityLevel; +import android.system.keystore2.KeyDescriptor; +import android.system.keystore2.KeyEntryResponse; +import android.system.keystore2.KeyMetadata; +import android.system.keystore2.KeyParameter; +import android.system.keystore2.ResponseCode; +import android.system.keystore2.SecurityLevel; +import android.util.Log; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.security.Key; +import java.security.KeyStore.Entry; +import java.security.KeyStore.LoadStoreParameter; +import java.security.KeyStore.PrivateKeyEntry; +import java.security.KeyStore.ProtectionParameter; +import java.security.KeyStore.SecretKeyEntry; +import java.security.KeyStoreException; +import java.security.KeyStoreSpi; +import java.security.NoSuchAlgorithmException; +import java.security.PrivateKey; +import java.security.ProviderException; +import java.security.UnrecoverableKeyException; +import java.security.cert.Certificate; +import java.security.cert.CertificateEncodingException; +import java.security.cert.CertificateException; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.Date; +import java.util.Enumeration; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Set; + +import javax.crypto.SecretKey; + +/** + * A java.security.KeyStore interface for the Android KeyStore. An instance of + * it can be created via the {@link java.security.KeyStore#getInstance(String) + * KeyStore.getInstance("AndroidKeyStore")} interface. This returns a + * java.security.KeyStore backed by this "AndroidKeyStore" implementation. + * <p> + * This is built on top of Android's keystore daemon. The convention of alias + * use is: + * <p> + * PrivateKeyEntry will have a Credentials.USER_PRIVATE_KEY as the private key, + * Credentials.USER_CERTIFICATE as the first certificate in the chain (the one + * that corresponds to the private key), and then a Credentials.CA_CERTIFICATE + * entry which will have the rest of the chain concatenated in BER format. + * <p> + * TrustedCertificateEntry will just have a Credentials.CA_CERTIFICATE entry + * with a single certificate. + * + * @hide + */ +public class AndroidKeyStoreSpi extends KeyStoreSpi { + public static final String TAG = "AndroidKeyStoreSpi"; + public static final String NAME = "AndroidKeyStore"; + + private KeyStore2 mKeyStore; + private int mNamespace = KeyProperties.NAMESPACE_APPLICATION; + + @Override + public Key engineGetKey(String alias, char[] password) throws NoSuchAlgorithmException, + UnrecoverableKeyException { + try { + return AndroidKeyStoreProvider.loadAndroidKeyStoreKeyFromKeystore(mKeyStore, + alias, + mNamespace); + } catch (KeyPermanentlyInvalidatedException e) { + throw new UnrecoverableKeyException(e.getMessage()); + } catch (UnrecoverableKeyException e) { + Throwable cause = e.getCause(); + if (cause instanceof android.security.KeyStoreException) { + if (((android.security.KeyStoreException) cause).getErrorCode() + == ResponseCode.KEY_NOT_FOUND) { + return null; + } + } + throw e; + } + } + + /** + * Make a key descriptor from the given alias and the mNamespace member. + * If mNamespace is -1 it sets the domain field to {@link Domain#APP} and {@link Domain#SELINUX} + * otherwise. The blob field is always set to null and the alias field to {@code alias} + * @param alias The alias of the new key descriptor. + * @return A new key descriptor. + */ + private KeyDescriptor makeKeyDescriptor(@NonNull String alias) { + KeyDescriptor descriptor = new KeyDescriptor(); + descriptor.domain = getTargetDomain(); + descriptor.nspace = mNamespace; // ignored if Domain.App; + descriptor.alias = alias; + descriptor.blob = null; + return descriptor; + } + + private @Domain int getTargetDomain() { + return mNamespace == KeyProperties.NAMESPACE_APPLICATION + ? Domain.APP + : Domain.SELINUX; + } + private KeyEntryResponse getKeyMetadata(String alias) { + if (alias == null) { + throw new NullPointerException("alias == null"); + } + + KeyDescriptor descriptor = makeKeyDescriptor(alias); + + try { + return mKeyStore.getKeyEntry(descriptor); + } catch (android.security.KeyStoreException e) { + if (e.getErrorCode() != ResponseCode.KEY_NOT_FOUND) { + Log.w(TAG, "Could not get key metadata from Keystore.", e); + } + return null; + } + } + + @Override + public Certificate[] engineGetCertificateChain(String alias) { + KeyEntryResponse response = getKeyMetadata(alias); + + if (response == null || response.metadata.certificate == null) { + return null; + } + + final X509Certificate leaf = (X509Certificate) toCertificate(response.metadata.certificate); + if (leaf == null) { + return null; + } + + final Certificate[] caList; + + final byte[] caBytes = response.metadata.certificateChain; + + if (caBytes != null) { + final Collection<X509Certificate> caChain = toCertificates(caBytes); + + caList = new Certificate[caChain.size() + 1]; + + final Iterator<X509Certificate> it = caChain.iterator(); + int i = 1; + while (it.hasNext()) { + caList[i++] = it.next(); + } + } else { + caList = new Certificate[1]; + } + + caList[0] = leaf; + + return caList; + } + + @Override + public Certificate engineGetCertificate(String alias) { + KeyEntryResponse response = getKeyMetadata(alias); + + if (response == null) { + return null; + } + + byte[] encodedCert = response.metadata.certificate; + if (encodedCert != null) { + return toCertificate(encodedCert); + } + + encodedCert = response.metadata.certificateChain; + if (encodedCert != null) { + return toCertificate(encodedCert); + } + + // This entry/alias does not contain a certificate. + return null; + } + + private static X509Certificate toCertificate(byte[] bytes) { + try { + final CertificateFactory certFactory = CertificateFactory.getInstance("X.509"); + return (X509Certificate) certFactory.generateCertificate( + new ByteArrayInputStream(bytes)); + } catch (CertificateException e) { + Log.w(NAME, "Couldn't parse certificate in keystore", e); + return null; + } + } + + @SuppressWarnings("unchecked") + private static Collection<X509Certificate> toCertificates(byte[] bytes) { + try { + final CertificateFactory certFactory = CertificateFactory.getInstance("X.509"); + return (Collection<X509Certificate>) certFactory.generateCertificates( + new ByteArrayInputStream(bytes)); + } catch (CertificateException e) { + Log.w(NAME, "Couldn't parse certificates in keystore", e); + return new ArrayList<X509Certificate>(); + } + } + + @Override + public Date engineGetCreationDate(String alias) { + KeyEntryResponse response = getKeyMetadata(alias); + + if (response == null) { + return null; + } + + + // TODO add modification time to key metadata. + return null; + // if (response.metadata.modificationTime == -1) { + // return null; + // } + // return new Date(response.metadata.modificationTime); + } + + @Override + public void engineSetKeyEntry(String alias, Key key, char[] password, Certificate[] chain) + throws KeyStoreException { + if ((password != null) && (password.length > 0)) { + throw new KeyStoreException("entries cannot be protected with passwords"); + } + + if (key instanceof PrivateKey) { + setPrivateKeyEntry(alias, (PrivateKey) key, chain, null); + } else if (key instanceof SecretKey) { + setSecretKeyEntry(alias, (SecretKey) key, null); + } else { + throw new KeyStoreException("Only PrivateKey and SecretKey are supported"); + } + } + + private static KeyProtection getLegacyKeyProtectionParameter(PrivateKey key) + throws KeyStoreException { + String keyAlgorithm = key.getAlgorithm(); + KeyProtection.Builder specBuilder; + if (KeyProperties.KEY_ALGORITHM_EC.equalsIgnoreCase(keyAlgorithm)) { + specBuilder = + new KeyProtection.Builder( + KeyProperties.PURPOSE_SIGN | KeyProperties.PURPOSE_VERIFY); + // Authorized to be used with any digest (including no digest). + // MD5 was never offered for Android Keystore for ECDSA. + specBuilder.setDigests( + KeyProperties.DIGEST_NONE, + KeyProperties.DIGEST_SHA1, + KeyProperties.DIGEST_SHA224, + KeyProperties.DIGEST_SHA256, + KeyProperties.DIGEST_SHA384, + KeyProperties.DIGEST_SHA512); + } else if (KeyProperties.KEY_ALGORITHM_RSA.equalsIgnoreCase(keyAlgorithm)) { + specBuilder = + new KeyProtection.Builder( + KeyProperties.PURPOSE_ENCRYPT + | KeyProperties.PURPOSE_DECRYPT + | KeyProperties.PURPOSE_SIGN + | KeyProperties.PURPOSE_VERIFY); + // Authorized to be used with any digest (including no digest). + specBuilder.setDigests( + KeyProperties.DIGEST_NONE, + KeyProperties.DIGEST_MD5, + KeyProperties.DIGEST_SHA1, + KeyProperties.DIGEST_SHA224, + KeyProperties.DIGEST_SHA256, + KeyProperties.DIGEST_SHA384, + KeyProperties.DIGEST_SHA512); + // Authorized to be used with any encryption and signature padding + // schemes (including no padding). + specBuilder.setEncryptionPaddings( + KeyProperties.ENCRYPTION_PADDING_NONE, + KeyProperties.ENCRYPTION_PADDING_RSA_PKCS1, + KeyProperties.ENCRYPTION_PADDING_RSA_OAEP); + specBuilder.setSignaturePaddings( + KeyProperties.SIGNATURE_PADDING_RSA_PKCS1, + KeyProperties.SIGNATURE_PADDING_RSA_PSS); + // Disable randomized encryption requirement to support encryption + // padding NONE above. + specBuilder.setRandomizedEncryptionRequired(false); + } else { + throw new KeyStoreException("Unsupported key algorithm: " + keyAlgorithm); + } + specBuilder.setUserAuthenticationRequired(false); + + return specBuilder.build(); + } + + private void setPrivateKeyEntry(String alias, PrivateKey key, Certificate[] chain, + java.security.KeyStore.ProtectionParameter param) throws KeyStoreException { + @SecurityLevel int securitylevel = SecurityLevel.TRUSTED_ENVIRONMENT; + int flags = 0; + KeyProtection spec; + if (param == null) { + spec = getLegacyKeyProtectionParameter(key); + } else if (param instanceof KeyStoreParameter) { + spec = getLegacyKeyProtectionParameter(key); + KeyStoreParameter legacySpec = (KeyStoreParameter) param; + } else if (param instanceof KeyProtection) { + spec = (KeyProtection) param; + if (spec.isCriticalToDeviceEncryption()) { + // This key is should not be bound to the LSKF even if it is auth bound. + // This indicates that this key is used in the derivation for of the + // master key, that is used for the LSKF binding of other auth bound + // keys. This breaks up a circular dependency while retaining logical + // authentication binding of the key. + flags |= IKeystoreSecurityLevel + .KEY_FLAG_AUTH_BOUND_WITHOUT_CRYPTOGRAPHIC_LSKF_BINDING; + } + + if (spec.isStrongBoxBacked()) { + securitylevel = SecurityLevel.STRONGBOX; + } + } else { + throw new KeyStoreException( + "Unsupported protection parameter class:" + param.getClass().getName() + + ". Supported: " + KeyProtection.class.getName() + ", " + + KeyStoreParameter.class.getName()); + } + + // Make sure the chain exists since this is a PrivateKey + if ((chain == null) || (chain.length == 0)) { + throw new KeyStoreException("Must supply at least one Certificate with PrivateKey"); + } + + // Do chain type checking. + X509Certificate[] x509chain = new X509Certificate[chain.length]; + for (int i = 0; i < chain.length; i++) { + if (!"X.509".equals(chain[i].getType())) { + throw new KeyStoreException("Certificates must be in X.509 format: invalid cert #" + + i); + } + + if (!(chain[i] instanceof X509Certificate)) { + throw new KeyStoreException("Certificates must be in X.509 format: invalid cert #" + + i); + } + + x509chain[i] = (X509Certificate) chain[i]; + } + + final byte[] userCertBytes; + try { + userCertBytes = x509chain[0].getEncoded(); + } catch (CertificateEncodingException e) { + throw new KeyStoreException("Failed to encode certificate #0", e); + } + + /* + * If we have a chain, store it in the CA certificate slot for this + * alias as concatenated DER-encoded certificates. These can be + * deserialized by {@link CertificateFactory#generateCertificates}. + */ + final byte[] chainBytes; + if (chain.length > 1) { + /* + * The chain is passed in as {user_cert, ca_cert_1, ca_cert_2, ...} + * so we only need the certificates starting at index 1. + */ + final byte[][] certsBytes = new byte[x509chain.length - 1][]; + int totalCertLength = 0; + for (int i = 0; i < certsBytes.length; i++) { + try { + certsBytes[i] = x509chain[i + 1].getEncoded(); + totalCertLength += certsBytes[i].length; + } catch (CertificateEncodingException e) { + throw new KeyStoreException("Failed to encode certificate #" + i, e); + } + } + + /* + * Serialize this into one byte array so we can later call + * CertificateFactory#generateCertificates to recover them. + */ + chainBytes = new byte[totalCertLength]; + int outputOffset = 0; + for (int i = 0; i < certsBytes.length; i++) { + final int certLength = certsBytes[i].length; + System.arraycopy(certsBytes[i], 0, chainBytes, outputOffset, certLength); + outputOffset += certLength; + certsBytes[i] = null; + } + } else { + chainBytes = null; + } + + @Domain int targetDomain = getTargetDomain(); + + // If the given key is an AndroidKeyStorePrivateKey, we attempt to update + // its subcomponents with the given certificate and certificate chain. + if (key instanceof AndroidKeyStorePrivateKey) { + AndroidKeyStoreKey ksKey = (AndroidKeyStoreKey) key; + KeyDescriptor descriptor = ksKey.getUserKeyDescriptor(); + + // This throws if the request cannot replace the entry. + assertCanReplace(alias, targetDomain, mNamespace, descriptor); + + try { + mKeyStore.updateSubcomponents( + ((AndroidKeyStorePrivateKey) key).getKeyIdDescriptor(), + userCertBytes, chainBytes); + } catch (android.security.KeyStoreException e) { + throw new KeyStoreException("Failed to store certificate and certificate chain", e); + } + return; + } + + // Make sure the PrivateKey format is the one we support. + final String keyFormat = key.getFormat(); + if ((keyFormat == null) || (!"PKCS#8".equals(keyFormat))) { + throw new KeyStoreException( + "Unsupported private key export format: " + keyFormat + + ". Only private keys which export their key material in PKCS#8 format are" + + " supported."); + } + + // Make sure we can actually encode the key. + byte[] pkcs8EncodedPrivateKeyBytes = key.getEncoded(); + if (pkcs8EncodedPrivateKeyBytes == null) { + throw new KeyStoreException("Private key did not export any key material"); + } + + final List<KeyParameter> importArgs = new ArrayList<>(); + + try { + importArgs.add(KeyStore2ParameterUtils.makeEnum( + KeymasterDefs.KM_TAG_ALGORITHM, + KeyProperties.KeyAlgorithm.toKeymasterAsymmetricKeyAlgorithm( + key.getAlgorithm())) + ); + KeyStore2ParameterUtils.forEachSetFlag(spec.getPurposes(), (purpose) -> { + importArgs.add(KeyStore2ParameterUtils.makeEnum( + KeymasterDefs.KM_TAG_PURPOSE, + KeyProperties.Purpose.toKeymaster(purpose) + )); + }); + if (spec.isDigestsSpecified()) { + for (String digest : spec.getDigests()) { + importArgs.add(KeyStore2ParameterUtils.makeEnum( + KeymasterDefs.KM_TAG_DIGEST, + KeyProperties.Digest.toKeymaster(digest) + )); + } + } + for (String blockMode : spec.getBlockModes()) { + importArgs.add(KeyStore2ParameterUtils.makeEnum( + KeymasterDefs.KM_TAG_BLOCK_MODE, + KeyProperties.BlockMode.toKeymaster(blockMode) + )); + } + int[] keymasterEncryptionPaddings = + KeyProperties.EncryptionPadding.allToKeymaster( + spec.getEncryptionPaddings()); + if (((spec.getPurposes() & KeyProperties.PURPOSE_DECRYPT) != 0) + && (spec.isRandomizedEncryptionRequired())) { + for (int keymasterPadding : keymasterEncryptionPaddings) { + if (!KeymasterUtils + .isKeymasterPaddingSchemeIndCpaCompatibleWithAsymmetricCrypto( + keymasterPadding)) { + throw new KeyStoreException( + "Randomized encryption (IND-CPA) required but is violated by" + + " encryption padding mode: " + + KeyProperties.EncryptionPadding.fromKeymaster( + keymasterPadding) + + ". See KeyProtection documentation."); + } + } + } + for (int padding : keymasterEncryptionPaddings) { + importArgs.add(KeyStore2ParameterUtils.makeEnum( + KeymasterDefs.KM_TAG_PADDING, + padding + )); + } + for (String padding : spec.getSignaturePaddings()) { + importArgs.add(KeyStore2ParameterUtils.makeEnum( + KeymasterDefs.KM_TAG_PADDING, + KeyProperties.SignaturePadding.toKeymaster(padding) + )); + } + KeyStore2ParameterUtils.addUserAuthArgs(importArgs, spec); + if (spec.getKeyValidityStart() != null) { + importArgs.add(KeyStore2ParameterUtils.makeDate( + KeymasterDefs.KM_TAG_ACTIVE_DATETIME, spec.getKeyValidityStart() + )); + } + if (spec.getKeyValidityForOriginationEnd() != null) { + importArgs.add(KeyStore2ParameterUtils.makeDate( + KeymasterDefs.KM_TAG_ORIGINATION_EXPIRE_DATETIME, + spec.getKeyValidityForOriginationEnd() + )); + } + if (spec.getKeyValidityForConsumptionEnd() != null) { + importArgs.add(KeyStore2ParameterUtils.makeDate( + KeymasterDefs.KM_TAG_USAGE_EXPIRE_DATETIME, + spec.getKeyValidityForConsumptionEnd() + )); + } + } catch (IllegalArgumentException | IllegalStateException e) { + throw new KeyStoreException(e); + } + + try { + KeyStoreSecurityLevel securityLevelInterface = mKeyStore.getSecurityLevel( + securitylevel); + + KeyDescriptor descriptor = makeKeyDescriptor(alias); + + KeyMetadata metadata = securityLevelInterface.importKey(descriptor, null, + importArgs, flags, pkcs8EncodedPrivateKeyBytes); + + try { + mKeyStore.updateSubcomponents(metadata.key, userCertBytes, chainBytes); + } catch (android.security.KeyStoreException e) { + mKeyStore.deleteKey(metadata.key); + throw new KeyStoreException("Failed to store certificate and certificate chain", e); + } + + } catch (android.security.KeyStoreException e) { + throw new KeyStoreException("Failed to store private key", e); + } + } + + private static void assertCanReplace(String alias, @Domain int targetDomain, + int targetNamespace, KeyDescriptor descriptor) + throws KeyStoreException { + // If + // * the alias does not match, or + // * the domain does not match, or + // * the domain is Domain.SELINUX and the namespaces don not match, + // then the designated key location is not equivalent to the location of the + // given key parameter and cannot be updated. + // + // Note: mNamespace == KeyProperties.NAMESPACE_APPLICATION implies that the target domain + // is Domain.APP and Domain.SELINUX is the target domain otherwise. + if (alias != descriptor.alias + || descriptor.domain != targetDomain + || (descriptor.domain == Domain.SELINUX && descriptor.nspace != targetNamespace)) { + throw new KeyStoreException("Can only replace keys with same alias: " + alias + + " != " + descriptor.alias + " in the same target domain: " + targetDomain + + " != " + descriptor.domain + + (targetDomain == Domain.SELINUX ? " in the same target namespace: " + + targetNamespace + " != " + descriptor.nspace : "") + ); + } + } + + private void setSecretKeyEntry(String alias, SecretKey key, + java.security.KeyStore.ProtectionParameter param) + throws KeyStoreException { + if ((param != null) && (!(param instanceof KeyProtection))) { + throw new KeyStoreException( + "Unsupported protection parameter class: " + param.getClass().getName() + + ". Supported: " + KeyProtection.class.getName()); + } + KeyProtection params = (KeyProtection) param; + + @SecurityLevel int securityLevel = params.isStrongBoxBacked() ? SecurityLevel.STRONGBOX : + SecurityLevel.TRUSTED_ENVIRONMENT; + @Domain int targetDomain = (getTargetDomain()); + + if (key instanceof AndroidKeyStoreSecretKey) { + String keyAliasInKeystore = + ((AndroidKeyStoreSecretKey) key).getUserKeyDescriptor().alias; + + KeyDescriptor descriptor = ((AndroidKeyStoreSecretKey) key).getUserKeyDescriptor(); + + // This throws if the request cannot replace the existing key. + assertCanReplace(alias, targetDomain, mNamespace, descriptor); + + // This is the entry where this key is already stored. No need to do anything. + if (params != null) { + throw new KeyStoreException("Modifying KeyStore-backed key using protection" + + " parameters not supported"); + } + return; + } + + if (params == null) { + throw new KeyStoreException( + "Protection parameters must be specified when importing a symmetric key"); + } + + // Not a KeyStore-backed secret key -- import its key material into keystore. + String keyExportFormat = key.getFormat(); + if (keyExportFormat == null) { + throw new KeyStoreException( + "Only secret keys that export their key material are supported"); + } else if (!"RAW".equals(keyExportFormat)) { + throw new KeyStoreException( + "Unsupported secret key material export format: " + keyExportFormat); + } + byte[] keyMaterial = key.getEncoded(); + if (keyMaterial == null) { + throw new KeyStoreException("Key did not export its key material despite supporting" + + " RAW format export"); + } + + final List<KeyParameter> importArgs = new ArrayList<>(); + + try { + int keymasterAlgorithm = + KeyProperties.KeyAlgorithm.toKeymasterSecretKeyAlgorithm( + key.getAlgorithm()); + + importArgs.add(KeyStore2ParameterUtils.makeEnum( + KeymasterDefs.KM_TAG_ALGORITHM, + keymasterAlgorithm + )); + + if (keymasterAlgorithm == KeymasterDefs.KM_ALGORITHM_HMAC) { + // JCA HMAC key algorithm implies a digest (e.g., HmacSHA256 key algorithm + // implies SHA-256 digest). Because keymaster HMAC key is authorized only for one + // digest, we don't let import parameters override the digest implied by the key. + // If the parameters specify digests at all, they must specify only one digest, the + // only implied by key algorithm. + int keymasterImpliedDigest = + KeyProperties.KeyAlgorithm.toKeymasterDigest(key.getAlgorithm()); + if (keymasterImpliedDigest == -1) { + throw new ProviderException( + "HMAC key algorithm digest unknown for key algorithm " + + key.getAlgorithm()); + } + + if (params.isDigestsSpecified()) { + // Digest(s) explicitly specified in params -- check that the list consists of + // exactly one digest, the one implied by key algorithm. + int[] keymasterDigestsFromParams = + KeyProperties.Digest.allToKeymaster(params.getDigests()); + if ((keymasterDigestsFromParams.length != 1) + || (keymasterDigestsFromParams[0] != keymasterImpliedDigest)) { + throw new KeyStoreException( + "Unsupported digests specification: " + + Arrays.asList(params.getDigests()) + ". Only " + + KeyProperties.Digest.fromKeymaster(keymasterImpliedDigest) + + " supported for HMAC key algorithm " + + key.getAlgorithm()); + } + } + int outputBits = KeymasterUtils.getDigestOutputSizeBits(keymasterImpliedDigest); + if (outputBits == -1) { + throw new ProviderException( + "HMAC key authorized for unsupported digest: " + + KeyProperties.Digest.fromKeymaster(keymasterImpliedDigest)); + } + importArgs.add(KeyStore2ParameterUtils.makeEnum( + KeymasterDefs.KM_TAG_DIGEST, keymasterImpliedDigest + )); + importArgs.add(KeyStore2ParameterUtils.makeInt( + KeymasterDefs.KM_TAG_MIN_MAC_LENGTH, outputBits + )); + } else { + if (params.isDigestsSpecified()) { + for (String digest : params.getDigests()) { + importArgs.add(KeyStore2ParameterUtils.makeEnum( + KeymasterDefs.KM_TAG_DIGEST, + KeyProperties.Digest.toKeymaster(digest) + )); + } + } + } + + KeyStore2ParameterUtils.forEachSetFlag(params.getPurposes(), (purpose) -> { + importArgs.add(KeyStore2ParameterUtils.makeEnum( + KeymasterDefs.KM_TAG_PURPOSE, + KeyProperties.Purpose.toKeymaster(purpose) + )); + }); + + boolean indCpa = false; + if ((params.getPurposes() & KeyProperties.PURPOSE_ENCRYPT) != 0) { + if (((KeyProtection) param).isRandomizedEncryptionRequired()) { + indCpa = true; + } else { + importArgs.add(KeyStore2ParameterUtils.makeBool( + KeymasterDefs.KM_TAG_CALLER_NONCE + )); + } + } + + for (String blockMode : params.getBlockModes()) { + int keymasterBlockMode = KeyProperties.BlockMode.toKeymaster(blockMode); + if (indCpa + && !KeymasterUtils.isKeymasterBlockModeIndCpaCompatibleWithSymmetricCrypto( + keymasterBlockMode)) { + throw new KeyStoreException( + "Randomized encryption (IND-CPA) required but may be violated by" + + " block mode: " + blockMode + + ". See KeyProtection documentation."); + + } + if (keymasterAlgorithm == KeymasterDefs.KM_ALGORITHM_AES + && keymasterBlockMode == KeymasterDefs.KM_MODE_GCM) { + importArgs.add(KeyStore2ParameterUtils.makeInt( + KeymasterDefs.KM_TAG_MIN_MAC_LENGTH, + AndroidKeyStoreAuthenticatedAESCipherSpi.GCM + .MIN_SUPPORTED_TAG_LENGTH_BITS + )); + } + importArgs.add(KeyStore2ParameterUtils.makeEnum( + KeymasterDefs.KM_TAG_BLOCK_MODE, + keymasterBlockMode + )); + } + + if (params.getSignaturePaddings().length > 0) { + throw new KeyStoreException("Signature paddings not supported for symmetric keys"); + } + + for (String padding : params.getEncryptionPaddings()) { + importArgs.add(KeyStore2ParameterUtils.makeEnum( + KeymasterDefs.KM_TAG_PADDING, + KeyProperties.EncryptionPadding.toKeymaster(padding) + )); + } + + KeyStore2ParameterUtils.addUserAuthArgs(importArgs, params); + + if (params.getKeyValidityStart() != null) { + importArgs.add(KeyStore2ParameterUtils.makeDate( + KeymasterDefs.KM_TAG_ACTIVE_DATETIME, params.getKeyValidityStart() + )); + } + if (params.getKeyValidityForOriginationEnd() != null) { + importArgs.add(KeyStore2ParameterUtils.makeDate( + KeymasterDefs.KM_TAG_ORIGINATION_EXPIRE_DATETIME, + params.getKeyValidityForOriginationEnd() + )); + } + if (params.getKeyValidityForConsumptionEnd() != null) { + importArgs.add(KeyStore2ParameterUtils.makeDate( + KeymasterDefs.KM_TAG_USAGE_EXPIRE_DATETIME, + params.getKeyValidityForConsumptionEnd() + )); + } + } catch (IllegalArgumentException | IllegalStateException e) { + throw new KeyStoreException(e); + } + + int flags = 0; + if (params.isCriticalToDeviceEncryption()) { + flags |= IKeystoreSecurityLevel.KEY_FLAG_AUTH_BOUND_WITHOUT_CRYPTOGRAPHIC_LSKF_BINDING; + } + + try { + KeyStoreSecurityLevel securityLevelInterface = mKeyStore.getSecurityLevel( + securityLevel); + + KeyDescriptor descriptor = makeKeyDescriptor(alias); + + securityLevelInterface.importKey(descriptor, null /* TODO attestationKey */, + importArgs, flags, keyMaterial); + } catch (android.security.KeyStoreException e) { + throw new KeyStoreException("Failed to import secret key.", e); + } + } + + private void setWrappedKeyEntry(String alias, WrappedKeyEntry entry, + 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]; + + String[] parts = entry.getTransformation().split("/"); + + List<KeyParameter> args = new ArrayList<>(); + + String algorithm = parts[0]; + if (KeyProperties.KEY_ALGORITHM_RSA.equalsIgnoreCase(algorithm)) { + args.add(KeyStore2ParameterUtils.makeEnum( + KeymasterDefs.KM_TAG_ALGORITHM, + KeymasterDefs.KM_ALGORITHM_RSA + )); + } else { + throw new KeyStoreException("Algorithm \"" + algorithm + "\" not supported for " + + "wrapping. Only RSA wrapping keys are supported."); + } + + if (parts.length > 1) { + String mode = parts[1]; + args.add(KeyStore2ParameterUtils.makeEnum( + KeymasterDefs.KM_TAG_BLOCK_MODE, + KeyProperties.BlockMode.toKeymaster(mode) + )); + } + + if (parts.length > 2) { + @KeyProperties.EncryptionPaddingEnum int padding = + KeyProperties.EncryptionPadding.toKeymaster(parts[2]); + if (padding != KeymasterDefs.KM_PAD_NONE) { + args.add(KeyStore2ParameterUtils.makeEnum( + KeymasterDefs.KM_TAG_PADDING, + padding + )); + } + } + + KeyGenParameterSpec spec = (KeyGenParameterSpec) entry.getAlgorithmParameterSpec(); + if (spec.isDigestsSpecified()) { + @KeyProperties.DigestEnum int digest = + KeyProperties.Digest.toKeymaster(spec.getDigests()[0]); + if (digest != KeymasterDefs.KM_DIGEST_NONE) { + args.add(KeyStore2ParameterUtils.makeEnum( + KeymasterDefs.KM_TAG_DIGEST, + digest + )); + } + } + + KeyDescriptor wrappingkey = makeKeyDescriptor(entry.getWrappingKeyAlias()); + + KeyEntryResponse response = null; + try { + response = mKeyStore.getKeyEntry(wrappingkey); + } catch (android.security.KeyStoreException e) { + throw new KeyStoreException("Failed to load wrapping key.", e); + } + + KeyDescriptor wrappedKey = makeKeyDescriptor(alias); + + KeyStoreSecurityLevel securityLevel = new KeyStoreSecurityLevel(response.iSecurityLevel); + + final BiometricManager bm = android.app.AppGlobals.getInitialApplication() + .getSystemService(BiometricManager.class); + + long[] biometricSids = bm.getAuthenticatorIds(); + + List<AuthenticatorSpec> authenticatorSpecs = new ArrayList<>(); + + AuthenticatorSpec authenticatorSpec = new AuthenticatorSpec(); + // TODO Replace with HardwareAuthenticatorType.PASSWORD when KeyMint AIDL spec has landed. + authenticatorSpec.authenticatorType = 1; // HardwareAuthenticatorType.PASSWORD + authenticatorSpec.authenticatorId = GateKeeper.getSecureUserId(); + authenticatorSpecs.add(authenticatorSpec); + + for (long sid : biometricSids) { + AuthenticatorSpec authSpec = new AuthenticatorSpec(); + // TODO Replace with HardwareAuthenticatorType.FINGERPRINT when KeyMint AIDL spec has + // landed. + authSpec.authenticatorType = 2; // HardwareAuthenticatorType.FINGERPRINT + authSpec.authenticatorId = sid; + authenticatorSpecs.add(authSpec); + } + + try { + securityLevel.importWrappedKey( + wrappedKey, wrappingkey, + entry.getWrappedKeyBytes(), + null /* masking key is set to 32 bytes if null is given here */, + args, + authenticatorSpecs.toArray(new AuthenticatorSpec[0])); + } catch (android.security.KeyStoreException e) { + switch (e.getErrorCode()) { + case KeymasterDefs.KM_ERROR_UNIMPLEMENTED: { + throw new SecureKeyImportUnavailableException("Could not import wrapped key"); + } + default: + throw new KeyStoreException("Failed to import wrapped key. Keystore error " + + "code: " + e.getErrorCode(), e); + } + } + } + + @Override + public void engineSetKeyEntry(String alias, byte[] userKey, Certificate[] chain) + throws KeyStoreException { + throw new KeyStoreException("Operation not supported because key encoding is unknown"); + } + + /** + * This function sets a trusted certificate entry. It fails if the given + * alias is already taken by an actual key entry. However, if the entry is a + * trusted certificate it will get silently replaced. + * @param alias the alias name + * @param cert the certificate + * + * @throws KeyStoreException if the alias is already taken by a secret or private + * key entry. + * @throws KeyStoreException with a nested {@link CertificateEncodingException} + * if the {@code cert.getEncoded()} throws. + * @throws KeyStoreException with a nested {@link android.security.KeyStoreException} if + * something went wrong while inserting the certificate into keystore. + * @throws NullPointerException if cert or alias is null. + * + * @hide + */ + @Override + public void engineSetCertificateEntry(String alias, Certificate cert) throws KeyStoreException { + if (isKeyEntry(alias)) { + throw new KeyStoreException("Entry exists and is not a trusted certificate"); + } + + // We can't set something to null. + if (cert == null) { + throw new NullPointerException("cert == null"); + } + + final byte[] encoded; + try { + encoded = cert.getEncoded(); + } catch (CertificateEncodingException e) { + throw new KeyStoreException(e); + } + + try { + mKeyStore.updateSubcomponents(makeKeyDescriptor(alias), + null /* publicCert - unused when used as pure certificate store. */, + encoded); + } catch (android.security.KeyStoreException e) { + throw new KeyStoreException("Couldn't insert certificate.", e); + } + } + + @Override + public void engineDeleteEntry(String alias) throws KeyStoreException { + KeyDescriptor descriptor = makeKeyDescriptor(alias); + try { + mKeyStore.deleteKey(descriptor); + } catch (android.security.KeyStoreException e) { + if (e.getErrorCode() != ResponseCode.KEY_NOT_FOUND) { + throw new KeyStoreException("Failed to delete entry: " + alias, e); + } + } + } + + private Set<String> getUniqueAliases() { + + try { + final KeyDescriptor[] keys = mKeyStore.list( + getTargetDomain(), + mNamespace + ); + final Set<String> aliases = new HashSet<>(keys.length); + for (KeyDescriptor d : keys) { + aliases.add(d.alias); + } + return aliases; + } catch (android.security.KeyStoreException e) { + Log.e(TAG, "Failed to list keystore entries.", e); + return null; + } + } + + @Override + public Enumeration<String> engineAliases() { + return Collections.enumeration(getUniqueAliases()); + } + + @Override + public boolean engineContainsAlias(String alias) { + if (alias == null) { + throw new NullPointerException("alias == null"); + } + + return getKeyMetadata(alias) != null; + } + + @Override + public int engineSize() { + return getUniqueAliases().size(); + } + + @Override + public boolean engineIsKeyEntry(String alias) { + return isKeyEntry(alias); + } + + private boolean isKeyEntry(String alias) { + if (alias == null) { + throw new NullPointerException("alias == null"); + } + + KeyEntryResponse response = getKeyMetadata(alias); + // If response is null, there is no such entry. + // If response.iSecurityLevel is null, there is no private or secret key material stored. + return response != null && response.iSecurityLevel != null; + } + + + @Override + public boolean engineIsCertificateEntry(String alias) { + if (alias == null) { + throw new NullPointerException("alias == null"); + } + KeyEntryResponse response = getKeyMetadata(alias); + // If response == null there is no such entry. + // If there is no certificateChain, then this is not a certificate entry. + // If there is a private key entry, this is the certificate chain for that + // key entry and not a CA certificate entry. + return response != null + && response.metadata.certificateChain != null + && response.iSecurityLevel == null; + } + + @Override + public String engineGetCertificateAlias(Certificate cert) { + if (cert == null) { + return null; + } + if (!"X.509".equalsIgnoreCase(cert.getType())) { + Log.e(TAG, "In engineGetCertificateAlias: only X.509 certificates are supported."); + return null; + } + byte[] targetCertBytes; + try { + targetCertBytes = cert.getEncoded(); + } catch (CertificateEncodingException e) { + Log.e(TAG, "While trying to get the alias for a certificate.", e); + return null; + } + if (targetCertBytes == null) { + return null; + } + + KeyDescriptor[] keyDescriptors = null; + try { + keyDescriptors = mKeyStore.list( + getTargetDomain(), + mNamespace + ); + } catch (android.security.KeyStoreException e) { + Log.w(TAG, "Failed to get list of keystore entries.", e); + } + + String caAlias = null; + for (KeyDescriptor d : keyDescriptors) { + KeyEntryResponse response = getKeyMetadata(d.alias); + if (response == null) { + continue; + } + /* + * The KeyStoreSpi documentation says to only compare the first certificate in the + * chain which is equivalent to the {@code response.metadata.certificate} field. + * So we look for a hit in this field first. For pure CA certificate entries, + * we check the {@code response.metadata.certificateChain} field. But we only + * return a CA alias if there was no hit in the certificate field of any other + * entry. + */ + if (response.metadata.certificate != null) { + if (Arrays.equals(response.metadata.certificate, targetCertBytes)) { + return d.alias; + } + } else if (response.metadata.certificateChain != null && caAlias == null) { + if (Arrays.equals(response.metadata.certificateChain, targetCertBytes)) { + caAlias = d.alias; + } + } + } + return caAlias; + } + + @Override + public void engineStore(OutputStream stream, char[] password) throws IOException, + NoSuchAlgorithmException, CertificateException { + throw new UnsupportedOperationException("Can not serialize AndroidKeyStore to OutputStream"); + } + + @Override + public void engineLoad(InputStream stream, char[] password) throws IOException, + NoSuchAlgorithmException, CertificateException { + if (stream != null) { + throw new IllegalArgumentException("InputStream not supported"); + } + + if (password != null) { + throw new IllegalArgumentException("password not supported"); + } + + // Unfortunate name collision. + mKeyStore = KeyStore2.getInstance(); + mNamespace = KeyProperties.NAMESPACE_APPLICATION; + } + + @Override + public void engineLoad(LoadStoreParameter param) throws IOException, + NoSuchAlgorithmException, CertificateException { + int namespace = KeyProperties.NAMESPACE_APPLICATION; + if (param != null) { + if (param instanceof AndroidKeyStoreLoadStoreParameter) { + namespace = ((AndroidKeyStoreLoadStoreParameter) param).getNamespace(); + } else { + throw new IllegalArgumentException( + "Unsupported param type: " + param.getClass()); + } + } + mKeyStore = KeyStore2.getInstance(); + mNamespace = namespace; + } + + @Override + public void engineSetEntry(String alias, Entry entry, ProtectionParameter param) + throws KeyStoreException { + if (entry == null) { + throw new KeyStoreException("entry == null"); + } + + if (entry instanceof java.security.KeyStore.TrustedCertificateEntry) { + java.security.KeyStore.TrustedCertificateEntry trE = + (java.security.KeyStore.TrustedCertificateEntry) entry; + // engineSetCertificateEntry does not overwrite if the existing entry + // is a key entry, but the semantic of engineSetEntry is such that it + // overwrites any existing entry. Thus we delete any possible existing + // entry by this alias. + engineDeleteEntry(alias); + engineSetCertificateEntry(alias, trE.getTrustedCertificate()); + return; + } + + if (entry instanceof PrivateKeyEntry) { + PrivateKeyEntry prE = (PrivateKeyEntry) entry; + setPrivateKeyEntry(alias, prE.getPrivateKey(), prE.getCertificateChain(), param); + } 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, param); + } else { + throw new KeyStoreException( + "Entry must be a PrivateKeyEntry, SecretKeyEntry, WrappedKeyEntry " + + "or TrustedCertificateEntry; was " + entry); + } + } +} diff --git a/keystore/java/android/security/keystore2/AndroidKeyStoreUnauthenticatedAESCipherSpi.java b/keystore/java/android/security/keystore2/AndroidKeyStoreUnauthenticatedAESCipherSpi.java new file mode 100644 index 000000000000..3d5a8f63e7f9 --- /dev/null +++ b/keystore/java/android/security/keystore2/AndroidKeyStoreUnauthenticatedAESCipherSpi.java @@ -0,0 +1,335 @@ +/* + * Copyright (C) 2015 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.keystore2; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.security.keymaster.KeymasterDefs; +import android.security.keystore.ArrayUtils; +import android.security.keystore.KeyProperties; +import android.system.keystore2.KeyParameter; + +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 java.util.List; + +import javax.crypto.CipherSpi; +import javax.crypto.spec.IvParameterSpec; + +/** + * Base class for Android Keystore unauthenticated AES {@link CipherSpi} implementations. + * + * @hide + */ +class AndroidKeyStoreUnauthenticatedAESCipherSpi extends AndroidKeyStoreCipherSpiBase { + + abstract static class ECB extends AndroidKeyStoreUnauthenticatedAESCipherSpi { + 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 AndroidKeyStoreUnauthenticatedAESCipherSpi { + 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); + } + } + } + + abstract static class CTR extends AndroidKeyStoreUnauthenticatedAESCipherSpi { + protected CTR(int keymasterPadding) { + super(KeymasterDefs.KM_MODE_CTR, keymasterPadding, true); + } + + public static class NoPadding extends CTR { + public NoPadding() { + super(KeymasterDefs.KM_PAD_NONE); + } + } + } + + private static final int BLOCK_SIZE_BYTES = 16; + + 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; + + AndroidKeyStoreUnauthenticatedAESCipherSpi( + int keymasterBlockMode, + int keymasterPadding, + boolean ivRequired) { + mKeymasterBlockMode = keymasterBlockMode; + mKeymasterPadding = keymasterPadding; + mIvRequired = ivRequired; + } + + @Override + protected final void resetAll() { + mIv = null; + mIvHasBeenUsed = false; + super.resetAll(); + } + + @Override + protected final void resetWhilePreservingInitState() { + super.resetWhilePreservingInitState(); + } + + @Override + protected final void initKey(int opmode, Key key) throws InvalidKeyException { + if (!(key instanceof AndroidKeyStoreSecretKey)) { + throw new InvalidKeyException( + "Unsupported key: " + ((key != null) ? key.getClass().getName() : "null")); + } + if (!KeyProperties.KEY_ALGORITHM_AES.equalsIgnoreCase(key.getAlgorithm())) { + throw new InvalidKeyException( + "Unsupported key algorithm: " + key.getAlgorithm() + ". Only " + + KeyProperties.KEY_ALGORITHM_AES + " supported"); + } + setKey((AndroidKeyStoreSecretKey) key); + } + + @Override + protected final 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 final 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 final 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 (!"AES".equalsIgnoreCase(params.getAlgorithm())) { + throw new InvalidAlgorithmParameterException( + "Unsupported AlgorithmParameters algorithm: " + params.getAlgorithm() + + ". Supported: AES"); + } + + 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 final int getAdditionalEntropyAmountForFinish() { + return 0; + } + + @Override + protected final void addAlgorithmSpecificParametersToBegin( + @NonNull List<KeyParameter> parameters) { + 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."); + } + + parameters.add(KeyStore2ParameterUtils.makeEnum( + KeymasterDefs.KM_TAG_ALGORITHM, KeymasterDefs.KM_ALGORITHM_AES + )); + parameters.add(KeyStore2ParameterUtils.makeEnum( + KeymasterDefs.KM_TAG_BLOCK_MODE, mKeymasterBlockMode + )); + parameters.add(KeyStore2ParameterUtils.makeEnum( + KeymasterDefs.KM_TAG_PADDING, mKeymasterPadding + )); + if ((mIvRequired) && (mIv != null)) { + parameters.add(KeyStore2ParameterUtils.makeBytes( + KeymasterDefs.KM_TAG_NONCE, mIv + )); + } + } + + @Override + protected final void loadAlgorithmSpecificParametersFromBeginResult( + KeyParameter[] parameters) { + mIvHasBeenUsed = true; + + // NOTE: Keymaster doesn't always return an IV, even if it's used. + byte[] returnedIv = null; + if (parameters != null) { + for (KeyParameter p : parameters) { + if (p.tag == KeymasterDefs.KM_TAG_NONCE) { + returnedIv = p.blob; + break; + } + } + } + + 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 int engineGetBlockSize() { + return BLOCK_SIZE_BYTES; + } + + @Override + protected final int engineGetOutputSize(int inputLen) { + return inputLen + 3 * BLOCK_SIZE_BYTES; + } + + @Override + protected final byte[] engineGetIV() { + return ArrayUtils.cloneIfNotEmpty(mIv); + } + + @Nullable + @Override + protected final AlgorithmParameters engineGetParameters() { + if (!mIvRequired) { + return null; + } + if ((mIv != null) && (mIv.length > 0)) { + try { + AlgorithmParameters params = AlgorithmParameters.getInstance("AES"); + params.init(new IvParameterSpec(mIv)); + return params; + } catch (NoSuchAlgorithmException e) { + throw new ProviderException( + "Failed to obtain AES AlgorithmParameters", e); + } catch (InvalidParameterSpecException e) { + throw new ProviderException( + "Failed to initialize AES AlgorithmParameters with an IV", + e); + } + } + return null; + } +} diff --git a/keystore/java/android/security/keystore2/KeyStore2ParameterUtils.java b/keystore/java/android/security/keystore2/KeyStore2ParameterUtils.java new file mode 100644 index 000000000000..ee67ed3f76d8 --- /dev/null +++ b/keystore/java/android/security/keystore2/KeyStore2ParameterUtils.java @@ -0,0 +1,313 @@ +/* + * Copyright (C) 2020 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.keystore2; + +import android.annotation.NonNull; +import android.hardware.biometrics.BiometricManager; +import android.security.GateKeeper; +import android.security.keymaster.KeymasterDefs; +import android.security.keystore.KeyProperties; +import android.security.keystore.UserAuthArgs; +import android.system.keystore2.Authorization; +import android.system.keystore2.KeyParameter; +import android.system.keystore2.SecurityLevel; + +import java.security.ProviderException; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.function.Consumer; + +/** + * @hide + */ +public abstract class KeyStore2ParameterUtils { + + /** + * This function constructs a {@link KeyParameter} expressing a boolean value. + * @param tag Must be KeyMint tag with the associated type BOOL. + * @return An instance of {@link KeyParameter}. + * @hide + */ + static @NonNull KeyParameter makeBool(int tag) { + int type = KeymasterDefs.getTagType(tag); + if (type != KeymasterDefs.KM_BOOL) { + throw new IllegalArgumentException("Not a boolean tag: " + tag); + } + KeyParameter p = new KeyParameter(); + p.tag = tag; + p.boolValue = true; + return p; + } + + /** + * This function constructs a {@link KeyParameter} expressing an enum value. + * @param tag Must be KeyMint tag with the associated type ENUM or ENUM_REP. + * @param v A 32bit integer. + * @return An instance of {@link KeyParameter}. + * @hide + */ + static @NonNull KeyParameter makeEnum(int tag, int v) { + int type = KeymasterDefs.getTagType(tag); + if (type != KeymasterDefs.KM_ENUM && type != KeymasterDefs.KM_ENUM_REP) { + throw new IllegalArgumentException("Not an enum or repeatable enum tag: " + tag); + } + KeyParameter p = new KeyParameter(); + p.tag = tag; + p.integer = v; + return p; + } + + /** + * This function constructs a {@link KeyParameter} expressing an integer value. + * @param tag Must be KeyMint tag with the associated type UINT or UINT_REP. + * @param v A 32bit integer. + * @return An instance of {@link KeyParameter}. + * @hide + */ + static @NonNull KeyParameter makeInt(int tag, int v) { + int type = KeymasterDefs.getTagType(tag); + if (type != KeymasterDefs.KM_UINT && type != KeymasterDefs.KM_UINT_REP) { + throw new IllegalArgumentException("Not an int or repeatable int tag: " + tag); + } + KeyParameter p = new KeyParameter(); + p.tag = tag; + p.integer = v; + return p; + } + + /** + * This function constructs a {@link KeyParameter} expressing a long integer value. + * @param tag Must be KeyMint tag with the associated type ULONG or ULONG_REP. + * @param v A 64bit integer. + * @return An instance of {@link KeyParameter}. + * @hide + */ + static @NonNull KeyParameter makeLong(int tag, long v) { + int type = KeymasterDefs.getTagType(tag); + if (type != KeymasterDefs.KM_ULONG && type != KeymasterDefs.KM_ULONG_REP) { + throw new IllegalArgumentException("Not a long or repeatable long tag: " + tag); + } + KeyParameter p = new KeyParameter(); + p.tag = tag; + p.longInteger = v; + return p; + } + + /** + * This function constructs a {@link KeyParameter} expressing a blob. + * @param tag Must be KeyMint tag with the associated type BYTES. + * @param b A byte array to be stored in the new key parameter. + * @return An instance of {@link KeyParameter}. + * @hide + */ + static @NonNull KeyParameter makeBytes(int tag, @NonNull byte[] b) { + if (KeymasterDefs.getTagType(tag) != KeymasterDefs.KM_BYTES) { + throw new IllegalArgumentException("Not a bytes tag: " + tag); + } + KeyParameter p = new KeyParameter(); + p.tag = tag; + p.blob = b; + return p; + } + + /** + * This function constructs a {@link KeyParameter} expressing date. + * @param tag Must be KeyMint tag with the associated type DATE. + * @param date A date + * @return An instance of {@link KeyParameter}. + * @hide + */ + static @NonNull KeyParameter makeDate(int tag, @NonNull Date date) { + if (KeymasterDefs.getTagType(tag) != KeymasterDefs.KM_DATE) { + throw new IllegalArgumentException("Not a date tag: " + tag); + } + KeyParameter p = new KeyParameter(); + p.tag = tag; + p.longInteger = date.getTime(); + if (p.longInteger < 0) { + throw new IllegalArgumentException("Date tag value out of range: " + p.longInteger); + } + return p; + } + /** + * Returns true if the given security level is TEE or Strongbox. + * + * @param securityLevel the security level to query + * @return truw if the given security level is TEE or Strongbox. + */ + static boolean isSecureHardware(@SecurityLevel int securityLevel) { + return securityLevel == SecurityLevel.TRUSTED_ENVIRONMENT + || securityLevel == SecurityLevel.STRONGBOX; + } + + static long getUnsignedInt(@NonNull Authorization param) { + if (KeymasterDefs.getTagType(param.keyParameter.tag) != KeymasterDefs.KM_UINT) { + throw new IllegalArgumentException("Not an int tag: " + param.keyParameter.tag); + } + // KM_UINT is 32 bits wide so we must suppress sign extension. + return ((long) param.keyParameter.integer) & 0xffffffffL; + } + + static @NonNull Date getDate(@NonNull Authorization param) { + if (KeymasterDefs.getTagType(param.keyParameter.tag) != KeymasterDefs.KM_DATE) { + throw new IllegalArgumentException("Not a date tag: " + param.keyParameter.tag); + } + if (param.keyParameter.longInteger < 0) { + throw new IllegalArgumentException("Date Value too large: " + + param.keyParameter.longInteger); + } + return new Date(param.keyParameter.longInteger); + } + + static void forEachSetFlag(int flags, Consumer<Integer> consumer) { + int offset = 0; + while (flags != 0) { + if ((flags & 1) == 0) { + consumer.accept(1 << offset); + } + offset += 1; + flags >>>= 1; + } + } + + private static long getRootSid() { + long rootSid = GateKeeper.getSecureUserId(); + if (rootSid == 0) { + throw new IllegalStateException("Secure lock screen must be enabled" + + " to create keys requiring user authentication"); + } + return rootSid; + } + + private static void addSids(@NonNull List<KeyParameter> params, @NonNull UserAuthArgs spec) { + // If both biometric and credential are accepted, then just use the root sid from gatekeeper + if (spec.getUserAuthenticationType() == (KeyProperties.AUTH_BIOMETRIC_STRONG + | KeyProperties.AUTH_DEVICE_CREDENTIAL)) { + if (spec.getBoundToSpecificSecureUserId() != GateKeeper.INVALID_SECURE_USER_ID) { + params.add(makeLong( + KeymasterDefs.KM_TAG_USER_SECURE_ID, + spec.getBoundToSpecificSecureUserId() + )); + } else { + // The key is authorized for use for the specified amount of time after the user has + // authenticated. Whatever unlocks the secure lock screen should authorize this key. + params.add(makeLong(KeymasterDefs.KM_TAG_USER_SECURE_ID, getRootSid())); + } + } else { + List<Long> sids = new ArrayList<>(); + if ((spec.getUserAuthenticationType() & KeyProperties.AUTH_BIOMETRIC_STRONG) != 0) { + final BiometricManager bm = android.app.AppGlobals.getInitialApplication() + .getSystemService(BiometricManager.class); + + // TODO: Restore permission check in getAuthenticatorIds once the ID is no longer + // needed here. + + final long[] biometricSids = bm.getAuthenticatorIds(); + + if (biometricSids.length == 0) { + throw new IllegalStateException( + "At least one biometric must be enrolled to create keys requiring user" + + " authentication for every use"); + } + + if (spec.getBoundToSpecificSecureUserId() != GateKeeper.INVALID_SECURE_USER_ID) { + sids.add(spec.getBoundToSpecificSecureUserId()); + } else if (spec.isInvalidatedByBiometricEnrollment()) { + // The biometric-only SIDs will change on biometric enrollment or removal of all + // enrolled templates, invalidating the key. + for (long sid : biometricSids) { + sids.add(sid); + } + } else { + // The root SID will *not* change on fingerprint enrollment, or removal of all + // enrolled fingerprints, allowing the key to remain valid. + sids.add(getRootSid()); + } + } else if ((spec.getUserAuthenticationType() & KeyProperties.AUTH_DEVICE_CREDENTIAL) + != 0) { + sids.add(getRootSid()); + } else { + throw new IllegalStateException("Invalid or no authentication type specified."); + } + + for (int i = 0; i < sids.size(); i++) { + params.add(makeLong(KeymasterDefs.KM_TAG_USER_SECURE_ID, sids.get(i))); + } + } + } + + /** + * Adds keymaster arguments to express the key's authorization policy supported by user + * authentication. + * + * @param args The arguments sent to keymaster that need to be populated from the spec + * @param spec The user authentication relevant portions of the spec passed in from the caller. + * This spec will be translated into the relevant keymaster tags to be loaded into args. + * @throws IllegalStateException if user authentication is required but the system is in a wrong + * state (e.g., secure lock screen not set up) for generating or importing keys that + * require user authentication. + */ + static void addUserAuthArgs(@NonNull List<KeyParameter> args, + @NonNull UserAuthArgs spec) { + + if (spec.isUserConfirmationRequired()) { + args.add(KeyStore2ParameterUtils.makeBool( + KeymasterDefs.KM_TAG_TRUSTED_CONFIRMATION_REQUIRED)); + } + if (spec.isUserPresenceRequired()) { + args.add(KeyStore2ParameterUtils.makeBool( + KeymasterDefs.KM_TAG_TRUSTED_USER_PRESENCE_REQUIRED)); + } + if (spec.isUnlockedDeviceRequired()) { + args.add(KeyStore2ParameterUtils.makeBool( + KeymasterDefs.KM_TAG_UNLOCKED_DEVICE_REQUIRED)); + } + if (!spec.isUserAuthenticationRequired()) { + args.add(KeyStore2ParameterUtils.makeBool( + KeymasterDefs.KM_TAG_NO_AUTH_REQUIRED)); + } else { + if (spec.getUserAuthenticationValidityDurationSeconds() == 0) { + // Every use of this key needs to be authorized by the user. + addSids(args, spec); + args.add(KeyStore2ParameterUtils.makeEnum( + KeymasterDefs.KM_TAG_USER_AUTH_TYPE, spec.getUserAuthenticationType() + )); + + if (spec.isUserAuthenticationValidWhileOnBody()) { + throw new ProviderException( + "Key validity extension while device is on-body is not " + + "supported for keys requiring fingerprint authentication"); + } + } else { + addSids(args, spec); + args.add(KeyStore2ParameterUtils.makeEnum( + KeymasterDefs.KM_TAG_USER_AUTH_TYPE, spec.getUserAuthenticationType() + )); + args.add(KeyStore2ParameterUtils.makeInt( + KeymasterDefs.KM_TAG_AUTH_TIMEOUT, + spec.getUserAuthenticationValidityDurationSeconds() + )); + if (spec.isUserAuthenticationValidWhileOnBody()) { + args.add(KeyStore2ParameterUtils.makeBool( + KeymasterDefs.KM_TAG_ALLOW_WHILE_ON_BODY + )); + } + } + } + } +} diff --git a/keystore/java/android/security/keystore2/KeyStoreCryptoOperationChunkedStreamer.java b/keystore/java/android/security/keystore2/KeyStoreCryptoOperationChunkedStreamer.java new file mode 100644 index 000000000000..6c733ba712d5 --- /dev/null +++ b/keystore/java/android/security/keystore2/KeyStoreCryptoOperationChunkedStreamer.java @@ -0,0 +1,229 @@ +/* + * Copyright (C) 2015 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.keystore2; + +import android.annotation.NonNull; +import android.security.KeyStoreException; +import android.security.KeyStoreOperation; +import android.security.keymaster.KeymasterDefs; +import android.security.keystore.ArrayUtils; + +import libcore.util.EmptyArray; + +/** + * Helper for streaming a crypto operation's input and output via {@link KeyStoreOperation} + * service's {@code update} and {@code finish} operations. + * + * <p>The helper abstracts away issues that need to be solved in most code that uses KeyStore's + * update and finish operations. Firstly, KeyStore's update operation can consume only a limited + * amount of data in one go because the operations are marshalled via Binder. Secondly, the update + * operation may consume less data than provided, in which case the caller has to buffer the + * remainder for next time. Thirdly, when the input is smaller than a threshold, skipping update + * and passing input data directly to final improves performance. This threshold is configurable; + * using a threshold <= 1 causes the helper act eagerly, which may be required for some types of + * operations (e.g. ciphers). + * + * <p>The helper exposes {@link #update(byte[], int, int) update} and + * {@link #doFinal(byte[], int, int, byte[], byte[]) doFinal} operations which can be used to + * conveniently implement various JCA crypto primitives. + * + * <p>Bidirectional chunked streaming of data via a KeyStore crypto operation is abstracted away as + * a {@link Stream} to avoid having this class deal with operation tokens and occasional additional + * parameters to {@code update} and {@code final} operations. + * + * @hide + */ +class KeyStoreCryptoOperationChunkedStreamer implements KeyStoreCryptoOperationStreamer { + + /** + * Bidirectional chunked data stream over a KeyStore crypto operation. + */ + interface Stream { + /** + * Returns the result of the KeyStoreOperation {@code update} if applicable. + * The return value may be null, e.g., when supplying AAD or to-be-signed data. + * + * @param input Data to update a KeyStoreOperation with. + * + * @throws KeyStoreException in case of error. + */ + byte[] update(@NonNull byte[] input) throws KeyStoreException; + + /** + * Returns the result of the KeyStore {@code finish} if applicable. + * + * @param input Optional data to update the operation with one last time. + * + * @param signature Optional HMAC signature when verifying an HMAC signature, must be + * null otherwise. + * + * @return Optional output data. Depending on the operation this may be a signature, + * some final bit of cipher, or plain text. + * + * @throws KeyStoreException in case of error. + */ + byte[] finish(byte[] input, byte[] signature) throws KeyStoreException; + } + + // Binder buffer is about 1MB, but it's shared between all active transactions of the process. + // Thus, it's safer to use a much smaller upper bound. + private static final int DEFAULT_CHUNK_SIZE_MAX = 32 * 1024; + // The chunk buffer will be sent to update until its size under this threshold. + // This threshold should be <= the max input allowed for finish. + // Setting this threshold <= 1 will effectivley disable buffering between updates. + private static final int DEFAULT_CHUNK_SIZE_THRESHOLD = 2 * 1024; + + private final Stream mKeyStoreStream; + private final int mChunkSizeMax; + private final int mChunkSizeThreshold; + private final byte[] mChunk; + private int mChunkLength = 0; + private long mConsumedInputSizeBytes; + private long mProducedOutputSizeBytes; + + KeyStoreCryptoOperationChunkedStreamer(Stream operation) { + this(operation, DEFAULT_CHUNK_SIZE_THRESHOLD, DEFAULT_CHUNK_SIZE_MAX); + } + + KeyStoreCryptoOperationChunkedStreamer(Stream operation, int chunkSizeThreshold) { + this(operation, chunkSizeThreshold, DEFAULT_CHUNK_SIZE_MAX); + } + + KeyStoreCryptoOperationChunkedStreamer(Stream operation, int chunkSizeThreshold, + int chunkSizeMax) { + mChunkLength = 0; + mConsumedInputSizeBytes = 0; + mProducedOutputSizeBytes = 0; + mKeyStoreStream = operation; + mChunkSizeMax = chunkSizeMax; + if (chunkSizeThreshold <= 0) { + mChunkSizeThreshold = 1; + } else if (chunkSizeThreshold > chunkSizeMax) { + mChunkSizeThreshold = chunkSizeMax; + } else { + mChunkSizeThreshold = chunkSizeThreshold; + } + mChunk = new byte[mChunkSizeMax]; + } + + public byte[] update(byte[] input, int inputOffset, int inputLength) throws KeyStoreException { + if (inputLength == 0 || input == null) { + // No input provided + return EmptyArray.BYTE; + } + if (inputLength < 0 || inputOffset < 0 || (inputOffset + inputLength) > input.length) { + throw new KeyStoreException(KeymasterDefs.KM_ERROR_UNKNOWN_ERROR, + "Input offset and length out of bounds of input array"); + } + + byte[] output = EmptyArray.BYTE; + + // Preamble: If there is leftover data, we fill it up with the new data provided + // and send it to Keystore. + if (mChunkLength > 0) { + // Fill current chunk and send it to Keystore + int inputConsumed = ArrayUtils.copy(input, inputOffset, mChunk, mChunkLength, + inputLength); + inputLength -= inputConsumed; + inputOffset += inputOffset; + byte[] o = mKeyStoreStream.update(mChunk); + if (o != null) { + output = ArrayUtils.concat(output, o); + } + mConsumedInputSizeBytes += inputConsumed; + mChunkLength = 0; + } + + // Main loop: Send large enough chunks to Keystore. + while (inputLength >= mChunkSizeThreshold) { + int nextChunkSize = inputLength < mChunkSizeMax ? inputLength : mChunkSizeMax; + byte[] o = mKeyStoreStream.update(ArrayUtils.subarray(input, inputOffset, + nextChunkSize)); + inputLength -= nextChunkSize; + inputOffset += nextChunkSize; + mConsumedInputSizeBytes += nextChunkSize; + if (o != null) { + output = ArrayUtils.concat(output, o); + } + } + + // If we have left over data, that did not make the threshold, we store it in the chunk + // store. + if (inputLength > 0) { + mChunkLength = ArrayUtils.copy(input, inputOffset, mChunk, 0, inputLength); + mConsumedInputSizeBytes += inputLength; + } + + mProducedOutputSizeBytes += output.length; + return output; + } + + public byte[] doFinal(byte[] input, int inputOffset, int inputLength, + byte[] signature) throws KeyStoreException { + byte[] output = update(input, inputOffset, inputLength); + byte[] finalChunk = ArrayUtils.subarray(mChunk, 0, mChunkLength); + byte[] o = mKeyStoreStream.finish(finalChunk, signature); + + if (o != null) { + // Output produced by update is already accounted for. We only add the bytes + // produced by finish. + mProducedOutputSizeBytes += o.length; + if (output != null) { + output = ArrayUtils.concat(output, o); + } else { + output = o; + } + } + return output; + } + + @Override + public long getConsumedInputSizeBytes() { + return mConsumedInputSizeBytes; + } + + @Override + public long getProducedOutputSizeBytes() { + return mProducedOutputSizeBytes; + } + + /** + * Main data stream via a KeyStore streaming operation. + * + * <p>For example, for an encryption operation, this is the stream through which plaintext is + * provided and ciphertext is obtained. + */ + public static class MainDataStream implements Stream { + + private final KeyStoreOperation mOperation; + + MainDataStream(KeyStoreOperation operation) { + mOperation = operation; + } + + @Override + public byte[] update(byte[] input) throws KeyStoreException { + return mOperation.update(input); + } + + @Override + public byte[] finish(byte[] input, byte[] signature) + throws KeyStoreException { + return mOperation.finish(input, signature); + } + } +} diff --git a/keystore/java/android/security/keystore2/KeyStoreCryptoOperationStreamer.java b/keystore/java/android/security/keystore2/KeyStoreCryptoOperationStreamer.java new file mode 100644 index 000000000000..07d6a69eda01 --- /dev/null +++ b/keystore/java/android/security/keystore2/KeyStoreCryptoOperationStreamer.java @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2015 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.keystore2; + +import android.security.KeyStore; +import android.security.KeyStoreException; + +/** + * Helper for streaming a crypto operation's input and output via {@link KeyStore} service's + * {@code update} and {@code finish} operations. + * + * <p>The helper abstracts away to issues that need to be solved in most code that uses KeyStore's + * update and finish operations. Firstly, KeyStore's update operation can consume only a limited + * amount of data in one go because the operations are marshalled via Binder. Secondly, the update + * operation may consume less data than provided, in which case the caller has to buffer the + * remainder for next time. The helper exposes {@link #update(byte[], int, int) update} and + * {@link #doFinal(byte[], int, int, byte[]) doFinal} operations which can be used to + * conveniently implement various JCA crypto primitives. + * + * @hide + */ +interface KeyStoreCryptoOperationStreamer { + byte[] update(byte[] input, int inputOffset, int inputLength) throws KeyStoreException; + byte[] doFinal(byte[] input, int inputOffset, int inputLength, byte[] signature) + throws KeyStoreException; + long getConsumedInputSizeBytes(); + long getProducedOutputSizeBytes(); +} diff --git a/keystore/java/android/security/keystore2/KeyStoreCryptoOperationUtils.java b/keystore/java/android/security/keystore2/KeyStoreCryptoOperationUtils.java new file mode 100644 index 000000000000..3b11854bf7cb --- /dev/null +++ b/keystore/java/android/security/keystore2/KeyStoreCryptoOperationUtils.java @@ -0,0 +1,211 @@ +/* + * Copyright (C) 2015 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.keystore2; + +import android.app.ActivityThread; +import android.hardware.biometrics.BiometricManager; +import android.security.GateKeeper; +import android.security.KeyStore; +import android.security.KeyStoreException; +import android.security.KeyStoreOperation; +import android.security.keymaster.KeymasterDefs; +import android.security.keystore.KeyExpiredException; +import android.security.keystore.KeyNotYetValidException; +import android.security.keystore.KeyPermanentlyInvalidatedException; +import android.security.keystore.UserNotAuthenticatedException; +import android.system.keystore2.Authorization; +import android.system.keystore2.ResponseCode; +import android.util.Log; + +import libcore.util.EmptyArray; + +import java.security.GeneralSecurityException; +import java.security.InvalidAlgorithmParameterException; +import java.security.InvalidKeyException; +import java.security.SecureRandom; +import java.util.ArrayList; +import java.util.List; + +/** + * Assorted utility methods for implementing crypto operations on top of KeyStore. + * + * @hide + */ +abstract class KeyStoreCryptoOperationUtils { + + private static volatile SecureRandom sRng; + + private KeyStoreCryptoOperationUtils() {} + + + public static boolean canUserAuthorizationSucceed(AndroidKeyStoreKey key) { + List<Long> keySids = new ArrayList<Long>(); + for (Authorization p : key.getAuthorizations()) { + switch(p.keyParameter.tag) { + case KeymasterDefs.KM_TAG_USER_SECURE_ID: + keySids.add(p.keyParameter.longInteger); + break; + default: + break; + } + } + if (keySids.isEmpty()) { + // Key is not bound to any SIDs -- no amount of authentication will help here. + return false; + } + long rootSid = GateKeeper.getSecureUserId(); + if ((rootSid != 0) && (keySids.contains(rootSid))) { + // One of the key's SIDs is the current root SID -- user can be authenticated + // against that SID. + return true; + } + + long[] biometricSids = ActivityThread + .currentApplication() + .getSystemService(BiometricManager.class) + .getAuthenticatorIds(); + + // The key must contain every biometric SID. This is because the current API surface + // treats all biometrics (capable of keystore integration) equally. e.g. if the + // device has multiple keystore-capable sensors, and one of the sensor's SIDs + // changed, 1) there is no way for a developer to specify authentication with a + // specific sensor (the one that hasn't changed), and 2) currently the only + // signal to developers is the UserNotAuthenticatedException, which doesn't + // indicate a specific sensor. + boolean canUnlockViaBiometrics = true; + for (long sid : biometricSids) { + if (!keySids.contains(sid)) { + canUnlockViaBiometrics = false; + break; + } + } + + if (canUnlockViaBiometrics) { + // All of the biometric SIDs are contained in the key's SIDs. + return true; + } + + // None of the key's SIDs can ever be authenticated + return false; + } + + /** + * Returns an {@link InvalidKeyException} corresponding to the provided + * {@link KeyStoreException}. + */ + public static InvalidKeyException getInvalidKeyException( + AndroidKeyStoreKey key, KeyStoreException e) { + switch (e.getErrorCode()) { + case KeymasterDefs.KM_ERROR_KEY_EXPIRED: + return new KeyExpiredException(); + case KeymasterDefs.KM_ERROR_KEY_NOT_YET_VALID: + return new KeyNotYetValidException(); + case ResponseCode.KEY_NOT_FOUND: + // TODO is this the right exception in this case? + case ResponseCode.KEY_PERMANENTLY_INVALIDATED: + return new KeyPermanentlyInvalidatedException(); + case ResponseCode.LOCKED: + case ResponseCode.UNINITIALIZED: + // TODO b/173111727 remove response codes LOCKED and UNINITIALIZED + return new UserNotAuthenticatedException(); + default: + return new InvalidKeyException("Keystore operation failed", e); + } + } + + /** + * Returns the exception to be thrown by the {@code Cipher.init} method of the crypto operation + * in response to {@code KeyStore.begin} operation or {@code null} if the {@code init} method + * should succeed. + */ + public static GeneralSecurityException getExceptionForCipherInit( + AndroidKeyStoreKey key, KeyStoreException e) { + if (e.getErrorCode() == KeyStore.NO_ERROR) { + return null; + } + + // Cipher-specific cases + switch (e.getErrorCode()) { + case KeymasterDefs.KM_ERROR_INVALID_NONCE: + return new InvalidAlgorithmParameterException("Invalid IV"); + case KeymasterDefs.KM_ERROR_CALLER_NONCE_PROHIBITED: + return new InvalidAlgorithmParameterException("Caller-provided IV not permitted"); + } + + // General cases + return getInvalidKeyException(key, e); + } + + /** + * Returns the requested number of random bytes to mix into keystore/keymaster RNG. + * + * @param rng RNG from which to obtain the random bytes or {@code null} for the platform-default + * RNG. + */ + static byte[] getRandomBytesToMixIntoKeystoreRng(SecureRandom rng, int sizeBytes) { + if (sizeBytes <= 0) { + return EmptyArray.BYTE; + } + if (rng == null) { + rng = getRng(); + } + byte[] result = new byte[sizeBytes]; + rng.nextBytes(result); + return result; + } + + private static SecureRandom getRng() { + // IMPLEMENTATION NOTE: It's OK to share a SecureRandom instance because SecureRandom is + // required to be thread-safe. + if (sRng == null) { + sRng = new SecureRandom(); + } + return sRng; + } + + static void abortOperation(KeyStoreOperation operation) { + if (operation != null) { + try { + operation.abort(); + } catch (KeyStoreException e) { + // We log this error, but we can afford to ignore it. Dropping the reference + // to the KeyStoreOperation is enough to clean up all related resources even + // in the Keystore daemon. We log it anyway, because it may indicate some + // underlying problem that is worth debugging. + Log.w( + "KeyStoreCryptoOperationUtils", + "Encountered error trying to abort a keystore operation.", + e + ); + } + } + } + + static long getOrMakeOperationChallenge(KeyStoreOperation operation, AndroidKeyStoreKey key) + throws KeyPermanentlyInvalidatedException { + if (operation.getChallenge() != null) { + if (!KeyStoreCryptoOperationUtils.canUserAuthorizationSucceed(key)) { + throw new KeyPermanentlyInvalidatedException(); + } + return operation.getChallenge(); + } else { + // Keystore won't give us an operation challenge if the operation doesn't + // need user authorization. So we make our own. + return Math.randomLongInternal(); + } + } +} |