| /* |
| * 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; |
| |
| import android.annotation.NonNull; |
| import android.annotation.Nullable; |
| import android.hardware.weaver.V1_0.IWeaver; |
| import android.hardware.weaver.V1_0.WeaverConfig; |
| import android.hardware.weaver.V1_0.WeaverReadResponse; |
| import android.hardware.weaver.V1_0.WeaverReadStatus; |
| import android.hardware.weaver.V1_0.WeaverStatus; |
| import android.os.RemoteException; |
| import android.security.GateKeeper; |
| import android.service.gatekeeper.GateKeeperResponse; |
| import android.service.gatekeeper.IGateKeeperService; |
| import android.util.ArrayMap; |
| import android.util.Log; |
| import android.util.Slog; |
| |
| import com.android.internal.annotations.VisibleForTesting; |
| import com.android.internal.util.ArrayUtils; |
| import com.android.internal.widget.LockPatternUtils; |
| import com.android.internal.widget.VerifyCredentialResponse; |
| |
| import libcore.util.HexEncoding; |
| |
| import java.nio.ByteBuffer; |
| import java.security.NoSuchAlgorithmException; |
| import java.security.SecureRandom; |
| import java.util.ArrayList; |
| 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.Set; |
| |
| |
| /** |
| * A class that maintains the wrapping of synthetic password by user credentials or escrow tokens. |
| * It's (mostly) a pure storage for synthetic passwords, providing APIs to creating and destroying |
| * synthetic password blobs which are wrapped by user credentials or escrow tokens. |
| * |
| * Here is the assumptions it makes: |
| * Each user has one single synthetic password at any time. |
| * The SP has an associated password handle, which binds to the SID for that user. The password |
| * handle is persisted by SyntheticPasswordManager internally. |
| * If the user credential is null, it's treated as if the credential is DEFAULT_PASSWORD |
| * |
| * Information persisted on disk: |
| * for each user (stored under DEFAULT_HANDLE): |
| * SP_HANDLE_NAME: GateKeeper password handle of synthetic password. Only available if user |
| * credential exists, cleared when user clears their credential. |
| * SP_E0_NAME, SP_P1_NAME: Secret to derive synthetic password when combined with escrow |
| * tokens. Destroyed when escrow support is turned off for the given user. |
| * |
| * for each SP blob under the user (stored under the corresponding handle): |
| * SP_BLOB_NAME: The encrypted synthetic password. Always exists. |
| * PASSWORD_DATA_NAME: Metadata about user credential. Only exists for password based SP. |
| * SECDISCARDABLE_NAME: Part of the necessary ingredient to decrypt SP_BLOB_NAME for the |
| * purpose of secure deletion. Exists if this is a non-weaver SP |
| * (both password and token based), or it's a token-based SP under weaver. |
| * WEAVER_SLOT: Metadata about the weaver slot used. Only exists if this is a SP under weaver. |
| * |
| * |
| */ |
| public 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"; |
| |
| public static final long DEFAULT_HANDLE = 0L; |
| private static final String DEFAULT_PASSWORD = "default-password"; |
| |
| private static final byte WEAVER_VERSION = 1; |
| private static final int INVALID_WEAVER_SLOT = -1; |
| |
| private static final byte SYNTHETIC_PASSWORD_VERSION = 1; |
| private static final byte SYNTHETIC_PASSWORD_PASSWORD_BASED = 0; |
| private static final byte SYNTHETIC_PASSWORD_TOKEN_BASED = 1; |
| |
| // 256-bit synthetic password |
| private static final byte SYNTHETIC_PASSWORD_LENGTH = 256 / 8; |
| |
| private static final int PASSWORD_SCRYPT_N = 13; |
| private static final int PASSWORD_SCRYPT_R = 3; |
| private static final int PASSWORD_SCRYPT_P = 1; |
| private static final int PASSWORD_SALT_LENGTH = 16; |
| private static final int PASSWORD_TOKEN_LENGTH = 32; |
| private static final String TAG = "SyntheticPasswordManager"; |
| |
| private static final byte[] PERSONALISATION_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_SP_SPLIT = "sp-split".getBytes(); |
| private static final byte[] PERSONALIZATION_E0 = "e0-encryption".getBytes(); |
| private static final byte[] PERSONALISATION_WEAVER_PASSWORD = "weaver-pwd".getBytes(); |
| private static final byte[] PERSONALISATION_WEAVER_KEY = "weaver-key".getBytes(); |
| private static final byte[] PERSONALISATION_WEAVER_TOKEN = "weaver-token".getBytes(); |
| |
| static class AuthenticationResult { |
| public AuthenticationToken authToken; |
| public VerifyCredentialResponse gkResponse; |
| } |
| |
| static class AuthenticationToken { |
| /* |
| * Here is the relationship between all three fields: |
| * P0 and P1 are two randomly-generated blocks. P1 is stored on disk but P0 is not. |
| * syntheticPassword = hash(P0 || P1) |
| * E0 = P0 encrypted under syntheticPassword, stored on disk. |
| */ |
| private @Nullable byte[] E0; |
| private @Nullable byte[] P1; |
| private @NonNull String syntheticPassword; |
| |
| public String deriveKeyStorePassword() { |
| return bytesToHex(SyntheticPasswordCrypto.personalisedHash( |
| PERSONALIZATION_KEY_STORE_PASSWORD, syntheticPassword.getBytes())); |
| } |
| |
| public byte[] deriveGkPassword() { |
| return SyntheticPasswordCrypto.personalisedHash(PERSONALIZATION_SP_GK_AUTH, |
| syntheticPassword.getBytes()); |
| } |
| |
| public byte[] deriveDiskEncryptionKey() { |
| return SyntheticPasswordCrypto.personalisedHash(PERSONALIZATION_FBE_KEY, |
| syntheticPassword.getBytes()); |
| } |
| |
| private void initialize(byte[] P0, byte[] P1) { |
| this.P1 = P1; |
| this.syntheticPassword = String.valueOf(HexEncoding.encode( |
| SyntheticPasswordCrypto.personalisedHash( |
| PERSONALIZATION_SP_SPLIT, P0, P1))); |
| this.E0 = SyntheticPasswordCrypto.encrypt(this.syntheticPassword.getBytes(), |
| PERSONALIZATION_E0, P0); |
| } |
| |
| public void recreate(byte[] secret) { |
| initialize(secret, this.P1); |
| } |
| |
| protected static AuthenticationToken create() { |
| AuthenticationToken result = new AuthenticationToken(); |
| result.initialize(secureRandom(SYNTHETIC_PASSWORD_LENGTH), |
| secureRandom(SYNTHETIC_PASSWORD_LENGTH)); |
| return result; |
| } |
| |
| public byte[] computeP0() { |
| if (E0 == null) { |
| return null; |
| } |
| return SyntheticPasswordCrypto.decrypt(syntheticPassword.getBytes(), PERSONALIZATION_E0, |
| E0); |
| } |
| } |
| |
| static class PasswordData { |
| byte scryptN; |
| byte scryptR; |
| byte scryptP; |
| public int passwordType; |
| byte[] salt; |
| // For GateKeeper-based credential, this is the password handle returned by GK, |
| // for weaver-based credential, this is empty. |
| public byte[] passwordHandle; |
| |
| public static PasswordData create(int passwordType) { |
| PasswordData result = new PasswordData(); |
| result.scryptN = PASSWORD_SCRYPT_N; |
| result.scryptR = PASSWORD_SCRYPT_R; |
| result.scryptP = PASSWORD_SCRYPT_P; |
| result.passwordType = passwordType; |
| result.salt = secureRandom(PASSWORD_SALT_LENGTH); |
| return result; |
| } |
| |
| public static PasswordData fromBytes(byte[] data) { |
| PasswordData result = new PasswordData(); |
| ByteBuffer buffer = ByteBuffer.allocate(data.length); |
| buffer.put(data, 0, data.length); |
| buffer.flip(); |
| result.passwordType = buffer.getInt(); |
| result.scryptN = buffer.get(); |
| result.scryptR = buffer.get(); |
| result.scryptP = 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; |
| } |
| return result; |
| } |
| |
| public byte[] toBytes() { |
| |
| ByteBuffer buffer = ByteBuffer.allocate(Integer.BYTES + 3 * Byte.BYTES |
| + Integer.BYTES + salt.length + Integer.BYTES + |
| (passwordHandle != null ? passwordHandle.length : 0)); |
| buffer.putInt(passwordType); |
| buffer.put(scryptN); |
| buffer.put(scryptR); |
| buffer.put(scryptP); |
| buffer.putInt(salt.length); |
| buffer.put(salt); |
| if (passwordHandle != null && passwordHandle.length > 0) { |
| buffer.putInt(passwordHandle.length); |
| buffer.put(passwordHandle); |
| } else { |
| buffer.putInt(0); |
| } |
| return buffer.array(); |
| } |
| } |
| |
| static class TokenData { |
| byte[] secdiscardableOnDisk; |
| byte[] weaverSecret; |
| byte[] aggregatedSecret; |
| } |
| |
| private LockSettingsStorage mStorage; |
| private IWeaver mWeaver; |
| private WeaverConfig mWeaverConfig; |
| |
| public SyntheticPasswordManager(LockSettingsStorage storage) { |
| mStorage = storage; |
| } |
| |
| @VisibleForTesting |
| protected IWeaver getWeaverService() throws RemoteException { |
| try { |
| return IWeaver.getService(); |
| } catch (NoSuchElementException e) { |
| Slog.i(TAG, "Device does not support weaver"); |
| return null; |
| } |
| } |
| |
| public synchronized void initWeaverService() { |
| if (mWeaver != null) { |
| return; |
| } |
| try { |
| mWeaverConfig = null; |
| mWeaver = getWeaverService(); |
| if (mWeaver != null) { |
| mWeaver.getConfig((int status, WeaverConfig config) -> { |
| if (status == WeaverStatus.OK && config.slots > 0) { |
| mWeaverConfig = config; |
| } else { |
| Slog.e(TAG, "Failed to get weaver config, status " + status |
| + " slots: " + config.slots); |
| mWeaver = null; |
| } |
| }); |
| } |
| } catch (RemoteException e) { |
| Slog.e(TAG, "Failed to get weaver service", e); |
| } |
| } |
| |
| private synchronized boolean isWeaverAvailable() { |
| if (mWeaver == null) { |
| //Re-initializing weaver in case there was a transient error preventing access to it. |
| initWeaverService(); |
| } |
| return mWeaver != null && mWeaverConfig.slots > 0; |
| } |
| |
| /** |
| * 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 |
| * @throws RemoteException |
| */ |
| private byte[] weaverEnroll(int slot, byte[] key, @Nullable byte[] value) |
| throws RemoteException { |
| if (slot == INVALID_WEAVER_SLOT || slot >= mWeaverConfig.slots) { |
| throw new RuntimeException("Invalid slot for weaver"); |
| } |
| if (key == null) { |
| key = new byte[mWeaverConfig.keySize]; |
| } else if (key.length != mWeaverConfig.keySize) { |
| throw new RuntimeException("Invalid key size for weaver"); |
| } |
| if (value == null) { |
| value = secureRandom(mWeaverConfig.valueSize); |
| } |
| int writeStatus = mWeaver.write(slot, toByteArrayList(key), toByteArrayList(value)); |
| if (writeStatus != WeaverStatus.OK) { |
| Log.e(TAG, "weaver write failed, slot: " + slot + " status: " + writeStatus); |
| return null; |
| } |
| return value; |
| } |
| |
| /** |
| * 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. |
| * @throws RemoteException |
| */ |
| private VerifyCredentialResponse weaverVerify(int slot, byte[] key) throws RemoteException { |
| if (slot == INVALID_WEAVER_SLOT || slot >= mWeaverConfig.slots) { |
| throw new RuntimeException("Invalid slot for weaver"); |
| } |
| if (key == null) { |
| key = new byte[mWeaverConfig.keySize]; |
| } else if (key.length != mWeaverConfig.keySize) { |
| throw new RuntimeException("Invalid key size for weaver"); |
| } |
| final VerifyCredentialResponse[] response = new VerifyCredentialResponse[1]; |
| mWeaver.read(slot, toByteArrayList(key), (int status, WeaverReadResponse readResponse) -> { |
| switch (status) { |
| case WeaverReadStatus.OK: |
| response[0] = new VerifyCredentialResponse( |
| fromByteArrayList(readResponse.value)); |
| break; |
| case WeaverReadStatus.THROTTLE: |
| response[0] = new VerifyCredentialResponse(readResponse.timeout); |
| Log.e(TAG, "weaver read failed (THROTTLE), slot: " + slot); |
| break; |
| case WeaverReadStatus.INCORRECT_KEY: |
| if (readResponse.timeout == 0) { |
| response[0] = VerifyCredentialResponse.ERROR; |
| Log.e(TAG, "weaver read failed (INCORRECT_KEY), slot: " + slot); |
| } else { |
| response[0] = new VerifyCredentialResponse(readResponse.timeout); |
| Log.e(TAG, "weaver read failed (INCORRECT_KEY/THROTTLE), slot: " + slot); |
| } |
| break; |
| case WeaverReadStatus.FAILED: |
| response[0] = VerifyCredentialResponse.ERROR; |
| Log.e(TAG, "weaver read failed (FAILED), slot: " + slot); |
| break; |
| default: |
| response[0] = VerifyCredentialResponse.ERROR; |
| Log.e(TAG, "weaver read unknown status " + status + ", slot: " + slot); |
| break; |
| } |
| }); |
| return response[0]; |
| } |
| |
| public void removeUser(int userId) { |
| if (isWeaverAvailable()) { |
| for (long handle : mStorage.listSyntheticPasswordHandlesForUser(WEAVER_SLOT_NAME, |
| userId)) { |
| destroyWeaverSlot(handle, userId); |
| } |
| } |
| } |
| |
| public int getCredentialType(long handle, int userId) { |
| byte[] passwordData = loadState(PASSWORD_DATA_NAME, handle, userId); |
| if (passwordData == null) { |
| Log.w(TAG, "getCredentialType: encountered empty password data for user " + userId); |
| return LockPatternUtils.CREDENTIAL_TYPE_NONE; |
| } |
| return PasswordData.fromBytes(passwordData).passwordType; |
| } |
| |
| /** |
| * Initializing a new Authentication token, possibly from an existing credential and hash. |
| * |
| * The authentication token would bear a randomly-generated synthetic password. |
| * |
| * This method has the side effect of rebinding the SID of the given user to the |
| * newly-generated SP. |
| * |
| * If the existing credential hash is non-null, the existing SID mill be migrated so |
| * the synthetic password in the authentication token will produce the same SID |
| * (the corresponding synthetic password handle is persisted by SyntheticPasswordManager |
| * in a per-user data storage.) |
| * |
| * If the existing credential hash is null, it means the given user should have no SID so |
| * SyntheticPasswordManager will nuke any SP handle previously persisted. In this case, |
| * the supplied credential parameter is also ignored. |
| * |
| * 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. |
| * |
| */ |
| public AuthenticationToken newSyntheticPasswordAndSid(IGateKeeperService gatekeeper, |
| byte[] hash, String credential, int userId) throws RemoteException { |
| AuthenticationToken result = AuthenticationToken.create(); |
| GateKeeperResponse response; |
| if (hash != null) { |
| response = gatekeeper.enroll(userId, hash, credential.getBytes(), |
| result.deriveGkPassword()); |
| if (response.getResponseCode() != GateKeeperResponse.RESPONSE_OK) { |
| Log.w(TAG, "Fail to migrate SID, assuming no SID, user " + userId); |
| clearSidForUser(userId); |
| } else { |
| saveSyntheticPasswordHandle(response.getPayload(), userId); |
| } |
| } else { |
| clearSidForUser(userId); |
| } |
| saveEscrowData(result, userId); |
| return result; |
| } |
| |
| /** |
| * Enroll a new password handle and SID for the given synthetic password and persist it on disk. |
| * Used when adding password to previously-unsecured devices. |
| */ |
| public void newSidForUser(IGateKeeperService gatekeeper, AuthenticationToken authToken, |
| int userId) throws RemoteException { |
| GateKeeperResponse response = gatekeeper.enroll(userId, null, null, |
| authToken.deriveGkPassword()); |
| if (response.getResponseCode() != GateKeeperResponse.RESPONSE_OK) { |
| Log.e(TAG, "Fail to create new SID for user " + userId); |
| return; |
| } |
| 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, true, DEFAULT_HANDLE, userId); |
| } |
| |
| public boolean hasSidForUser(int userId) { |
| return hasState(SP_HANDLE_NAME, DEFAULT_HANDLE, userId); |
| } |
| |
| // if null, it means there is no SID associated with the user |
| // This can happen if the user is migrated to SP but currently |
| // do not have a lockscreen password. |
| private byte[] loadSyntheticPasswordHandle(int userId) { |
| return loadState(SP_HANDLE_NAME, DEFAULT_HANDLE, userId); |
| } |
| |
| private void saveSyntheticPasswordHandle(byte[] spHandle, int userId) { |
| saveState(SP_HANDLE_NAME, spHandle, DEFAULT_HANDLE, userId); |
| } |
| |
| private boolean loadEscrowData(AuthenticationToken authToken, int userId) { |
| authToken.E0 = loadState(SP_E0_NAME, DEFAULT_HANDLE, userId); |
| authToken.P1 = loadState(SP_P1_NAME, DEFAULT_HANDLE, userId); |
| return authToken.E0 != null && authToken.P1 != null; |
| } |
| |
| private void saveEscrowData(AuthenticationToken authToken, int userId) { |
| saveState(SP_E0_NAME, authToken.E0, DEFAULT_HANDLE, userId); |
| saveState(SP_P1_NAME, authToken.P1, DEFAULT_HANDLE, userId); |
| } |
| |
| public boolean hasEscrowData(int userId) { |
| return hasState(SP_E0_NAME, DEFAULT_HANDLE, userId) |
| && hasState(SP_P1_NAME, DEFAULT_HANDLE, userId); |
| } |
| |
| public void destroyEscrowData(int userId) { |
| destroyState(SP_E0_NAME, true, DEFAULT_HANDLE, userId); |
| destroyState(SP_P1_NAME, true, DEFAULT_HANDLE, userId); |
| } |
| |
| private int loadWeaverSlot(long handle, int userId) { |
| final int LENGTH = Byte.BYTES + Integer.BYTES; |
| byte[] data = loadState(WEAVER_SLOT_NAME, handle, 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) { |
| Log.e(TAG, "Invalid weaver slot version of handle " + handle); |
| return INVALID_WEAVER_SLOT; |
| } |
| return buffer.getInt(); |
| } |
| |
| private void saveWeaverSlot(int slot, long handle, int userId) { |
| ByteBuffer buffer = ByteBuffer.allocate(Byte.BYTES + Integer.BYTES); |
| buffer.put(WEAVER_VERSION); |
| buffer.putInt(slot); |
| saveState(WEAVER_SLOT_NAME, buffer.array(), handle, userId); |
| } |
| |
| private void destroyWeaverSlot(long handle, int userId) { |
| int slot = loadWeaverSlot(handle, userId); |
| if (slot != INVALID_WEAVER_SLOT) { |
| try { |
| weaverEnroll(slot, null, null); |
| } catch (RemoteException e) { |
| Log.w(TAG, "Failed to destroy slot", e); |
| } |
| } |
| destroyState(WEAVER_SLOT_NAME, true, handle, userId); |
| } |
| |
| private int getNextAvailableWeaverSlot() { |
| Map<Integer, List<Long>> slotHandles = mStorage.listSyntheticPasswordHandlesForAllUsers( |
| WEAVER_SLOT_NAME); |
| HashSet<Integer> slots = new HashSet<>(); |
| for (Map.Entry<Integer, List<Long>> entry : slotHandles.entrySet()) { |
| for (Long handle : entry.getValue()) { |
| int slot = loadWeaverSlot(handle, entry.getKey()); |
| slots.add(slot); |
| } |
| } |
| for (int i = 0; i < mWeaverConfig.slots; i++) { |
| if (!slots.contains(i)) { |
| return i; |
| } |
| } |
| throw new RuntimeException("Run out of weaver slots."); |
| } |
| |
| /** |
| * Create a new password based SP blob based on the supplied authentication token, such that |
| * a future successful authentication with unwrapPasswordBasedSyntheticPassword() would result |
| * in the same authentication token. |
| * |
| * This method only creates SP blob wrapping around the given synthetic password and does not |
| * handle logic around SID or SP handle. The caller should separately ensure that the user's SID |
| * is consistent with the device state by calling other APIs in this class. |
| * |
| * @see #newSidForUser |
| * @see #clearSidForUser |
| */ |
| public long createPasswordBasedSyntheticPassword(IGateKeeperService gatekeeper, |
| String credential, int credentialType, AuthenticationToken authToken, int userId) |
| throws RemoteException { |
| if (credential == null || credentialType == LockPatternUtils.CREDENTIAL_TYPE_NONE) { |
| credentialType = LockPatternUtils.CREDENTIAL_TYPE_NONE; |
| credential = DEFAULT_PASSWORD; |
| } |
| |
| long handle = generateHandle(); |
| PasswordData pwd = PasswordData.create(credentialType); |
| byte[] pwdToken = computePasswordToken(credential, pwd); |
| final long sid; |
| final byte[] applicationId; |
| |
| if (isWeaverAvailable()) { |
| // Weaver based user password |
| int weaverSlot = getNextAvailableWeaverSlot(); |
| byte[] weaverSecret = weaverEnroll(weaverSlot, passwordTokenToWeaverKey(pwdToken), null); |
| if (weaverSecret == null) { |
| Log.e(TAG, "Fail to enroll user password under weaver " + userId); |
| return DEFAULT_HANDLE; |
| } |
| saveWeaverSlot(weaverSlot, handle, userId); |
| |
| pwd.passwordHandle = null; |
| sid = GateKeeper.INVALID_SECURE_USER_ID; |
| applicationId = transformUnderWeaverSecret(pwdToken, weaverSecret); |
| } else { |
| // GateKeeper based user password |
| GateKeeperResponse response = gatekeeper.enroll(fakeUid(userId), null, null, |
| passwordTokenToGkInput(pwdToken)); |
| if (response.getResponseCode() != GateKeeperResponse.RESPONSE_OK) { |
| Log.e(TAG, "Fail to enroll user password when creating SP for user " + userId); |
| return DEFAULT_HANDLE; |
| } |
| pwd.passwordHandle = response.getPayload(); |
| sid = sidFromPasswordHandle(pwd.passwordHandle); |
| applicationId = transformUnderSecdiscardable(pwdToken, |
| createSecdiscardable(handle, userId)); |
| } |
| saveState(PASSWORD_DATA_NAME, pwd.toBytes(), handle, userId); |
| |
| createSyntheticPasswordBlob(handle, SYNTHETIC_PASSWORD_PASSWORD_BASED, authToken, |
| applicationId, sid, userId); |
| return handle; |
| } |
| |
| private ArrayMap<Integer, ArrayMap<Long, TokenData>> tokenMap = new ArrayMap<>(); |
| |
| public long createTokenBasedSyntheticPassword(byte[] token, int userId) { |
| long handle = generateHandle(); |
| if (!tokenMap.containsKey(userId)) { |
| tokenMap.put(userId, new ArrayMap<>()); |
| } |
| TokenData tokenData = new TokenData(); |
| final byte[] secdiscardable = secureRandom(SECDISCARDABLE_LENGTH); |
| if (isWeaverAvailable()) { |
| tokenData.weaverSecret = secureRandom(mWeaverConfig.valueSize); |
| tokenData.secdiscardableOnDisk = SyntheticPasswordCrypto.encrypt(tokenData.weaverSecret, |
| PERSONALISATION_WEAVER_TOKEN, secdiscardable); |
| } else { |
| tokenData.secdiscardableOnDisk = secdiscardable; |
| tokenData.weaverSecret = null; |
| } |
| tokenData.aggregatedSecret = transformUnderSecdiscardable(token, secdiscardable); |
| |
| tokenMap.get(userId).put(handle, tokenData); |
| return handle; |
| } |
| |
| public Set<Long> getPendingTokensForUser(int userId) { |
| if (!tokenMap.containsKey(userId)) { |
| return Collections.emptySet(); |
| } |
| return tokenMap.get(userId).keySet(); |
| } |
| |
| public boolean removePendingToken(long handle, int userId) { |
| if (!tokenMap.containsKey(userId)) { |
| return false; |
| } |
| return tokenMap.get(userId).remove(handle) != null; |
| } |
| |
| public boolean activateTokenBasedSyntheticPassword(long handle, AuthenticationToken authToken, |
| int userId) { |
| if (!tokenMap.containsKey(userId)) { |
| return false; |
| } |
| TokenData tokenData = tokenMap.get(userId).get(handle); |
| if (tokenData == null) { |
| return false; |
| } |
| if (!loadEscrowData(authToken, userId)) { |
| Log.w(TAG, "User is not escrowable"); |
| return false; |
| } |
| if (isWeaverAvailable()) { |
| int slot = getNextAvailableWeaverSlot(); |
| try { |
| weaverEnroll(slot, null, tokenData.weaverSecret); |
| } catch (RemoteException e) { |
| Log.e(TAG, "Failed to enroll weaver secret when activating token", e); |
| return false; |
| } |
| saveWeaverSlot(slot, handle, userId); |
| } |
| saveSecdiscardable(handle, tokenData.secdiscardableOnDisk, userId); |
| createSyntheticPasswordBlob(handle, SYNTHETIC_PASSWORD_TOKEN_BASED, authToken, |
| tokenData.aggregatedSecret, 0L, userId); |
| tokenMap.get(userId).remove(handle); |
| return true; |
| } |
| |
| private void createSyntheticPasswordBlob(long handle, byte type, AuthenticationToken authToken, |
| byte[] applicationId, long sid, int userId) { |
| final byte[] secret; |
| if (type == SYNTHETIC_PASSWORD_TOKEN_BASED) { |
| secret = authToken.computeP0(); |
| } else { |
| secret = authToken.syntheticPassword.getBytes(); |
| } |
| byte[] content = createSPBlob(getHandleName(handle), secret, applicationId, sid); |
| byte[] blob = new byte[content.length + 1 + 1]; |
| blob[0] = SYNTHETIC_PASSWORD_VERSION; |
| blob[1] = type; |
| System.arraycopy(content, 0, blob, 2, content.length); |
| saveState(SP_BLOB_NAME, blob, handle, userId); |
| } |
| |
| /** |
| * Decrypt a synthetic password by supplying the user credential and corresponding password |
| * blob handle generated previously. If the decryption is successful, initiate a GateKeeper |
| * verification to referesh the SID & Auth token maintained by the system. |
| */ |
| public AuthenticationResult unwrapPasswordBasedSyntheticPassword(IGateKeeperService gatekeeper, |
| long handle, String credential, int userId) throws RemoteException { |
| if (credential == null) { |
| credential = DEFAULT_PASSWORD; |
| } |
| AuthenticationResult result = new AuthenticationResult(); |
| PasswordData pwd = PasswordData.fromBytes(loadState(PASSWORD_DATA_NAME, handle, userId)); |
| byte[] pwdToken = computePasswordToken(credential, pwd); |
| |
| final byte[] applicationId; |
| int weaverSlot = loadWeaverSlot(handle, userId); |
| if (weaverSlot != INVALID_WEAVER_SLOT) { |
| // Weaver based user password |
| if (!isWeaverAvailable()) { |
| Log.e(TAG, "No weaver service to unwrap password based SP"); |
| result.gkResponse = VerifyCredentialResponse.ERROR; |
| return result; |
| } |
| result.gkResponse = weaverVerify(weaverSlot, passwordTokenToWeaverKey(pwdToken)); |
| if (result.gkResponse.getResponseCode() != VerifyCredentialResponse.RESPONSE_OK) { |
| return result; |
| } |
| applicationId = transformUnderWeaverSecret(pwdToken, result.gkResponse.getPayload()); |
| } else { |
| byte[] gkPwdToken = passwordTokenToGkInput(pwdToken); |
| GateKeeperResponse response = gatekeeper.verifyChallenge(fakeUid(userId), 0L, |
| pwd.passwordHandle, gkPwdToken); |
| int responseCode = response.getResponseCode(); |
| if (responseCode == GateKeeperResponse.RESPONSE_OK) { |
| result.gkResponse = VerifyCredentialResponse.OK; |
| if (response.getShouldReEnroll()) { |
| GateKeeperResponse reenrollResponse = gatekeeper.enroll(fakeUid(userId), |
| pwd.passwordHandle, gkPwdToken, gkPwdToken); |
| if (reenrollResponse.getResponseCode() == GateKeeperResponse.RESPONSE_OK) { |
| pwd.passwordHandle = reenrollResponse.getPayload(); |
| saveState(PASSWORD_DATA_NAME, pwd.toBytes(), handle, userId); |
| } else { |
| Log.w(TAG, "Fail to re-enroll user password for user " + userId); |
| // continue the flow anyway |
| } |
| } |
| } else if (responseCode == GateKeeperResponse.RESPONSE_RETRY) { |
| result.gkResponse = new VerifyCredentialResponse(response.getTimeout()); |
| return result; |
| } else { |
| result.gkResponse = VerifyCredentialResponse.ERROR; |
| return result; |
| } |
| applicationId = transformUnderSecdiscardable(pwdToken, |
| loadSecdiscardable(handle, userId)); |
| } |
| |
| result.authToken = unwrapSyntheticPasswordBlob(handle, SYNTHETIC_PASSWORD_PASSWORD_BASED, |
| applicationId, userId); |
| |
| // Perform verifyChallenge to refresh auth tokens for GK if user password exists. |
| result.gkResponse = verifyChallenge(gatekeeper, result.authToken, 0L, userId); |
| return result; |
| } |
| |
| /** |
| * Decrypt a synthetic password by supplying an escrow token and corresponding token |
| * blob handle generated previously. If the decryption is successful, initiate a GateKeeper |
| * verification to referesh the SID & Auth token maintained by the system. |
| */ |
| public @NonNull AuthenticationResult unwrapTokenBasedSyntheticPassword( |
| IGateKeeperService gatekeeper, long handle, byte[] token, int userId) |
| throws RemoteException { |
| AuthenticationResult result = new AuthenticationResult(); |
| byte[] secdiscardable = loadSecdiscardable(handle, userId); |
| int slotId = loadWeaverSlot(handle, userId); |
| if (slotId != INVALID_WEAVER_SLOT) { |
| if (!isWeaverAvailable()) { |
| Log.e(TAG, "No weaver service to unwrap token based SP"); |
| result.gkResponse = VerifyCredentialResponse.ERROR; |
| return result; |
| } |
| VerifyCredentialResponse response = weaverVerify(slotId, null); |
| if (response.getResponseCode() != VerifyCredentialResponse.RESPONSE_OK || |
| response.getPayload() == null) { |
| Log.e(TAG, "Failed to retrieve weaver secret when unwrapping token"); |
| result.gkResponse = VerifyCredentialResponse.ERROR; |
| return result; |
| } |
| secdiscardable = SyntheticPasswordCrypto.decrypt(response.getPayload(), |
| PERSONALISATION_WEAVER_TOKEN, secdiscardable); |
| } |
| byte[] applicationId = transformUnderSecdiscardable(token, secdiscardable); |
| result.authToken = unwrapSyntheticPasswordBlob(handle, SYNTHETIC_PASSWORD_TOKEN_BASED, |
| applicationId, userId); |
| if (result.authToken != null) { |
| result.gkResponse = verifyChallenge(gatekeeper, result.authToken, 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 AuthenticationToken unwrapSyntheticPasswordBlob(long handle, byte type, |
| byte[] applicationId, int userId) { |
| byte[] blob = loadState(SP_BLOB_NAME, handle, userId); |
| if (blob == null) { |
| return null; |
| } |
| if (blob[0] != SYNTHETIC_PASSWORD_VERSION) { |
| throw new RuntimeException("Unknown blob version"); |
| } |
| if (blob[1] != type) { |
| throw new RuntimeException("Invalid blob type"); |
| } |
| byte[] secret = decryptSPBlob(getHandleName(handle), |
| Arrays.copyOfRange(blob, 2, blob.length), applicationId); |
| if (secret == null) { |
| Log.e(TAG, "Fail to decrypt SP for user " + userId); |
| return null; |
| } |
| AuthenticationToken result = new AuthenticationToken(); |
| if (type == SYNTHETIC_PASSWORD_TOKEN_BASED) { |
| if (!loadEscrowData(result, userId)) { |
| Log.e(TAG, "User is not escrowable: " + userId); |
| return null; |
| } |
| result.recreate(secret); |
| } else { |
| result.syntheticPassword = new String(secret); |
| } |
| return result; |
| } |
| |
| /** |
| * performs GK verifyChallenge and returns auth token, re-enrolling SP password handle |
| * if required. |
| * |
| * Normally performing verifyChallenge with an AuthenticationToken should always return |
| * RESPONSE_OK, since user authentication failures are detected earlier when trying to |
| * decrypt SP. |
| */ |
| public @Nullable VerifyCredentialResponse verifyChallenge(IGateKeeperService gatekeeper, |
| @NonNull AuthenticationToken auth, long challenge, int userId) throws RemoteException { |
| 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; |
| } |
| VerifyCredentialResponse result; |
| GateKeeperResponse response = gatekeeper.verifyChallenge(userId, challenge, |
| spHandle, auth.deriveGkPassword()); |
| int responseCode = response.getResponseCode(); |
| if (responseCode == GateKeeperResponse.RESPONSE_OK) { |
| result = new VerifyCredentialResponse(response.getPayload()); |
| if (response.getShouldReEnroll()) { |
| response = gatekeeper.enroll(userId, spHandle, |
| spHandle, auth.deriveGkPassword()); |
| if (response.getResponseCode() == GateKeeperResponse.RESPONSE_OK) { |
| spHandle = response.getPayload(); |
| saveSyntheticPasswordHandle(spHandle, userId); |
| // Call self again to re-verify with updated handle |
| return verifyChallenge(gatekeeper, auth, challenge, userId); |
| } else { |
| Log.w(TAG, "Fail to re-enroll SP handle for user " + userId); |
| // Fall through, return existing handle |
| } |
| } |
| } else if (responseCode == GateKeeperResponse.RESPONSE_RETRY) { |
| result = new VerifyCredentialResponse(response.getTimeout()); |
| } else { |
| result = VerifyCredentialResponse.ERROR; |
| } |
| return result; |
| } |
| |
| public boolean existsHandle(long handle, int userId) { |
| return hasState(SP_BLOB_NAME, handle, userId); |
| } |
| |
| public void destroyTokenBasedSyntheticPassword(long handle, int userId) { |
| destroySyntheticPassword(handle, userId); |
| destroyState(SECDISCARDABLE_NAME, true, handle, userId); |
| } |
| |
| public void destroyPasswordBasedSyntheticPassword(long handle, int userId) { |
| destroySyntheticPassword(handle, userId); |
| destroyState(SECDISCARDABLE_NAME, true, handle, userId); |
| destroyState(PASSWORD_DATA_NAME, true, handle, userId); |
| } |
| |
| private void destroySyntheticPassword(long handle, int userId) { |
| destroyState(SP_BLOB_NAME, true, handle, userId); |
| destroySPBlobKey(getHandleName(handle)); |
| if (hasState(WEAVER_SLOT_NAME, handle, userId)) { |
| destroyWeaverSlot(handle, userId); |
| } |
| } |
| |
| private byte[] transformUnderWeaverSecret(byte[] data, byte[] secret) { |
| byte[] weaverSecret = SyntheticPasswordCrypto.personalisedHash( |
| PERSONALISATION_WEAVER_PASSWORD, secret); |
| byte[] result = new byte[data.length + weaverSecret.length]; |
| System.arraycopy(data, 0, result, 0, data.length); |
| System.arraycopy(weaverSecret, 0, result, data.length, weaverSecret.length); |
| return result; |
| } |
| |
| private byte[] transformUnderSecdiscardable(byte[] data, byte[] rawSecdiscardable) { |
| byte[] secdiscardable = SyntheticPasswordCrypto.personalisedHash( |
| PERSONALISATION_SECDISCARDABLE, rawSecdiscardable); |
| byte[] result = new byte[data.length + secdiscardable.length]; |
| System.arraycopy(data, 0, result, 0, data.length); |
| System.arraycopy(secdiscardable, 0, result, data.length, secdiscardable.length); |
| return result; |
| } |
| |
| private byte[] createSecdiscardable(long handle, int userId) { |
| byte[] data = secureRandom(SECDISCARDABLE_LENGTH); |
| saveSecdiscardable(handle, data, userId); |
| return data; |
| } |
| |
| private void saveSecdiscardable(long handle, byte[] secdiscardable, int userId) { |
| saveState(SECDISCARDABLE_NAME, secdiscardable, handle, userId); |
| } |
| |
| private byte[] loadSecdiscardable(long handle, int userId) { |
| return loadState(SECDISCARDABLE_NAME, handle, userId); |
| } |
| |
| private boolean hasState(String stateName, long handle, int userId) { |
| return !ArrayUtils.isEmpty(loadState(stateName, handle, userId)); |
| } |
| |
| private byte[] loadState(String stateName, long handle, int userId) { |
| return mStorage.readSyntheticPasswordState(userId, handle, stateName); |
| } |
| |
| private void saveState(String stateName, byte[] data, long handle, int userId) { |
| mStorage.writeSyntheticPasswordState(userId, handle, stateName, data); |
| } |
| |
| private void destroyState(String stateName, boolean secure, long handle, int userId) { |
| mStorage.deleteSyntheticPasswordState(userId, handle, stateName, secure); |
| } |
| |
| protected byte[] decryptSPBlob(String blobKeyName, byte[] blob, byte[] applicationId) { |
| return SyntheticPasswordCrypto.decryptBlob(blobKeyName, blob, applicationId); |
| } |
| |
| protected byte[] createSPBlob(String blobKeyName, byte[] data, byte[] applicationId, long sid) { |
| return SyntheticPasswordCrypto.createBlob(blobKeyName, data, applicationId, sid); |
| } |
| |
| protected void destroySPBlobKey(String keyAlias) { |
| SyntheticPasswordCrypto.destroyBlobKey(keyAlias); |
| } |
| |
| public static long generateHandle() { |
| SecureRandom rng = new SecureRandom(); |
| long result; |
| do { |
| result = rng.nextLong(); |
| } while (result == DEFAULT_HANDLE); |
| return result; |
| } |
| |
| private int fakeUid(int uid) { |
| return 100000 + uid; |
| } |
| |
| protected static byte[] secureRandom(int length) { |
| try { |
| return SecureRandom.getInstance("SHA1PRNG").generateSeed(length); |
| } catch (NoSuchAlgorithmException e) { |
| e.printStackTrace(); |
| return null; |
| } |
| } |
| |
| private String getHandleName(long handle) { |
| return String.format("%s%x", LockPatternUtils.SYNTHETIC_PASSWORD_KEY_PREFIX, handle); |
| } |
| |
| private byte[] computePasswordToken(String password, PasswordData data) { |
| return scrypt(password, data.salt, 1 << data.scryptN, 1 << data.scryptR, 1 << data.scryptP, |
| PASSWORD_TOKEN_LENGTH); |
| } |
| |
| private byte[] passwordTokenToGkInput(byte[] token) { |
| return SyntheticPasswordCrypto.personalisedHash(PERSONALIZATION_USER_GK_AUTH, token); |
| } |
| |
| private byte[] passwordTokenToWeaverKey(byte[] token) { |
| byte[] key = SyntheticPasswordCrypto.personalisedHash(PERSONALISATION_WEAVER_KEY, token); |
| if (key.length < mWeaverConfig.keySize) { |
| throw new RuntimeException("weaver key length too small"); |
| } |
| return Arrays.copyOf(key, mWeaverConfig.keySize); |
| } |
| |
| protected long sidFromPasswordHandle(byte[] handle) { |
| return nativeSidFromPasswordHandle(handle); |
| } |
| |
| protected byte[] scrypt(String password, byte[] salt, int N, int r, int p, int outLen) { |
| return nativeScrypt(password.getBytes(), salt, N, r, p, outLen); |
| } |
| |
| native long nativeSidFromPasswordHandle(byte[] handle); |
| native byte[] nativeScrypt(byte[] password, byte[] salt, int N, int r, int p, int outLen); |
| |
| protected static ArrayList<Byte> toByteArrayList(byte[] data) { |
| ArrayList<Byte> result = new ArrayList<Byte>(data.length); |
| for (int i = 0; i < data.length; i++) { |
| result.add(data[i]); |
| } |
| return result; |
| } |
| |
| protected static byte[] fromByteArrayList(ArrayList<Byte> data) { |
| byte[] result = new byte[data.size()]; |
| for (int i = 0; i < data.size(); i++) { |
| result[i] = data.get(i); |
| } |
| return result; |
| } |
| |
| final protected static char[] hexArray = "0123456789ABCDEF".toCharArray(); |
| public static String bytesToHex(byte[] bytes) { |
| if (bytes == null) { |
| return "null"; |
| } |
| char[] hexChars = new char[bytes.length * 2]; |
| for ( int j = 0; j < bytes.length; j++ ) { |
| int v = bytes[j] & 0xFF; |
| hexChars[j * 2] = hexArray[v >>> 4]; |
| hexChars[j * 2 + 1] = hexArray[v & 0x0F]; |
| } |
| return new String(hexChars); |
| } |
| } |