From 856868753f48332552495bb2244d680afbcbb47b Mon Sep 17 00:00:00 2001 From: Haining Chen Date: Thu, 12 Mar 2020 17:27:59 -0700 Subject: [PATCH] Add unit tests for LockSettingsStrongAuth Bug: 151877480 Bug: 141025588 Test: make -j Test: atest LockSettingsStrongAuthTest Change-Id: Icaa3f5ab10d94944761700c2e072a3f86d35cdc0 --- .../locksettings/LockSettingsStrongAuth.java | 103 +++++-- .../LockSettingsStrongAuthTest.java | 252 ++++++++++++++++++ 2 files changed, 331 insertions(+), 24 deletions(-) create mode 100644 services/tests/servicestests/src/com/android/server/locksettings/LockSettingsStrongAuthTest.java diff --git a/services/core/java/com/android/server/locksettings/LockSettingsStrongAuth.java b/services/core/java/com/android/server/locksettings/LockSettingsStrongAuth.java index fbee6f4bcbf05..848019738abe4 100644 --- a/services/core/java/com/android/server/locksettings/LockSettingsStrongAuth.java +++ b/services/core/java/com/android/server/locksettings/LockSettingsStrongAuth.java @@ -26,6 +26,7 @@ import android.app.admin.DevicePolicyManager; import android.app.trust.IStrongAuthTracker; import android.content.Context; import android.os.Handler; +import android.os.Looper; import android.os.Message; import android.os.RemoteCallbackList; import android.os.RemoteException; @@ -36,6 +37,7 @@ import android.util.Slog; import android.util.SparseBooleanArray; import android.util.SparseIntArray; +import com.android.internal.annotations.VisibleForTesting; import com.android.internal.util.IndentingPrintWriter; import com.android.internal.widget.LockPatternUtils.StrongAuthTracker; @@ -57,11 +59,14 @@ public class LockSettingsStrongAuth { private static final int MSG_STRONG_BIOMETRIC_UNLOCK = 8; private static final int MSG_SCHEDULE_NON_STRONG_BIOMETRIC_IDLE_TIMEOUT = 9; - private static final String STRONG_AUTH_TIMEOUT_ALARM_TAG = + @VisibleForTesting + protected static final String STRONG_AUTH_TIMEOUT_ALARM_TAG = "LockSettingsStrongAuth.timeoutForUser"; - private static final String NON_STRONG_BIOMETRIC_TIMEOUT_ALARM_TAG = + @VisibleForTesting + protected static final String NON_STRONG_BIOMETRIC_TIMEOUT_ALARM_TAG = "LockSettingsPrimaryAuth.nonStrongBiometricTimeoutForUser"; - private static final String NON_STRONG_BIOMETRIC_IDLE_TIMEOUT_ALARM_TAG = + @VisibleForTesting + protected static final String NON_STRONG_BIOMETRIC_IDLE_TIMEOUT_ALARM_TAG = "LockSettingsPrimaryAuth.nonStrongBiometricIdleTimeoutForUser"; /** @@ -73,28 +78,71 @@ public class LockSettingsStrongAuth { 4 * 60 * 60 * 1000; // 4h private final RemoteCallbackList mTrackers = new RemoteCallbackList<>(); - private final SparseIntArray mStrongAuthForUser = new SparseIntArray(); - private final SparseBooleanArray mIsNonStrongBiometricAllowedForUser = new SparseBooleanArray(); - private final ArrayMap + @VisibleForTesting + protected final SparseIntArray mStrongAuthForUser = new SparseIntArray(); + @VisibleForTesting + protected final SparseBooleanArray mIsNonStrongBiometricAllowedForUser = + new SparseBooleanArray(); + @VisibleForTesting + protected final ArrayMap mStrongAuthTimeoutAlarmListenerForUser = new ArrayMap<>(); // Track non-strong biometric timeout - private final ArrayMap + @VisibleForTesting + protected final ArrayMap mNonStrongBiometricTimeoutAlarmListener = new ArrayMap<>(); // Track non-strong biometric idle timeout - private final ArrayMap + @VisibleForTesting + protected final ArrayMap mNonStrongBiometricIdleTimeoutAlarmListener = new ArrayMap<>(); private final int mDefaultStrongAuthFlags; private final boolean mDefaultIsNonStrongBiometricAllowed = true; private final Context mContext; - - private AlarmManager mAlarmManager; + private final Injector mInjector; + private final AlarmManager mAlarmManager; public LockSettingsStrongAuth(Context context) { + this(context, new Injector()); + } + + @VisibleForTesting + protected LockSettingsStrongAuth(Context context, Injector injector) { mContext = context; - mDefaultStrongAuthFlags = StrongAuthTracker.getDefaultFlags(context); - mAlarmManager = context.getSystemService(AlarmManager.class); + mInjector = injector; + mDefaultStrongAuthFlags = mInjector.getDefaultStrongAuthFlags(context); + mAlarmManager = mInjector.getAlarmManager(context); + } + + /** + * Class for injecting dependencies into LockSettingsStrongAuth. + */ + @VisibleForTesting + public static class Injector { + + /** + * Allows to mock AlarmManager for testing. + */ + @VisibleForTesting + public AlarmManager getAlarmManager(Context context) { + return context.getSystemService(AlarmManager.class); + } + + /** + * Allows to get different default StrongAuthFlags for testing. + */ + @VisibleForTesting + public int getDefaultStrongAuthFlags(Context context) { + return StrongAuthTracker.getDefaultFlags(context); + } + + /** + * Allows to get different triggerAtMillis values when setting alarms for testing. + */ + @VisibleForTesting + public long getNextAlarmTimeMs(long timeout) { + return SystemClock.elapsedRealtime() + timeout; + } } private void handleAddStrongAuthTracker(IStrongAuthTracker tracker) { @@ -186,7 +234,8 @@ public class LockSettingsStrongAuth { private void handleScheduleStrongAuthTimeout(int userId) { final DevicePolicyManager dpm = (DevicePolicyManager) mContext.getSystemService(Context.DEVICE_POLICY_SERVICE); - long when = SystemClock.elapsedRealtime() + dpm.getRequiredStrongAuthTimeout(null, userId); + long nextAlarmTime = + mInjector.getNextAlarmTimeMs(dpm.getRequiredStrongAuthTimeout(null, userId)); // cancel current alarm listener for the user (if there was one) StrongAuthTimeoutAlarmListener alarm = mStrongAuthTimeoutAlarmListenerForUser.get(userId); if (alarm != null) { @@ -196,8 +245,8 @@ public class LockSettingsStrongAuth { mStrongAuthTimeoutAlarmListenerForUser.put(userId, alarm); } // schedule a new alarm listener for the user - mAlarmManager.set(AlarmManager.ELAPSED_REALTIME, when, STRONG_AUTH_TIMEOUT_ALARM_TAG, - alarm, mHandler); + mAlarmManager.set(AlarmManager.ELAPSED_REALTIME, nextAlarmTime, + STRONG_AUTH_TIMEOUT_ALARM_TAG, alarm, mHandler); // cancel current non-strong biometric alarm listener for the user (if there was one) cancelNonStrongBiometricAlarmListener(userId); @@ -209,7 +258,7 @@ public class LockSettingsStrongAuth { private void handleScheduleNonStrongBiometricTimeout(int userId) { if (DEBUG) Slog.d(TAG, "handleScheduleNonStrongBiometricTimeout for userId=" + userId); - long when = SystemClock.elapsedRealtime() + DEFAULT_NON_STRONG_BIOMETRIC_TIMEOUT_MS; + long nextAlarmTime = mInjector.getNextAlarmTimeMs(DEFAULT_NON_STRONG_BIOMETRIC_TIMEOUT_MS); NonStrongBiometricTimeoutAlarmListener alarm = mNonStrongBiometricTimeoutAlarmListener .get(userId); if (alarm != null) { @@ -226,7 +275,7 @@ public class LockSettingsStrongAuth { alarm = new NonStrongBiometricTimeoutAlarmListener(userId); mNonStrongBiometricTimeoutAlarmListener.put(userId, alarm); // schedule a new alarm listener for the user - mAlarmManager.set(AlarmManager.ELAPSED_REALTIME, when, + mAlarmManager.set(AlarmManager.ELAPSED_REALTIME, nextAlarmTime, NON_STRONG_BIOMETRIC_TIMEOUT_ALARM_TAG, alarm, mHandler); } @@ -268,7 +317,8 @@ public class LockSettingsStrongAuth { } } - private void setIsNonStrongBiometricAllowed(boolean allowed, int userId) { + @VisibleForTesting + protected void setIsNonStrongBiometricAllowed(boolean allowed, int userId) { if (DEBUG) { Slog.d(TAG, "setIsNonStrongBiometricAllowed for allowed=" + allowed + ", userId=" + userId); @@ -302,7 +352,8 @@ public class LockSettingsStrongAuth { private void handleScheduleNonStrongBiometricIdleTimeout(int userId) { if (DEBUG) Slog.d(TAG, "handleScheduleNonStrongBiometricIdleTimeout for userId=" + userId); - long when = SystemClock.elapsedRealtime() + DEFAULT_NON_STRONG_BIOMETRIC_IDLE_TIMEOUT_MS; + long nextAlarmTime = + mInjector.getNextAlarmTimeMs(DEFAULT_NON_STRONG_BIOMETRIC_IDLE_TIMEOUT_MS); // cancel current alarm listener for the user (if there was one) NonStrongBiometricIdleTimeoutAlarmListener alarm = mNonStrongBiometricIdleTimeoutAlarmListener.get(userId); @@ -315,7 +366,7 @@ public class LockSettingsStrongAuth { } // schedule a new alarm listener for the user if (DEBUG) Slog.d(TAG, "Schedule a new alarm for non-strong biometric idle timeout"); - mAlarmManager.set(AlarmManager.ELAPSED_REALTIME, when, + mAlarmManager.set(AlarmManager.ELAPSED_REALTIME, nextAlarmTime, NON_STRONG_BIOMETRIC_IDLE_TIMEOUT_ALARM_TAG, alarm, mHandler); } @@ -435,7 +486,8 @@ public class LockSettingsStrongAuth { /** * Alarm of fallback timeout for primary auth */ - private class StrongAuthTimeoutAlarmListener implements OnAlarmListener { + @VisibleForTesting + protected class StrongAuthTimeoutAlarmListener implements OnAlarmListener { private final int mUserId; @@ -452,7 +504,8 @@ public class LockSettingsStrongAuth { /** * Alarm of fallback timeout for non-strong biometric (i.e. weak or convenience) */ - private class NonStrongBiometricTimeoutAlarmListener implements OnAlarmListener { + @VisibleForTesting + protected class NonStrongBiometricTimeoutAlarmListener implements OnAlarmListener { private final int mUserId; @@ -469,7 +522,8 @@ public class LockSettingsStrongAuth { /** * Alarm of idle timeout for non-strong biometric (i.e. weak or convenience biometric) */ - private class NonStrongBiometricIdleTimeoutAlarmListener implements OnAlarmListener { + @VisibleForTesting + protected class NonStrongBiometricIdleTimeoutAlarmListener implements OnAlarmListener { private final int mUserId; @@ -484,7 +538,8 @@ public class LockSettingsStrongAuth { } } - private final Handler mHandler = new Handler() { + @VisibleForTesting + protected final Handler mHandler = new Handler(Looper.getMainLooper()) { @Override public void handleMessage(Message msg) { switch (msg.what) { diff --git a/services/tests/servicestests/src/com/android/server/locksettings/LockSettingsStrongAuthTest.java b/services/tests/servicestests/src/com/android/server/locksettings/LockSettingsStrongAuthTest.java new file mode 100644 index 0000000000000..c9dbdd2364cc0 --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/locksettings/LockSettingsStrongAuthTest.java @@ -0,0 +1,252 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.locksettings; + +import static com.android.internal.widget.LockPatternUtils.StrongAuthTracker.STRONG_AUTH_NOT_REQUIRED; +import static com.android.internal.widget.LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_NON_STRONG_BIOMETRICS_TIMEOUT; +import static com.android.server.locksettings.LockSettingsStrongAuth.DEFAULT_NON_STRONG_BIOMETRIC_IDLE_TIMEOUT_MS; +import static com.android.server.locksettings.LockSettingsStrongAuth.DEFAULT_NON_STRONG_BIOMETRIC_TIMEOUT_MS; +import static com.android.server.locksettings.LockSettingsStrongAuth.NON_STRONG_BIOMETRIC_IDLE_TIMEOUT_ALARM_TAG; +import static com.android.server.locksettings.LockSettingsStrongAuth.NON_STRONG_BIOMETRIC_TIMEOUT_ALARM_TAG; +import static com.android.server.locksettings.LockSettingsStrongAuth.STRONG_AUTH_TIMEOUT_ALARM_TAG; + +import static junit.framework.Assert.assertFalse; +import static junit.framework.Assert.assertNotNull; +import static junit.framework.Assert.assertNull; +import static junit.framework.Assert.assertTrue; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.app.AlarmManager; +import android.app.admin.DevicePolicyManager; +import android.content.Context; +import android.util.Log; + +import androidx.test.InstrumentationRegistry; +import androidx.test.filters.SmallTest; + +import com.android.server.locksettings.LockSettingsStrongAuth.NonStrongBiometricIdleTimeoutAlarmListener; +import com.android.server.locksettings.LockSettingsStrongAuth.NonStrongBiometricTimeoutAlarmListener; +import com.android.server.locksettings.LockSettingsStrongAuth.StrongAuthTimeoutAlarmListener; + +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +@SmallTest +public class LockSettingsStrongAuthTest { + + private static final String TAG = LockSettingsStrongAuthTest.class.getSimpleName(); + + private static final int PRIMARY_USER_ID = 0; + + private LockSettingsStrongAuth mStrongAuth; + private final int mDefaultStrongAuthFlags = STRONG_AUTH_NOT_REQUIRED; + private final boolean mDefaultIsNonStrongBiometricAllowed = true; + + @Mock + private Context mContext; + @Mock + private LockSettingsStrongAuth.Injector mInjector; + @Mock + private AlarmManager mAlarmManager; + @Mock + private DevicePolicyManager mDPM; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + + when(mInjector.getAlarmManager(mContext)).thenReturn(mAlarmManager); + when(mInjector.getDefaultStrongAuthFlags(mContext)).thenReturn(mDefaultStrongAuthFlags); + when(mContext.getSystemService(Context.DEVICE_POLICY_SERVICE)).thenReturn(mDPM); + + mStrongAuth = new LockSettingsStrongAuth(mContext, mInjector); + } + + @Test + public void testScheduleNonStrongBiometricIdleTimeout() { + final long nextAlarmTime = 1000; + when(mInjector.getNextAlarmTimeMs(DEFAULT_NON_STRONG_BIOMETRIC_IDLE_TIMEOUT_MS)) + .thenReturn(nextAlarmTime); + mStrongAuth.scheduleNonStrongBiometricIdleTimeout(PRIMARY_USER_ID); + + waitForIdle(); + NonStrongBiometricIdleTimeoutAlarmListener alarm = mStrongAuth + .mNonStrongBiometricIdleTimeoutAlarmListener.get(PRIMARY_USER_ID); + // verify that a new alarm for idle timeout is added for the user + assertNotNull(alarm); + // verify that the alarm is scheduled + verifyAlarm(nextAlarmTime, NON_STRONG_BIOMETRIC_IDLE_TIMEOUT_ALARM_TAG, alarm); + } + + @Test + public void testSetIsNonStrongBiometricAllowed_disallowed() { + mStrongAuth.setIsNonStrongBiometricAllowed(false /* allowed */, PRIMARY_USER_ID); + + waitForIdle(); + // verify that unlocking with non-strong biometrics is not allowed + assertFalse(mStrongAuth.mIsNonStrongBiometricAllowedForUser + .get(PRIMARY_USER_ID, mDefaultIsNonStrongBiometricAllowed)); + } + + @Test + public void testReportSuccessfulBiometricUnlock_nonStrongBiometric_fallbackTimeout() { + final long nextAlarmTime = 1000; + when(mInjector.getNextAlarmTimeMs(DEFAULT_NON_STRONG_BIOMETRIC_TIMEOUT_MS)) + .thenReturn(nextAlarmTime); + mStrongAuth.reportSuccessfulBiometricUnlock(false /* isStrongBiometric */, PRIMARY_USER_ID); + + waitForIdle(); + NonStrongBiometricTimeoutAlarmListener alarm = + mStrongAuth.mNonStrongBiometricTimeoutAlarmListener.get(PRIMARY_USER_ID); + // verify that a new alarm for fallback timeout is added for the user + assertNotNull(alarm); + // verify that the alarm is scheduled + verifyAlarm(nextAlarmTime, NON_STRONG_BIOMETRIC_TIMEOUT_ALARM_TAG, alarm); + } + + @Test + public void testRequireStrongAuth_nonStrongBiometric_fallbackTimeout() { + mStrongAuth.requireStrongAuth( + STRONG_AUTH_REQUIRED_AFTER_NON_STRONG_BIOMETRICS_TIMEOUT /* strongAuthReason */, + PRIMARY_USER_ID); + + waitForIdle(); + // verify that the StrongAuthFlags for the user contains the expected flag + final int expectedFlag = STRONG_AUTH_REQUIRED_AFTER_NON_STRONG_BIOMETRICS_TIMEOUT; + verifyStrongAuthFlags(expectedFlag, PRIMARY_USER_ID); + } + + @Test + public void testReportSuccessfulBiometricUnlock_nonStrongBiometric_cancelIdleTimeout() { + // lock device and schedule an alarm for non-strong biometric idle timeout + mStrongAuth.scheduleNonStrongBiometricIdleTimeout(PRIMARY_USER_ID); + // unlock with non-strong biometric + mStrongAuth.reportSuccessfulBiometricUnlock(false /* isStrongBiometric */, PRIMARY_USER_ID); + + waitForIdle(); + + // verify that the current alarm for idle timeout is cancelled after a successful unlock + verify(mAlarmManager).cancel(any(NonStrongBiometricIdleTimeoutAlarmListener.class)); + } + + @Test + public void testReportSuccessfulBiometricUnlock_strongBio_cancelAlarmsAndAllowNonStrongBio() { + setupAlarms(PRIMARY_USER_ID); + mStrongAuth.reportSuccessfulBiometricUnlock(true /* isStrongBiometric */, PRIMARY_USER_ID); + + waitForIdle(); + // verify that unlocking with strong biometric cancels alarms for fallback and idle timeout + // and re-allow unlocking with non-strong biometric + verifyAlarmsCancelledAndNonStrongBiometricAllowed(PRIMARY_USER_ID); + } + + @Test + public void testReportSuccessfulStrongAuthUnlock_schedulePrimaryAuthTimeout() { + final long nextAlarmTime = 1000; + when(mInjector.getNextAlarmTimeMs(mDPM.getRequiredStrongAuthTimeout(null, PRIMARY_USER_ID))) + .thenReturn(nextAlarmTime); + mStrongAuth.reportSuccessfulStrongAuthUnlock(PRIMARY_USER_ID); + + waitForIdle(); + StrongAuthTimeoutAlarmListener alarm = + mStrongAuth.mStrongAuthTimeoutAlarmListenerForUser.get(PRIMARY_USER_ID); + // verify that a new alarm for primary auth timeout is added for the user + assertNotNull(alarm); + // verify that the alarm is scheduled + verifyAlarm(nextAlarmTime, STRONG_AUTH_TIMEOUT_ALARM_TAG, alarm); + } + + @Test + public void testReportSuccessfulStrongAuthUnlock_cancelAlarmsAndAllowNonStrongBio() { + setupAlarms(PRIMARY_USER_ID); + mStrongAuth.reportSuccessfulStrongAuthUnlock(PRIMARY_USER_ID); + + waitForIdle(); + // verify that unlocking with primary auth (PIN/pattern/password) cancels alarms + // for fallback and idle timeout and re-allow unlocking with non-strong biometric + verifyAlarmsCancelledAndNonStrongBiometricAllowed(PRIMARY_USER_ID); + } + + @Test + public void testFallbackTimeout_convenienceBiometric_weakBiometric() { + // assume that unlock with convenience biometric + mStrongAuth.reportSuccessfulBiometricUnlock(false /* isStrongBiometric */, PRIMARY_USER_ID); + // assume that unlock again with weak biometric + mStrongAuth.reportSuccessfulBiometricUnlock(false /* isStrongBiometric */, PRIMARY_USER_ID); + + waitForIdle(); + // verify that the fallback alarm scheduled when unlocking with convenience biometric is + // not affected when unlocking again with weak biometric + verify(mAlarmManager, never()).cancel(any(NonStrongBiometricTimeoutAlarmListener.class)); + assertNotNull(mStrongAuth.mNonStrongBiometricTimeoutAlarmListener.get(PRIMARY_USER_ID)); + } + + private void verifyAlarm(long when, String tag, AlarmManager.OnAlarmListener alarm) { + verify(mAlarmManager).set( + eq(AlarmManager.ELAPSED_REALTIME), + eq(when), + eq(tag), + eq(alarm), + eq(mStrongAuth.mHandler)); + } + + private void verifyStrongAuthFlags(int reason, int userId) { + final int flags = mStrongAuth.mStrongAuthForUser.get(userId, mDefaultStrongAuthFlags); + Log.d(TAG, "verifyStrongAuthFlags:" + + " reason=" + Integer.toHexString(reason) + + " userId=" + userId + + " flags=" + Integer.toHexString(flags)); + assertTrue(containsFlag(flags, reason)); + } + + private void setupAlarms(int userId) { + // schedule (a) an alarm for non-strong biometric fallback timeout and (b) an alarm for + // non-strong biometric idle timeout, so later we can verify that unlocking with + // strong biometric or primary auth will cancel those alarms + mStrongAuth.reportSuccessfulBiometricUnlock(false /* isStrongBiometric */, PRIMARY_USER_ID); + mStrongAuth.scheduleNonStrongBiometricIdleTimeout(PRIMARY_USER_ID); + } + + private void verifyAlarmsCancelledAndNonStrongBiometricAllowed(int userId) { + // verify that the current alarm for non-strong biometric fallback timeout is cancelled and + // removed + verify(mAlarmManager).cancel(any(NonStrongBiometricTimeoutAlarmListener.class)); + assertNull(mStrongAuth.mNonStrongBiometricTimeoutAlarmListener.get(userId)); + + // verify that the current alarm for non-strong biometric idle timeout is cancelled + verify(mAlarmManager).cancel(any(NonStrongBiometricIdleTimeoutAlarmListener.class)); + + // verify that unlocking with non-strong biometrics is allowed + assertTrue(mStrongAuth.mIsNonStrongBiometricAllowedForUser + .get(userId, mDefaultIsNonStrongBiometricAllowed)); + } + + private static boolean containsFlag(int haystack, int needle) { + return (haystack & needle) != 0; + } + + private static void waitForIdle() { + InstrumentationRegistry.getInstrumentation().waitForIdleSync(); + } +}