diff options
-rw-r--r-- | api/current.txt | 6 | ||||
-rw-r--r-- | core/api/current.txt | 6 | ||||
-rw-r--r-- | keystore/java/android/security/CheckedRemoteRequest.java | 33 | ||||
-rw-r--r-- | keystore/java/android/security/KeyStore2.java | 277 | ||||
-rw-r--r-- | keystore/java/android/security/KeyStoreOperation.java | 141 | ||||
-rw-r--r-- | keystore/java/android/security/KeyStoreSecurityLevel.java | 217 | ||||
-rw-r--r-- | keystore/java/android/security/keystore/BackendBusyException.java | 52 |
7 files changed, 732 insertions, 0 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/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/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); + } + +} |