Add weak escrow token APIs

Bug: 206485383
Test: atest LockPatternUtilsTest WeakEscrowTokenTests
Change-Id: I24cdf9af6c8f29db03ef214a1a0f8effad394264
This commit is contained in:
Erin Yan
2021-10-27 13:47:45 -07:00
parent 566f1860a5
commit 459f61152e
11 changed files with 927 additions and 45 deletions

View File

@@ -176,6 +176,7 @@ package android {
field public static final String MANAGE_USB = "android.permission.MANAGE_USB";
field public static final String MANAGE_USERS = "android.permission.MANAGE_USERS";
field public static final String MANAGE_USER_OEM_UNLOCK_STATE = "android.permission.MANAGE_USER_OEM_UNLOCK_STATE";
field public static final String MANAGE_WEAK_ESCROW_TOKEN = "android.permission.MANAGE_WEAK_ESCROW_TOKEN";
field public static final String MANAGE_WIFI_AUTO_JOIN = "android.permission.MANAGE_WIFI_AUTO_JOIN";
field public static final String MANAGE_WIFI_COUNTRY_CODE = "android.permission.MANAGE_WIFI_COUNTRY_CODE";
field public static final String MARK_DEVICE_ORGANIZATION_OWNED = "android.permission.MARK_DEVICE_ORGANIZATION_OWNED";
@@ -757,18 +758,32 @@ package android.app {
}
public class KeyguardManager {
method @RequiresPermission(android.Manifest.permission.MANAGE_WEAK_ESCROW_TOKEN) public long addWeakEscrowToken(@NonNull byte[], @NonNull android.os.UserHandle, @NonNull java.util.concurrent.Executor, @NonNull android.app.KeyguardManager.WeakEscrowTokenActivatedListener);
method public android.content.Intent createConfirmFactoryResetCredentialIntent(CharSequence, CharSequence, CharSequence);
method @RequiresPermission("android.permission.SET_INITIAL_LOCK") public int getMinLockLength(boolean, int);
method @RequiresPermission(android.Manifest.permission.CONTROL_KEYGUARD_SECURE_NOTIFICATIONS) public boolean getPrivateNotificationsAllowed();
method @RequiresPermission("android.permission.SET_INITIAL_LOCK") public boolean isValidLockPasswordComplexity(int, @NonNull byte[], int);
method @RequiresPermission(android.Manifest.permission.MANAGE_WEAK_ESCROW_TOKEN) public boolean isWeakEscrowTokenActive(long, @NonNull android.os.UserHandle);
method @RequiresPermission(android.Manifest.permission.MANAGE_WEAK_ESCROW_TOKEN) public boolean isWeakEscrowTokenValid(long, @NonNull byte[], @NonNull android.os.UserHandle);
method @RequiresPermission(android.Manifest.permission.MANAGE_WEAK_ESCROW_TOKEN) public boolean registerWeakEscrowTokenRemovedListener(@NonNull java.util.concurrent.Executor, @NonNull android.app.KeyguardManager.WeakEscrowTokenRemovedListener);
method @RequiresPermission(android.Manifest.permission.MANAGE_WEAK_ESCROW_TOKEN) public boolean removeWeakEscrowToken(long, @NonNull android.os.UserHandle);
method @RequiresPermission(android.Manifest.permission.SHOW_KEYGUARD_MESSAGE) public void requestDismissKeyguard(@NonNull android.app.Activity, @Nullable CharSequence, @Nullable android.app.KeyguardManager.KeyguardDismissCallback);
method @RequiresPermission("android.permission.SET_INITIAL_LOCK") public boolean setLock(int, @NonNull byte[], int);
method @RequiresPermission(android.Manifest.permission.CONTROL_KEYGUARD_SECURE_NOTIFICATIONS) public void setPrivateNotificationsAllowed(boolean);
method @RequiresPermission(android.Manifest.permission.MANAGE_WEAK_ESCROW_TOKEN) public boolean unregisterWeakEscrowTokenRemovedListener(@NonNull android.app.KeyguardManager.WeakEscrowTokenRemovedListener);
field public static final int PASSWORD = 0; // 0x0
field public static final int PATTERN = 2; // 0x2
field public static final int PIN = 1; // 0x1
}
public static interface KeyguardManager.WeakEscrowTokenActivatedListener {
method public void onWeakEscrowTokenActivated(long, @NonNull android.os.UserHandle);
}
public static interface KeyguardManager.WeakEscrowTokenRemovedListener {
method public void onWeakEscrowTokenRemoved(long, @NonNull android.os.UserHandle);
}
public class LocaleManager {
method @NonNull @RequiresPermission(android.Manifest.permission.READ_APP_SPECIFIC_LOCALES) public android.os.LocaleList getApplicationLocales(@NonNull String);
method @RequiresPermission(android.Manifest.permission.CHANGE_CONFIGURATION) public void setApplicationLocales(@NonNull String, @NonNull android.os.LocaleList);

View File

@@ -17,6 +17,7 @@
package android.app;
import android.Manifest;
import android.annotation.CallbackExecutor;
import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
@@ -40,8 +41,10 @@ import android.os.IBinder;
import android.os.RemoteException;
import android.os.ServiceManager;
import android.os.ServiceManager.ServiceNotFoundException;
import android.os.UserHandle;
import android.provider.Settings;
import android.service.persistentdata.IPersistentDataBlockService;
import android.util.ArrayMap;
import android.util.Log;
import android.view.IOnKeyguardExitResult;
import android.view.IWindowManager;
@@ -49,6 +52,9 @@ import android.view.WindowManager.LayoutParams;
import android.view.WindowManagerGlobal;
import com.android.internal.policy.IKeyguardDismissCallback;
import com.android.internal.util.Preconditions;
import com.android.internal.widget.IWeakEscrowTokenActivatedListener;
import com.android.internal.widget.IWeakEscrowTokenRemovedListener;
import com.android.internal.widget.LockPatternUtils;
import com.android.internal.widget.LockPatternView;
import com.android.internal.widget.LockscreenCredential;
@@ -57,6 +63,8 @@ import com.android.internal.widget.VerifyCredentialResponse;
import java.nio.charset.Charset;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.Executor;
/**
* Class that can be used to lock and unlock the keyguard. The
@@ -69,10 +77,13 @@ public class KeyguardManager {
private static final String TAG = "KeyguardManager";
private final Context mContext;
private final LockPatternUtils mLockPatternUtils;
private final IWindowManager mWM;
private final IActivityManager mAm;
private final ITrustManager mTrustManager;
private final INotificationManager mNotificationManager;
private final ArrayMap<WeakEscrowTokenRemovedListener, IWeakEscrowTokenRemovedListener>
mListeners = new ArrayMap<>();
/**
* Intent used to prompt user for device credentials.
@@ -455,8 +466,42 @@ public class KeyguardManager {
public void onDismissCancelled() { }
}
/**
* Callback passed to
* {@link KeyguardManager#addWeakEscrowToken}
* to notify caller of state change.
* @hide
*/
@SystemApi
public interface WeakEscrowTokenActivatedListener {
/**
* The method to be called when the token is activated.
* @param handle 64 bit handle corresponding to the escrow token
* @param user user for whom the weak escrow token has been added
*/
void onWeakEscrowTokenActivated(long handle, @NonNull UserHandle user);
}
/**
* Listener passed to
* {@link KeyguardManager#registerWeakEscrowTokenRemovedListener} and
* {@link KeyguardManager#unregisterWeakEscrowTokenRemovedListener}
* to notify caller of an weak escrow token has been removed.
* @hide
*/
@SystemApi
public interface WeakEscrowTokenRemovedListener {
/**
* The method to be called when the token is removed.
* @param handle 64 bit handle corresponding to the escrow token
* @param user user for whom the escrow token has been added
*/
void onWeakEscrowTokenRemoved(long handle, @NonNull UserHandle user);
}
KeyguardManager(Context context) throws ServiceNotFoundException {
mContext = context;
mLockPatternUtils = new LockPatternUtils(context);
mWM = WindowManagerGlobal.getWindowManagerService();
mAm = ActivityManager.getService();
mTrustManager = ITrustManager.Stub.asInterface(
@@ -785,7 +830,6 @@ public class KeyguardManager {
return false;
}
LockPatternUtils lockPatternUtils = new LockPatternUtils(mContext);
int userId = mContext.getUserId();
if (isDeviceSecure(userId)) {
Log.e(TAG, "Password already set, rejecting call to setLock");
@@ -799,7 +843,7 @@ public class KeyguardManager {
try {
LockscreenCredential credential = createLockscreenCredential(
lockType, password);
success = lockPatternUtils.setLockCredential(
success = mLockPatternUtils.setLockCredential(
credential,
/* savedPassword= */ LockscreenCredential.createNone(),
userId);
@@ -812,6 +856,150 @@ public class KeyguardManager {
return success;
}
/**
* Create a weak escrow token for the current user, which can later be used to unlock FBE
* or change user password.
*
* After adding, if the user currently has a secure lockscreen, they will need to perform a
* confirm credential operation in order to activate the token for future use. If the user
* has no secure lockscreen, then the token is activated immediately.
*
* If the user changes or removes the lockscreen password, any activated weak escrow token will
* be removed.
*
* @return a unique 64-bit token handle which is needed to refer to this token later.
* @hide
*/
@RequiresFeature(PackageManager.FEATURE_AUTOMOTIVE)
@RequiresPermission(Manifest.permission.MANAGE_WEAK_ESCROW_TOKEN)
@SystemApi
public long addWeakEscrowToken(@NonNull byte[] token, @NonNull UserHandle user,
@NonNull @CallbackExecutor Executor executor,
@NonNull WeakEscrowTokenActivatedListener listener) {
Objects.requireNonNull(token, "Token cannot be null.");
Objects.requireNonNull(user, "User cannot be null.");
Objects.requireNonNull(executor, "Executor cannot be null.");
Objects.requireNonNull(listener, "Listener cannot be null.");
int userId = user.getIdentifier();
IWeakEscrowTokenActivatedListener internalListener =
new IWeakEscrowTokenActivatedListener.Stub() {
@Override
public void onWeakEscrowTokenActivated(long handle, int userId) {
UserHandle user = UserHandle.of(userId);
final long restoreToken = Binder.clearCallingIdentity();
try {
executor.execute(() -> listener.onWeakEscrowTokenActivated(handle, user));
} finally {
Binder.restoreCallingIdentity(restoreToken);
}
Log.i(TAG, "Weak escrow token activated.");
}
};
return mLockPatternUtils.addWeakEscrowToken(token, userId, internalListener);
}
/**
* Remove a weak escrow token.
*
* @return true if the given handle refers to a valid weak token previously returned from
* {@link #addWeakEscrowToken}, whether it's active or not. return false otherwise.
* @hide
*/
@RequiresFeature(PackageManager.FEATURE_AUTOMOTIVE)
@RequiresPermission(Manifest.permission.MANAGE_WEAK_ESCROW_TOKEN)
@SystemApi
public boolean removeWeakEscrowToken(long handle, @NonNull UserHandle user) {
Objects.requireNonNull(user, "User cannot be null.");
return mLockPatternUtils.removeWeakEscrowToken(handle, user.getIdentifier());
}
/**
* Check if the given weak escrow token is active or not.
* @hide
*/
@RequiresFeature(PackageManager.FEATURE_AUTOMOTIVE)
@RequiresPermission(Manifest.permission.MANAGE_WEAK_ESCROW_TOKEN)
@SystemApi
public boolean isWeakEscrowTokenActive(long handle, @NonNull UserHandle user) {
Objects.requireNonNull(user, "User cannot be null.");
return mLockPatternUtils.isWeakEscrowTokenActive(handle, user.getIdentifier());
}
/**
* Check if the given weak escrow token is validate.
* @hide
*/
@RequiresFeature(PackageManager.FEATURE_AUTOMOTIVE)
@RequiresPermission(Manifest.permission.MANAGE_WEAK_ESCROW_TOKEN)
@SystemApi
public boolean isWeakEscrowTokenValid(long handle, @NonNull byte[] token,
@NonNull UserHandle user) {
Objects.requireNonNull(token, "Token cannot be null.");
Objects.requireNonNull(user, "User cannot be null.");
return mLockPatternUtils.isWeakEscrowTokenValid(handle, token, user.getIdentifier());
}
/**
* Register the given WeakEscrowTokenRemovedListener.
*
* @return true if the listener is registered successfully, return false otherwise.
* @hide
*/
@RequiresFeature(PackageManager.FEATURE_AUTOMOTIVE)
@RequiresPermission(Manifest.permission.MANAGE_WEAK_ESCROW_TOKEN)
@SystemApi
public boolean registerWeakEscrowTokenRemovedListener(
@NonNull @CallbackExecutor Executor executor,
@NonNull WeakEscrowTokenRemovedListener listener) {
Objects.requireNonNull(listener, "Listener cannot be null.");
Objects.requireNonNull(executor, "Executor cannot be null.");
Preconditions.checkArgument(!mListeners.containsKey(listener),
"Listener already registered: %s", listener);
IWeakEscrowTokenRemovedListener internalListener =
new IWeakEscrowTokenRemovedListener.Stub() {
@Override
public void onWeakEscrowTokenRemoved(long handle, int userId) {
UserHandle user = UserHandle.of(userId);
final long token = Binder.clearCallingIdentity();
try {
executor.execute(() -> listener.onWeakEscrowTokenRemoved(handle, user));
} finally {
Binder.restoreCallingIdentity(token);
}
}
};
if (mLockPatternUtils.registerWeakEscrowTokenRemovedListener(internalListener)) {
mListeners.put(listener, internalListener);
return true;
} else {
Log.e(TAG, "Listener failed to register");
return false;
}
}
/**
* Unregister the given WeakEscrowTokenRemovedListener.
*
* @return true if the listener is unregistered successfully, return false otherwise.
* @hide
*/
@RequiresFeature(PackageManager.FEATURE_AUTOMOTIVE)
@RequiresPermission(Manifest.permission.MANAGE_WEAK_ESCROW_TOKEN)
@SystemApi
public boolean unregisterWeakEscrowTokenRemovedListener(
@NonNull WeakEscrowTokenRemovedListener listener) {
Objects.requireNonNull(listener, "Listener cannot be null.");
IWeakEscrowTokenRemovedListener internalListener = mListeners.get(listener);
Preconditions.checkArgument(internalListener != null, "Listener was not registered");
if (mLockPatternUtils.unregisterWeakEscrowTokenRemovedListener(internalListener)) {
mListeners.remove(listener);
return true;
} else {
Log.e(TAG, "Listener failed to unregister.");
return false;
}
}
/**
* Set the lockscreen password to {@code newPassword} after validating the current password
* against {@code currentPassword}.
@@ -832,13 +1020,12 @@ public class KeyguardManager {
})
public boolean setLock(@LockTypes int newLockType, @Nullable byte[] newPassword,
@LockTypes int currentLockType, @Nullable byte[] currentPassword) {
final LockPatternUtils lockPatternUtils = new LockPatternUtils(mContext);
final int userId = mContext.getUserId();
LockscreenCredential currentCredential = createLockscreenCredential(
currentLockType, currentPassword);
LockscreenCredential newCredential = createLockscreenCredential(
newLockType, newPassword);
return lockPatternUtils.setLockCredential(newCredential, currentCredential, userId);
return mLockPatternUtils.setLockCredential(newCredential, currentCredential, userId);
}
/**
@@ -857,10 +1044,9 @@ public class KeyguardManager {
Manifest.permission.ACCESS_KEYGUARD_SECURE_STORAGE
})
public boolean checkLock(@LockTypes int lockType, @Nullable byte[] password) {
final LockPatternUtils lockPatternUtils = new LockPatternUtils(mContext);
final LockscreenCredential credential = createLockscreenCredential(
lockType, password);
final VerifyCredentialResponse response = lockPatternUtils.verifyCredential(
final VerifyCredentialResponse response = mLockPatternUtils.verifyCredential(
credential, mContext.getUserId(), /* flags= */ 0);
if (response == null) {
return false;

View File

@@ -24,6 +24,8 @@ import android.security.keystore.recovery.KeyChainSnapshot;
import android.security.keystore.recovery.KeyChainProtectionParams;
import android.security.keystore.recovery.RecoveryCertPath;
import com.android.internal.widget.ICheckCredentialProgressCallback;
import com.android.internal.widget.IWeakEscrowTokenActivatedListener;
import com.android.internal.widget.IWeakEscrowTokenRemovedListener;
import com.android.internal.widget.LockscreenCredential;
import com.android.internal.widget.VerifyCredentialResponse;
@@ -96,4 +98,10 @@ interface ILockSettings {
boolean tryUnlockWithCachedUnifiedChallenge(int userId);
void removeCachedUnifiedChallenge(int userId);
void updateEncryptionPassword(int type, in byte[] password);
boolean registerWeakEscrowTokenRemovedListener(in IWeakEscrowTokenRemovedListener listener);
boolean unregisterWeakEscrowTokenRemovedListener(in IWeakEscrowTokenRemovedListener listener);
long addWeakEscrowToken(in byte[] token, int userId, in IWeakEscrowTokenActivatedListener callback);
boolean removeWeakEscrowToken(long handle, int userId);
boolean isWeakEscrowTokenActive(long handle, int userId);
boolean isWeakEscrowTokenValid(long handle, in byte[] token, int userId);
}

View File

@@ -0,0 +1,22 @@
/*
* Copyright (C) 2022 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.internal.widget;
/** @hide */
oneway interface IWeakEscrowTokenActivatedListener {
void onWeakEscrowTokenActivated(long handle, int userId);
}

View File

@@ -0,0 +1,22 @@
/*
* Copyright (C) 2022 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.internal.widget;
/** @hide */
oneway interface IWeakEscrowTokenRemovedListener {
void onWeakEscrowTokenRemoved(long handle, int userId);
}

View File

@@ -1236,6 +1236,28 @@ public class LockPatternUtils {
}
}
/** Register the given WeakEscrowTokenRemovedListener. */
public boolean registerWeakEscrowTokenRemovedListener(
@NonNull final IWeakEscrowTokenRemovedListener listener) {
try {
return getLockSettings().registerWeakEscrowTokenRemovedListener(listener);
} catch (RemoteException e) {
Log.e(TAG, "Could not register WeakEscrowTokenRemovedListener.");
throw e.rethrowFromSystemServer();
}
}
/** Unregister the given WeakEscrowTokenRemovedListener. */
public boolean unregisterWeakEscrowTokenRemovedListener(
@NonNull final IWeakEscrowTokenRemovedListener listener) {
try {
return getLockSettings().unregisterWeakEscrowTokenRemovedListener(listener);
} catch (RemoteException e) {
Log.e(TAG, "Could not register WeakEscrowTokenRemovedListener.");
throw e.rethrowFromSystemServer();
}
}
public void reportSuccessfulBiometricUnlock(boolean isStrongBiometric, int userId) {
try {
getLockSettings().reportSuccessfulBiometricUnlock(isStrongBiometric, userId);
@@ -1354,6 +1376,29 @@ public class LockPatternUtils {
return getLockSettingsInternal().addEscrowToken(token, userId, callback);
}
/**
* Create a weak escrow token for the current user, which can later be used to unlock FBE
* or change user password.
*
* After adding, if the user currently has lockscreen password, they will need to perform a
* confirm credential operation in order to activate the token for future use. If the user
* has no secure lockscreen, then the token is activated immediately.
*
* If the user changes or removes lockscreen password, activated weak escrow tokens will be
* removed.
*
* @return a unique 64-bit token handle which is needed to refer to this token later.
*/
public long addWeakEscrowToken(byte[] token, int userId,
@NonNull IWeakEscrowTokenActivatedListener callback) {
try {
return getLockSettings().addWeakEscrowToken(token, userId, callback);
} catch (RemoteException e) {
Log.e(TAG, "Could not add weak token.");
throw e.rethrowFromSystemServer();
}
}
/**
* Callback interface to notify when an added escrow token has been activated.
*/
@@ -1361,9 +1406,9 @@ public class LockPatternUtils {
/**
* The method to be called when the token is activated.
* @param handle 64 bit handle corresponding to the escrow token
* @param userid user for whom the escrow token has been added
* @param userId user for whom the escrow token has been added
*/
void onEscrowTokenActivated(long handle, int userid);
void onEscrowTokenActivated(long handle, int userId);
}
/**
@@ -1378,6 +1423,21 @@ public class LockPatternUtils {
return getLockSettingsInternal().removeEscrowToken(handle, userId);
}
/**
* Remove a weak escrow token.
*
* @return true if the given handle refers to a valid weak token previously returned from
* {@link #addWeakEscrowToken}, whether it's active or not. return false otherwise.
*/
public boolean removeWeakEscrowToken(long handle, int userId) {
try {
return getLockSettings().removeWeakEscrowToken(handle, userId);
} catch (RemoteException e) {
Log.e(TAG, "Could not remove the weak token.");
throw e.rethrowFromSystemServer();
}
}
/**
* Check if the given escrow token is active or not. Only active token can be used to call
* {@link #setLockCredentialWithToken} and {@link #unlockUserWithToken}
@@ -1388,6 +1448,29 @@ public class LockPatternUtils {
return getLockSettingsInternal().isEscrowTokenActive(handle, userId);
}
/**
* Check if the given weak escrow token is active or not. Only active token can be used to call
* {@link #setLockCredentialWithToken} and {@link #unlockUserWithToken}
*/
public boolean isWeakEscrowTokenActive(long handle, int userId) {
try {
return getLockSettings().isWeakEscrowTokenActive(handle, userId);
} catch (RemoteException e) {
Log.e(TAG, "Could not check the weak token.");
throw e.rethrowFromSystemServer();
}
}
/** Check if the given weak escrow token is valid. */
public boolean isWeakEscrowTokenValid(long handle, byte[] token, int userId) {
try {
return getLockSettings().isWeakEscrowTokenValid(handle, token, userId);
} catch (RemoteException e) {
Log.e(TAG, "Could not validate the weak token.");
throw e.rethrowFromSystemServer();
}
}
/**
* Change a user's lock credential with a pre-configured escrow token.
*

View File

@@ -5294,6 +5294,12 @@
<permission android:name="android.permission.CONTROL_KEYGUARD_SECURE_NOTIFICATIONS"
android:protectionLevel="signature|privileged" />
<!-- @SystemApi Allows an application to manage weak escrow token on the device. This permission
is not available to third party applications.
@hide -->
<permission android:name="android.permission.MANAGE_WEAK_ESCROW_TOKEN"
android:protectionLevel="signature|privileged" />
<!-- Allows an application to listen to trust changes. Only allowed for system processes.
@hide -->
<permission android:name="android.permission.TRUST_LISTENER"

View File

@@ -21,13 +21,16 @@ import static android.app.admin.DevicePolicyManager.PASSWORD_QUALITY_UNSPECIFIED
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import android.content.Context;
import android.content.ContextWrapper;
import android.content.pm.UserInfo;
import android.os.RemoteException;
import android.os.UserManager;
import android.provider.Settings;
import android.test.mock.MockContentResolver;
@@ -38,12 +41,16 @@ import androidx.test.runner.AndroidJUnit4;
import com.android.internal.util.test.FakeSettingsProvider;
import com.android.internal.widget.ILockSettings;
import com.android.internal.widget.IWeakEscrowTokenActivatedListener;
import com.android.internal.widget.IWeakEscrowTokenRemovedListener;
import com.android.internal.widget.LockPatternUtils;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mockito;
import java.nio.charset.StandardCharsets;
@RunWith(AndroidJUnit4.class)
@SmallTest
public class LockPatternUtilsTest {
@@ -102,4 +109,84 @@ public class LockPatternUtilsTest {
configureTest(false, true, 0);
assertFalse(mLockPatternUtils.isLockScreenDisabled(DEMO_USER_ID));
}
@Test
public void testAddWeakEscrowToken() throws RemoteException {
ILockSettings ils = createTestLockSettings();
byte[] testToken = "test_token".getBytes(StandardCharsets.UTF_8);
int testUserId = 10;
IWeakEscrowTokenActivatedListener listener = createWeakEscrowTokenListener();
mLockPatternUtils.addWeakEscrowToken(testToken, testUserId, listener);
verify(ils).addWeakEscrowToken(eq(testToken), eq(testUserId), eq(listener));
}
@Test
public void testRegisterWeakEscrowTokenRemovedListener() throws RemoteException {
ILockSettings ils = createTestLockSettings();
IWeakEscrowTokenRemovedListener testListener = createTestAutoEscrowTokenRemovedListener();
mLockPatternUtils.registerWeakEscrowTokenRemovedListener(testListener);
verify(ils).registerWeakEscrowTokenRemovedListener(eq(testListener));
}
@Test
public void testUnregisterWeakEscrowTokenRemovedListener() throws RemoteException {
ILockSettings ils = createTestLockSettings();
IWeakEscrowTokenRemovedListener testListener = createTestAutoEscrowTokenRemovedListener();
mLockPatternUtils.unregisterWeakEscrowTokenRemovedListener(testListener);
verify(ils).unregisterWeakEscrowTokenRemovedListener(eq(testListener));
}
@Test
public void testRemoveAutoEscrowToken() throws RemoteException {
ILockSettings ils = createTestLockSettings();
int testUserId = 10;
long testHandle = 100L;
mLockPatternUtils.removeWeakEscrowToken(testHandle, testUserId);
verify(ils).removeWeakEscrowToken(eq(testHandle), eq(testUserId));
}
@Test
public void testIsAutoEscrowTokenActive() throws RemoteException {
ILockSettings ils = createTestLockSettings();
int testUserId = 10;
long testHandle = 100L;
mLockPatternUtils.isWeakEscrowTokenActive(testHandle, testUserId);
verify(ils).isWeakEscrowTokenActive(eq(testHandle), eq(testUserId));
}
@Test
public void testIsAutoEscrowTokenValid() throws RemoteException {
ILockSettings ils = createTestLockSettings();
int testUserId = 10;
byte[] testToken = "test_token".getBytes(StandardCharsets.UTF_8);
long testHandle = 100L;
mLockPatternUtils.isWeakEscrowTokenValid(testHandle, testToken, testUserId);
verify(ils).isWeakEscrowTokenValid(eq(testHandle), eq(testToken), eq(testUserId));
}
private ILockSettings createTestLockSettings() {
final Context context = spy(new ContextWrapper(InstrumentationRegistry.getTargetContext()));
mLockPatternUtils = spy(new LockPatternUtils(context));
final ILockSettings ils = Mockito.mock(ILockSettings.class);
when(mLockPatternUtils.getLockSettings()).thenReturn(ils);
return ils;
}
private IWeakEscrowTokenActivatedListener createWeakEscrowTokenListener() {
return new IWeakEscrowTokenActivatedListener.Stub() {
@Override
public void onWeakEscrowTokenActivated(long handle, int userId) {
// Do nothing.
}
};
}
private IWeakEscrowTokenRemovedListener createTestAutoEscrowTokenRemovedListener() {
return new IWeakEscrowTokenRemovedListener.Stub() {
@Override
public void onWeakEscrowTokenRemoved(long handle, int userId) {
// Do nothing.
}
};
}
}

View File

@@ -39,7 +39,10 @@ import static com.android.internal.widget.LockPatternUtils.USER_FRP;
import static com.android.internal.widget.LockPatternUtils.VERIFY_FLAG_REQUEST_GK_PW_HANDLE;
import static com.android.internal.widget.LockPatternUtils.frpCredentialEnabled;
import static com.android.internal.widget.LockPatternUtils.userOwnsFrpCredential;
import static com.android.server.locksettings.SyntheticPasswordManager.TOKEN_TYPE_STRONG;
import static com.android.server.locksettings.SyntheticPasswordManager.TOKEN_TYPE_WEAK;
import android.Manifest;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.UserIdInt;
@@ -123,6 +126,8 @@ import com.android.internal.util.IndentingPrintWriter;
import com.android.internal.util.Preconditions;
import com.android.internal.widget.ICheckCredentialProgressCallback;
import com.android.internal.widget.ILockSettings;
import com.android.internal.widget.IWeakEscrowTokenActivatedListener;
import com.android.internal.widget.IWeakEscrowTokenRemovedListener;
import com.android.internal.widget.LockPatternUtils;
import com.android.internal.widget.LockSettingsInternal;
import com.android.internal.widget.LockscreenCredential;
@@ -135,6 +140,7 @@ import com.android.server.locksettings.LockSettingsStorage.CredentialHash;
import com.android.server.locksettings.LockSettingsStorage.PersistentData;
import com.android.server.locksettings.SyntheticPasswordManager.AuthenticationResult;
import com.android.server.locksettings.SyntheticPasswordManager.AuthenticationToken;
import com.android.server.locksettings.SyntheticPasswordManager.TokenType;
import com.android.server.locksettings.recoverablekeystore.RecoverableKeyStoreManager;
import com.android.server.pm.UserManagerInternal;
import com.android.server.wm.WindowManagerInternal;
@@ -1123,6 +1129,16 @@ public class LockSettingsService extends ILockSettings.Stub {
return mContext.checkCallingOrSelfPermission(permission) == PERMISSION_GRANTED;
}
private void checkManageWeakEscrowTokenMethodUsage() {
mContext.enforceCallingOrSelfPermission(
Manifest.permission.MANAGE_WEAK_ESCROW_TOKEN,
"Requires MANAGE_WEAK_ESCROW_TOKEN permission.");
if (!mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_AUTOMOTIVE)) {
throw new IllegalArgumentException(
"Weak escrow token are only for automotive devices.");
}
}
@Override
public boolean hasSecureLockScreen() {
return mHasSecureLockScreen;
@@ -1911,6 +1927,97 @@ public class LockSettingsService extends ILockSettings.Stub {
});
}
/** Register the given WeakEscrowTokenRemovedListener. */
@Override
public boolean registerWeakEscrowTokenRemovedListener(
@NonNull IWeakEscrowTokenRemovedListener listener) {
checkManageWeakEscrowTokenMethodUsage();
final long token = Binder.clearCallingIdentity();
try {
return mSpManager.registerWeakEscrowTokenRemovedListener(listener);
} finally {
Binder.restoreCallingIdentity(token);
}
}
/** Unregister the given WeakEscrowTokenRemovedListener. */
@Override
public boolean unregisterWeakEscrowTokenRemovedListener(
@NonNull IWeakEscrowTokenRemovedListener listener) {
checkManageWeakEscrowTokenMethodUsage();
final long token = Binder.clearCallingIdentity();
try {
return mSpManager.unregisterWeakEscrowTokenRemovedListener(listener);
} finally {
Binder.restoreCallingIdentity(token);
}
}
@Override
public long addWeakEscrowToken(byte[] token, int userId,
@NonNull IWeakEscrowTokenActivatedListener listener) {
checkManageWeakEscrowTokenMethodUsage();
Objects.requireNonNull(listener, "Listener can not be null.");
EscrowTokenStateChangeCallback internalListener = (handle, userId1) -> {
try {
listener.onWeakEscrowTokenActivated(handle, userId1);
} catch (RemoteException e) {
Slog.e(TAG, "Exception while notifying weak escrow token has been activated", e);
}
};
final long restoreToken = Binder.clearCallingIdentity();
try {
return addEscrowToken(token, TOKEN_TYPE_WEAK, userId, internalListener);
} finally {
Binder.restoreCallingIdentity(restoreToken);
}
}
@Override
public boolean removeWeakEscrowToken(long handle, int userId) {
checkManageWeakEscrowTokenMethodUsage();
final long token = Binder.clearCallingIdentity();
try {
return removeEscrowToken(handle, userId);
} finally {
Binder.restoreCallingIdentity(token);
}
}
@Override
public boolean isWeakEscrowTokenActive(long handle, int userId) {
checkManageWeakEscrowTokenMethodUsage();
final long token = Binder.clearCallingIdentity();
try {
return isEscrowTokenActive(handle, userId);
} finally {
Binder.restoreCallingIdentity(token);
}
}
@Override
public boolean isWeakEscrowTokenValid(long handle, byte[] token, int userId) {
checkManageWeakEscrowTokenMethodUsage();
final long restoreToken = Binder.clearCallingIdentity();
try {
synchronized (mSpManager) {
if (!mSpManager.hasEscrowData(userId)) {
Slog.w(TAG, "Escrow token is disabled on the current user");
return false;
}
AuthenticationResult authResult = mSpManager.unwrapWeakTokenBasedSyntheticPassword(
getGateKeeperService(), handle, token, userId);
if (authResult.authToken == null) {
Slog.w(TAG, "Invalid escrow token supplied");
return false;
}
return true;
}
} finally {
Binder.restoreCallingIdentity(restoreToken);
}
}
@VisibleForTesting /** Note: this method is overridden in unit tests */
protected void tieProfileLockToParent(int userId, LockscreenCredential password) {
if (DEBUG) Slog.v(TAG, "tieProfileLockToParent for user: " + userId);
@@ -3029,6 +3136,7 @@ public class LockSettingsService extends ILockSettings.Stub {
private long setLockCredentialWithAuthTokenLocked(LockscreenCredential credential,
AuthenticationToken auth, int userId) {
if (DEBUG) Slog.d(TAG, "setLockCredentialWithAuthTokenLocked: user=" + userId);
final int savedCredentialType = getCredentialTypeInternal(userId);
long newHandle = mSpManager.createPasswordBasedSyntheticPassword(getGateKeeperService(),
credential, auth, userId);
final Map<Integer, LockscreenCredential> profilePasswords;
@@ -3075,6 +3183,9 @@ public class LockSettingsService extends ILockSettings.Stub {
setUserPasswordMetrics(credential, userId);
mManagedProfilePasswordCache.removePassword(userId);
if (savedCredentialType != CREDENTIAL_TYPE_NONE) {
mSpManager.destroyAllWeakTokenBasedSyntheticPasswords(userId);
}
if (profilePasswords != null) {
for (Map.Entry<Integer, LockscreenCredential> entry : profilePasswords.entrySet()) {
@@ -3242,8 +3353,9 @@ public class LockSettingsService extends ILockSettings.Stub {
}
}
private long addEscrowToken(byte[] token, int userId, EscrowTokenStateChangeCallback callback) {
if (DEBUG) Slog.d(TAG, "addEscrowToken: user=" + userId);
private long addEscrowToken(@NonNull byte[] token, @TokenType int type, int userId,
@NonNull EscrowTokenStateChangeCallback callback) {
if (DEBUG) Slog.d(TAG, "addEscrowToken: user=" + userId + ", type=" + type);
synchronized (mSpManager) {
// Migrate to synthetic password based credentials if the user has no password,
// the token can then be activated immediately.
@@ -3264,7 +3376,8 @@ public class LockSettingsService extends ILockSettings.Stub {
throw new SecurityException("Escrow token is disabled on the current user");
}
}
long handle = mSpManager.createTokenBasedSyntheticPassword(token, userId, callback);
long handle = mSpManager.createTokenBasedSyntheticPassword(token, type, userId,
callback);
if (auth != null) {
mSpManager.activateTokenBasedSyntheticPassword(handle, auth, userId);
}
@@ -3345,8 +3458,8 @@ public class LockSettingsService extends ILockSettings.Stub {
private boolean setLockCredentialWithTokenInternalLocked(LockscreenCredential credential,
long tokenHandle, byte[] token, int userId) {
final AuthenticationResult result;
result = mSpManager.unwrapTokenBasedSyntheticPassword(
getGateKeeperService(), tokenHandle, token, userId);
result = mSpManager.unwrapTokenBasedSyntheticPassword(getGateKeeperService(), tokenHandle,
token, userId);
if (result.authToken == null) {
Slog.w(TAG, "Invalid escrow token supplied");
return false;
@@ -3369,7 +3482,8 @@ public class LockSettingsService extends ILockSettings.Stub {
AuthenticationResult authResult;
synchronized (mSpManager) {
if (!mSpManager.hasEscrowData(userId)) {
throw new SecurityException("Escrow token is disabled on the current user");
Slog.w(TAG, "Escrow token is disabled on the current user");
return false;
}
authResult = mSpManager.unwrapTokenBasedSyntheticPassword(getGateKeeperService(),
tokenHandle, token, userId);
@@ -3643,7 +3757,8 @@ public class LockSettingsService extends ILockSettings.Stub {
@Override
public long addEscrowToken(byte[] token, int userId,
EscrowTokenStateChangeCallback callback) {
return LockSettingsService.this.addEscrowToken(token, userId, callback);
return LockSettingsService.this.addEscrowToken(token, TOKEN_TYPE_STRONG, userId,
callback);
}
@Override

View File

@@ -18,6 +18,7 @@ package com.android.server.locksettings;
import static com.android.internal.widget.LockPatternUtils.EscrowTokenStateChangeCallback;
import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.admin.PasswordMetrics;
@@ -28,6 +29,7 @@ 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.RemoteCallbackList;
import android.os.RemoteException;
import android.os.UserManager;
import android.security.GateKeeper;
@@ -41,6 +43,7 @@ import android.util.Slog;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.ArrayUtils;
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;
@@ -48,6 +51,8 @@ import com.android.server.locksettings.LockSettingsStorage.PersistentData;
import libcore.util.HexEncoding;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.nio.ByteBuffer;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
@@ -111,7 +116,8 @@ public class SyntheticPasswordManager {
private static final byte SYNTHETIC_PASSWORD_VERSION_V2 = 2;
private static final byte SYNTHETIC_PASSWORD_VERSION_V3 = 3;
private static final byte SYNTHETIC_PASSWORD_PASSWORD_BASED = 0;
private static final byte SYNTHETIC_PASSWORD_TOKEN_BASED = 1;
private static final byte SYNTHETIC_PASSWORD_STRONG_TOKEN_BASED = 1;
private static final byte SYNTHETIC_PASSWORD_WEAK_TOKEN_BASED = 2;
// 256-bit synthetic password
private static final byte SYNTHETIC_PASSWORD_LENGTH = 256 / 8;
@@ -366,10 +372,47 @@ public class SyntheticPasswordManager {
}
}
static class SyntheticPasswordBlob {
byte mVersion;
byte mType;
byte[] mContent;
public static SyntheticPasswordBlob create(byte version, byte type, byte[] content) {
SyntheticPasswordBlob result = new SyntheticPasswordBlob();
result.mVersion = version;
result.mType = type;
result.mContent = content;
return result;
}
public static SyntheticPasswordBlob fromBytes(byte[] data) {
SyntheticPasswordBlob result = new SyntheticPasswordBlob();
result.mVersion = data[0];
result.mType = 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] = mType;
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;
static class TokenData {
byte[] secdiscardableOnDisk;
byte[] weaverSecret;
byte[] aggregatedSecret;
@TokenType int mType;
EscrowTokenStateChangeCallback mCallback;
}
@@ -381,6 +424,9 @@ public class SyntheticPasswordManager {
private final UserManager mUserManager;
private final RemoteCallbackList<IWeakEscrowTokenRemovedListener> mListeners =
new RemoteCallbackList<>();
public SyntheticPasswordManager(Context context, LockSettingsStorage storage,
UserManager userManager, PasswordSlotManager passwordSlotManager) {
mContext = context;
@@ -880,13 +926,34 @@ public class SyntheticPasswordManager {
* Create a token based Synthetic password for the given user.
* @return the handle of the token
*/
public long createTokenBasedSyntheticPassword(byte[] token, int userId,
public long createStrongTokenBasedSyntheticPassword(byte[] token, int userId,
@Nullable EscrowTokenStateChangeCallback changeCallback) {
return createTokenBasedSyntheticPassword(token, TOKEN_TYPE_STRONG, userId,
changeCallback);
}
/**
* Create a weak token based Synthetic password for the given user.
* @return the handle of the weak token
*/
public long createWeakTokenBasedSyntheticPassword(byte[] token, int userId,
@Nullable EscrowTokenStateChangeCallback changeCallback) {
return createTokenBasedSyntheticPassword(token, TOKEN_TYPE_WEAK, userId,
changeCallback);
}
/**
* Create a token based Synthetic password of the given type for the given user.
* @return the handle of the token
*/
public long createTokenBasedSyntheticPassword(byte[] token, @TokenType int type, int userId,
@Nullable EscrowTokenStateChangeCallback changeCallback) {
long handle = generateHandle();
if (!tokenMap.containsKey(userId)) {
tokenMap.put(userId, new ArrayMap<>());
}
TokenData tokenData = new TokenData();
tokenData.mType = type;
final byte[] secdiscardable = secureRandom(SECDISCARDABLE_LENGTH);
if (isWeaverAvailable()) {
tokenData.weaverSecret = secureRandom(mWeaverConfig.valueSize);
@@ -910,6 +977,7 @@ public class SyntheticPasswordManager {
return new ArraySet<>(tokenMap.get(userId).keySet());
}
/** Remove the given pending token. */
public boolean removePendingToken(long handle, int userId) {
if (!tokenMap.containsKey(userId)) {
return false;
@@ -941,7 +1009,7 @@ public class SyntheticPasswordManager {
mPasswordSlotManager.markSlotInUse(slot);
}
saveSecdiscardable(handle, tokenData.secdiscardableOnDisk, userId);
createSyntheticPasswordBlob(handle, SYNTHETIC_PASSWORD_TOKEN_BASED, authToken,
createSyntheticPasswordBlob(handle, getTokenBasedBlobType(tokenData.mType), authToken,
tokenData.aggregatedSecret, 0L, userId);
tokenMap.get(userId).remove(handle);
if (tokenData.mCallback != null) {
@@ -953,26 +1021,23 @@ public class SyntheticPasswordManager {
private void createSyntheticPasswordBlob(long handle, byte type, AuthenticationToken authToken,
byte[] applicationId, long sid, int userId) {
final byte[] secret;
if (type == SYNTHETIC_PASSWORD_TOKEN_BASED) {
if (type == SYNTHETIC_PASSWORD_STRONG_TOKEN_BASED
|| type == SYNTHETIC_PASSWORD_WEAK_TOKEN_BASED) {
secret = authToken.getEscrowSecret();
} else {
secret = authToken.getSyntheticPassword();
}
byte[] content = createSPBlob(getKeyName(handle), secret, applicationId, sid);
byte[] blob = new byte[content.length + 1 + 1];
/*
* 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.
*/
if (authToken.mVersion == SYNTHETIC_PASSWORD_VERSION_V3) {
blob[0] = SYNTHETIC_PASSWORD_VERSION_V3;
} else {
blob[0] = SYNTHETIC_PASSWORD_VERSION_V2;
}
blob[1] = type;
System.arraycopy(content, 0, blob, 2, content.length);
saveState(SP_BLOB_NAME, blob, handle, userId);
byte version = authToken.mVersion == SYNTHETIC_PASSWORD_VERSION_V3
? SYNTHETIC_PASSWORD_VERSION_V3 : SYNTHETIC_PASSWORD_VERSION_V2;
SyntheticPasswordBlob blob = SyntheticPasswordBlob.create(version, type, content);
saveState(SP_BLOB_NAME, blob.toByte(), handle, userId);
}
/**
@@ -1089,6 +1154,36 @@ public class SyntheticPasswordManager {
*/
public @NonNull AuthenticationResult unwrapTokenBasedSyntheticPassword(
IGateKeeperService gatekeeper, long handle, byte[] token, int userId) {
SyntheticPasswordBlob blob = SyntheticPasswordBlob
.fromBytes(loadState(SP_BLOB_NAME, handle, userId));
return unwrapTokenBasedSyntheticPasswordInternal(gatekeeper, handle,
blob.mType, token, userId);
}
/**
* Decrypt a synthetic password by supplying an strong 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 unwrapStrongTokenBasedSyntheticPassword(
IGateKeeperService gatekeeper, long handle, byte[] token, int userId) {
return unwrapTokenBasedSyntheticPasswordInternal(gatekeeper, handle,
SYNTHETIC_PASSWORD_STRONG_TOKEN_BASED, token, userId);
}
/**
* Decrypt a synthetic password by supplying a weak 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 unwrapWeakTokenBasedSyntheticPassword(
IGateKeeperService gatekeeper, long handle, byte[] token, int userId) {
return unwrapTokenBasedSyntheticPasswordInternal(gatekeeper, handle,
SYNTHETIC_PASSWORD_WEAK_TOKEN_BASED, token, userId);
}
private @NonNull AuthenticationResult unwrapTokenBasedSyntheticPasswordInternal(
IGateKeeperService gatekeeper, long handle, byte type, byte[] token, int userId) {
AuthenticationResult result = new AuthenticationResult();
byte[] secdiscardable = loadSecdiscardable(handle, userId);
int slotId = loadWeaverSlot(handle, userId);
@@ -1109,8 +1204,7 @@ public class SyntheticPasswordManager {
PERSONALISATION_WEAVER_TOKEN, secdiscardable);
}
byte[] applicationId = transformUnderSecdiscardable(token, secdiscardable);
result.authToken = unwrapSyntheticPasswordBlob(handle, SYNTHETIC_PASSWORD_TOKEN_BASED,
applicationId, 0L, userId);
result.authToken = unwrapSyntheticPasswordBlob(handle, type, applicationId, 0L, userId);
if (result.authToken != null) {
result.gkResponse = verifyChallenge(gatekeeper, result.authToken, 0L, userId);
if (result.gkResponse == null) {
@@ -1126,33 +1220,33 @@ public class SyntheticPasswordManager {
private AuthenticationToken unwrapSyntheticPasswordBlob(long handle, byte type,
byte[] applicationId, long sid, int userId) {
byte[] blob = loadState(SP_BLOB_NAME, handle, userId);
if (blob == null) {
byte[] data = loadState(SP_BLOB_NAME, handle, userId);
if (data == null) {
return null;
}
final byte version = blob[0];
if (version != SYNTHETIC_PASSWORD_VERSION_V3
&& version != SYNTHETIC_PASSWORD_VERSION_V2
&& version != SYNTHETIC_PASSWORD_VERSION_V1) {
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");
}
if (blob[1] != type) {
if (blob.mType != type) {
throw new IllegalArgumentException("Invalid blob type");
}
final byte[] secret;
if (version == SYNTHETIC_PASSWORD_VERSION_V1) {
secret = SyntheticPasswordCrypto.decryptBlobV1(getKeyName(handle),
Arrays.copyOfRange(blob, 2, blob.length), applicationId);
if (blob.mVersion == SYNTHETIC_PASSWORD_VERSION_V1) {
secret = SyntheticPasswordCrypto.decryptBlobV1(getKeyName(handle), blob.mContent,
applicationId);
} else {
secret = decryptSPBlob(getKeyName(handle),
Arrays.copyOfRange(blob, 2, blob.length), applicationId);
secret = decryptSPBlob(getKeyName(handle), blob.mContent, applicationId);
}
if (secret == null) {
Slog.e(TAG, "Fail to decrypt SP for user " + userId);
return null;
}
AuthenticationToken result = new AuthenticationToken(version);
if (type == SYNTHETIC_PASSWORD_TOKEN_BASED) {
AuthenticationToken result = new AuthenticationToken(blob.mVersion);
if (type == SYNTHETIC_PASSWORD_STRONG_TOKEN_BASED
|| type == SYNTHETIC_PASSWORD_WEAK_TOKEN_BASED) {
if (!loadEscrowData(result, userId)) {
Slog.e(TAG, "User is not escrowable: " + userId);
return null;
@@ -1161,7 +1255,7 @@ public class SyntheticPasswordManager {
} else {
result.recreateDirectly(secret);
}
if (version == SYNTHETIC_PASSWORD_VERSION_V1) {
if (blob.mVersion == SYNTHETIC_PASSWORD_VERSION_V1) {
Slog.i(TAG, "Upgrade v1 SP blob for user " + userId + ", type = " + type);
createSyntheticPasswordBlob(handle, type, result, applicationId, sid, userId);
}
@@ -1233,9 +1327,28 @@ public class SyntheticPasswordManager {
return hasState(SP_BLOB_NAME, handle, userId);
}
/** Destroy the escrow token with the given handle for the given user. */
public void destroyTokenBasedSyntheticPassword(long handle, int userId) {
SyntheticPasswordBlob blob = SyntheticPasswordBlob.fromBytes(loadState(SP_BLOB_NAME, handle,
userId));
destroySyntheticPassword(handle, userId);
destroyState(SECDISCARDABLE_NAME, handle, userId);
if (blob.mType == SYNTHETIC_PASSWORD_WEAK_TOKEN_BASED) {
notifyWeakEscrowTokenRemovedListeners(handle, userId);
}
}
/** Destroy all weak escrow tokens for the given user. */
public void destroyAllWeakTokenBasedSyntheticPasswords(int userId) {
List<Long> handles = mStorage.listSyntheticPasswordHandlesForUser(SECDISCARDABLE_NAME,
userId);
for (long handle: handles) {
SyntheticPasswordBlob blob = SyntheticPasswordBlob.fromBytes(loadState(SP_BLOB_NAME,
handle, userId));
if (blob.mType == SYNTHETIC_PASSWORD_WEAK_TOKEN_BASED) {
destroyTokenBasedSyntheticPassword(handle, userId);
}
}
}
public void destroyPasswordBasedSyntheticPassword(long handle, int userId) {
@@ -1285,6 +1398,16 @@ public class SyntheticPasswordManager {
return loadState(SECDISCARDABLE_NAME, handle, userId);
}
private byte getTokenBasedBlobType(@TokenType int type) {
switch (type) {
case TOKEN_TYPE_WEAK:
return SYNTHETIC_PASSWORD_WEAK_TOKEN_BASED;
case TOKEN_TYPE_STRONG:
default:
return SYNTHETIC_PASSWORD_STRONG_TOKEN_BASED;
}
}
/**
* Retrieves the saved password metrics associated with a SP handle. Only meaningful to be
* called on the handle of a password-based synthetic password. A valid AuthenticationToken for
@@ -1439,4 +1562,33 @@ public class SyntheticPasswordManager {
}
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 handle, int userId) {
int i = mListeners.beginBroadcast();
try {
while (i > 0) {
i--;
try {
mListeners.getBroadcastItem(i).onWeakEscrowTokenRemoved(handle, userId);
} catch (RemoteException e) {
Slog.e(TAG, "Exception while notifying WeakEscrowTokenRemovedListener.",
e);
}
}
} finally {
mListeners.finishBroadcast();
}
}
}

View File

@@ -0,0 +1,186 @@
/*
* Copyright (C) 2022 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 org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import android.app.admin.PasswordMetrics;
import android.content.pm.PackageManager;
import android.os.IBinder;
import android.os.RemoteException;
import android.platform.test.annotations.Presubmit;
import androidx.test.filters.SmallTest;
import androidx.test.runner.AndroidJUnit4;
import com.android.internal.widget.IWeakEscrowTokenActivatedListener;
import com.android.internal.widget.IWeakEscrowTokenRemovedListener;
import com.android.internal.widget.LockscreenCredential;
import com.android.internal.widget.VerifyCredentialResponse;
import org.junit.Test;
import org.junit.runner.RunWith;
/** atest FrameworksServicesTests:WeakEscrowTokenTests */
@SmallTest
@Presubmit
@RunWith(AndroidJUnit4.class)
public class WeakEscrowTokenTests extends BaseLockSettingsServiceTests{
@Test
public void testWeakTokenActivatedImmediatelyIfNoUserPassword()
throws RemoteException {
mockAutoHardware();
final byte[] token = "some-high-entropy-secure-token".getBytes();
IWeakEscrowTokenActivatedListener mockListener =
mock(IWeakEscrowTokenActivatedListener.Stub.class);
long handle = mService.addWeakEscrowToken(token, PRIMARY_USER_ID, mockListener);
assertTrue(mService.isWeakEscrowTokenActive(handle, PRIMARY_USER_ID));
assertTrue(mService.isWeakEscrowTokenValid(handle, token, PRIMARY_USER_ID));
verify(mockListener).onWeakEscrowTokenActivated(handle, PRIMARY_USER_ID);
}
@Test
public void testWeakTokenActivatedLaterWithUserPassword()
throws RemoteException {
mockAutoHardware();
byte[] token = "some-high-entropy-secure-token".getBytes();
IWeakEscrowTokenActivatedListener mockListener =
mock(IWeakEscrowTokenActivatedListener.Stub.class);
LockscreenCredential password = newPassword("password");
mService.setLockCredential(password, nonePassword(), PRIMARY_USER_ID);
long handle = mService.addWeakEscrowToken(token, PRIMARY_USER_ID, mockListener);
// Token not activated immediately since user password exists
assertFalse(mService.isWeakEscrowTokenActive(handle, PRIMARY_USER_ID));
// Activate token (password gets migrated to SP at the same time)
assertEquals(VerifyCredentialResponse.RESPONSE_OK, mService.verifyCredential(
password, PRIMARY_USER_ID, 0 /* flags */).getResponseCode());
// Verify token is activated and valid
assertTrue(mService.isWeakEscrowTokenActive(handle, PRIMARY_USER_ID));
assertTrue(mService.isWeakEscrowTokenValid(handle, token, PRIMARY_USER_ID));
verify(mockListener).onWeakEscrowTokenActivated(handle, PRIMARY_USER_ID);
}
@Test
public void testWeakTokensRemovedIfCredentialChanged() throws Exception {
mockAutoHardware();
byte[] token = "some-high-entropy-secure-token".getBytes();
IWeakEscrowTokenRemovedListener mockRemoveListener = mockAliveRemoveListener();
IWeakEscrowTokenActivatedListener mockActivateListener =
mock(IWeakEscrowTokenActivatedListener.Stub.class);
LockscreenCredential password = newPassword("password");
LockscreenCredential pattern = newPattern("123654");
mService.setLockCredential(password, nonePassword(), PRIMARY_USER_ID);
mService.registerWeakEscrowTokenRemovedListener(mockRemoveListener);
long handle = mService.addWeakEscrowToken(token, PRIMARY_USER_ID, mockActivateListener);
// Activate token
assertEquals(VerifyCredentialResponse.RESPONSE_OK, mService.verifyCredential(
password, PRIMARY_USER_ID, 0 /* flags */).getResponseCode());
// Verify token removed
assertTrue(mService.isWeakEscrowTokenActive(handle, PRIMARY_USER_ID));
assertTrue(mLocalService.setLockCredentialWithToken(
pattern, handle, token, PRIMARY_USER_ID));
assertFalse(mLocalService.isEscrowTokenActive(handle, PRIMARY_USER_ID));
verify(mockRemoveListener).onWeakEscrowTokenRemoved(handle, PRIMARY_USER_ID);
}
@Test
public void testWeakTokenRemovedListenerRegistered() throws Exception {
mockAutoHardware();
IWeakEscrowTokenRemovedListener mockRemoveListener = mockAliveRemoveListener();
IWeakEscrowTokenActivatedListener mockActivateListener =
mock(IWeakEscrowTokenActivatedListener.Stub.class);
byte[] token = "some-high-entropy-secure-token".getBytes();
long handle = mService.addWeakEscrowToken(token, PRIMARY_USER_ID, mockActivateListener);
mService.registerWeakEscrowTokenRemovedListener(mockRemoveListener);
mService.removeWeakEscrowToken(handle, PRIMARY_USER_ID);
verify(mockRemoveListener).onWeakEscrowTokenRemoved(handle, PRIMARY_USER_ID);
}
@Test
public void testWeakTokenRemovedListenerUnregistered() throws Exception {
mockAutoHardware();
IWeakEscrowTokenRemovedListener mockRemoveListener = mockAliveRemoveListener();
IWeakEscrowTokenActivatedListener mockActivateListener =
mock(IWeakEscrowTokenActivatedListener.Stub.class);
byte[] token0 = "some-high-entropy-secure-token-0".getBytes();
byte[] token1 = "some-high-entropy-secure-token-1".getBytes();
long handle0 = mService.addWeakEscrowToken(token0, PRIMARY_USER_ID, mockActivateListener);
long handle1 = mService.addWeakEscrowToken(token1, PRIMARY_USER_ID, mockActivateListener);
mService.registerWeakEscrowTokenRemovedListener(mockRemoveListener);
mService.removeWeakEscrowToken(handle0, PRIMARY_USER_ID);
verify(mockRemoveListener).onWeakEscrowTokenRemoved(handle0, PRIMARY_USER_ID);
mService.unregisterWeakEscrowTokenRemovedListener(mockRemoveListener);
mService.removeWeakEscrowToken(handle1, PRIMARY_USER_ID);
verify(mockRemoveListener, never()).onWeakEscrowTokenRemoved(handle1, PRIMARY_USER_ID);
}
@Test
public void testUnlockUserWithToken_weakEscrowToken() throws Exception {
mockAutoHardware();
IWeakEscrowTokenActivatedListener mockActivateListener =
mock(IWeakEscrowTokenActivatedListener.Stub.class);
LockscreenCredential password = newPassword("password");
byte[] token = "some-high-entropy-secure-token".getBytes();
mService.setLockCredential(password, nonePassword(), PRIMARY_USER_ID);
// Disregard any reportPasswordChanged() invocations as part of credential setup.
flushHandlerTasks();
reset(mDevicePolicyManager);
long handle = mService.addWeakEscrowToken(token, PRIMARY_USER_ID, mockActivateListener);
assertEquals(VerifyCredentialResponse.RESPONSE_OK, mService.verifyCredential(
password, PRIMARY_USER_ID, 0 /* flags */).getResponseCode());
assertTrue(mService.isWeakEscrowTokenActive(handle, PRIMARY_USER_ID));
assertTrue(mService.isWeakEscrowTokenValid(handle, token, PRIMARY_USER_ID));
mService.onCleanupUser(PRIMARY_USER_ID);
assertNull(mLocalService.getUserPasswordMetrics(PRIMARY_USER_ID));
assertTrue(mLocalService.unlockUserWithToken(handle, token, PRIMARY_USER_ID));
assertEquals(PasswordMetrics.computeForCredential(password),
mLocalService.getUserPasswordMetrics(PRIMARY_USER_ID));
}
private void mockAutoHardware() {
when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_AUTOMOTIVE)).thenReturn(true);
}
private IWeakEscrowTokenRemovedListener mockAliveRemoveListener() {
IWeakEscrowTokenRemovedListener mockListener =
mock(IWeakEscrowTokenRemovedListener.Stub.class);
IBinder mockIBinder = mock(IBinder.class);
when(mockIBinder.isBinderAlive()).thenReturn(true);
when(mockListener.asBinder()).thenReturn(mockIBinder);
return mockListener;
}
}