--- /dev/null
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.security.keystore;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.function.Function;
+
+/**
+ * Helpers for converting classes between old and new API, so we can preserve backwards
+ * compatibility while teamfooding. This will be removed soon.
+ *
+ * @hide
+ */
+class BackwardsCompat {
+
+
+ static KeychainProtectionParams toLegacyKeychainProtectionParams(
+ android.security.keystore.recovery.KeychainProtectionParams keychainProtectionParams
+ ) {
+ return new KeychainProtectionParams.Builder()
+ .setUserSecretType(keychainProtectionParams.getUserSecretType())
+ .setSecret(keychainProtectionParams.getSecret())
+ .setLockScreenUiFormat(keychainProtectionParams.getLockScreenUiFormat())
+ .setKeyDerivationParams(
+ toLegacyKeyDerivationParams(
+ keychainProtectionParams.getKeyDerivationParams()))
+ .build();
+ }
+
+ static KeyDerivationParams toLegacyKeyDerivationParams(
+ android.security.keystore.recovery.KeyDerivationParams keyDerivationParams
+ ) {
+ return new KeyDerivationParams(
+ keyDerivationParams.getAlgorithm(), keyDerivationParams.getSalt());
+ }
+
+ static WrappedApplicationKey toLegacyWrappedApplicationKey(
+ android.security.keystore.recovery.WrappedApplicationKey wrappedApplicationKey
+ ) {
+ return new WrappedApplicationKey.Builder()
+ .setAlias(wrappedApplicationKey.getAlias())
+ .setEncryptedKeyMaterial(wrappedApplicationKey.getEncryptedKeyMaterial())
+ .build();
+ }
+
+ static android.security.keystore.recovery.KeyDerivationParams fromLegacyKeyDerivationParams(
+ KeyDerivationParams keyDerivationParams
+ ) {
+ return new android.security.keystore.recovery.KeyDerivationParams(
+ keyDerivationParams.getAlgorithm(), keyDerivationParams.getSalt());
+ }
+
+ static android.security.keystore.recovery.WrappedApplicationKey fromLegacyWrappedApplicationKey(
+ WrappedApplicationKey wrappedApplicationKey
+ ) {
+ return new android.security.keystore.recovery.WrappedApplicationKey.Builder()
+ .setAlias(wrappedApplicationKey.getAlias())
+ .setEncryptedKeyMaterial(wrappedApplicationKey.getEncryptedKeyMaterial())
+ .build();
+ }
+
+ static List<android.security.keystore.recovery.WrappedApplicationKey>
+ fromLegacyWrappedApplicationKeys(List<WrappedApplicationKey> wrappedApplicationKeys
+ ) {
+ return map(wrappedApplicationKeys, BackwardsCompat::fromLegacyWrappedApplicationKey);
+ }
+
+ static List<android.security.keystore.recovery.KeychainProtectionParams>
+ fromLegacyKeychainProtectionParams(
+ List<KeychainProtectionParams> keychainProtectionParams) {
+ return map(keychainProtectionParams, BackwardsCompat::fromLegacyKeychainProtectionParam);
+ }
+
+ static android.security.keystore.recovery.KeychainProtectionParams
+ fromLegacyKeychainProtectionParam(KeychainProtectionParams keychainProtectionParams) {
+ return new android.security.keystore.recovery.KeychainProtectionParams.Builder()
+ .setUserSecretType(keychainProtectionParams.getUserSecretType())
+ .setSecret(keychainProtectionParams.getSecret())
+ .setLockScreenUiFormat(keychainProtectionParams.getLockScreenUiFormat())
+ .setKeyDerivationParams(
+ fromLegacyKeyDerivationParams(
+ keychainProtectionParams.getKeyDerivationParams()))
+ .build();
+ }
+
+ static KeychainSnapshot toLegacyKeychainSnapshot(
+ android.security.keystore.recovery.KeychainSnapshot keychainSnapshot
+ ) {
+ return new KeychainSnapshot.Builder()
+ .setCounterId(keychainSnapshot.getCounterId())
+ .setEncryptedRecoveryKeyBlob(keychainSnapshot.getEncryptedRecoveryKeyBlob())
+ .setTrustedHardwarePublicKey(keychainSnapshot.getTrustedHardwarePublicKey())
+ .setSnapshotVersion(keychainSnapshot.getSnapshotVersion())
+ .setMaxAttempts(keychainSnapshot.getMaxAttempts())
+ .setServerParams(keychainSnapshot.getServerParams())
+ .setKeychainProtectionParams(
+ map(keychainSnapshot.getKeychainProtectionParams(),
+ BackwardsCompat::toLegacyKeychainProtectionParams))
+ .setWrappedApplicationKeys(
+ map(keychainSnapshot.getWrappedApplicationKeys(),
+ BackwardsCompat::toLegacyWrappedApplicationKey))
+ .build();
+ }
+
+ static <A, B> List<B> map(List<A> as, Function<A, B> f) {
+ ArrayList<B> bs = new ArrayList<>(as.size());
+ for (A a : as) {
+ bs.add(f.apply(a));
+ }
+ return bs;
+ }
+}
return new KeyDerivationParams(ALGORITHM_SHA256, salt);
}
- private KeyDerivationParams(@KeyDerivationAlgorithm int algorithm, @NonNull byte[] salt) {
+ KeyDerivationParams(@KeyDerivationAlgorithm int algorithm, @NonNull byte[] salt) {
mAlgorithm = algorithm;
mSalt = Preconditions.checkNotNull(salt);
}
public @NonNull KeychainSnapshot getRecoveryData(@NonNull byte[] account)
throws InternalRecoveryServiceException {
try {
- return mBinder.getRecoveryData(account);
+ return BackwardsCompat.toLegacyKeychainSnapshot(mBinder.getRecoveryData(account));
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
} catch (ServiceSpecificException e) {
}
/**
- * Method notifies KeyStore that a user-generated secret is available. This method generates a
- * symmetric session key which a trusted remote device can use to return a recovery key. Caller
- * should use {@link KeychainProtectionParams#clearSecret} to override the secret value in
- * memory.
- *
- * @param recoverySecret user generated secret together with parameters necessary to regenerate
- * it on a new device.
- * @throws InternalRecoveryServiceException if an unexpected error occurred in the recovery
- * service.
- */
- public void recoverySecretAvailable(@NonNull KeychainProtectionParams recoverySecret)
- throws InternalRecoveryServiceException {
- try {
- mBinder.recoverySecretAvailable(recoverySecret);
- } catch (RemoteException e) {
- throw e.rethrowFromSystemServer();
- } catch (ServiceSpecificException e) {
- throw wrapUnexpectedServiceSpecificException(e);
- }
- }
-
- /**
* Initializes recovery session and returns a blob with proof of recovery secrets possession.
* The method generates symmetric key for a session, which trusted remote device can use to
* return recovery key.
verifierPublicKey,
vaultParams,
vaultChallenge,
- secrets);
+ BackwardsCompat.fromLegacyKeychainProtectionParams(secrets));
return new RecoveryClaim(recoverySession, recoveryClaim);
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
InternalRecoveryServiceException {
try {
return (Map<String, byte[]>) mBinder.recoverKeys(
- session.getSessionId(), recoveryKeyBlob, applicationKeys);
+ session.getSessionId(),
+ recoveryKeyBlob,
+ BackwardsCompat.fromLegacyWrappedApplicationKeys(applicationKeys));
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
} catch (ServiceSpecificException e) {
--- /dev/null
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.security.keystore.recovery;
+
+/**
+ * Error thrown when the recovery agent supplies an invalid X509 certificate.
+ *
+ * @hide
+ */
+public class BadCertificateFormatException extends RecoveryControllerException {
+ public BadCertificateFormatException(String msg) {
+ super(msg);
+ }
+}
--- /dev/null
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.security.keystore.recovery;
+
+/**
+ * Error thrown when decryption failed, due to an agent error. i.e., using the incorrect key,
+ * trying to decrypt garbage data, trying to decrypt data that has somehow been corrupted, etc.
+ *
+ * @hide
+ */
+public class DecryptionFailedException extends RecoveryControllerException {
+
+ public DecryptionFailedException(String msg) {
+ super(msg);
+ }
+}
--- /dev/null
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.security.keystore.recovery;
+
+/**
+ * An error thrown when something went wrong internally in the recovery service.
+ *
+ * <p>This is an unexpected error, and indicates a problem with the service itself, rather than the
+ * caller having performed some kind of illegal action.
+ *
+ * @hide
+ */
+public class InternalRecoveryServiceException extends RecoveryControllerException {
+ public InternalRecoveryServiceException(String msg) {
+ super(msg);
+ }
+
+ public InternalRecoveryServiceException(String message, Throwable cause) {
+ super(message, cause);
+ }
+}
* limitations under the License.
*/
-package android.security.keystore;
+package android.security.keystore.recovery;
/* @hide */
parcelable KeyDerivationParams;
--- /dev/null
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.security.keystore.recovery;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import com.android.internal.util.Preconditions;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * Collection of parameters which define a key derivation function.
+ * Currently only supports salted SHA-256
+ *
+ * @hide
+ */
+public final class KeyDerivationParams implements Parcelable {
+ private final int mAlgorithm;
+ private byte[] mSalt;
+
+ /** @hide */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(prefix = {"ALGORITHM_"}, value = {ALGORITHM_SHA256, ALGORITHM_ARGON2ID})
+ public @interface KeyDerivationAlgorithm {
+ }
+
+ /**
+ * Salted SHA256
+ */
+ public static final int ALGORITHM_SHA256 = 1;
+
+ /**
+ * Argon2ID
+ * @hide
+ */
+ // TODO: add Argon2ID support.
+ public static final int ALGORITHM_ARGON2ID = 2;
+
+ /**
+ * Creates instance of the class to to derive key using salted SHA256 hash.
+ */
+ public static KeyDerivationParams createSha256Params(@NonNull byte[] salt) {
+ return new KeyDerivationParams(ALGORITHM_SHA256, salt);
+ }
+
+ // TODO: Make private once legacy API is removed
+ public KeyDerivationParams(@KeyDerivationAlgorithm int algorithm, @NonNull byte[] salt) {
+ mAlgorithm = algorithm;
+ mSalt = Preconditions.checkNotNull(salt);
+ }
+
+ /**
+ * Gets algorithm.
+ */
+ public @KeyDerivationAlgorithm int getAlgorithm() {
+ return mAlgorithm;
+ }
+
+ /**
+ * Gets salt.
+ */
+ public @NonNull byte[] getSalt() {
+ return mSalt;
+ }
+
+ public static final Creator<KeyDerivationParams> CREATOR =
+ new Creator<KeyDerivationParams>() {
+ public KeyDerivationParams createFromParcel(Parcel in) {
+ return new KeyDerivationParams(in);
+ }
+
+ public KeyDerivationParams[] newArray(int length) {
+ return new KeyDerivationParams[length];
+ }
+ };
+
+ /**
+ * @hide
+ */
+ @Override
+ public void writeToParcel(Parcel out, int flags) {
+ out.writeInt(mAlgorithm);
+ out.writeByteArray(mSalt);
+ }
+
+ /**
+ * @hide
+ */
+ protected KeyDerivationParams(Parcel in) {
+ mAlgorithm = in.readInt();
+ mSalt = in.createByteArray();
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+}
* limitations under the License.
*/
-package android.security.keystore;
+package android.security.keystore.recovery;
/* @hide */
parcelable KeychainProtectionParams;
--- /dev/null
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.security.keystore.recovery;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import com.android.internal.util.Preconditions;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.Arrays;
+
+/**
+ * A {@link KeychainSnapshot} is protected with a key derived from the user's lock screen. This
+ * class wraps all the data necessary to derive the same key on a recovering device:
+ *
+ * <ul>
+ * <li>UI parameters for the user's lock screen - so that if e.g., the user was using a pattern,
+ * the recovering device can display the pattern UI to the user when asking them to enter
+ * the lock screen from their previous device.
+ * <li>The algorithm used to derive a key from the user's lock screen, e.g. SHA-256 with a salt.
+ * </ul>
+ *
+ * <p>As such, this data is sent along with the {@link KeychainSnapshot} when syncing the current
+ * version of the keychain.
+ *
+ * <p>For now, the recoverable keychain only supports a single layer of protection, which is the
+ * user's lock screen. In the future, the keychain will support multiple layers of protection
+ * (e.g. an additional keychain password, along with the lock screen).
+ *
+ * @hide
+ */
+public final class KeychainProtectionParams implements Parcelable {
+ /** @hide */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({TYPE_LOCKSCREEN, TYPE_CUSTOM_PASSWORD})
+ public @interface UserSecretType {
+ }
+
+ /**
+ * Lockscreen secret is required to recover KeyStore.
+ */
+ public static final int TYPE_LOCKSCREEN = 100;
+
+ /**
+ * Custom passphrase, unrelated to lock screen, is required to recover KeyStore.
+ */
+ public static final int TYPE_CUSTOM_PASSWORD = 101;
+
+ /** @hide */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({TYPE_PIN, TYPE_PASSWORD, TYPE_PATTERN})
+ public @interface LockScreenUiFormat {
+ }
+
+ /**
+ * Pin with digits only.
+ */
+ public static final int TYPE_PIN = 1;
+
+ /**
+ * Password. String with latin-1 characters only.
+ */
+ public static final int TYPE_PASSWORD = 2;
+
+ /**
+ * Pattern with 3 by 3 grid.
+ */
+ public static final int TYPE_PATTERN = 3;
+
+ @UserSecretType
+ private Integer mUserSecretType;
+
+ @LockScreenUiFormat
+ private Integer mLockScreenUiFormat;
+
+ /**
+ * Parameters of the key derivation function, including algorithm, difficulty, salt.
+ */
+ private KeyDerivationParams mKeyDerivationParams;
+ private byte[] mSecret; // Derived from user secret. The field must have limited visibility.
+
+ /**
+ * @param secret Constructor creates a reference to the secret. Caller must use
+ * @link {#clearSecret} to overwrite its value in memory.
+ * @hide
+ */
+ public KeychainProtectionParams(@UserSecretType int userSecretType,
+ @LockScreenUiFormat int lockScreenUiFormat,
+ @NonNull KeyDerivationParams keyDerivationParams,
+ @NonNull byte[] secret) {
+ mUserSecretType = userSecretType;
+ mLockScreenUiFormat = lockScreenUiFormat;
+ mKeyDerivationParams = Preconditions.checkNotNull(keyDerivationParams);
+ mSecret = Preconditions.checkNotNull(secret);
+ }
+
+ private KeychainProtectionParams() {
+
+ }
+
+ /**
+ * @see TYPE_LOCKSCREEN
+ * @see TYPE_CUSTOM_PASSWORD
+ */
+ public @UserSecretType int getUserSecretType() {
+ return mUserSecretType;
+ }
+
+ /**
+ * Specifies UX shown to user during recovery.
+ * Default value is {@code TYPE_LOCKSCREEN}
+ *
+ * @see TYPE_PIN
+ * @see TYPE_PASSWORD
+ * @see TYPE_PATTERN
+ */
+ public @LockScreenUiFormat int getLockScreenUiFormat() {
+ return mLockScreenUiFormat;
+ }
+
+ /**
+ * Specifies function used to derive symmetric key from user input
+ * Format is defined in separate util class.
+ */
+ @NonNull public KeyDerivationParams getKeyDerivationParams() {
+ return mKeyDerivationParams;
+ }
+
+ /**
+ * Secret derived from user input.
+ * Default value is empty array
+ *
+ * @return secret or empty array
+ */
+ public @NonNull byte[] getSecret() {
+ return mSecret;
+ }
+
+ /**
+ * Builder for creating {@link KeychainProtectionParams}.
+ */
+ public static class Builder {
+ private KeychainProtectionParams
+ mInstance = new KeychainProtectionParams();
+
+ /**
+ * Sets user secret type.
+ *
+ * @see TYPE_LOCKSCREEN
+ * @see TYPE_CUSTOM_PASSWORD
+ * @param userSecretType The secret type
+ * @return This builder.
+ */
+ public Builder setUserSecretType(@UserSecretType int userSecretType) {
+ mInstance.mUserSecretType = userSecretType;
+ return this;
+ }
+
+ /**
+ * Sets UI format.
+ *
+ * @see TYPE_PIN
+ * @see TYPE_PASSWORD
+ * @see TYPE_PATTERN
+ * @param lockScreenUiFormat The UI format
+ * @return This builder.
+ */
+ public Builder setLockScreenUiFormat(@LockScreenUiFormat int lockScreenUiFormat) {
+ mInstance.mLockScreenUiFormat = lockScreenUiFormat;
+ return this;
+ }
+
+ /**
+ * Sets parameters of the key derivation function.
+ *
+ * @param keyDerivationParams Key derivation Params
+ * @return This builder.
+ */
+ public Builder setKeyDerivationParams(@NonNull KeyDerivationParams
+ keyDerivationParams) {
+ mInstance.mKeyDerivationParams = keyDerivationParams;
+ return this;
+ }
+
+ /**
+ * Secret derived from user input, or empty array.
+ *
+ * @param secret The secret.
+ * @return This builder.
+ */
+ public Builder setSecret(@NonNull byte[] secret) {
+ mInstance.mSecret = secret;
+ return this;
+ }
+
+
+ /**
+ * Creates a new {@link KeychainProtectionParams} instance.
+ * The instance will include default values, if {@link setSecret}
+ * or {@link setUserSecretType} were not called.
+ *
+ * @return new instance
+ * @throws NullPointerException if some required fields were not set.
+ */
+ @NonNull public KeychainProtectionParams build() {
+ if (mInstance.mUserSecretType == null) {
+ mInstance.mUserSecretType = TYPE_LOCKSCREEN;
+ }
+ Preconditions.checkNotNull(mInstance.mLockScreenUiFormat);
+ Preconditions.checkNotNull(mInstance.mKeyDerivationParams);
+ if (mInstance.mSecret == null) {
+ mInstance.mSecret = new byte[]{};
+ }
+ return mInstance;
+ }
+ }
+
+ /**
+ * Removes secret from memory than object is no longer used.
+ * Since finalizer call is not reliable, please use @link {#clearSecret} directly.
+ */
+ @Override
+ protected void finalize() throws Throwable {
+ clearSecret();
+ super.finalize();
+ }
+
+ /**
+ * Fills mSecret with zeroes.
+ */
+ public void clearSecret() {
+ Arrays.fill(mSecret, (byte) 0);
+ }
+
+ public static final Creator<KeychainProtectionParams> CREATOR =
+ new Creator<KeychainProtectionParams>() {
+ public KeychainProtectionParams createFromParcel(Parcel in) {
+ return new KeychainProtectionParams(in);
+ }
+
+ public KeychainProtectionParams[] newArray(int length) {
+ return new KeychainProtectionParams[length];
+ }
+ };
+
+ /**
+ * @hide
+ */
+ @Override
+ public void writeToParcel(Parcel out, int flags) {
+ out.writeInt(mUserSecretType);
+ out.writeInt(mLockScreenUiFormat);
+ out.writeTypedObject(mKeyDerivationParams, flags);
+ out.writeByteArray(mSecret);
+ }
+
+ /**
+ * @hide
+ */
+ protected KeychainProtectionParams(Parcel in) {
+ mUserSecretType = in.readInt();
+ mLockScreenUiFormat = in.readInt();
+ mKeyDerivationParams = in.readTypedObject(KeyDerivationParams.CREATOR);
+ mSecret = in.createByteArray();
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+}
* limitations under the License.
*/
-package android.security.keystore;
+package android.security.keystore.recovery;
/* @hide */
parcelable KeychainSnapshot;
--- /dev/null
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.security.keystore.recovery;
+
+import android.annotation.NonNull;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import com.android.internal.util.Preconditions;
+
+import java.util.List;
+
+/**
+ * A snapshot of a version of the keystore. Two events can trigger the generation of a new snapshot:
+ *
+ * <ul>
+ * <li>The user's lock screen changes. (A key derived from the user's lock screen is used to
+ * protected the keychain, which is why this forces a new snapshot.)
+ * <li>A key is added to or removed from the recoverable keychain.
+ * </ul>
+ *
+ * <p>The snapshot data is also encrypted with the remote trusted hardware's public key, so even
+ * the recovery agent itself should not be able to decipher the data. The recovery agent sends an
+ * instance of this to the remote trusted hardware whenever a new snapshot is generated. During a
+ * recovery flow, the recovery agent retrieves a snapshot from the remote trusted hardware. It then
+ * sends it to the framework, where it is decrypted using the user's lock screen from their previous
+ * device.
+ *
+ * @hide
+ */
+public final class KeychainSnapshot implements Parcelable {
+ private static final int DEFAULT_MAX_ATTEMPTS = 10;
+ private static final long DEFAULT_COUNTER_ID = 1L;
+
+ private int mSnapshotVersion;
+ private int mMaxAttempts = DEFAULT_MAX_ATTEMPTS;
+ private long mCounterId = DEFAULT_COUNTER_ID;
+ private byte[] mServerParams;
+ private byte[] mPublicKey;
+ private List<KeychainProtectionParams> mKeychainProtectionParams;
+ private List<WrappedApplicationKey> mEntryRecoveryData;
+ private byte[] mEncryptedRecoveryKeyBlob;
+
+ /**
+ * @hide
+ * Deprecated, consider using builder.
+ */
+ public KeychainSnapshot(
+ int snapshotVersion,
+ @NonNull List<KeychainProtectionParams> keychainProtectionParams,
+ @NonNull List<WrappedApplicationKey> wrappedApplicationKeys,
+ @NonNull byte[] encryptedRecoveryKeyBlob) {
+ mSnapshotVersion = snapshotVersion;
+ mKeychainProtectionParams =
+ Preconditions.checkCollectionElementsNotNull(keychainProtectionParams,
+ "keychainProtectionParams");
+ mEntryRecoveryData = Preconditions.checkCollectionElementsNotNull(wrappedApplicationKeys,
+ "wrappedApplicationKeys");
+ mEncryptedRecoveryKeyBlob = Preconditions.checkNotNull(encryptedRecoveryKeyBlob);
+ }
+
+ private KeychainSnapshot() {
+
+ }
+
+ /**
+ * Snapshot version for given account. It is incremented when user secret or list of application
+ * keys changes.
+ */
+ public int getSnapshotVersion() {
+ return mSnapshotVersion;
+ }
+
+ /**
+ * Number of user secret guesses allowed during Keychain recovery.
+ */
+ public int getMaxAttempts() {
+ return mMaxAttempts;
+ }
+
+ /**
+ * CounterId which is rotated together with user secret.
+ */
+ public long getCounterId() {
+ return mCounterId;
+ }
+
+ /**
+ * Server parameters.
+ */
+ public @NonNull byte[] getServerParams() {
+ return mServerParams;
+ }
+
+ /**
+ * Public key used to encrypt {@code encryptedRecoveryKeyBlob}.
+ *
+ * See implementation for binary key format
+ */
+ // TODO: document key format.
+ public @NonNull byte[] getTrustedHardwarePublicKey() {
+ return mPublicKey;
+ }
+
+ /**
+ * UI and key derivation parameters. Note that combination of secrets may be used.
+ */
+ public @NonNull List<KeychainProtectionParams> getKeychainProtectionParams() {
+ return mKeychainProtectionParams;
+ }
+
+ /**
+ * List of application keys, with key material encrypted by
+ * the recovery key ({@link #getEncryptedRecoveryKeyBlob}).
+ */
+ public @NonNull List<WrappedApplicationKey> getWrappedApplicationKeys() {
+ return mEntryRecoveryData;
+ }
+
+ /**
+ * Recovery key blob, encrypted by user secret and recovery service public key.
+ */
+ public @NonNull byte[] getEncryptedRecoveryKeyBlob() {
+ return mEncryptedRecoveryKeyBlob;
+ }
+
+ public static final Creator<KeychainSnapshot> CREATOR =
+ new Creator<KeychainSnapshot>() {
+ public KeychainSnapshot createFromParcel(Parcel in) {
+ return new KeychainSnapshot(in);
+ }
+
+ public KeychainSnapshot[] newArray(int length) {
+ return new KeychainSnapshot[length];
+ }
+ };
+
+ /**
+ * Builder for creating {@link KeychainSnapshot}.
+ */
+ public static class Builder {
+ private KeychainSnapshot
+ mInstance = new KeychainSnapshot();
+
+ /**
+ * Snapshot version for given account.
+ *
+ * @param snapshotVersion The snapshot version
+ * @return This builder.
+ */
+ public Builder setSnapshotVersion(int snapshotVersion) {
+ mInstance.mSnapshotVersion = snapshotVersion;
+ return this;
+ }
+
+ /**
+ * Sets the number of user secret guesses allowed during Keychain recovery.
+ *
+ * @param maxAttempts The maximum number of guesses.
+ * @return This builder.
+ */
+ public Builder setMaxAttempts(int maxAttempts) {
+ mInstance.mMaxAttempts = maxAttempts;
+ return this;
+ }
+
+ /**
+ * Sets counter id.
+ *
+ * @param counterId The counter id.
+ * @return This builder.
+ */
+ public Builder setCounterId(long counterId) {
+ mInstance.mCounterId = counterId;
+ return this;
+ }
+
+ /**
+ * Sets server parameters.
+ *
+ * @param serverParams The server parameters
+ * @return This builder.
+ */
+ public Builder setServerParams(byte[] serverParams) {
+ mInstance.mServerParams = serverParams;
+ return this;
+ }
+
+ /**
+ * Sets public key used to encrypt recovery blob.
+ *
+ * @param publicKey The public key
+ * @return This builder.
+ */
+ public Builder setTrustedHardwarePublicKey(byte[] publicKey) {
+ mInstance.mPublicKey = publicKey;
+ return this;
+ }
+
+ /**
+ * Sets UI and key derivation parameters
+ *
+ * @param recoveryMetadata The UI and key derivation parameters
+ * @return This builder.
+ */
+ public Builder setKeychainProtectionParams(
+ @NonNull List<KeychainProtectionParams> recoveryMetadata) {
+ mInstance.mKeychainProtectionParams = recoveryMetadata;
+ return this;
+ }
+
+ /**
+ * List of application keys.
+ *
+ * @param entryRecoveryData List of application keys
+ * @return This builder.
+ */
+ public Builder setWrappedApplicationKeys(List<WrappedApplicationKey> entryRecoveryData) {
+ mInstance.mEntryRecoveryData = entryRecoveryData;
+ return this;
+ }
+
+ /**
+ * Sets recovery key blob
+ *
+ * @param encryptedRecoveryKeyBlob The recovery key blob.
+ * @return This builder.
+ */
+ public Builder setEncryptedRecoveryKeyBlob(@NonNull byte[] encryptedRecoveryKeyBlob) {
+ mInstance.mEncryptedRecoveryKeyBlob = encryptedRecoveryKeyBlob;
+ return this;
+ }
+
+
+ /**
+ * Creates a new {@link KeychainSnapshot} instance.
+ *
+ * @return new instance
+ * @throws NullPointerException if some required fields were not set.
+ */
+ @NonNull public KeychainSnapshot build() {
+ Preconditions.checkCollectionElementsNotNull(mInstance.mKeychainProtectionParams,
+ "recoveryMetadata");
+ Preconditions.checkCollectionElementsNotNull(mInstance.mEntryRecoveryData,
+ "entryRecoveryData");
+ Preconditions.checkNotNull(mInstance.mEncryptedRecoveryKeyBlob);
+ Preconditions.checkNotNull(mInstance.mServerParams);
+ Preconditions.checkNotNull(mInstance.mPublicKey);
+ return mInstance;
+ }
+ }
+
+ /**
+ * @hide
+ */
+ @Override
+ public void writeToParcel(Parcel out, int flags) {
+ out.writeInt(mSnapshotVersion);
+ out.writeTypedList(mKeychainProtectionParams);
+ out.writeByteArray(mEncryptedRecoveryKeyBlob);
+ out.writeTypedList(mEntryRecoveryData);
+ out.writeInt(mMaxAttempts);
+ out.writeLong(mCounterId);
+ out.writeByteArray(mServerParams);
+ out.writeByteArray(mPublicKey);
+ }
+
+ /**
+ * @hide
+ */
+ protected KeychainSnapshot(Parcel in) {
+ mSnapshotVersion = in.readInt();
+ mKeychainProtectionParams = in.createTypedArrayList(KeychainProtectionParams.CREATOR);
+ mEncryptedRecoveryKeyBlob = in.createByteArray();
+ mEntryRecoveryData = in.createTypedArrayList(WrappedApplicationKey.CREATOR);
+ mMaxAttempts = in.readInt();
+ mCounterId = in.readLong();
+ mServerParams = in.createByteArray();
+ mPublicKey = in.createByteArray();
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+}
--- /dev/null
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.security.keystore.recovery;
+
+/**
+ * Error thrown when trying to generate keys for a profile that has no lock screen set.
+ *
+ * <p>A lock screen must be set, as the lock screen is used to encrypt the snapshot.
+ *
+ * @hide
+ */
+public class LockScreenRequiredException extends RecoveryControllerException {
+ public LockScreenRequiredException(String msg) {
+ super(msg);
+ }
+}
--- /dev/null
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.security.keystore.recovery;
+
+/**
+ * An attempt to recover a keychain protected by remote secure hardware.
+ *
+ * @hide
+ */
+public class RecoveryClaim {
+
+ private final RecoverySession mRecoverySession;
+ private final byte[] mClaimBytes;
+
+ RecoveryClaim(RecoverySession recoverySession, byte[] claimBytes) {
+ mRecoverySession = recoverySession;
+ mClaimBytes = claimBytes;
+ }
+
+ /**
+ * Returns the session associated with the recovery attempt. This is used to match the symmetric
+ * key, which remains internal to the framework, for decrypting the claim response.
+ *
+ * @return The session data.
+ */
+ public RecoverySession getRecoverySession() {
+ return mRecoverySession;
+ }
+
+ /**
+ * Returns the encrypted claim's bytes.
+ *
+ * <p>This should be sent by the recovery agent to the remote secure hardware, which will use
+ * it to decrypt the keychain, before sending it re-encrypted with the session's symmetric key
+ * to the device.
+ */
+ public byte[] getClaimBytes() {
+ return mClaimBytes;
+ }
+}
--- /dev/null
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.security.keystore.recovery;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.app.PendingIntent;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.os.RemoteException;
+import android.os.ServiceManager;
+import android.os.ServiceSpecificException;
+import android.util.Log;
+
+import com.android.internal.widget.ILockSettings;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * An assistant for generating {@link javax.crypto.SecretKey} instances that can be recovered by
+ * other Android devices belonging to the user. The exported keychain is protected by the user's
+ * lock screen.
+ *
+ * <p>The RecoveryController must be paired with a recovery agent. The recovery agent is responsible
+ * for transporting the keychain to remote trusted hardware. This hardware must prevent brute force
+ * attempts against the user's lock screen by limiting the number of allowed guesses (to, e.g., 10).
+ * After that number of incorrect guesses, the trusted hardware no longer allows access to the
+ * key chain.
+ *
+ * <p>For now only the recovery agent itself is able to create keys, so it is expected that the
+ * recovery agent is itself the system app.
+ *
+ * <p>A recovery agent requires the privileged permission
+ * {@code android.Manifest.permission#RECOVER_KEYSTORE}.
+ *
+ * @hide
+ */
+public class RecoveryController {
+ private static final String TAG = "RecoveryController";
+
+ /** Key has been successfully synced. */
+ public static final int RECOVERY_STATUS_SYNCED = 0;
+ /** Waiting for recovery agent to sync the key. */
+ public static final int RECOVERY_STATUS_SYNC_IN_PROGRESS = 1;
+ /** Recovery account is not available. */
+ public static final int RECOVERY_STATUS_MISSING_ACCOUNT = 2;
+ /** Key cannot be synced. */
+ public static final int RECOVERY_STATUS_PERMANENT_FAILURE = 3;
+
+ /**
+ * Failed because no snapshot is yet pending to be synced for the user.
+ *
+ * @hide
+ */
+ public static final int ERROR_NO_SNAPSHOT_PENDING = 21;
+
+ /**
+ * Failed due to an error internal to the recovery service. This is unexpected and indicates
+ * either a problem with the logic in the service, or a problem with a dependency of the
+ * service (such as AndroidKeyStore).
+ *
+ * @hide
+ */
+ public static final int ERROR_SERVICE_INTERNAL_ERROR = 22;
+
+ /**
+ * Failed because the user does not have a lock screen set.
+ *
+ * @hide
+ */
+ public static final int ERROR_INSECURE_USER = 23;
+
+ /**
+ * Error thrown when attempting to use a recovery session that has since been closed.
+ *
+ * @hide
+ */
+ public static final int ERROR_SESSION_EXPIRED = 24;
+
+ /**
+ * Failed because the provided certificate was not a valid X509 certificate.
+ *
+ * @hide
+ */
+ public static final int ERROR_BAD_CERTIFICATE_FORMAT = 25;
+
+ /**
+ * Error thrown if decryption failed. This might be because the tag is wrong, the key is wrong,
+ * the data has become corrupted, the data has been tampered with, etc.
+ *
+ * @hide
+ */
+ public static final int ERROR_DECRYPTION_FAILED = 26;
+
+
+ private final ILockSettings mBinder;
+
+ private RecoveryController(ILockSettings binder) {
+ mBinder = binder;
+ }
+
+ /**
+ * Gets a new instance of the class.
+ */
+ public static RecoveryController getInstance() {
+ ILockSettings lockSettings =
+ ILockSettings.Stub.asInterface(ServiceManager.getService("lock_settings"));
+ return new RecoveryController(lockSettings);
+ }
+
+ /**
+ * Initializes key recovery service for the calling application. RecoveryController
+ * randomly chooses one of the keys from the list and keeps it to use for future key export
+ * operations. Collection of all keys in the list must be signed by the provided {@code
+ * rootCertificateAlias}, which must also be present in the list of root certificates
+ * preinstalled on the device. The random selection allows RecoveryController to select
+ * which of a set of remote recovery service devices will be used.
+ *
+ * <p>In addition, RecoveryController enforces a delay of three months between
+ * consecutive initialization attempts, to limit the ability of an attacker to often switch
+ * remote recovery devices and significantly increase number of recovery attempts.
+ *
+ * @param rootCertificateAlias alias of a root certificate preinstalled on the device
+ * @param signedPublicKeyList binary blob a list of X509 certificates and signature
+ * @throws BadCertificateFormatException if the {@code signedPublicKeyList} is in a bad format.
+ * @throws InternalRecoveryServiceException if an unexpected error occurred in the recovery
+ * service.
+ */
+ public void initRecoveryService(
+ @NonNull String rootCertificateAlias, @NonNull byte[] signedPublicKeyList)
+ throws BadCertificateFormatException, InternalRecoveryServiceException {
+ try {
+ mBinder.initRecoveryService(rootCertificateAlias, signedPublicKeyList);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ } catch (ServiceSpecificException e) {
+ if (e.errorCode == ERROR_BAD_CERTIFICATE_FORMAT) {
+ throw new BadCertificateFormatException(e.getMessage());
+ }
+ throw wrapUnexpectedServiceSpecificException(e);
+ }
+ }
+
+ /**
+ * Returns data necessary to store all recoverable keys for given account. Key material is
+ * encrypted with user secret and recovery public key.
+ *
+ * @param account specific to Recovery agent.
+ * @return Data necessary to recover keystore.
+ * @throws InternalRecoveryServiceException if an unexpected error occurred in the recovery
+ * service.
+ */
+ @NonNull public KeychainSnapshot getRecoveryData(@NonNull byte[] account)
+ throws InternalRecoveryServiceException {
+ try {
+ return mBinder.getRecoveryData(account);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ } catch (ServiceSpecificException e) {
+ if (e.errorCode == ERROR_NO_SNAPSHOT_PENDING) {
+ return null;
+ }
+ throw wrapUnexpectedServiceSpecificException(e);
+ }
+ }
+
+ /**
+ * Sets a listener which notifies recovery agent that new recovery snapshot is available. {@link
+ * #getRecoveryData} can be used to get the snapshot. Note that every recovery agent can have at
+ * most one registered listener at any time.
+ *
+ * @param intent triggered when new snapshot is available. Unregisters listener if the value is
+ * {@code null}.
+ * @throws InternalRecoveryServiceException if an unexpected error occurred in the recovery
+ * service.
+ */
+ public void setSnapshotCreatedPendingIntent(@Nullable PendingIntent intent)
+ throws InternalRecoveryServiceException {
+ try {
+ mBinder.setSnapshotCreatedPendingIntent(intent);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ } catch (ServiceSpecificException e) {
+ throw wrapUnexpectedServiceSpecificException(e);
+ }
+ }
+
+ /**
+ * Returns a map from recovery agent accounts to corresponding KeyStore recovery snapshot
+ * version. Version zero is used, if no snapshots were created for the account.
+ *
+ * @return Map from recovery agent accounts to snapshot versions.
+ * @see KeychainSnapshot#getSnapshotVersion
+ * @throws InternalRecoveryServiceException if an unexpected error occurred in the recovery
+ * service.
+ */
+ public @NonNull Map<byte[], Integer> getRecoverySnapshotVersions()
+ throws InternalRecoveryServiceException {
+ try {
+ // IPC doesn't support generic Maps.
+ @SuppressWarnings("unchecked")
+ Map<byte[], Integer> result =
+ (Map<byte[], Integer>) mBinder.getRecoverySnapshotVersions();
+ return result;
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ } catch (ServiceSpecificException e) {
+ throw wrapUnexpectedServiceSpecificException(e);
+ }
+ }
+
+ /**
+ * Server parameters used to generate new recovery key blobs. This value will be included in
+ * {@code KeychainSnapshot.getEncryptedRecoveryKeyBlob()}. The same value must be included
+ * in vaultParams {@link #startRecoverySession}
+ *
+ * @param serverParams included in recovery key blob.
+ * @see #getRecoveryData
+ * @throws InternalRecoveryServiceException if an unexpected error occurred in the recovery
+ * service.
+ */
+ public void setServerParams(byte[] serverParams) throws InternalRecoveryServiceException {
+ try {
+ mBinder.setServerParams(serverParams);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ } catch (ServiceSpecificException e) {
+ throw wrapUnexpectedServiceSpecificException(e);
+ }
+ }
+
+ /**
+ * Updates recovery status for given keys. It is used to notify keystore that key was
+ * successfully stored on the server or there were an error. Application can check this value
+ * using {@code getRecoveyStatus}.
+ *
+ * @param packageName Application whose recoverable keys' statuses are to be updated.
+ * @param aliases List of application-specific key aliases. If the array is empty, updates the
+ * status for all existing recoverable keys.
+ * @param status Status specific to recovery agent.
+ * @throws InternalRecoveryServiceException if an unexpected error occurred in the recovery
+ * service.
+ */
+ public void setRecoveryStatus(
+ @NonNull String packageName, @Nullable String[] aliases, int status)
+ throws NameNotFoundException, InternalRecoveryServiceException {
+ try {
+ mBinder.setRecoveryStatus(packageName, aliases, status);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ } catch (ServiceSpecificException e) {
+ throw wrapUnexpectedServiceSpecificException(e);
+ }
+ }
+
+ /**
+ * Returns a {@code Map} from Application's KeyStore key aliases to their recovery status.
+ * Negative status values are reserved for recovery agent specific codes. List of common codes:
+ *
+ * <ul>
+ * <li>{@link #RECOVERY_STATUS_SYNCED}
+ * <li>{@link #RECOVERY_STATUS_SYNC_IN_PROGRESS}
+ * <li>{@link #RECOVERY_STATUS_MISSING_ACCOUNT}
+ * <li>{@link #RECOVERY_STATUS_PERMANENT_FAILURE}
+ * </ul>
+ *
+ * @return {@code Map} from KeyStore alias to recovery status.
+ * @see #setRecoveryStatus
+ * @throws InternalRecoveryServiceException if an unexpected error occurred in the recovery
+ * service.
+ */
+ public Map<String, Integer> getRecoveryStatus() throws InternalRecoveryServiceException {
+ try {
+ // IPC doesn't support generic Maps.
+ @SuppressWarnings("unchecked")
+ Map<String, Integer> result =
+ (Map<String, Integer>) mBinder.getRecoveryStatus(/*packageName=*/ null);
+ return result;
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ } catch (ServiceSpecificException e) {
+ throw wrapUnexpectedServiceSpecificException(e);
+ }
+ }
+
+ /**
+ * Specifies a set of secret types used for end-to-end keystore encryption. Knowing all of them
+ * is necessary to recover data.
+ *
+ * @param secretTypes {@link KeychainProtectionParams#TYPE_LOCKSCREEN} or {@link
+ * KeychainProtectionParams#TYPE_CUSTOM_PASSWORD}
+ * @throws InternalRecoveryServiceException if an unexpected error occurred in the recovery
+ * service.
+ */
+ public void setRecoverySecretTypes(
+ @NonNull @KeychainProtectionParams.UserSecretType int[] secretTypes)
+ throws InternalRecoveryServiceException {
+ try {
+ mBinder.setRecoverySecretTypes(secretTypes);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ } catch (ServiceSpecificException e) {
+ throw wrapUnexpectedServiceSpecificException(e);
+ }
+ }
+
+ /**
+ * Defines a set of secret types used for end-to-end keystore encryption. Knowing all of them is
+ * necessary to generate KeychainSnapshot.
+ *
+ * @return list of recovery secret types
+ * @see KeychainSnapshot
+ * @throws InternalRecoveryServiceException if an unexpected error occurred in the recovery
+ * service.
+ */
+ public @NonNull @KeychainProtectionParams.UserSecretType int[] getRecoverySecretTypes()
+ throws InternalRecoveryServiceException {
+ try {
+ return mBinder.getRecoverySecretTypes();
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ } catch (ServiceSpecificException e) {
+ throw wrapUnexpectedServiceSpecificException(e);
+ }
+ }
+
+ /**
+ * Returns a list of recovery secret types, necessary to create a pending recovery snapshot.
+ * When user enters a secret of a pending type {@link #recoverySecretAvailable} should be
+ * called.
+ *
+ * @return list of recovery secret types
+ * @throws InternalRecoveryServiceException if an unexpected error occurred in the recovery
+ * service.
+ */
+ @NonNull
+ public @KeychainProtectionParams.UserSecretType int[] getPendingRecoverySecretTypes()
+ throws InternalRecoveryServiceException {
+ try {
+ return mBinder.getPendingRecoverySecretTypes();
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ } catch (ServiceSpecificException e) {
+ throw wrapUnexpectedServiceSpecificException(e);
+ }
+ }
+
+ /**
+ * Method notifies KeyStore that a user-generated secret is available. This method generates a
+ * symmetric session key which a trusted remote device can use to return a recovery key. Caller
+ * should use {@link KeychainProtectionParams#clearSecret} to override the secret value in
+ * memory.
+ *
+ * @param recoverySecret user generated secret together with parameters necessary to regenerate
+ * it on a new device.
+ * @throws InternalRecoveryServiceException if an unexpected error occurred in the recovery
+ * service.
+ */
+ public void recoverySecretAvailable(@NonNull KeychainProtectionParams recoverySecret)
+ throws InternalRecoveryServiceException {
+ try {
+ mBinder.recoverySecretAvailable(recoverySecret);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ } catch (ServiceSpecificException e) {
+ throw wrapUnexpectedServiceSpecificException(e);
+ }
+ }
+
+ /**
+ * Initializes recovery session and returns a blob with proof of recovery secrets possession.
+ * The method generates symmetric key for a session, which trusted remote device can use to
+ * return recovery key.
+ *
+ * @param verifierPublicKey Encoded {@code java.security.cert.X509Certificate} with Public key
+ * used to create the recovery blob on the source device.
+ * Keystore will verify the certificate using root of trust.
+ * @param vaultParams Must match the parameters in the corresponding field in the recovery blob.
+ * Used to limit number of guesses.
+ * @param vaultChallenge Data passed from server for this recovery session and used to prevent
+ * replay attacks
+ * @param secrets Secrets provided by user, the method only uses type and secret fields.
+ * @return The recovery claim. Claim provides a b binary blob with recovery claim. It is
+ * encrypted with verifierPublicKey and contains a proof of user secrets, session symmetric
+ * key and parameters necessary to identify the counter with the number of failed recovery
+ * attempts.
+ * @throws BadCertificateFormatException if the {@code verifierPublicKey} is in an incorrect
+ * format.
+ * @throws InternalRecoveryServiceException if an unexpected error occurred in the recovery
+ * service.
+ */
+ @NonNull public RecoveryClaim startRecoverySession(
+ @NonNull byte[] verifierPublicKey,
+ @NonNull byte[] vaultParams,
+ @NonNull byte[] vaultChallenge,
+ @NonNull List<KeychainProtectionParams> secrets)
+ throws BadCertificateFormatException, InternalRecoveryServiceException {
+ try {
+ RecoverySession recoverySession = RecoverySession.newInstance(this);
+ byte[] recoveryClaim =
+ mBinder.startRecoverySession(
+ recoverySession.getSessionId(),
+ verifierPublicKey,
+ vaultParams,
+ vaultChallenge,
+ secrets);
+ return new RecoveryClaim(recoverySession, recoveryClaim);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ } catch (ServiceSpecificException e) {
+ if (e.errorCode == ERROR_BAD_CERTIFICATE_FORMAT) {
+ throw new BadCertificateFormatException(e.getMessage());
+ }
+ throw wrapUnexpectedServiceSpecificException(e);
+ }
+ }
+
+ /**
+ * Imports keys.
+ *
+ * @param session Related recovery session, as originally created by invoking
+ * {@link #startRecoverySession(byte[], byte[], byte[], List)}.
+ * @param recoveryKeyBlob Recovery blob encrypted by symmetric key generated for this session.
+ * @param applicationKeys Application keys. Key material can be decrypted using recoveryKeyBlob
+ * and session. KeyStore only uses package names from the application info in {@link
+ * WrappedApplicationKey}. Caller is responsibility to perform certificates check.
+ * @return Map from alias to raw key material.
+ * @throws SessionExpiredException if {@code session} has since been closed.
+ * @throws DecryptionFailedException if unable to decrypt the snapshot.
+ * @throws InternalRecoveryServiceException if an error occurs internal to the recovery service.
+ */
+ public Map<String, byte[]> recoverKeys(
+ @NonNull RecoverySession session,
+ @NonNull byte[] recoveryKeyBlob,
+ @NonNull List<WrappedApplicationKey> applicationKeys)
+ throws SessionExpiredException, DecryptionFailedException,
+ InternalRecoveryServiceException {
+ try {
+ return (Map<String, byte[]>) mBinder.recoverKeys(
+ session.getSessionId(), recoveryKeyBlob, applicationKeys);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ } catch (ServiceSpecificException e) {
+ if (e.errorCode == ERROR_DECRYPTION_FAILED) {
+ throw new DecryptionFailedException(e.getMessage());
+ }
+ if (e.errorCode == ERROR_SESSION_EXPIRED) {
+ throw new SessionExpiredException(e.getMessage());
+ }
+ throw wrapUnexpectedServiceSpecificException(e);
+ }
+ }
+
+ /**
+ * Deletes all data associated with {@code session}. Should not be invoked directly but via
+ * {@link RecoverySession#close()}.
+ *
+ * @hide
+ */
+ void closeSession(RecoverySession session) {
+ try {
+ mBinder.closeSession(session.getSessionId());
+ } catch (RemoteException | ServiceSpecificException e) {
+ Log.e(TAG, "Unexpected error trying to close session", e);
+ }
+ }
+
+ /**
+ * Generates a key called {@code alias} and loads it into the recoverable key store. Returns the
+ * raw material of the key.
+ *
+ * @param alias The key alias.
+ * @throws InternalRecoveryServiceException if an unexpected error occurred in the recovery
+ * service.
+ * @throws LockScreenRequiredException if the user has not set a lock screen. This is required
+ * to generate recoverable keys, as the snapshots are encrypted using a key derived from the
+ * lock screen.
+ */
+ public byte[] generateAndStoreKey(@NonNull String alias)
+ throws InternalRecoveryServiceException, LockScreenRequiredException {
+ try {
+ return mBinder.generateAndStoreKey(alias);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ } catch (ServiceSpecificException e) {
+ if (e.errorCode == ERROR_INSECURE_USER) {
+ throw new LockScreenRequiredException(e.getMessage());
+ }
+ throw wrapUnexpectedServiceSpecificException(e);
+ }
+ }
+
+ /**
+ * Removes a key called {@code alias} from the recoverable key store.
+ *
+ * @param alias The key alias.
+ * @throws InternalRecoveryServiceException if an unexpected error occurred in the recovery
+ * service.
+ */
+ public void removeKey(@NonNull String alias) throws InternalRecoveryServiceException {
+ try {
+ mBinder.removeKey(alias);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ } catch (ServiceSpecificException e) {
+ throw wrapUnexpectedServiceSpecificException(e);
+ }
+ }
+
+ private InternalRecoveryServiceException wrapUnexpectedServiceSpecificException(
+ ServiceSpecificException e) {
+ if (e.errorCode == ERROR_SERVICE_INTERNAL_ERROR) {
+ return new InternalRecoveryServiceException(e.getMessage());
+ }
+
+ // Should never happen. If it does, it's a bug, and we need to update how the method that
+ // called this throws its exceptions.
+ return new InternalRecoveryServiceException("Unexpected error code for method: "
+ + e.errorCode, e);
+ }
+}
--- /dev/null
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.security.keystore.recovery;
+
+import java.security.GeneralSecurityException;
+
+/**
+ * Base exception for errors thrown by {@link RecoveryController}.
+ *
+ * @hide
+ */
+public abstract class RecoveryControllerException extends GeneralSecurityException {
+ RecoveryControllerException() { }
+
+ RecoveryControllerException(String msg) {
+ super(msg);
+ }
+
+ public RecoveryControllerException(String message, Throwable cause) {
+ super(message, cause);
+ }
+}
--- /dev/null
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.security.keystore.recovery;
+
+import java.security.SecureRandom;
+
+/**
+ * Session to recover a {@link KeychainSnapshot} from the remote trusted hardware, initiated by a
+ * recovery agent.
+ *
+ * @hide
+ */
+public class RecoverySession implements AutoCloseable {
+
+ private static final int SESSION_ID_LENGTH_BYTES = 16;
+
+ private final String mSessionId;
+ private final RecoveryController mRecoveryController;
+
+ private RecoverySession(RecoveryController recoveryController, String sessionId) {
+ mRecoveryController = recoveryController;
+ mSessionId = sessionId;
+ }
+
+ /**
+ * A new session, started by {@code recoveryManager}.
+ */
+ static RecoverySession newInstance(RecoveryController recoveryController) {
+ return new RecoverySession(recoveryController, newSessionId());
+ }
+
+ /**
+ * Returns a new random session ID.
+ */
+ private static String newSessionId() {
+ SecureRandom secureRandom = new SecureRandom();
+ byte[] sessionId = new byte[SESSION_ID_LENGTH_BYTES];
+ secureRandom.nextBytes(sessionId);
+ StringBuilder sb = new StringBuilder();
+ for (byte b : sessionId) {
+ sb.append(Byte.toHexString(b, /*upperCase=*/ false));
+ }
+ return sb.toString();
+ }
+
+ /**
+ * An internal session ID, used by the framework to match recovery claims to snapshot responses.
+ */
+ String getSessionId() {
+ return mSessionId;
+ }
+
+ @Override
+ public void close() {
+ mRecoveryController.closeSession(this);
+ }
+}
--- /dev/null
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.security.keystore.recovery;
+
+/**
+ * Error thrown when attempting to use a {@link RecoverySession} that has since expired.
+ *
+ * @hide
+ */
+public class SessionExpiredException extends RecoveryControllerException {
+ public SessionExpiredException(String msg) {
+ super(msg);
+ }
+}
* limitations under the License.
*/
-package android.security.keystore;
+package android.security.keystore.recovery;
/* @hide */
parcelable WrappedApplicationKey;
--- /dev/null
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.security.keystore.recovery;
+
+import android.annotation.NonNull;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import com.android.internal.util.Preconditions;
+
+/**
+ * Helper class with data necessary recover a single application key, given a recovery key.
+ *
+ * <ul>
+ * <li>Alias - Keystore alias of the key.
+ * <li>Encrypted key material.
+ * </ul>
+ *
+ * Note that Application info is not included. Recovery Agent can only make its own keys
+ * recoverable.
+ *
+ * @hide
+ */
+public final class WrappedApplicationKey implements Parcelable {
+ private String mAlias;
+ // The only supported format is AES-256 symmetric key.
+ private byte[] mEncryptedKeyMaterial;
+
+ /**
+ * Builder for creating {@link WrappedApplicationKey}.
+ */
+ public static class Builder {
+ private WrappedApplicationKey
+ mInstance = new WrappedApplicationKey();
+
+ /**
+ * Sets Application-specific alias of the key.
+ *
+ * @param alias The alias.
+ * @return This builder.
+ */
+ public Builder setAlias(@NonNull String alias) {
+ mInstance.mAlias = alias;
+ return this;
+ }
+
+ /**
+ * Sets key material encrypted by recovery key.
+ *
+ * @param encryptedKeyMaterial The key material
+ * @return This builder
+ */
+
+ public Builder setEncryptedKeyMaterial(@NonNull byte[] encryptedKeyMaterial) {
+ mInstance.mEncryptedKeyMaterial = encryptedKeyMaterial;
+ return this;
+ }
+
+ /**
+ * Creates a new {@link WrappedApplicationKey} instance.
+ *
+ * @return new instance
+ * @throws NullPointerException if some required fields were not set.
+ */
+ @NonNull public WrappedApplicationKey build() {
+ Preconditions.checkNotNull(mInstance.mAlias);
+ Preconditions.checkNotNull(mInstance.mEncryptedKeyMaterial);
+ return mInstance;
+ }
+ }
+
+ private WrappedApplicationKey() {
+
+ }
+
+ /**
+ * Deprecated - consider using Builder.
+ * @hide
+ */
+ public WrappedApplicationKey(@NonNull String alias, @NonNull byte[] encryptedKeyMaterial) {
+ mAlias = Preconditions.checkNotNull(alias);
+ mEncryptedKeyMaterial = Preconditions.checkNotNull(encryptedKeyMaterial);
+ }
+
+ /**
+ * Application-specific alias of the key.
+ *
+ * @see java.security.KeyStore.aliases
+ */
+ public @NonNull String getAlias() {
+ return mAlias;
+ }
+
+ /** Key material encrypted by recovery key. */
+ public @NonNull byte[] getEncryptedKeyMaterial() {
+ return mEncryptedKeyMaterial;
+ }
+
+ public static final Creator<WrappedApplicationKey> CREATOR =
+ new Creator<WrappedApplicationKey>() {
+ public WrappedApplicationKey createFromParcel(Parcel in) {
+ return new WrappedApplicationKey(in);
+ }
+
+ public WrappedApplicationKey[] newArray(int length) {
+ return new WrappedApplicationKey[length];
+ }
+ };
+
+ /**
+ * @hide
+ */
+ @Override
+ public void writeToParcel(Parcel out, int flags) {
+ out.writeString(mAlias);
+ out.writeByteArray(mEncryptedKeyMaterial);
+ }
+
+ /**
+ * @hide
+ */
+ protected WrappedApplicationKey(Parcel in) {
+ mAlias = in.readString();
+ mEncryptedKeyMaterial = in.createByteArray();
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+}
import android.app.PendingIntent;
import android.app.trust.IStrongAuthTracker;
import android.os.Bundle;
-import android.security.keystore.WrappedApplicationKey;
-import android.security.keystore.KeychainSnapshot;
-import android.security.keystore.KeychainProtectionParams;
+import android.security.keystore.recovery.WrappedApplicationKey;
+import android.security.keystore.recovery.KeychainSnapshot;
+import android.security.keystore.recovery.KeychainProtectionParams;
import com.android.internal.widget.ICheckCredentialProgressCallback;
import com.android.internal.widget.VerifyCredentialResponse;
import android.security.keystore.AndroidKeyStoreProvider;
import android.security.keystore.KeyProperties;
import android.security.keystore.KeyProtection;
-import android.security.keystore.KeychainProtectionParams;
import android.security.keystore.UserNotAuthenticatedException;
-import android.security.keystore.WrappedApplicationKey;
-import android.security.keystore.KeychainSnapshot;
+import android.security.keystore.recovery.KeychainProtectionParams;
+import android.security.keystore.recovery.WrappedApplicationKey;
+import android.security.keystore.recovery.KeychainSnapshot;
import android.service.gatekeeper.GateKeeperResponse;
import android.service.gatekeeper.IGateKeeperService;
import android.text.TextUtils;
package com.android.server.locksettings.recoverablekeystore;
-import static android.security.keystore.KeychainProtectionParams.TYPE_LOCKSCREEN;
+import static android.security.keystore.recovery.KeychainProtectionParams.TYPE_LOCKSCREEN;
import android.annotation.Nullable;
import android.content.Context;
-import android.security.keystore.KeyDerivationParams;
-import android.security.keystore.KeychainProtectionParams;
-import android.security.keystore.KeychainSnapshot;
-import android.security.keystore.WrappedApplicationKey;
+import android.security.keystore.recovery.KeyDerivationParams;
+import android.security.keystore.recovery.KeychainProtectionParams;
+import android.security.keystore.recovery.KeychainSnapshot;
+import android.security.keystore.recovery.WrappedApplicationKey;
import android.util.Log;
import com.android.internal.annotations.VisibleForTesting;
import android.os.ServiceSpecificException;
import android.os.UserHandle;
-import android.security.keystore.KeychainProtectionParams;
-import android.security.keystore.KeychainSnapshot;
-import android.security.keystore.RecoveryController;
-import android.security.keystore.WrappedApplicationKey;
+import android.security.keystore.recovery.KeychainProtectionParams;
+import android.security.keystore.recovery.KeychainSnapshot;
+import android.security.keystore.recovery.RecoveryController;
+import android.security.keystore.recovery.WrappedApplicationKey;
import android.util.Log;
import com.android.internal.annotations.VisibleForTesting;
package com.android.server.locksettings.recoverablekeystore.storage;
import android.annotation.Nullable;
-import android.security.keystore.KeychainSnapshot;
+import android.security.keystore.recovery.KeychainSnapshot;
import android.util.SparseArray;
import com.android.internal.annotations.GuardedBy;
package com.android.server.locksettings.recoverablekeystore;
-import static android.security.keystore.KeychainProtectionParams.TYPE_LOCKSCREEN;
+import static android.security.keystore.recovery.KeychainProtectionParams.TYPE_LOCKSCREEN;
-import static android.security.keystore.KeychainProtectionParams.TYPE_PASSWORD;
-import static android.security.keystore.KeychainProtectionParams.TYPE_PATTERN;
-import static android.security.keystore.KeychainProtectionParams.TYPE_PIN;
+import static android.security.keystore.recovery.KeychainProtectionParams.TYPE_PASSWORD;
+import static android.security.keystore.recovery.KeychainProtectionParams.TYPE_PATTERN;
+import static android.security.keystore.recovery.KeychainProtectionParams.TYPE_PIN;
import static com.android.internal.widget.LockPatternUtils.CREDENTIAL_TYPE_PASSWORD;
import static com.android.internal.widget.LockPatternUtils.CREDENTIAL_TYPE_PATTERN;
import android.security.keystore.AndroidKeyStoreSecretKey;
import android.security.keystore.KeyGenParameterSpec;
import android.security.keystore.KeyProperties;
-import android.security.keystore.KeyDerivationParams;
-import android.security.keystore.KeychainSnapshot;
-import android.security.keystore.WrappedApplicationKey;
+import android.security.keystore.recovery.KeyDerivationParams;
+import android.security.keystore.recovery.KeychainSnapshot;
+import android.security.keystore.recovery.WrappedApplicationKey;
import android.support.test.InstrumentationRegistry;
import android.support.test.filters.SmallTest;
import android.support.test.runner.AndroidJUnit4;
package com.android.server.locksettings.recoverablekeystore;
-import static android.security.keystore.KeychainProtectionParams.TYPE_LOCKSCREEN;
-import static android.security.keystore.KeychainProtectionParams.TYPE_PASSWORD;
+import static android.security.keystore.recovery.KeychainProtectionParams.TYPE_LOCKSCREEN;
+import static android.security.keystore.recovery.KeychainProtectionParams.TYPE_PASSWORD;
import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.assertArrayEquals;
import android.security.keystore.AndroidKeyStoreSecretKey;
import android.security.keystore.KeyGenParameterSpec;
import android.security.keystore.KeyProperties;
-import android.security.keystore.KeyDerivationParams;
-import android.security.keystore.KeychainProtectionParams;
-import android.security.keystore.WrappedApplicationKey;
+import android.security.keystore.recovery.KeyDerivationParams;
+import android.security.keystore.recovery.KeychainProtectionParams;
+import android.security.keystore.recovery.WrappedApplicationKey;
import android.support.test.filters.SmallTest;
import android.support.test.InstrumentationRegistry;
import android.support.test.runner.AndroidJUnit4;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
-import android.security.keystore.KeychainSnapshot;
+import android.security.keystore.recovery.KeychainSnapshot;
import android.support.test.filters.SmallTest;
import android.support.test.runner.AndroidJUnit4;