| /* |
| * Copyright (C) 2018 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package android.security.keystore.recovery; |
| |
| import android.Manifest; |
| import android.annotation.NonNull; |
| import android.annotation.RequiresPermission; |
| import android.annotation.SystemApi; |
| import android.os.RemoteException; |
| import android.os.ServiceSpecificException; |
| import android.security.keystore.KeyPermanentlyInvalidatedException; |
| import android.util.ArrayMap; |
| import android.util.Log; |
| |
| import java.security.Key; |
| import java.security.SecureRandom; |
| import java.security.UnrecoverableKeyException; |
| import java.security.cert.CertPath; |
| import java.security.cert.CertificateException; |
| import java.util.List; |
| import java.util.Locale; |
| import java.util.Map; |
| |
| /** |
| * Session to recover a {@link KeyChainSnapshot} from the remote trusted hardware, initiated by a |
| * recovery agent. |
| * |
| * @hide |
| */ |
| @SystemApi |
| public class RecoverySession implements AutoCloseable { |
| private static final String TAG = "RecoverySession"; |
| |
| private static final int SESSION_ID_LENGTH_BYTES = 16; |
| |
| private final String mSessionId; |
| private final RecoveryController mRecoveryController; |
| |
| private RecoverySession(@NonNull RecoveryController recoveryController, |
| @NonNull String sessionId) { |
| mRecoveryController = recoveryController; |
| mSessionId = sessionId; |
| } |
| |
| /** |
| * A new session, started by the {@link RecoveryController}. |
| */ |
| @RequiresPermission(android.Manifest.permission.RECOVER_KEYSTORE) |
| static @NonNull RecoverySession newInstance(RecoveryController recoveryController) { |
| return new RecoverySession(recoveryController, newSessionId()); |
| } |
| |
| /** |
| * Returns a new random session ID. |
| */ |
| private static @NonNull String newSessionId() { |
| SecureRandom secureRandom = new SecureRandom(); |
| byte[] sessionId = new byte[SESSION_ID_LENGTH_BYTES]; |
| secureRandom.nextBytes(sessionId); |
| StringBuilder sb = new StringBuilder(); |
| for (byte b : sessionId) { |
| sb.append(Byte.toHexString(b, /*upperCase=*/ false)); |
| } |
| return sb.toString(); |
| } |
| |
| /** |
| * Starts a recovery session and returns a blob with proof of recovery secret possession. |
| * The method generates a symmetric key for a session, which trusted remote device can use to |
| * return recovery key. |
| * |
| * @param rootCertificateAlias The alias of the root certificate that is already in the Android |
| * OS. The root certificate will be used for validating {@code verifierCertPath}. |
| * @param verifierCertPath The certificate path used to create the recovery blob on the source |
| * device. Keystore will verify the certificate path by using the root of trust. |
| * @param vaultParams Must match the parameters in the corresponding field in the recovery blob. |
| * Used to limit number of guesses. |
| * @param vaultChallenge Data passed from server for this recovery session and used to prevent |
| * replay attacks. |
| * @param secrets Secrets provided by user, the method only uses type and secret fields. |
| * @return The binary blob with recovery claim. It is encrypted with verifierPublicKey |
| * and contains a proof of user secrets possession, session symmetric |
| * key and parameters necessary to identify the counter with the number of failed recovery |
| * attempts. |
| * @throws CertificateException if the {@code verifierCertPath} is invalid. |
| * @throws InternalRecoveryServiceException if an unexpected error occurred in the recovery |
| * service. |
| */ |
| @RequiresPermission(android.Manifest.permission.RECOVER_KEYSTORE) |
| @NonNull public byte[] start( |
| @NonNull String rootCertificateAlias, |
| @NonNull CertPath verifierCertPath, |
| @NonNull byte[] vaultParams, |
| @NonNull byte[] vaultChallenge, |
| @NonNull List<KeyChainProtectionParams> secrets) |
| throws CertificateException, InternalRecoveryServiceException { |
| // Wrap the CertPath in a Parcelable so it can be passed via Binder calls. |
| RecoveryCertPath recoveryCertPath = |
| RecoveryCertPath.createRecoveryCertPath(verifierCertPath); |
| try { |
| byte[] recoveryClaim = |
| mRecoveryController.getBinder().startRecoverySessionWithCertPath( |
| mSessionId, |
| rootCertificateAlias, |
| recoveryCertPath, |
| vaultParams, |
| vaultChallenge, |
| secrets); |
| return recoveryClaim; |
| } catch (RemoteException e) { |
| throw e.rethrowFromSystemServer(); |
| } catch (ServiceSpecificException e) { |
| if (e.errorCode == RecoveryController.ERROR_BAD_CERTIFICATE_FORMAT |
| || e.errorCode == RecoveryController.ERROR_INVALID_CERTIFICATE) { |
| throw new CertificateException("Invalid certificate for recovery session", e); |
| } |
| throw mRecoveryController.wrapUnexpectedServiceSpecificException(e); |
| } |
| } |
| |
| /** |
| * Imports key chain snapshot recovered from a remote vault. |
| * |
| * @param recoveryKeyBlob Recovery blob encrypted by symmetric key generated for this session. |
| * @param applicationKeys Application keys. Key material can be decrypted using recoveryKeyBlob |
| * and session key generated by {@link #start}. |
| * @return {@code Map} from recovered keys aliases to their references. |
| * @throws SessionExpiredException if {@code session} has since been closed. |
| * @throws DecryptionFailedException if unable to decrypt the snapshot. |
| * @throws InternalRecoveryServiceException if an error occurs internal to the recovery service. |
| */ |
| @RequiresPermission(Manifest.permission.RECOVER_KEYSTORE) |
| @NonNull public Map<String, Key> recoverKeyChainSnapshot( |
| @NonNull byte[] recoveryKeyBlob, |
| @NonNull List<WrappedApplicationKey> applicationKeys |
| ) throws SessionExpiredException, DecryptionFailedException, InternalRecoveryServiceException { |
| try { |
| Map<String, String> grantAliases = mRecoveryController |
| .getBinder() |
| .recoverKeyChainSnapshot(mSessionId, recoveryKeyBlob, applicationKeys); |
| return getKeysFromGrants(grantAliases); |
| } catch (RemoteException e) { |
| throw e.rethrowFromSystemServer(); |
| } catch (ServiceSpecificException e) { |
| if (e.errorCode == RecoveryController.ERROR_DECRYPTION_FAILED) { |
| throw new DecryptionFailedException(e.getMessage()); |
| } |
| if (e.errorCode == RecoveryController.ERROR_SESSION_EXPIRED) { |
| throw new SessionExpiredException(e.getMessage()); |
| } |
| throw mRecoveryController.wrapUnexpectedServiceSpecificException(e); |
| } |
| } |
| |
| /** Given a map from alias to grant alias, returns a map from alias to a {@link Key} handle. */ |
| private @NonNull Map<String, Key> getKeysFromGrants(@NonNull Map<String, String> grantAliases) |
| throws InternalRecoveryServiceException { |
| ArrayMap<String, Key> keysByAlias = new ArrayMap<>(grantAliases.size()); |
| for (String alias : grantAliases.keySet()) { |
| String grantAlias = grantAliases.get(alias); |
| Key key; |
| try { |
| key = mRecoveryController.getKeyFromGrant(grantAlias); |
| } catch (KeyPermanentlyInvalidatedException | UnrecoverableKeyException e) { |
| throw new InternalRecoveryServiceException( |
| String.format( |
| Locale.US, |
| "Failed to get key '%s' from grant '%s'", |
| alias, |
| grantAlias), e); |
| } |
| keysByAlias.put(alias, key); |
| } |
| return keysByAlias; |
| } |
| |
| /** |
| * An internal session ID, used by the framework to match recovery claims to snapshot responses. |
| * |
| * @hide |
| */ |
| String getSessionId() { |
| return mSessionId; |
| } |
| |
| /** |
| * Deletes all data associated with {@code session}. |
| */ |
| @RequiresPermission(android.Manifest.permission.RECOVER_KEYSTORE) |
| @Override |
| public void close() { |
| try { |
| mRecoveryController.getBinder().closeSession(mSessionId); |
| } catch (RemoteException | ServiceSpecificException e) { |
| Log.e(TAG, "Unexpected error trying to close session", e); |
| } |
| } |
| } |