| /* |
| * 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 com.android.server.locksettings; |
| |
| import static com.android.internal.widget.LockPatternUtils.CREDENTIAL_TYPE_PIN; |
| import static com.android.internal.widget.LockPatternUtils.EscrowTokenStateChangeCallback; |
| import static com.android.internal.widget.LockPatternUtils.PIN_LENGTH_UNAVAILABLE; |
| |
| import android.annotation.IntDef; |
| import android.annotation.NonNull; |
| import android.annotation.Nullable; |
| import android.annotation.UserIdInt; |
| import android.app.admin.PasswordMetrics; |
| import android.content.Context; |
| import android.content.pm.UserInfo; |
| import android.hardware.weaver.IWeaver; |
| import android.hardware.weaver.WeaverConfig; |
| import android.hardware.weaver.WeaverReadResponse; |
| import android.hardware.weaver.WeaverReadStatus; |
| import android.os.RemoteCallbackList; |
| import android.os.RemoteException; |
| import android.os.ServiceManager; |
| import android.os.ServiceSpecificException; |
| import android.os.UserManager; |
| import android.provider.Settings; |
| import android.security.GateKeeper; |
| import android.security.Scrypt; |
| import android.service.gatekeeper.GateKeeperResponse; |
| import android.service.gatekeeper.IGateKeeperService; |
| import android.text.TextUtils; |
| import android.util.ArrayMap; |
| import android.util.ArraySet; |
| import android.util.Slog; |
| |
| import com.android.internal.annotations.VisibleForTesting; |
| import com.android.internal.util.ArrayUtils; |
| import com.android.internal.util.Preconditions; |
| import com.android.internal.widget.ICheckCredentialProgressCallback; |
| import com.android.internal.widget.IWeakEscrowTokenRemovedListener; |
| import com.android.internal.widget.LockPatternUtils; |
| import com.android.internal.widget.LockscreenCredential; |
| import com.android.internal.widget.VerifyCredentialResponse; |
| import com.android.server.locksettings.LockSettingsStorage.PersistentData; |
| import com.android.server.utils.Slogf; |
| |
| import libcore.util.HexEncoding; |
| |
| import java.lang.annotation.Retention; |
| import java.lang.annotation.RetentionPolicy; |
| import java.nio.ByteBuffer; |
| import java.util.Arrays; |
| import java.util.Collections; |
| import java.util.HashSet; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.NoSuchElementException; |
| import java.util.Objects; |
| import java.util.Set; |
| |
| |
| /** |
| * A class that manages a user's synthetic password (SP) ({@link #SyntheticPassword}), along with a |
| * set of SP protectors that are independent ways that the SP is protected. |
| * |
| * Invariants for SPs: |
| * |
| * - A user's SP never changes, but SP protectors can be added and removed. There is always a |
| * protector that protects the SP with the user's Lock Screen Knowledge Factor (LSKF), a.k.a. |
| * LockscreenCredential. The LSKF may be empty (none). There may be escrow token-based |
| * protectors as well, only for specific use cases such as enterprise-managed users. |
| * |
| * - The user's credential-encrypted storage is always protected by the SP. |
| * |
| * - The user's auth-bound Keystore keys are protected by the SP, but only while an LSKF is set. |
| * This works by setting the user's Keystore and Gatekeeper passwords to SP-derived secrets, but |
| * only while an LSKF is set. When the LSKF is removed, these passwords are cleared, |
| * invalidating the user's auth-bound keys. |
| * |
| * Files stored on disk for each user: |
| * For the SP itself, stored under NULL_PROTECTOR_ID: |
| * SP_HANDLE_NAME: GateKeeper password handle of a password derived from the SP. Only exists |
| * while the LSKF is nonempty. |
| * SP_E0_NAME, SP_P1_NAME: Information needed to create and use escrow token-based protectors. |
| * Deleted when escrow token support is disabled for the user. |
| * VENDOR_AUTH_SECRET_NAME: A copy of the secret passed using the IAuthSecret interface, |
| * encrypted using a secret derived from the SP using |
| * PERSONALIZATION_AUTHSECRET_ENCRYPTION_KEY. |
| * |
| * For each protector, stored under the corresponding protector ID: |
| * SP_BLOB_NAME: The encrypted SP secret (the SP itself or the P0 value). Always exists. |
| * PASSWORD_DATA_NAME: Data used for LSKF verification, such as the scrypt salt and |
| * parameters. Only exists for LSKF-based protectors. Doesn't exist when |
| * the LSKF is empty, except in old protectors. |
| * PASSWORD_METRICS_NAME: Metrics about the LSKF, encrypted by a key derived from the SP. |
| * Only exists for LSKF-based protectors. Doesn't exist when the LSKF |
| * is empty, except in old protectors. |
| * SECDISCARDABLE_NAME: A large number of random bytes that all need to be known in order to |
| * decrypt SP_BLOB_NAME. When the protector is deleted, this file is |
| * overwritten and deleted as a "best-effort" attempt to support secure |
| * deletion when hardware support for secure deletion is unavailable. |
| * Doesn't exist for LSKF-based protectors that use Weaver. |
| * WEAVER_SLOT: Contains the Weaver slot number used by this protector. Only exists if the |
| * protector uses Weaver. |
| */ |
| class SyntheticPasswordManager { |
| private static final String SP_BLOB_NAME = "spblob"; |
| private static final String SP_E0_NAME = "e0"; |
| private static final String SP_P1_NAME = "p1"; |
| private static final String SP_HANDLE_NAME = "handle"; |
| private static final String SECDISCARDABLE_NAME = "secdis"; |
| private static final int SECDISCARDABLE_LENGTH = 16 * 1024; |
| private static final String PASSWORD_DATA_NAME = "pwd"; |
| private static final String WEAVER_SLOT_NAME = "weaver"; |
| private static final String PASSWORD_METRICS_NAME = "metrics"; |
| private static final String VENDOR_AUTH_SECRET_NAME = "vendor_auth_secret"; |
| |
| // used for files associated with the SP itself, not with a particular protector |
| public static final long NULL_PROTECTOR_ID = 0L; |
| |
| private static final byte[] DEFAULT_PASSWORD = "default-password".getBytes(); |
| |
| private static final byte WEAVER_VERSION = 1; |
| private static final int INVALID_WEAVER_SLOT = -1; |
| |
| // Careful: the SYNTHETIC_PASSWORD_* version numbers are overloaded to identify both the version |
| // of the protector and the version of the synthetic password itself. All a user's protectors |
| // must use a version that treats the synthetic password itself in a compatible way. |
| private static final byte SYNTHETIC_PASSWORD_VERSION_V1 = 1; |
| private static final byte SYNTHETIC_PASSWORD_VERSION_V2 = 2; |
| private static final byte SYNTHETIC_PASSWORD_VERSION_V3 = 3; |
| |
| private static final byte PROTECTOR_TYPE_LSKF_BASED = 0; |
| private static final byte PROTECTOR_TYPE_STRONG_TOKEN_BASED = 1; |
| private static final byte PROTECTOR_TYPE_WEAK_TOKEN_BASED = 2; |
| |
| private static final String PROTECTOR_KEY_ALIAS_PREFIX = "synthetic_password_"; |
| |
| // The security strength of the synthetic password, in bytes |
| private static final int SYNTHETIC_PASSWORD_SECURITY_STRENGTH = 256 / 8; |
| |
| private static final int PASSWORD_SCRYPT_LOG_N = 11; |
| private static final int PASSWORD_SCRYPT_LOG_R = 3; |
| private static final int PASSWORD_SCRYPT_LOG_P = 1; |
| private static final int PASSWORD_SALT_LENGTH = 16; |
| private static final int STRETCHED_LSKF_LENGTH = 32; |
| private static final String TAG = "SyntheticPasswordManager"; |
| |
| private static final byte[] PERSONALIZATION_SECDISCARDABLE = "secdiscardable-transform".getBytes(); |
| private static final byte[] PERSONALIZATION_KEY_STORE_PASSWORD = "keystore-password".getBytes(); |
| private static final byte[] PERSONALIZATION_USER_GK_AUTH = "user-gk-authentication".getBytes(); |
| private static final byte[] PERSONALIZATION_SP_GK_AUTH = "sp-gk-authentication".getBytes(); |
| private static final byte[] PERSONALIZATION_FBE_KEY = "fbe-key".getBytes(); |
| private static final byte[] PERSONALIZATION_AUTHSECRET_KEY = "authsecret-hal".getBytes(); |
| private static final byte[] PERSONALIZATION_AUTHSECRET_ENCRYPTION_KEY = |
| "vendor-authsecret-encryption-key".getBytes(); |
| private static final byte[] PERSONALIZATION_SP_SPLIT = "sp-split".getBytes(); |
| private static final byte[] PERSONALIZATION_PASSWORD_HASH = "pw-hash".getBytes(); |
| private static final byte[] PERSONALIZATION_E0 = "e0-encryption".getBytes(); |
| private static final byte[] PERSONALIZATION_WEAVER_PASSWORD = "weaver-pwd".getBytes(); |
| private static final byte[] PERSONALIZATION_WEAVER_KEY = "weaver-key".getBytes(); |
| private static final byte[] PERSONALIZATION_WEAVER_TOKEN = "weaver-token".getBytes(); |
| private static final byte[] PERSONALIZATION_PASSWORD_METRICS = "password-metrics".getBytes(); |
| private static final byte[] PERSONALIZATION_CONTEXT = |
| "android-synthetic-password-personalization-context".getBytes(); |
| |
| static class AuthenticationResult { |
| // Non-null if password/token passes verification, null otherwise |
| @Nullable public SyntheticPassword syntheticPassword; |
| // OK: password / token passes verification, user has a lockscreen |
| // null: user does not have a lockscreen (but password / token passes verification) |
| // ERROR: password / token fails verification |
| // RETRY: password / token verification is throttled at the moment. |
| @Nullable public VerifyCredentialResponse gkResponse; |
| } |
| |
| /** |
| * A synthetic password (SP) is the main cryptographic secret for a user. The SP is used only |
| * as input to a Key Derivation Function (KDF) to derive other keys. |
| * |
| * SPs are created by {@link SyntheticPassword#create()} as the hash of two random values P0 and |
| * P1. E0 (P0 encrypted by an SP-derived key) and P1 can then be stored on-disk. This approach |
| * is used instead of direct random generation of the SP so that escrow token-based protectors |
| * can protect P0 instead of the SP itself. This makes it possible to cryptographically disable |
| * the ability to create and use such protectors by deleting (or never storing) E0 and P1. |
| * |
| * When protecting the SP directly, use {@link SyntheticPassword#getSyntheticPassword()} to get |
| * the raw SP, and later {@link SyntheticPassword#recreateDirectly(byte[])} to re-create the SP. |
| * When protecting P0, use {@link SyntheticPassword#getEscrowSecret()} to get P0, and later |
| * {@link SyntheticPassword#setEscrowData(byte[], byte[])} followed by |
| * {@link SyntheticPassword#recreateFromEscrow()} to re-create the SP. |
| */ |
| static class SyntheticPassword { |
| private final byte mVersion; |
| /** |
| * Here is the relationship between these fields: |
| * Generate two random block P0 and P1. P1 is recorded in mEscrowSplit1 but P0 is not. |
| * mSyntheticPassword = hash(P0 || P1) |
| * E0 = P0 encrypted under syntheticPassword, recorded in mEncryptedEscrowSplit0. |
| */ |
| private @NonNull byte[] mSyntheticPassword; |
| private @Nullable byte[] mEncryptedEscrowSplit0; |
| private @Nullable byte[] mEscrowSplit1; |
| |
| SyntheticPassword(byte version) { |
| mVersion = version; |
| } |
| |
| /** |
| * Derives a subkey from the synthetic password. For v3 and later synthetic passwords the |
| * subkeys are 256-bit; for v1 and v2 they are 512-bit. |
| */ |
| private byte[] deriveSubkey(byte[] personalization) { |
| if (mVersion == SYNTHETIC_PASSWORD_VERSION_V3) { |
| return (new SP800Derive(mSyntheticPassword)) |
| .withContext(personalization, PERSONALIZATION_CONTEXT); |
| } else { |
| return SyntheticPasswordCrypto.personalizedHash(personalization, |
| mSyntheticPassword); |
| } |
| } |
| |
| public byte[] deriveKeyStorePassword() { |
| return bytesToHex(deriveSubkey(PERSONALIZATION_KEY_STORE_PASSWORD)); |
| } |
| |
| public byte[] deriveGkPassword() { |
| return deriveSubkey(PERSONALIZATION_SP_GK_AUTH); |
| } |
| |
| public byte[] deriveFileBasedEncryptionKey() { |
| return deriveSubkey(PERSONALIZATION_FBE_KEY); |
| } |
| |
| public byte[] deriveVendorAuthSecret() { |
| return deriveSubkey(PERSONALIZATION_AUTHSECRET_KEY); |
| } |
| |
| public byte[] derivePasswordHashFactor() { |
| return deriveSubkey(PERSONALIZATION_PASSWORD_HASH); |
| } |
| |
| /** Derives key used to encrypt password metrics */ |
| public byte[] deriveMetricsKey() { |
| return deriveSubkey(PERSONALIZATION_PASSWORD_METRICS); |
| } |
| |
| public byte[] deriveVendorAuthSecretEncryptionKey() { |
| return deriveSubkey(PERSONALIZATION_AUTHSECRET_ENCRYPTION_KEY); |
| } |
| |
| /** |
| * Assigns escrow data to this synthetic password. This is a prerequisite to call |
| * {@link SyntheticPassword#recreateFromEscrow}. |
| */ |
| public void setEscrowData(@Nullable byte[] encryptedEscrowSplit0, |
| @Nullable byte[] escrowSplit1) { |
| mEncryptedEscrowSplit0 = encryptedEscrowSplit0; |
| mEscrowSplit1 = escrowSplit1; |
| } |
| |
| /** |
| * Re-creates a synthetic password from the escrow secret (escrowSplit0, returned from |
| * {@link SyntheticPassword#getEscrowSecret}). Escrow data needs to be loaded |
| * by {@link #setEscrowData} before calling this. |
| */ |
| public void recreateFromEscrow(byte[] escrowSplit0) { |
| Objects.requireNonNull(mEscrowSplit1); |
| Objects.requireNonNull(mEncryptedEscrowSplit0); |
| recreate(escrowSplit0, mEscrowSplit1); |
| } |
| |
| /** |
| * Re-creates a synthetic password from its raw bytes. |
| */ |
| public void recreateDirectly(byte[] syntheticPassword) { |
| this.mSyntheticPassword = Arrays.copyOf(syntheticPassword, syntheticPassword.length); |
| } |
| |
| /** |
| * Generates a new random synthetic password with escrow data. |
| */ |
| static SyntheticPassword create() { |
| SyntheticPassword result = new SyntheticPassword(SYNTHETIC_PASSWORD_VERSION_V3); |
| byte[] escrowSplit0 = |
| SecureRandomUtils.randomBytes(SYNTHETIC_PASSWORD_SECURITY_STRENGTH); |
| byte[] escrowSplit1 = |
| SecureRandomUtils.randomBytes(SYNTHETIC_PASSWORD_SECURITY_STRENGTH); |
| result.recreate(escrowSplit0, escrowSplit1); |
| byte[] encrypteEscrowSplit0 = SyntheticPasswordCrypto.encrypt(result.mSyntheticPassword, |
| PERSONALIZATION_E0, escrowSplit0); |
| result.setEscrowData(encrypteEscrowSplit0, escrowSplit1); |
| return result; |
| } |
| |
| /** |
| * Re-creates synthetic password from both escrow splits. See javadoc for |
| * SyntheticPassword.mSyntheticPassword for details on what each block means. |
| */ |
| private void recreate(byte[] escrowSplit0, byte[] escrowSplit1) { |
| mSyntheticPassword = bytesToHex(SyntheticPasswordCrypto.personalizedHash( |
| PERSONALIZATION_SP_SPLIT, escrowSplit0, escrowSplit1)); |
| } |
| |
| /** |
| * Returns the escrow secret that can be used later to reconstruct this synthetic password |
| * from {@link #recreateFromEscrow(byte[])}. Only possible if escrow is not disabled |
| * (encryptedEscrowSplit0 known). |
| */ |
| public byte[] getEscrowSecret() { |
| if (mEncryptedEscrowSplit0 == null) { |
| return null; |
| } |
| return SyntheticPasswordCrypto.decrypt(mSyntheticPassword, PERSONALIZATION_E0, |
| mEncryptedEscrowSplit0); |
| } |
| |
| /** |
| * Returns the raw synthetic password, for later use with {@link #recreateDirectly(byte[])}. |
| */ |
| public byte[] getSyntheticPassword() { |
| return mSyntheticPassword; |
| } |
| |
| /** |
| * Returns the version number of this synthetic password. This version number determines |
| * the algorithm used to derive subkeys. |
| */ |
| public byte getVersion() { |
| return mVersion; |
| } |
| } |
| |
| static class PasswordData { |
| byte scryptLogN; |
| byte scryptLogR; |
| byte scryptLogP; |
| public int credentialType; |
| byte[] salt; |
| // When Weaver is unavailable, this is the Gatekeeper password handle that resulted from |
| // enrolling the stretched LSKF. |
| public byte[] passwordHandle; |
| /** |
| * Pin length field, only stored in version 2 of the password data and when auto confirm |
| * flag is enabled, otherwise this field contains PIN_LENGTH_UNAVAILABLE |
| */ |
| public int pinLength; |
| |
| public static PasswordData create(int credentialType, int pinLength) { |
| PasswordData result = new PasswordData(); |
| result.scryptLogN = PASSWORD_SCRYPT_LOG_N; |
| result.scryptLogR = PASSWORD_SCRYPT_LOG_R; |
| result.scryptLogP = PASSWORD_SCRYPT_LOG_P; |
| result.credentialType = credentialType; |
| result.pinLength = pinLength; |
| result.salt = SecureRandomUtils.randomBytes(PASSWORD_SALT_LENGTH); |
| return result; |
| } |
| |
| /** |
| * Returns true if the given serialized PasswordData begins with the value 2 as a short. |
| * This detects the "bad" (non-forwards-compatible) PasswordData format that was temporarily |
| * used during development of Android 14. For more details, see fromBytes() below. |
| */ |
| public static boolean isBadFormatFromAndroid14Beta(byte[] data) { |
| return data != null && data.length >= 2 && data[0] == 0 && data[1] == 2; |
| } |
| |
| public static PasswordData fromBytes(byte[] data) { |
| PasswordData result = new PasswordData(); |
| ByteBuffer buffer = ByteBuffer.allocate(data.length); |
| buffer.put(data, 0, data.length); |
| buffer.flip(); |
| |
| /* |
| * The serialized PasswordData is supposed to begin with credentialType as an int. |
| * However, all credentialType values fit in a short and the byte order is big endian, |
| * so the first two bytes don't convey any non-redundant information. For this reason, |
| * temporarily during development of Android 14, the first two bytes were "stolen" from |
| * credentialType to use for a data format version number. |
| * |
| * However, this change was reverted as it was a non-forwards-compatible change. (See |
| * toBytes() for why this data format needs to be forwards-compatible.) Therefore, |
| * recover from this misstep by ignoring the first two bytes. |
| */ |
| result.credentialType = (short) buffer.getInt(); |
| result.scryptLogN = buffer.get(); |
| result.scryptLogR = buffer.get(); |
| result.scryptLogP = buffer.get(); |
| int saltLen = buffer.getInt(); |
| result.salt = new byte[saltLen]; |
| buffer.get(result.salt); |
| int handleLen = buffer.getInt(); |
| if (handleLen > 0) { |
| result.passwordHandle = new byte[handleLen]; |
| buffer.get(result.passwordHandle); |
| } else { |
| result.passwordHandle = null; |
| } |
| if (buffer.remaining() >= Integer.BYTES) { |
| result.pinLength = buffer.getInt(); |
| } else { |
| result.pinLength = PIN_LENGTH_UNAVAILABLE; |
| } |
| return result; |
| } |
| |
| /** |
| * Serializes this PasswordData into a byte array. |
| * <p> |
| * Careful: all changes to the format of the serialized PasswordData must be forwards |
| * compatible. I.e., older versions of Android must still accept the latest PasswordData. |
| * This is because a serialized PasswordData is stored in the Factory Reset Protection (FRP) |
| * persistent data block. It's possible that a device has FRP set up on a newer version of |
| * Android, is factory reset, and then is set up with an older version of Android. |
| */ |
| public byte[] toBytes() { |
| |
| ByteBuffer buffer = ByteBuffer.allocate(Integer.BYTES + 3 * Byte.BYTES |
| + Integer.BYTES + salt.length + Integer.BYTES + |
| (passwordHandle != null ? passwordHandle.length : 0) + Integer.BYTES); |
| // credentialType must fit in a short. For an explanation, see fromBytes(). |
| if (credentialType < Short.MIN_VALUE || credentialType > Short.MAX_VALUE) { |
| throw new IllegalArgumentException("Unknown credential type: " + credentialType); |
| } |
| buffer.putInt(credentialType); |
| buffer.put(scryptLogN); |
| buffer.put(scryptLogR); |
| buffer.put(scryptLogP); |
| buffer.putInt(salt.length); |
| buffer.put(salt); |
| if (passwordHandle != null && passwordHandle.length > 0) { |
| buffer.putInt(passwordHandle.length); |
| buffer.put(passwordHandle); |
| } else { |
| buffer.putInt(0); |
| } |
| buffer.putInt(pinLength); |
| return buffer.array(); |
| } |
| } |
| |
| private static class SyntheticPasswordBlob { |
| byte mVersion; |
| byte mProtectorType; |
| byte[] mContent; |
| |
| public static SyntheticPasswordBlob create(byte version, byte protectorType, |
| byte[] content) { |
| SyntheticPasswordBlob result = new SyntheticPasswordBlob(); |
| result.mVersion = version; |
| result.mProtectorType = protectorType; |
| result.mContent = content; |
| return result; |
| } |
| |
| public static SyntheticPasswordBlob fromBytes(byte[] data) { |
| SyntheticPasswordBlob result = new SyntheticPasswordBlob(); |
| result.mVersion = data[0]; |
| result.mProtectorType = data[1]; |
| result.mContent = Arrays.copyOfRange(data, 2, data.length); |
| return result; |
| } |
| |
| public byte[] toByte() { |
| byte[] blob = new byte[mContent.length + 1 + 1]; |
| blob[0] = mVersion; |
| blob[1] = mProtectorType; |
| System.arraycopy(mContent, 0, blob, 2, mContent.length); |
| return blob; |
| } |
| } |
| |
| @Retention(RetentionPolicy.SOURCE) |
| @IntDef({TOKEN_TYPE_STRONG, TOKEN_TYPE_WEAK}) |
| @interface TokenType {} |
| static final int TOKEN_TYPE_STRONG = 0; |
| static final int TOKEN_TYPE_WEAK = 1; |
| |
| private static class TokenData { |
| byte[] secdiscardableOnDisk; |
| byte[] weaverSecret; |
| byte[] aggregatedSecret; |
| @TokenType int mType; |
| EscrowTokenStateChangeCallback mCallback; |
| } |
| |
| private final Context mContext; |
| private LockSettingsStorage mStorage; |
| private IWeaver mWeaver; |
| private WeaverConfig mWeaverConfig; |
| private PasswordSlotManager mPasswordSlotManager; |
| |
| private final UserManager mUserManager; |
| |
| private final RemoteCallbackList<IWeakEscrowTokenRemovedListener> mListeners = |
| new RemoteCallbackList<>(); |
| |
| public SyntheticPasswordManager(Context context, LockSettingsStorage storage, |
| UserManager userManager, PasswordSlotManager passwordSlotManager) { |
| mContext = context; |
| mStorage = storage; |
| mUserManager = userManager; |
| mPasswordSlotManager = passwordSlotManager; |
| } |
| |
| private boolean isDeviceProvisioned() { |
| return Settings.Global.getInt(mContext.getContentResolver(), |
| Settings.Global.DEVICE_PROVISIONED, 0) != 0; |
| } |
| |
| @VisibleForTesting |
| protected android.hardware.weaver.V1_0.IWeaver getWeaverHidlService() throws RemoteException { |
| try { |
| return android.hardware.weaver.V1_0.IWeaver.getService(/* retry */ true); |
| } catch (NoSuchElementException e) { |
| return null; |
| } |
| } |
| |
| private IWeaver getWeaverService() { |
| // Try to get the AIDL service first |
| try { |
| IWeaver aidlWeaver = IWeaver.Stub.asInterface( |
| ServiceManager.waitForDeclaredService(IWeaver.DESCRIPTOR + "/default")); |
| if (aidlWeaver != null) { |
| Slog.i(TAG, "Using AIDL weaver service"); |
| return aidlWeaver; |
| } |
| } catch (SecurityException e) { |
| Slog.w(TAG, "Does not have permissions to get AIDL weaver service"); |
| } |
| |
| // If the AIDL service can't be found, look for the HIDL service |
| try { |
| android.hardware.weaver.V1_0.IWeaver hidlWeaver = getWeaverHidlService(); |
| if (hidlWeaver != null) { |
| Slog.i(TAG, "Using HIDL weaver service"); |
| return new WeaverHidlAdapter(hidlWeaver); |
| } |
| } catch (RemoteException e) { |
| Slog.w(TAG, "Failed to get HIDL weaver service.", e); |
| } |
| Slog.w(TAG, "Device does not support weaver"); |
| return null; |
| } |
| |
| @VisibleForTesting |
| public boolean isAutoPinConfirmationFeatureAvailable() { |
| return LockPatternUtils.isAutoPinConfirmFeatureAvailable(); |
| } |
| |
| private synchronized boolean isWeaverAvailable() { |
| if (mWeaver != null) { |
| return true; |
| } |
| |
| // Re-initialize weaver in case there was a transient error preventing access to it. |
| IWeaver weaver = getWeaverService(); |
| if (weaver == null) { |
| return false; |
| } |
| |
| final WeaverConfig weaverConfig; |
| try { |
| weaverConfig = weaver.getConfig(); |
| } catch (RemoteException | ServiceSpecificException e) { |
| Slog.e(TAG, "Failed to get weaver config", e); |
| return false; |
| } |
| if (weaverConfig == null || weaverConfig.slots <= 0) { |
| Slog.e(TAG, "Invalid weaver config"); |
| return false; |
| } |
| |
| mWeaver = weaver; |
| mWeaverConfig = weaverConfig; |
| mPasswordSlotManager.refreshActiveSlots(getUsedWeaverSlots()); |
| Slog.i(TAG, "Weaver service initialized"); |
| |
| return true; |
| } |
| |
| /** |
| * Enroll the given key value pair into the specified weaver slot. if the given key is null, |
| * a default all-zero key is used. If the value is not specified, a fresh random secret is |
| * generated as the value. |
| * |
| * @return the value stored in the weaver slot, or null if the operation fails |
| */ |
| private byte[] weaverEnroll(int slot, byte[] key, @Nullable byte[] value) { |
| if (slot == INVALID_WEAVER_SLOT || slot >= mWeaverConfig.slots) { |
| throw new IllegalArgumentException("Invalid slot for weaver"); |
| } |
| if (key == null) { |
| key = new byte[mWeaverConfig.keySize]; |
| } else if (key.length != mWeaverConfig.keySize) { |
| throw new IllegalArgumentException("Invalid key size for weaver"); |
| } |
| if (value == null) { |
| value = SecureRandomUtils.randomBytes(mWeaverConfig.valueSize); |
| } |
| try { |
| mWeaver.write(slot, key, value); |
| } catch (RemoteException e) { |
| Slog.e(TAG, "weaver write binder call failed, slot: " + slot, e); |
| return null; |
| } catch (ServiceSpecificException e) { |
| Slog.e(TAG, "weaver write failed, slot: " + slot, e); |
| return null; |
| } |
| return value; |
| } |
| |
| /** |
| * Create a VerifyCredentialResponse from a timeout base on the WeaverReadResponse. |
| * This checks the received timeout(long) to make sure it sure it fits in an int before |
| * using it. If it doesn't fit, we use Integer.MAX_VALUE. |
| */ |
| private static VerifyCredentialResponse responseFromTimeout(WeaverReadResponse response) { |
| int timeout = |
| response.timeout > Integer.MAX_VALUE || response.timeout < 0 |
| ? Integer.MAX_VALUE |
| : (int) response.timeout; |
| return VerifyCredentialResponse.fromTimeout(timeout); |
| } |
| |
| /** |
| * Verify the supplied key against a weaver slot, returning a response indicating whether |
| * the verification is successful, throttled or failed. If successful, the bound secret |
| * is also returned. |
| */ |
| private VerifyCredentialResponse weaverVerify(int slot, byte[] key) { |
| if (slot == INVALID_WEAVER_SLOT || slot >= mWeaverConfig.slots) { |
| throw new IllegalArgumentException("Invalid slot for weaver"); |
| } |
| if (key == null) { |
| key = new byte[mWeaverConfig.keySize]; |
| } else if (key.length != mWeaverConfig.keySize) { |
| throw new IllegalArgumentException("Invalid key size for weaver"); |
| } |
| final WeaverReadResponse readResponse; |
| try { |
| readResponse = mWeaver.read(slot, key); |
| } catch (RemoteException e) { |
| Slog.e(TAG, "weaver read failed, slot: " + slot, e); |
| return VerifyCredentialResponse.ERROR; |
| } |
| |
| switch (readResponse.status) { |
| case WeaverReadStatus.OK: |
| return new VerifyCredentialResponse.Builder() |
| .setGatekeeperHAT(readResponse.value) |
| .build(); |
| case WeaverReadStatus.THROTTLE: |
| Slog.e(TAG, "weaver read failed (THROTTLE), slot: " + slot); |
| return responseFromTimeout(readResponse); |
| case WeaverReadStatus.INCORRECT_KEY: |
| if (readResponse.timeout == 0) { |
| Slog.e(TAG, "weaver read failed (INCORRECT_KEY), slot: " + slot); |
| return VerifyCredentialResponse.ERROR; |
| } else { |
| Slog.e(TAG, "weaver read failed (INCORRECT_KEY/THROTTLE), slot: " + slot); |
| return responseFromTimeout(readResponse); |
| } |
| case WeaverReadStatus.FAILED: |
| Slog.e(TAG, "weaver read failed (FAILED), slot: " + slot); |
| return VerifyCredentialResponse.ERROR; |
| default: |
| Slog.e(TAG, |
| "weaver read unknown status " + readResponse.status |
| + ", slot: " + slot); |
| return VerifyCredentialResponse.ERROR; |
| } |
| } |
| |
| public void removeUser(IGateKeeperService gatekeeper, int userId) { |
| for (long protectorId : mStorage.listSyntheticPasswordProtectorsForUser(SP_BLOB_NAME, |
| userId)) { |
| destroyWeaverSlot(protectorId, userId); |
| destroyProtectorKey(getProtectorKeyAlias(protectorId)); |
| } |
| // Remove potential persistent state (in RPMB), to prevent them from accumulating and |
| // causing problems. |
| try { |
| gatekeeper.clearSecureUserId(fakeUserId(userId)); |
| } catch (RemoteException ignore) { |
| Slog.w(TAG, "Failed to clear SID from gatekeeper"); |
| } |
| } |
| |
| int getPinLength(long protectorId, int userId) { |
| byte[] passwordData = loadState(PASSWORD_DATA_NAME, protectorId, userId); |
| if (passwordData == null) { |
| return LockPatternUtils.PIN_LENGTH_UNAVAILABLE; |
| } |
| return PasswordData.fromBytes(passwordData).pinLength; |
| } |
| |
| int getCredentialType(long protectorId, int userId) { |
| byte[] passwordData = loadState(PASSWORD_DATA_NAME, protectorId, userId); |
| if (passwordData == null) { |
| return LockPatternUtils.CREDENTIAL_TYPE_NONE; |
| } |
| return PasswordData.fromBytes(passwordData).credentialType; |
| } |
| |
| static int getFrpCredentialType(byte[] payload) { |
| if (payload == null) { |
| return LockPatternUtils.CREDENTIAL_TYPE_NONE; |
| } |
| return PasswordData.fromBytes(payload).credentialType; |
| } |
| |
| /** |
| * Creates a new synthetic password (SP) for the given user. |
| * <p> |
| * Any existing SID for the user is cleared. |
| * <p> |
| * Also saves the escrow information necessary to re-generate the synthetic password under |
| * an escrow scheme. This information can be removed with {@link #destroyEscrowData} if |
| * password escrow should be disabled completely on the given user. |
| * <p> |
| * {@link syncState()} is not called yet; the caller should create a protector afterwards, which |
| * handles this. This makes it so that all the user's initial SP state files, including the |
| * initial LSKF-based protector, are efficiently created with only a single {@link syncState()}. |
| */ |
| SyntheticPassword newSyntheticPassword(int userId) { |
| clearSidForUser(userId); |
| SyntheticPassword result = SyntheticPassword.create(); |
| saveEscrowData(result, userId); |
| return result; |
| } |
| |
| /** |
| * Enroll a new password handle and SID for the given synthetic password and persist it on disk. |
| * Used when the LSKF is changed from empty to nonempty. |
| */ |
| public void newSidForUser(IGateKeeperService gatekeeper, SyntheticPassword sp, int userId) { |
| GateKeeperResponse response; |
| try { |
| response = gatekeeper.enroll(userId, null, null, sp.deriveGkPassword()); |
| } catch (RemoteException e) { |
| throw new IllegalStateException("Failed to create new SID for user", e); |
| } |
| if (response.getResponseCode() != GateKeeperResponse.RESPONSE_OK) { |
| throw new IllegalStateException("Fail to create new SID for user " + userId |
| + " response: " + response.getResponseCode()); |
| } |
| saveSyntheticPasswordHandle(response.getPayload(), userId); |
| } |
| |
| // Nuke the SP handle (and as a result, its SID) for the given user. |
| public void clearSidForUser(int userId) { |
| destroyState(SP_HANDLE_NAME, NULL_PROTECTOR_ID, userId); |
| } |
| |
| public boolean hasSidForUser(int userId) { |
| return hasState(SP_HANDLE_NAME, NULL_PROTECTOR_ID, userId); |
| } |
| |
| // If this returns null, it means there is no SID associated with the user. This happens if the |
| // user has an empty LSKF, but does have an SP. |
| private byte[] loadSyntheticPasswordHandle(int userId) { |
| return loadState(SP_HANDLE_NAME, NULL_PROTECTOR_ID, userId); |
| } |
| |
| private void saveSyntheticPasswordHandle(byte[] spHandle, int userId) { |
| saveState(SP_HANDLE_NAME, spHandle, NULL_PROTECTOR_ID, userId); |
| syncState(userId); |
| } |
| |
| private boolean loadEscrowData(SyntheticPassword sp, int userId) { |
| byte[] e0 = loadState(SP_E0_NAME, NULL_PROTECTOR_ID, userId); |
| byte[] p1 = loadState(SP_P1_NAME, NULL_PROTECTOR_ID, userId); |
| sp.setEscrowData(e0, p1); |
| return e0 != null && p1 != null; |
| } |
| |
| /** |
| * Saves the escrow data for the synthetic password. The caller is responsible for calling |
| * {@link syncState()} afterwards, once the user's other initial synthetic password state files |
| * have been created. |
| */ |
| private void saveEscrowData(SyntheticPassword sp, int userId) { |
| saveState(SP_E0_NAME, sp.mEncryptedEscrowSplit0, NULL_PROTECTOR_ID, userId); |
| saveState(SP_P1_NAME, sp.mEscrowSplit1, NULL_PROTECTOR_ID, userId); |
| } |
| |
| public boolean hasEscrowData(int userId) { |
| return hasState(SP_E0_NAME, NULL_PROTECTOR_ID, userId) |
| && hasState(SP_P1_NAME, NULL_PROTECTOR_ID, userId); |
| } |
| |
| public boolean hasAnyEscrowData(int userId) { |
| return hasState(SP_E0_NAME, NULL_PROTECTOR_ID, userId) |
| || hasState(SP_P1_NAME, NULL_PROTECTOR_ID, userId); |
| } |
| |
| public void destroyEscrowData(int userId) { |
| destroyState(SP_E0_NAME, NULL_PROTECTOR_ID, userId); |
| destroyState(SP_P1_NAME, NULL_PROTECTOR_ID, userId); |
| } |
| |
| private int loadWeaverSlot(long protectorId, int userId) { |
| final int LENGTH = Byte.BYTES + Integer.BYTES; |
| byte[] data = loadState(WEAVER_SLOT_NAME, protectorId, userId); |
| if (data == null || data.length != LENGTH) { |
| return INVALID_WEAVER_SLOT; |
| } |
| ByteBuffer buffer = ByteBuffer.allocate(LENGTH); |
| buffer.put(data, 0, data.length); |
| buffer.flip(); |
| if (buffer.get() != WEAVER_VERSION) { |
| Slog.e(TAG, "Invalid weaver slot version for protector " + protectorId); |
| return INVALID_WEAVER_SLOT; |
| } |
| return buffer.getInt(); |
| } |
| |
| /** |
| * Creates a file that stores the Weaver slot the protector is using. The caller is responsible |
| * for calling {@link syncState()} afterwards, once all the protector's files have been created. |
| */ |
| private void saveWeaverSlot(int slot, long protectorId, int userId) { |
| ByteBuffer buffer = ByteBuffer.allocate(Byte.BYTES + Integer.BYTES); |
| buffer.put(WEAVER_VERSION); |
| buffer.putInt(slot); |
| saveState(WEAVER_SLOT_NAME, buffer.array(), protectorId, userId); |
| } |
| |
| private void destroyWeaverSlot(long protectorId, int userId) { |
| int slot = loadWeaverSlot(protectorId, userId); |
| destroyState(WEAVER_SLOT_NAME, protectorId, userId); |
| if (slot != INVALID_WEAVER_SLOT) { |
| if (!isWeaverAvailable()) { |
| Slog.e(TAG, "Cannot erase Weaver slot because Weaver is unavailable"); |
| return; |
| } |
| Set<Integer> usedSlots = getUsedWeaverSlots(); |
| if (!usedSlots.contains(slot)) { |
| Slogf.i(TAG, "Erasing Weaver slot %d", slot); |
| weaverEnroll(slot, null, null); |
| mPasswordSlotManager.markSlotDeleted(slot); |
| } else { |
| Slogf.i(TAG, "Weaver slot %d was already reused; not erasing it", slot); |
| } |
| } |
| } |
| |
| /** |
| * Return the set of weaver slots that are currently in use by all users on the device. |
| * <p> |
| * <em>Note:</em> Users who are in the process of being deleted are not tracked here |
| * (due to them being marked as partial in UserManager so not visible from |
| * {@link UserManager#getUsers}). As a result their weaver slots will not be considered |
| * taken and can be reused by new users. Care should be taken when cleaning up the |
| * deleted user in {@link #removeUser}, to prevent a reused slot from being erased |
| * unintentionally. |
| */ |
| private Set<Integer> getUsedWeaverSlots() { |
| Map<Integer, List<Long>> protectorIds = |
| mStorage.listSyntheticPasswordProtectorsForAllUsers(WEAVER_SLOT_NAME); |
| HashSet<Integer> slots = new HashSet<>(); |
| for (Map.Entry<Integer, List<Long>> entry : protectorIds.entrySet()) { |
| for (Long protectorId : entry.getValue()) { |
| int slot = loadWeaverSlot(protectorId, entry.getKey()); |
| slots.add(slot); |
| } |
| } |
| return slots; |
| } |
| |
| private int getNextAvailableWeaverSlot() { |
| Set<Integer> usedSlots = getUsedWeaverSlots(); |
| usedSlots.addAll(mPasswordSlotManager.getUsedSlots()); |
| // If the device is not yet provisioned, then the Weaver slot used by the FRP credential may |
| // be still needed and must not be reused yet. (This *should* instead check "has FRP been |
| // resolved yet?", which would allow reusing the slot a bit earlier. However, the |
| // SECURE_FRP_MODE setting gets set to 1 too late for it to be used here.) |
| if (!isDeviceProvisioned()) { |
| PersistentData persistentData = mStorage.readPersistentDataBlock(); |
| if (persistentData != null && persistentData.type == PersistentData.TYPE_SP_WEAVER) { |
| int slot = persistentData.userId; // Note: field name is misleading |
| usedSlots.add(slot); |
| } |
| } |
| for (int i = 0; i < mWeaverConfig.slots; i++) { |
| if (!usedSlots.contains(i)) { |
| return i; |
| } |
| } |
| throw new IllegalStateException("Run out of weaver slots."); |
| } |
| |
| /** |
| * Creates a protector that protects the user's SP with the given LSKF (which may be empty). |
| * |
| * This method only creates a new protector that isn't referenced by anything; it doesn't handle |
| * any higher-level tasks involved in changing the LSKF. |
| * |
| * @return the ID of the new protector |
| * @throws IllegalStateException on failure |
| */ |
| public long createLskfBasedProtector(IGateKeeperService gatekeeper, |
| LockscreenCredential credential, SyntheticPassword sp, int userId) { |
| long protectorId = generateProtectorId(); |
| int pinLength = PIN_LENGTH_UNAVAILABLE; |
| if (isAutoPinConfirmationFeatureAvailable()) { |
| pinLength = derivePinLength(credential.size(), credential.isPin(), userId); |
| } |
| // There's no need to store password data about an empty LSKF. |
| PasswordData pwd = credential.isNone() ? null : |
| PasswordData.create(credential.getType(), pinLength); |
| byte[] stretchedLskf = stretchLskf(credential, pwd); |
| long sid = GateKeeper.INVALID_SECURE_USER_ID; |
| final byte[] protectorSecret; |
| |
| Slogf.i(TAG, "Creating LSKF-based protector %016x for user %d", protectorId, userId); |
| |
| if (isWeaverAvailable()) { |
| // Weaver is available, so make the protector use it to verify the LSKF. Do this even |
| // if the LSKF is empty, as that gives us support for securely deleting the protector. |
| int weaverSlot = getNextAvailableWeaverSlot(); |
| Slogf.i(TAG, "Enrolling LSKF for user %d into Weaver slot %d", userId, weaverSlot); |
| byte[] weaverSecret = weaverEnroll(weaverSlot, stretchedLskfToWeaverKey(stretchedLskf), |
| null); |
| if (weaverSecret == null) { |
| throw new IllegalStateException( |
| "Fail to enroll user password under weaver " + userId); |
| } |
| saveWeaverSlot(weaverSlot, protectorId, userId); |
| mPasswordSlotManager.markSlotInUse(weaverSlot); |
| // No need to pass in quality since the credential type already encodes sufficient info |
| synchronizeWeaverFrpPassword(pwd, 0, userId, weaverSlot); |
| |
| protectorSecret = transformUnderWeaverSecret(stretchedLskf, weaverSecret); |
| } else { |
| // Weaver is unavailable, so make the protector use Gatekeeper (GK) to verify the LSKF. |
| // |
| // However, skip GK when the LSKF is empty. There are two reasons for this, one |
| // performance and one correctness. The performance reason is that GK wouldn't give any |
| // benefit with an empty LSKF anyway, since GK isn't expected to provide secure |
| // deletion. The correctness reason is that it is unsafe to enroll a password in the |
| // 'fakeUserId' GK range on an FRP-protected device that is in the setup wizard with FRP |
| // not passed yet, as that may overwrite the enrollment used by the FRP credential. |
| if (!credential.isNone()) { |
| // In case GK enrollment leaves persistent state around (in RPMB), this will nuke |
| // them to prevent them from accumulating and causing problems. |
| try { |
| gatekeeper.clearSecureUserId(fakeUserId(userId)); |
| } catch (RemoteException ignore) { |
| Slog.w(TAG, "Failed to clear SID from gatekeeper"); |
| } |
| Slogf.i(TAG, "Enrolling LSKF for user %d into Gatekeeper", userId); |
| GateKeeperResponse response; |
| try { |
| response = gatekeeper.enroll(fakeUserId(userId), null, null, |
| stretchedLskfToGkPassword(stretchedLskf)); |
| } catch (RemoteException e) { |
| throw new IllegalStateException("Failed to enroll LSKF for new SP protector" |
| + " for user " + userId, e); |
| } |
| if (response.getResponseCode() != GateKeeperResponse.RESPONSE_OK) { |
| throw new IllegalStateException("Failed to enroll LSKF for new SP protector" |
| + " for user " + userId); |
| } |
| pwd.passwordHandle = response.getPayload(); |
| sid = sidFromPasswordHandle(pwd.passwordHandle); |
| } |
| protectorSecret = transformUnderSecdiscardable(stretchedLskf, |
| createSecdiscardable(protectorId, userId)); |
| // No need to pass in quality since the credential type already encodes sufficient info |
| synchronizeGatekeeperFrpPassword(pwd, 0, userId); |
| } |
| if (!credential.isNone()) { |
| saveState(PASSWORD_DATA_NAME, pwd.toBytes(), protectorId, userId); |
| savePasswordMetrics(credential, sp, protectorId, userId); |
| } |
| createSyntheticPasswordBlob(protectorId, PROTECTOR_TYPE_LSKF_BASED, sp, protectorSecret, |
| sid, userId); |
| syncState(userId); // ensure the new files are really saved to disk |
| return protectorId; |
| } |
| |
| private int derivePinLength(int sizeOfCredential, boolean isPinCredential, int userId) { |
| if (!isPinCredential |
| || !mStorage.isAutoPinConfirmSettingEnabled(userId) |
| || sizeOfCredential < LockPatternUtils.MIN_AUTO_PIN_REQUIREMENT_LENGTH) { |
| return PIN_LENGTH_UNAVAILABLE; |
| } |
| return sizeOfCredential; |
| } |
| |
| public VerifyCredentialResponse verifyFrpCredential(IGateKeeperService gatekeeper, |
| LockscreenCredential userCredential, |
| ICheckCredentialProgressCallback progressCallback) { |
| PersistentData persistentData = mStorage.readPersistentDataBlock(); |
| if (persistentData.type == PersistentData.TYPE_SP_GATEKEEPER) { |
| PasswordData pwd = PasswordData.fromBytes(persistentData.payload); |
| byte[] stretchedLskf = stretchLskf(userCredential, pwd); |
| |
| GateKeeperResponse response; |
| try { |
| response = gatekeeper.verifyChallenge(fakeUserId(persistentData.userId), |
| 0 /* challenge */, pwd.passwordHandle, |
| stretchedLskfToGkPassword(stretchedLskf)); |
| } catch (RemoteException e) { |
| Slog.e(TAG, "FRP verifyChallenge failed", e); |
| return VerifyCredentialResponse.ERROR; |
| } |
| return VerifyCredentialResponse.fromGateKeeperResponse(response); |
| } else if (persistentData.type == PersistentData.TYPE_SP_WEAVER) { |
| if (!isWeaverAvailable()) { |
| Slog.e(TAG, "No weaver service to verify SP-based FRP credential"); |
| return VerifyCredentialResponse.ERROR; |
| } |
| PasswordData pwd = PasswordData.fromBytes(persistentData.payload); |
| byte[] stretchedLskf = stretchLskf(userCredential, pwd); |
| int weaverSlot = persistentData.userId; |
| |
| return weaverVerify(weaverSlot, stretchedLskfToWeaverKey(stretchedLskf)).stripPayload(); |
| } else { |
| Slog.e(TAG, "persistentData.type must be TYPE_SP_GATEKEEPER or TYPE_SP_WEAVER, but is " |
| + persistentData.type); |
| return VerifyCredentialResponse.ERROR; |
| } |
| } |
| |
| |
| public void migrateFrpPasswordLocked(long protectorId, UserInfo userInfo, |
| int requestedQuality) { |
| if (mStorage.getPersistentDataBlockManager() != null |
| && LockPatternUtils.userOwnsFrpCredential(mContext, userInfo) |
| && getCredentialType(protectorId, userInfo.id) != |
| LockPatternUtils.CREDENTIAL_TYPE_NONE) { |
| Slog.i(TAG, "Migrating FRP credential to persistent data block"); |
| PasswordData pwd = PasswordData.fromBytes(loadState(PASSWORD_DATA_NAME, protectorId, |
| userInfo.id)); |
| int weaverSlot = loadWeaverSlot(protectorId, userInfo.id); |
| if (weaverSlot != INVALID_WEAVER_SLOT) { |
| synchronizeWeaverFrpPassword(pwd, requestedQuality, userInfo.id, weaverSlot); |
| } else { |
| synchronizeGatekeeperFrpPassword(pwd, requestedQuality, userInfo.id); |
| } |
| } |
| } |
| |
| private static boolean isNoneCredential(PasswordData pwd) { |
| return pwd == null || pwd.credentialType == LockPatternUtils.CREDENTIAL_TYPE_NONE; |
| } |
| |
| private boolean shouldSynchronizeFrpCredential(@Nullable PasswordData pwd, int userId) { |
| if (mStorage.getPersistentDataBlockManager() == null) { |
| return false; |
| } |
| UserInfo userInfo = mUserManager.getUserInfo(userId); |
| if (!LockPatternUtils.userOwnsFrpCredential(mContext, userInfo)) { |
| return false; |
| } |
| // When initializing the synthetic password of the user that will own the FRP credential, |
| // the FRP data block must not be cleared if the device isn't provisioned yet, since in this |
| // case the old value of the block may still be needed for the FRP authentication step. The |
| // FRP data block will instead be cleared later, by |
| // LockSettingsService.DeviceProvisionedObserver.clearFrpCredentialIfOwnerNotSecure(). |
| // |
| // Don't check the SECURE_FRP_MODE setting here, as it gets set to 1 too late. |
| // |
| // Don't delay anything for a nonempty credential. A nonempty credential can be set before |
| // the device has been provisioned, but it's guaranteed to be after FRP was resolved. |
| if (isNoneCredential(pwd) && !isDeviceProvisioned()) { |
| Slog.d(TAG, "Not clearing FRP credential yet because device is not yet provisioned"); |
| return false; |
| } |
| return true; |
| } |
| |
| private void synchronizeGatekeeperFrpPassword(@Nullable PasswordData pwd, int requestedQuality, |
| int userId) { |
| if (shouldSynchronizeFrpCredential(pwd, userId)) { |
| Slogf.d(TAG, "Syncing Gatekeeper-based FRP credential tied to user %d", userId); |
| if (!isNoneCredential(pwd)) { |
| mStorage.writePersistentDataBlock(PersistentData.TYPE_SP_GATEKEEPER, userId, |
| requestedQuality, pwd.toBytes()); |
| } else { |
| mStorage.writePersistentDataBlock(PersistentData.TYPE_NONE, userId, 0, null); |
| } |
| } |
| } |
| |
| private void synchronizeWeaverFrpPassword(@Nullable PasswordData pwd, int requestedQuality, |
| int userId, int weaverSlot) { |
| if (shouldSynchronizeFrpCredential(pwd, userId)) { |
| Slogf.d(TAG, "Syncing Weaver-based FRP credential tied to user %d", userId); |
| if (!isNoneCredential(pwd)) { |
| mStorage.writePersistentDataBlock(PersistentData.TYPE_SP_WEAVER, weaverSlot, |
| requestedQuality, pwd.toBytes()); |
| } else { |
| mStorage.writePersistentDataBlock(PersistentData.TYPE_NONE, 0, 0, null); |
| } |
| } |
| } |
| |
| private ArrayMap<Integer, ArrayMap<Long, TokenData>> tokenMap = new ArrayMap<>(); |
| |
| /** |
| * Caches a pending escrow token in memory and pre-allocates an ID for a new SP protector. This |
| * ID also serves as a handle for the pending token. |
| * |
| * This method doesn't persist any data, and it doesn't require access to the SP. |
| * {@link #createTokenBasedProtector} can be called later to actually create the protector. |
| * |
| * @return the token handle |
| */ |
| public long addPendingToken(byte[] token, @TokenType int type, int userId, |
| @Nullable EscrowTokenStateChangeCallback changeCallback) { |
| long tokenHandle = generateProtectorId(); // tokenHandle is reused as protectorId later |
| if (!tokenMap.containsKey(userId)) { |
| tokenMap.put(userId, new ArrayMap<>()); |
| } |
| TokenData tokenData = new TokenData(); |
| tokenData.mType = type; |
| final byte[] secdiscardable = SecureRandomUtils.randomBytes(SECDISCARDABLE_LENGTH); |
| if (isWeaverAvailable()) { |
| tokenData.weaverSecret = SecureRandomUtils.randomBytes(mWeaverConfig.valueSize); |
| tokenData.secdiscardableOnDisk = SyntheticPasswordCrypto.encrypt(tokenData.weaverSecret, |
| PERSONALIZATION_WEAVER_TOKEN, secdiscardable); |
| } else { |
| tokenData.secdiscardableOnDisk = secdiscardable; |
| tokenData.weaverSecret = null; |
| } |
| tokenData.aggregatedSecret = transformUnderSecdiscardable(token, secdiscardable); |
| tokenData.mCallback = changeCallback; |
| |
| tokenMap.get(userId).put(tokenHandle, tokenData); |
| return tokenHandle; |
| } |
| |
| public Set<Long> getPendingTokensForUser(int userId) { |
| if (!tokenMap.containsKey(userId)) { |
| return Collections.emptySet(); |
| } |
| return new ArraySet<>(tokenMap.get(userId).keySet()); |
| } |
| |
| /** Remove the given pending token. */ |
| public boolean removePendingToken(long tokenHandle, int userId) { |
| if (!tokenMap.containsKey(userId)) { |
| return false; |
| } |
| return tokenMap.get(userId).remove(tokenHandle) != null; |
| } |
| |
| public boolean createTokenBasedProtector(long tokenHandle, SyntheticPassword sp, int userId) { |
| if (!tokenMap.containsKey(userId)) { |
| return false; |
| } |
| TokenData tokenData = tokenMap.get(userId).get(tokenHandle); |
| if (tokenData == null) { |
| return false; |
| } |
| if (!loadEscrowData(sp, userId)) { |
| Slog.w(TAG, "User is not escrowable"); |
| return false; |
| } |
| Slogf.i(TAG, "Creating token-based protector %016x for user %d", tokenHandle, userId); |
| if (isWeaverAvailable()) { |
| int slot = getNextAvailableWeaverSlot(); |
| Slogf.i(TAG, "Using Weaver slot %d for new token-based protector", slot); |
| if (weaverEnroll(slot, null, tokenData.weaverSecret) == null) { |
| Slog.e(TAG, "Failed to enroll weaver secret when activating token"); |
| return false; |
| } |
| saveWeaverSlot(slot, tokenHandle, userId); |
| mPasswordSlotManager.markSlotInUse(slot); |
| } |
| saveSecdiscardable(tokenHandle, tokenData.secdiscardableOnDisk, userId); |
| createSyntheticPasswordBlob(tokenHandle, getTokenBasedProtectorType(tokenData.mType), sp, |
| tokenData.aggregatedSecret, 0L, userId); |
| syncState(userId); // ensure the new files are really saved to disk |
| tokenMap.get(userId).remove(tokenHandle); |
| if (tokenData.mCallback != null) { |
| tokenData.mCallback.onEscrowTokenActivated(tokenHandle, userId); |
| } |
| return true; |
| } |
| |
| /** |
| * Creates a synthetic password blob, i.e. the file that stores the encrypted synthetic password |
| * (or encrypted escrow secret) for a protector. The caller is responsible for calling |
| * {@link syncState()} afterwards, once all the protector's files have been created. |
| */ |
| private void createSyntheticPasswordBlob(long protectorId, byte protectorType, |
| SyntheticPassword sp, byte[] protectorSecret, long sid, int userId) { |
| final byte[] spSecret; |
| if (protectorType == PROTECTOR_TYPE_STRONG_TOKEN_BASED |
| || protectorType == PROTECTOR_TYPE_WEAK_TOKEN_BASED) { |
| spSecret = sp.getEscrowSecret(); |
| } else { |
| spSecret = sp.getSyntheticPassword(); |
| } |
| byte[] content = createSpBlob(getProtectorKeyAlias(protectorId), spSecret, protectorSecret, |
| sid); |
| /* |
| * We can upgrade from v1 to v2 because that's just a change in the way that |
| * the SP is stored. However, we can't upgrade to v3 because that is a change |
| * in the way that passwords are derived from the SP. |
| */ |
| byte version = sp.mVersion == SYNTHETIC_PASSWORD_VERSION_V3 |
| ? SYNTHETIC_PASSWORD_VERSION_V3 : SYNTHETIC_PASSWORD_VERSION_V2; |
| |
| SyntheticPasswordBlob blob = SyntheticPasswordBlob.create(version, protectorType, content); |
| saveState(SP_BLOB_NAME, blob.toByte(), protectorId, userId); |
| } |
| |
| /** |
| * Tries to unlock a user's LSKF-based SP protector, given its ID and the claimed LSKF (which |
| * may be empty). On success, returns the user's synthetic password, and also does a Gatekeeper |
| * verification to refresh the SID and HardwareAuthToken maintained by the system. |
| */ |
| public AuthenticationResult unlockLskfBasedProtector(IGateKeeperService gatekeeper, |
| long protectorId, @NonNull LockscreenCredential credential, int userId, |
| ICheckCredentialProgressCallback progressCallback) { |
| AuthenticationResult result = new AuthenticationResult(); |
| |
| if (protectorId == SyntheticPasswordManager.NULL_PROTECTOR_ID) { |
| // This should never happen, due to the migration done in LSS.onThirdPartyAppsStarted(). |
| Slogf.wtf(TAG, "Synthetic password not found for user %d", userId); |
| result.gkResponse = VerifyCredentialResponse.ERROR; |
| return result; |
| } |
| |
| // Load the PasswordData file. If it doesn't exist, then the LSKF is empty (i.e., |
| // CREDENTIAL_TYPE_NONE), and we'll skip the scrypt and Gatekeeper steps. If it exists, |
| // then either the LSKF is nonempty, or it's an old protector that uses scrypt and |
| // Gatekeeper even though the LSKF is empty. |
| byte[] pwdDataBytes = loadState(PASSWORD_DATA_NAME, protectorId, userId); |
| PasswordData pwd = null; |
| int storedType = LockPatternUtils.CREDENTIAL_TYPE_NONE; |
| if (pwdDataBytes != null) { |
| pwd = PasswordData.fromBytes(pwdDataBytes); |
| storedType = pwd.credentialType; |
| } |
| if (!credential.checkAgainstStoredType(storedType)) { |
| Slogf.e(TAG, "Credential type mismatch: stored type is %s but provided type is %s", |
| LockPatternUtils.credentialTypeToString(storedType), |
| LockPatternUtils.credentialTypeToString(credential.getType())); |
| result.gkResponse = VerifyCredentialResponse.ERROR; |
| return result; |
| } |
| |
| byte[] stretchedLskf = stretchLskf(credential, pwd); |
| |
| final byte[] protectorSecret; |
| long sid = GateKeeper.INVALID_SECURE_USER_ID; |
| int weaverSlot = loadWeaverSlot(protectorId, userId); |
| if (weaverSlot != INVALID_WEAVER_SLOT) { |
| // Protector uses Weaver to verify the LSKF |
| if (!isWeaverAvailable()) { |
| Slog.e(TAG, "Protector uses Weaver, but Weaver is unavailable"); |
| result.gkResponse = VerifyCredentialResponse.ERROR; |
| return result; |
| } |
| result.gkResponse = weaverVerify(weaverSlot, stretchedLskfToWeaverKey(stretchedLskf)); |
| if (result.gkResponse.getResponseCode() != VerifyCredentialResponse.RESPONSE_OK) { |
| return result; |
| } |
| protectorSecret = transformUnderWeaverSecret(stretchedLskf, |
| result.gkResponse.getGatekeeperHAT()); |
| } else { |
| // Weaver is unavailable, so the protector uses Gatekeeper to verify the LSKF, unless |
| // the LSKF is empty in which case Gatekeeper might not have been used at all. |
| if (pwd == null || pwd.passwordHandle == null) { |
| if (!credential.isNone()) { |
| Slog.e(TAG, "Missing Gatekeeper password handle for nonempty LSKF"); |
| result.gkResponse = VerifyCredentialResponse.ERROR; |
| return result; |
| } |
| } else { |
| byte[] gkPassword = stretchedLskfToGkPassword(stretchedLskf); |
| GateKeeperResponse response; |
| try { |
| response = gatekeeper.verifyChallenge(fakeUserId(userId), 0L, |
| pwd.passwordHandle, gkPassword); |
| } catch (RemoteException e) { |
| Slog.e(TAG, "gatekeeper verify failed", e); |
| result.gkResponse = VerifyCredentialResponse.ERROR; |
| return result; |
| } |
| int responseCode = response.getResponseCode(); |
| if (responseCode == GateKeeperResponse.RESPONSE_OK) { |
| result.gkResponse = VerifyCredentialResponse.OK; |
| if (response.getShouldReEnroll()) { |
| GateKeeperResponse reenrollResponse; |
| try { |
| reenrollResponse = gatekeeper.enroll(fakeUserId(userId), |
| pwd.passwordHandle, gkPassword, gkPassword); |
| } catch (RemoteException e) { |
| Slog.w(TAG, "Fail to invoke gatekeeper.enroll", e); |
| reenrollResponse = GateKeeperResponse.ERROR; |
| // continue the flow anyway |
| } |
| if (reenrollResponse.getResponseCode() == GateKeeperResponse.RESPONSE_OK) { |
| pwd.passwordHandle = reenrollResponse.getPayload(); |
| // Use the reenrollment opportunity to update credential type |
| // (getting rid of CREDENTIAL_TYPE_PASSWORD_OR_PIN) |
| pwd.credentialType = credential.getType(); |
| saveState(PASSWORD_DATA_NAME, pwd.toBytes(), protectorId, userId); |
| syncState(userId); |
| synchronizeGatekeeperFrpPassword(pwd, 0, userId); |
| } else { |
| Slog.w(TAG, "Fail to re-enroll user password for user " + userId); |
| // continue the flow anyway |
| } |
| } |
| } else if (responseCode == GateKeeperResponse.RESPONSE_RETRY) { |
| result.gkResponse = VerifyCredentialResponse.fromTimeout(response.getTimeout()); |
| return result; |
| } else { |
| result.gkResponse = VerifyCredentialResponse.ERROR; |
| return result; |
| } |
| sid = sidFromPasswordHandle(pwd.passwordHandle); |
| } |
| byte[] secdiscardable = loadSecdiscardable(protectorId, userId); |
| if (secdiscardable == null) { |
| Slog.e(TAG, "secdiscardable file not found"); |
| result.gkResponse = VerifyCredentialResponse.ERROR; |
| return result; |
| } |
| protectorSecret = transformUnderSecdiscardable(stretchedLskf, secdiscardable); |
| } |
| // Supplied credential passes first stage weaver/gatekeeper check so it should be correct. |
| // Notify the callback so the keyguard UI can proceed immediately. |
| if (progressCallback != null) { |
| try { |
| progressCallback.onCredentialVerified(); |
| } catch (RemoteException e) { |
| Slog.w(TAG, "progressCallback throws exception", e); |
| } |
| } |
| result.syntheticPassword = unwrapSyntheticPasswordBlob(protectorId, |
| PROTECTOR_TYPE_LSKF_BASED, protectorSecret, sid, userId); |
| |
| // Perform verifyChallenge to refresh auth tokens for GK if user password exists. |
| result.gkResponse = verifyChallenge(gatekeeper, result.syntheticPassword, 0L, userId); |
| |
| // Upgrade case: store the metrics if the device did not have stored metrics before, should |
| // only happen once on old protectors. |
| if (result.syntheticPassword != null && !credential.isNone() |
| && !hasPasswordMetrics(protectorId, userId)) { |
| savePasswordMetrics(credential, result.syntheticPassword, protectorId, userId); |
| syncState(userId); // Not strictly needed as the upgrade can be re-done, but be safe. |
| } |
| return result; |
| } |
| |
| /** |
| * {@link LockPatternUtils#refreshStoredPinLength(int)} |
| * @param passwordMetrics passwordMetrics object containing the cached pin length |
| * @param userId userId of the user whose pin length we want to store on disk |
| * @param protectorId current LSKF based protectorId |
| * @return true/false depending on whether PIN length has been saved on disk |
| */ |
| public boolean refreshPinLengthOnDisk(PasswordMetrics passwordMetrics, |
| long protectorId, int userId) { |
| if (!isAutoPinConfirmationFeatureAvailable()) { |
| return false; |
| } |
| |
| byte[] pwdDataBytes = loadState(PASSWORD_DATA_NAME, protectorId, userId); |
| if (pwdDataBytes == null) { |
| return false; |
| } |
| |
| PasswordData pwd = PasswordData.fromBytes(pwdDataBytes); |
| int pinLength = derivePinLength(passwordMetrics.length, |
| passwordMetrics.credType == CREDENTIAL_TYPE_PIN, userId); |
| if (pwd.pinLength != pinLength) { |
| pwd.pinLength = pinLength; |
| saveState(PASSWORD_DATA_NAME, pwd.toBytes(), protectorId, userId); |
| syncState(userId); |
| } |
| return true; |
| } |
| |
| /** |
| * Tries to unlock a token-based SP protector (weak or strong), given its ID and the claimed |
| * token. On success, returns the user's synthetic password, and also does a Gatekeeper |
| * verification to refresh the SID and HardwareAuthToken maintained by the system. |
| */ |
| public @NonNull AuthenticationResult unlockTokenBasedProtector( |
| IGateKeeperService gatekeeper, long protectorId, byte[] token, int userId) { |
| SyntheticPasswordBlob blob = SyntheticPasswordBlob.fromBytes(loadState(SP_BLOB_NAME, |
| protectorId, userId)); |
| return unlockTokenBasedProtectorInternal(gatekeeper, protectorId, blob.mProtectorType, |
| token, userId); |
| } |
| |
| /** |
| * Like {@link #unlockTokenBasedProtector}, but throws an exception if the protector is not for |
| * a strong token specifically. |
| */ |
| public @NonNull AuthenticationResult unlockStrongTokenBasedProtector( |
| IGateKeeperService gatekeeper, long protectorId, byte[] token, int userId) { |
| return unlockTokenBasedProtectorInternal(gatekeeper, protectorId, |
| PROTECTOR_TYPE_STRONG_TOKEN_BASED, token, userId); |
| } |
| |
| /** |
| * Like {@link #unlockTokenBasedProtector}, but throws an exception if the protector is not for |
| * a weak token specifically. |
| */ |
| public @NonNull AuthenticationResult unlockWeakTokenBasedProtector( |
| IGateKeeperService gatekeeper, long protectorId, byte[] token, int userId) { |
| return unlockTokenBasedProtectorInternal(gatekeeper, protectorId, |
| PROTECTOR_TYPE_WEAK_TOKEN_BASED, token, userId); |
| } |
| |
| private @NonNull AuthenticationResult unlockTokenBasedProtectorInternal( |
| IGateKeeperService gatekeeper, long protectorId, byte expectedProtectorType, |
| byte[] token, int userId) { |
| AuthenticationResult result = new AuthenticationResult(); |
| byte[] secdiscardable = loadSecdiscardable(protectorId, userId); |
| if (secdiscardable == null) { |
| Slog.e(TAG, "secdiscardable file not found"); |
| result.gkResponse = VerifyCredentialResponse.ERROR; |
| return result; |
| } |
| int slotId = loadWeaverSlot(protectorId, userId); |
| if (slotId != INVALID_WEAVER_SLOT) { |
| if (!isWeaverAvailable()) { |
| Slog.e(TAG, "Protector uses Weaver, but Weaver is unavailable"); |
| result.gkResponse = VerifyCredentialResponse.ERROR; |
| return result; |
| } |
| VerifyCredentialResponse response = weaverVerify(slotId, null); |
| if (response.getResponseCode() != VerifyCredentialResponse.RESPONSE_OK || |
| response.getGatekeeperHAT() == null) { |
| Slog.e(TAG, |
| "Failed to retrieve Weaver secret when unlocking token-based protector"); |
| result.gkResponse = VerifyCredentialResponse.ERROR; |
| return result; |
| } |
| secdiscardable = SyntheticPasswordCrypto.decrypt(response.getGatekeeperHAT(), |
| PERSONALIZATION_WEAVER_TOKEN, secdiscardable); |
| } |
| byte[] protectorSecret = transformUnderSecdiscardable(token, secdiscardable); |
| result.syntheticPassword = unwrapSyntheticPasswordBlob(protectorId, expectedProtectorType, |
| protectorSecret, 0L, userId); |
| if (result.syntheticPassword != null) { |
| result.gkResponse = verifyChallenge(gatekeeper, result.syntheticPassword, 0L, userId); |
| if (result.gkResponse == null) { |
| // The user currently has no password. return OK with null payload so null |
| // is propagated to unlockUser() |
| result.gkResponse = VerifyCredentialResponse.OK; |
| } |
| } else { |
| result.gkResponse = VerifyCredentialResponse.ERROR; |
| } |
| return result; |
| } |
| |
| private SyntheticPassword unwrapSyntheticPasswordBlob(long protectorId, |
| byte expectedProtectorType, byte[] protectorSecret, long sid, int userId) { |
| byte[] data = loadState(SP_BLOB_NAME, protectorId, userId); |
| if (data == null) { |
| return null; |
| } |
| SyntheticPasswordBlob blob = SyntheticPasswordBlob.fromBytes(data); |
| if (blob.mVersion != SYNTHETIC_PASSWORD_VERSION_V3 |
| && blob.mVersion != SYNTHETIC_PASSWORD_VERSION_V2 |
| && blob.mVersion != SYNTHETIC_PASSWORD_VERSION_V1) { |
| throw new IllegalArgumentException("Unknown blob version: " + blob.mVersion); |
| } |
| if (blob.mProtectorType != expectedProtectorType) { |
| throw new IllegalArgumentException("Invalid protector type: " + blob.mProtectorType); |
| } |
| final byte[] spSecret; |
| if (blob.mVersion == SYNTHETIC_PASSWORD_VERSION_V1) { |
| spSecret = SyntheticPasswordCrypto.decryptBlobV1(getProtectorKeyAlias(protectorId), |
| blob.mContent, protectorSecret); |
| } else { |
| spSecret = decryptSpBlob(getProtectorKeyAlias(protectorId), blob.mContent, |
| protectorSecret); |
| } |
| if (spSecret == null) { |
| Slog.e(TAG, "Fail to decrypt SP for user " + userId); |
| return null; |
| } |
| SyntheticPassword result = new SyntheticPassword(blob.mVersion); |
| if (blob.mProtectorType == PROTECTOR_TYPE_STRONG_TOKEN_BASED |
| || blob.mProtectorType == PROTECTOR_TYPE_WEAK_TOKEN_BASED) { |
| if (!loadEscrowData(result, userId)) { |
| Slog.e(TAG, "User is not escrowable: " + userId); |
| return null; |
| } |
| result.recreateFromEscrow(spSecret); |
| } else { |
| result.recreateDirectly(spSecret); |
| } |
| if (blob.mVersion == SYNTHETIC_PASSWORD_VERSION_V1) { |
| Slog.i(TAG, "Upgrading v1 SP blob for user " + userId + ", protectorType = " |
| + blob.mProtectorType); |
| createSyntheticPasswordBlob(protectorId, blob.mProtectorType, result, protectorSecret, |
| sid, userId); |
| syncState(userId); // Not strictly needed as the upgrade can be re-done, but be safe. |
| } |
| return result; |
| } |
| |
| /** |
| * performs GK verifyChallenge and returns auth token, re-enrolling SP password handle |
| * if required. |
| * |
| * Normally performing verifyChallenge with an SP should always return RESPONSE_OK, since user |
| * authentication failures are detected earlier when trying to decrypt the SP. |
| */ |
| public @Nullable VerifyCredentialResponse verifyChallenge(IGateKeeperService gatekeeper, |
| @NonNull SyntheticPassword sp, long challenge, int userId) { |
| return verifyChallengeInternal(gatekeeper, sp.deriveGkPassword(), challenge, userId); |
| } |
| |
| protected @Nullable VerifyCredentialResponse verifyChallengeInternal( |
| IGateKeeperService gatekeeper, @NonNull byte[] gatekeeperPassword, long challenge, |
| int userId) { |
| byte[] spHandle = loadSyntheticPasswordHandle(userId); |
| if (spHandle == null) { |
| // There is no password handle associated with the given user, i.e. the user is not |
| // secured by lockscreen and has no SID, so just return here; |
| return null; |
| } |
| GateKeeperResponse response; |
| try { |
| response = gatekeeper.verifyChallenge(userId, challenge, |
| spHandle, gatekeeperPassword); |
| } catch (RemoteException e) { |
| Slog.e(TAG, "Fail to verify with gatekeeper " + userId, e); |
| return VerifyCredentialResponse.ERROR; |
| } |
| int responseCode = response.getResponseCode(); |
| if (responseCode == GateKeeperResponse.RESPONSE_OK) { |
| VerifyCredentialResponse result = new VerifyCredentialResponse.Builder() |
| .setGatekeeperHAT(response.getPayload()).build(); |
| if (response.getShouldReEnroll()) { |
| try { |
| response = gatekeeper.enroll(userId, spHandle, spHandle, |
| gatekeeperPassword); |
| } catch (RemoteException e) { |
| Slog.e(TAG, "Failed to invoke gatekeeper.enroll", e); |
| response = GateKeeperResponse.ERROR; |
| } |
| if (response.getResponseCode() == GateKeeperResponse.RESPONSE_OK) { |
| spHandle = response.getPayload(); |
| saveSyntheticPasswordHandle(spHandle, userId); |
| // Call self again to re-verify with updated handle |
| return verifyChallengeInternal(gatekeeper, gatekeeperPassword, challenge, |
| userId); |
| } else { |
| // Fall through, return result from the previous verification attempt. |
| Slog.w(TAG, "Fail to re-enroll SP handle for user " + userId); |
| } |
| } |
| return result; |
| } else if (responseCode == GateKeeperResponse.RESPONSE_RETRY) { |
| return VerifyCredentialResponse.fromTimeout(response.getTimeout()); |
| } else { |
| return VerifyCredentialResponse.ERROR; |
| } |
| } |
| |
| public boolean protectorExists(long protectorId, int userId) { |
| return hasState(SP_BLOB_NAME, protectorId, userId); |
| } |
| |
| /** Destroy a token-based SP protector. */ |
| public void destroyTokenBasedProtector(long protectorId, int userId) { |
| Slogf.i(TAG, "Destroying token-based protector %016x for user %d", protectorId, userId); |
| SyntheticPasswordBlob blob = SyntheticPasswordBlob.fromBytes(loadState(SP_BLOB_NAME, |
| protectorId, userId)); |
| destroyProtectorCommon(protectorId, userId); |
| if (blob.mProtectorType == PROTECTOR_TYPE_WEAK_TOKEN_BASED) { |
| notifyWeakEscrowTokenRemovedListeners(protectorId, userId); |
| } |
| } |
| |
| /** Destroy all weak token-based SP protectors for the given user. */ |
| public void destroyAllWeakTokenBasedProtectors(int userId) { |
| List<Long> protectorIds = |
| mStorage.listSyntheticPasswordProtectorsForUser(SECDISCARDABLE_NAME, userId); |
| for (long protectorId : protectorIds) { |
| SyntheticPasswordBlob blob = SyntheticPasswordBlob.fromBytes(loadState(SP_BLOB_NAME, |
| protectorId, userId)); |
| if (blob.mProtectorType == PROTECTOR_TYPE_WEAK_TOKEN_BASED) { |
| destroyTokenBasedProtector(protectorId, userId); |
| } |
| } |
| } |
| |
| /** |
| * Destroy an LSKF-based SP protector. This is used when the user's LSKF is changed. |
| */ |
| public void destroyLskfBasedProtector(long protectorId, int userId) { |
| Slogf.i(TAG, "Destroying LSKF-based protector %016x for user %d", protectorId, userId); |
| destroyProtectorCommon(protectorId, userId); |
| destroyState(PASSWORD_DATA_NAME, protectorId, userId); |
| destroyState(PASSWORD_METRICS_NAME, protectorId, userId); |
| } |
| |
| private void destroyProtectorCommon(long protectorId, int userId) { |
| destroyState(SP_BLOB_NAME, protectorId, userId); |
| destroyProtectorKey(getProtectorKeyAlias(protectorId)); |
| destroyState(SECDISCARDABLE_NAME, protectorId, userId); |
| if (hasState(WEAVER_SLOT_NAME, protectorId, userId)) { |
| destroyWeaverSlot(protectorId, userId); |
| } |
| } |
| |
| private byte[] transformUnderWeaverSecret(byte[] data, byte[] secret) { |
| byte[] weaverSecret = SyntheticPasswordCrypto.personalizedHash( |
| PERSONALIZATION_WEAVER_PASSWORD, secret); |
| return ArrayUtils.concat(data, weaverSecret); |
| } |
| |
| private byte[] transformUnderSecdiscardable(byte[] data, byte[] rawSecdiscardable) { |
| byte[] secdiscardable = SyntheticPasswordCrypto.personalizedHash( |
| PERSONALIZATION_SECDISCARDABLE, rawSecdiscardable); |
| return ArrayUtils.concat(data, secdiscardable); |
| } |
| |
| /** |
| * Generates and writes the secdiscardable file for the given protector. The caller is |
| * responsible for calling {@link syncState()} afterwards, once all the protector's files have |
| * been created. |
| */ |
| private byte[] createSecdiscardable(long protectorId, int userId) { |
| byte[] data = SecureRandomUtils.randomBytes(SECDISCARDABLE_LENGTH); |
| saveSecdiscardable(protectorId, data, userId); |
| return data; |
| } |
| |
| /** |
| * Writes the secdiscardable file for the given protector. The caller is responsible for |
| * calling {@link syncState()} afterwards, once all the protector's files have been created. |
| */ |
| private void saveSecdiscardable(long protectorId, byte[] secdiscardable, int userId) { |
| saveState(SECDISCARDABLE_NAME, secdiscardable, protectorId, userId); |
| } |
| |
| private byte[] loadSecdiscardable(long protectorId, int userId) { |
| return loadState(SECDISCARDABLE_NAME, protectorId, userId); |
| } |
| |
| private byte getTokenBasedProtectorType(@TokenType int type) { |
| switch (type) { |
| case TOKEN_TYPE_WEAK: |
| return PROTECTOR_TYPE_WEAK_TOKEN_BASED; |
| case TOKEN_TYPE_STRONG: |
| default: |
| return PROTECTOR_TYPE_STRONG_TOKEN_BASED; |
| } |
| } |
| |
| @VisibleForTesting |
| boolean hasPasswordData(long protectorId, int userId) { |
| return hasState(PASSWORD_DATA_NAME, protectorId, userId); |
| } |
| |
| /** |
| * Retrieves a user's saved password metrics from their LSKF-based SP protector. The |
| * SyntheticPassword itself is needed to decrypt the file containing the password metrics. |
| */ |
| public @Nullable PasswordMetrics getPasswordMetrics(SyntheticPassword sp, long protectorId, |
| int userId) { |
| final byte[] encrypted = loadState(PASSWORD_METRICS_NAME, protectorId, userId); |
| if (encrypted == null) { |
| Slogf.e(TAG, "Failed to read password metrics file for user %d", userId); |
| return null; |
| } |
| final byte[] decrypted = SyntheticPasswordCrypto.decrypt(sp.deriveMetricsKey(), |
| /* personalization= */ new byte[0], encrypted); |
| if (decrypted == null) { |
| Slogf.e(TAG, "Failed to decrypt password metrics file for user %d", userId); |
| return null; |
| } |
| return VersionedPasswordMetrics.deserialize(decrypted).getMetrics(); |
| } |
| |
| /** |
| * Creates the password metrics file: the file associated with the LSKF-based protector that |
| * contains the encrypted metrics about the LSKF. The caller is responsible for calling |
| * {@link syncState()} afterwards if needed. |
| */ |
| private void savePasswordMetrics(LockscreenCredential credential, SyntheticPassword sp, |
| long protectorId, int userId) { |
| final byte[] encrypted = SyntheticPasswordCrypto.encrypt(sp.deriveMetricsKey(), |
| /* personalization= */ new byte[0], |
| new VersionedPasswordMetrics(credential).serialize()); |
| saveState(PASSWORD_METRICS_NAME, encrypted, protectorId, userId); |
| } |
| |
| @VisibleForTesting |
| boolean hasPasswordMetrics(long protectorId, int userId) { |
| return hasState(PASSWORD_METRICS_NAME, protectorId, userId); |
| } |
| |
| private boolean hasState(String stateName, long protectorId, int userId) { |
| return !ArrayUtils.isEmpty(loadState(stateName, protectorId, userId)); |
| } |
| |
| private byte[] loadState(String stateName, long protectorId, int userId) { |
| return mStorage.readSyntheticPasswordState(userId, protectorId, stateName); |
| } |
| |
| /** |
| * Persists the given synthetic password state for the given user ID and protector ID. |
| * <p> |
| * For performance reasons, this doesn't sync the user's synthetic password state directory. As |
| * a result, it doesn't guarantee that the file will really be present after a crash. If that |
| * is needed, call {@link syncState()} afterwards, preferably after batching up related updates. |
| */ |
| private void saveState(String stateName, byte[] data, long protectorId, int userId) { |
| mStorage.writeSyntheticPasswordState(userId, protectorId, stateName, data); |
| } |
| |
| private void syncState(int userId) { |
| mStorage.syncSyntheticPasswordState(userId); |
| } |
| |
| private void destroyState(String stateName, long protectorId, int userId) { |
| mStorage.deleteSyntheticPasswordState(userId, protectorId, stateName); |
| } |
| |
| @VisibleForTesting |
| protected byte[] decryptSpBlob(String protectorKeyAlias, byte[] blob, byte[] protectorSecret) { |
| return SyntheticPasswordCrypto.decryptBlob(protectorKeyAlias, blob, protectorSecret); |
| } |
| |
| @VisibleForTesting |
| protected byte[] createSpBlob(String protectorKeyAlias, byte[] data, byte[] protectorSecret, |
| long sid) { |
| return SyntheticPasswordCrypto.createBlob(protectorKeyAlias, data, protectorSecret, sid); |
| } |
| |
| @VisibleForTesting |
| protected void destroyProtectorKey(String keyAlias) { |
| SyntheticPasswordCrypto.destroyProtectorKey(keyAlias); |
| } |
| |
| private static long generateProtectorId() { |
| while (true) { |
| final long result = SecureRandomUtils.randomLong(); |
| if (result != NULL_PROTECTOR_ID) { |
| return result; |
| } |
| } |
| } |
| |
| @VisibleForTesting |
| static int fakeUserId(int userId) { |
| return 100000 + userId; |
| } |
| |
| private String getProtectorKeyAlias(long protectorId) { |
| // Note, this arguably has a bug: %x should be %016x so that the protector ID is left-padded |
| // with zeroes, like how the synthetic password state files are named. It's too late to fix |
| // this, though, and it doesn't actually matter. |
| return TextUtils.formatSimple("%s%x", PROTECTOR_KEY_ALIAS_PREFIX, protectorId); |
| } |
| |
| /** |
| * Stretches <code>credential</code>, if needed, using the parameters from <code>data</code>. |
| * <p> |
| * When the credential is empty, stetching provides no security benefit. Thus, new protectors |
| * for an empty credential use <code>null</code> {@link PasswordData} and skip the stretching. |
| * <p> |
| * However, old protectors always stored {@link PasswordData} and did the stretching, regardless |
| * of whether the credential was empty or not. For this reason, this method also continues to |
| * support stretching of empty credentials so that old protectors can still be unlocked. |
| */ |
| @VisibleForTesting |
| byte[] stretchLskf(LockscreenCredential credential, @Nullable PasswordData data) { |
| final byte[] password = credential.isNone() ? DEFAULT_PASSWORD : credential.getCredential(); |
| if (data == null) { |
| Preconditions.checkArgument(credential.isNone()); |
| return Arrays.copyOf(password, STRETCHED_LSKF_LENGTH); |
| } |
| return scrypt(password, data.salt, 1 << data.scryptLogN, 1 << data.scryptLogR, |
| 1 << data.scryptLogP, STRETCHED_LSKF_LENGTH); |
| } |
| |
| private byte[] stretchedLskfToGkPassword(byte[] stretchedLskf) { |
| return SyntheticPasswordCrypto.personalizedHash(PERSONALIZATION_USER_GK_AUTH, |
| stretchedLskf); |
| } |
| |
| private byte[] stretchedLskfToWeaverKey(byte[] stretchedLskf) { |
| byte[] key = SyntheticPasswordCrypto.personalizedHash(PERSONALIZATION_WEAVER_KEY, |
| stretchedLskf); |
| if (key.length < mWeaverConfig.keySize) { |
| throw new IllegalArgumentException("weaver key length too small"); |
| } |
| return Arrays.copyOf(key, mWeaverConfig.keySize); |
| } |
| |
| @VisibleForTesting |
| protected long sidFromPasswordHandle(byte[] handle) { |
| return nativeSidFromPasswordHandle(handle); |
| } |
| |
| @VisibleForTesting |
| protected byte[] scrypt(byte[] password, byte[] salt, int n, int r, int p, int outLen) { |
| return new Scrypt().scrypt(password, salt, n, r, p, outLen); |
| } |
| |
| private native long nativeSidFromPasswordHandle(byte[] handle); |
| |
| @VisibleForTesting |
| static byte[] bytesToHex(byte[] bytes) { |
| return HexEncoding.encodeToString(bytes).getBytes(); |
| } |
| |
| /** |
| * Migrates all existing SP protector keys from uid 1000 app domain to LSS selinux domain. |
| */ |
| public boolean migrateKeyNamespace() { |
| boolean success = true; |
| final Map<Integer, List<Long>> allProtectors = |
| mStorage.listSyntheticPasswordProtectorsForAllUsers(SP_BLOB_NAME); |
| for (List<Long> userProtectors : allProtectors.values()) { |
| for (long protectorId : userProtectors) { |
| success &= SyntheticPasswordCrypto.migrateLockSettingsKey( |
| getProtectorKeyAlias(protectorId)); |
| } |
| } |
| return success; |
| } |
| |
| /** Register the given IWeakEscrowTokenRemovedListener. */ |
| public boolean registerWeakEscrowTokenRemovedListener( |
| IWeakEscrowTokenRemovedListener listener) { |
| return mListeners.register(listener); |
| } |
| |
| /** Unregister the given IWeakEscrowTokenRemovedListener. */ |
| public boolean unregisterWeakEscrowTokenRemovedListener( |
| IWeakEscrowTokenRemovedListener listener) { |
| return mListeners.unregister(listener); |
| } |
| |
| private void notifyWeakEscrowTokenRemovedListeners(long protectorId, int userId) { |
| int i = mListeners.beginBroadcast(); |
| try { |
| while (i > 0) { |
| i--; |
| try { |
| mListeners.getBroadcastItem(i).onWeakEscrowTokenRemoved(protectorId, userId); |
| } catch (RemoteException e) { |
| Slog.e(TAG, "Exception while notifying WeakEscrowTokenRemovedListener.", |
| e); |
| } |
| } |
| } finally { |
| mListeners.finishBroadcast(); |
| } |
| } |
| |
| public void writeVendorAuthSecret( |
| @NonNull final byte[] vendorAuthSecret, |
| @NonNull final SyntheticPassword sp, |
| @UserIdInt final int userId) { |
| final byte[] encrypted = |
| SyntheticPasswordCrypto.encrypt( |
| sp.deriveVendorAuthSecretEncryptionKey(), new byte[0], vendorAuthSecret); |
| saveState(VENDOR_AUTH_SECRET_NAME, encrypted, NULL_PROTECTOR_ID, userId); |
| syncState(userId); |
| } |
| |
| public @Nullable byte[] readVendorAuthSecret( |
| @NonNull final SyntheticPassword sp, @UserIdInt final int userId) { |
| final byte[] encrypted = loadState(VENDOR_AUTH_SECRET_NAME, NULL_PROTECTOR_ID, userId); |
| if (encrypted == null) { |
| return null; |
| } |
| return SyntheticPasswordCrypto.decrypt( |
| sp.deriveVendorAuthSecretEncryptionKey(), new byte[0], encrypted); |
| } |
| } |