diff options
11 files changed, 927 insertions, 45 deletions
diff --git a/core/api/system-current.txt b/core/api/system-current.txt index 1373ccfb8e3e..2f9d7d0c3823 100644 --- a/core/api/system-current.txt +++ b/core/api/system-current.txt @@ -179,6 +179,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"; @@ -771,18 +772,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); diff --git a/core/java/android/app/KeyguardManager.java b/core/java/android/app/KeyguardManager.java index dc71a3237b0b..14afd0fcebf8 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.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); @@ -813,6 +857,150 @@ public class KeyguardManager { } /** + * 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 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; diff --git a/core/java/com/android/internal/widget/ILockSettings.aidl b/core/java/com/android/internal/widget/ILockSettings.aidl index d16d9c619403..db4bc2c7e24a 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.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); } 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 000000000000..9c8d9d61848d --- /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 000000000000..70180484ce58 --- /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 5a032774c207..f91776eaf259 100644 --- a/core/java/com/android/internal/widget/LockPatternUtils.java +++ b/core/java/com/android/internal/widget/LockPatternUtils.java @@ -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); @@ -1355,15 +1377,38 @@ public class LockPatternUtils { } /** + * 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 @@ public class LockPatternUtils { } /** + * 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 @@ public class LockPatternUtils { } /** + * 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 713cb36bc33a..777ddaa9d645 100644 --- a/core/res/AndroidManifest.xml +++ b/core/res/AndroidManifest.xml @@ -5334,6 +5334,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 50e8474e8d52..b659f37690a3 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 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. + } + }; + } } diff --git a/services/core/java/com/android/server/locksettings/LockSettingsService.java b/services/core/java/com/android/server/locksettings/LockSettingsService.java index fab11a16f399..682a27adc15f 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.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 diff --git a/services/core/java/com/android/server/locksettings/SyntheticPasswordManager.java b/services/core/java/com/android/server/locksettings/SyntheticPasswordManager.java index 601a5727e545..2da443116a42 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 @@ 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(); + } + } } 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 000000000000..51ddcef425ce --- /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; + } +} |