Add weak escrow token APIs
Bug: 206485383
Test: atest LockPatternUtilsTest WeakEscrowTokenTests
Change-Id: I24cdf9af6c8f29db03ef214a1a0f8effad394264
diff --git a/core/api/system-current.txt b/core/api/system-current.txt
index c9b8ba2..06b2b2b 100644
--- a/core/api/system-current.txt
+++ b/core/api/system-current.txt
@@ -176,6 +176,7 @@
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 @@
}
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);
diff --git a/core/java/android/app/KeyguardManager.java b/core/java/android/app/KeyguardManager.java
index dc71a32..14afd0f 100644
--- a/core/java/android/app/KeyguardManager.java
+++ b/core/java/android/app/KeyguardManager.java
@@ -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.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.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 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 @@
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 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 @@
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 @@
try {
LockscreenCredential credential = createLockscreenCredential(
lockType, password);
- success = lockPatternUtils.setLockCredential(
+ success = mLockPatternUtils.setLockCredential(
credential,
/* savedPassword= */ LockscreenCredential.createNone(),
userId);
@@ -813,6 +857,150 @@
}
/**
+ * 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}.
* <p>If no password is currently set, {@code currentPassword} should be set to {@code null}.
@@ -832,13 +1020,12 @@
})
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 @@
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;
diff --git a/core/java/com/android/internal/widget/ILockSettings.aidl b/core/java/com/android/internal/widget/ILockSettings.aidl
index d16d9c6..db4bc2c 100644
--- a/core/java/com/android/internal/widget/ILockSettings.aidl
+++ b/core/java/com/android/internal/widget/ILockSettings.aidl
@@ -24,6 +24,8 @@
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 @@
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);
}
diff --git a/core/java/com/android/internal/widget/IWeakEscrowTokenActivatedListener.aidl b/core/java/com/android/internal/widget/IWeakEscrowTokenActivatedListener.aidl
new file mode 100644
index 0000000..9c8d9d6
--- /dev/null
+++ b/core/java/com/android/internal/widget/IWeakEscrowTokenActivatedListener.aidl
@@ -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);
+}
diff --git a/core/java/com/android/internal/widget/IWeakEscrowTokenRemovedListener.aidl b/core/java/com/android/internal/widget/IWeakEscrowTokenRemovedListener.aidl
new file mode 100644
index 0000000..7018048
--- /dev/null
+++ b/core/java/com/android/internal/widget/IWeakEscrowTokenRemovedListener.aidl
@@ -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);
+}
diff --git a/core/java/com/android/internal/widget/LockPatternUtils.java b/core/java/com/android/internal/widget/LockPatternUtils.java
index 5a03277..f91776e 100644
--- a/core/java/com/android/internal/widget/LockPatternUtils.java
+++ b/core/java/com/android/internal/widget/LockPatternUtils.java
@@ -1236,6 +1236,28 @@
}
}
+ /** 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);
@@ -1355,15 +1377,38 @@
}
/**
+ * 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.
*/
public interface EscrowTokenStateChangeCallback {
/**
* 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);
}
/**
@@ -1379,6 +1424,21 @@
}
/**
+ * 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}
*
@@ -1389,6 +1449,29 @@
}
/**
+ * 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.
*
* <p>This method is only available to code running in the system server process itself.
diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml
index 4c36732..1865d75 100644
--- a/core/res/AndroidManifest.xml
+++ b/core/res/AndroidManifest.xml
@@ -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"
diff --git a/core/tests/utiltests/src/com/android/internal/util/LockPatternUtilsTest.java b/core/tests/utiltests/src/com/android/internal/util/LockPatternUtilsTest.java
index 50e8474..b659f37 100644
--- a/core/tests/utiltests/src/com/android/internal/util/LockPatternUtilsTest.java
+++ b/core/tests/utiltests/src/com/android/internal/util/LockPatternUtilsTest.java
@@ -21,13 +21,16 @@
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 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 @@
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.
+ }
+ };
+ }
}
diff --git a/services/core/java/com/android/server/locksettings/LockSettingsService.java b/services/core/java/com/android/server/locksettings/LockSettingsService.java
index fab11a1..682a27a 100644
--- a/services/core/java/com/android/server/locksettings/LockSettingsService.java
+++ b/services/core/java/com/android/server/locksettings/LockSettingsService.java
@@ -39,7 +39,10 @@
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.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.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 @@
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 @@
});
}
+ /** 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 @@
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 @@
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 @@
}
}
- 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 @@
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 @@
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 @@
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 @@
@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
diff --git a/services/core/java/com/android/server/locksettings/SyntheticPasswordManager.java b/services/core/java/com/android/server/locksettings/SyntheticPasswordManager.java
index 601a572..2da4431 100644
--- a/services/core/java/com/android/server/locksettings/SyntheticPasswordManager.java
+++ b/services/core/java/com/android/server/locksettings/SyntheticPasswordManager.java
@@ -18,6 +18,7 @@
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.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 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 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 @@
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 @@
}
}
+ 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 @@
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 @@
* 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 @@
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 @@
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 @@
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 @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 @@
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 @@
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 @@
} 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 @@
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 @@
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 @@
}
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();
+ }
+ }
}
diff --git a/services/tests/servicestests/src/com/android/server/locksettings/WeakEscrowTokenTests.java b/services/tests/servicestests/src/com/android/server/locksettings/WeakEscrowTokenTests.java
new file mode 100644
index 0000000..51ddcef
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/locksettings/WeakEscrowTokenTests.java
@@ -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;
+ }
+}