Skip authentication if device was unlocked recently

- Sync the same behavior from SystemUI to Settings

Bug: 365611488
Flag: EXEMPT bugfix
Test: Manual testing
atest -c WifiNetworkDetailsFragmentTest \
         WifiDetailPreferenceController2Test \
         WifiTetherSSIDPreferenceControllerTest \
         com.android.settings.wifi.dpp.WifiDppUtilsTest
atest -c com.android.settings.spa.wifi.dpp.WifiDppUtilsTest

Change-Id: Ie3e8374b1fdbbc61e9e5bbf0f5162b18ba1452f3
This commit is contained in:
Weng Su
2024-12-13 08:44:05 +08:00
parent 626017d196
commit e5d19b38cb
6 changed files with 204 additions and 45 deletions

View File

@@ -706,7 +706,7 @@ public class NetworkProviderSettings extends RestrictedDashboardFragment
forget(mSelectedWifiEntry);
return true;
case MENU_ID_SHARE:
WifiDppUtils.showLockScreen(getContext(),
WifiDppUtils.showLockScreenForWifiSharing(getContext(),
() -> launchWifiDppConfiguratorActivity(mSelectedWifiEntry));
return true;
case MENU_ID_MODIFY:

View File

@@ -57,7 +57,8 @@ public class AddDevicePreferenceController2 extends BasePreferenceController {
@Override
public boolean handlePreferenceTreeClick(Preference preference) {
if (KEY_ADD_DEVICE.equals(preference.getKey())) {
WifiDppUtils.showLockScreen(mContext, () -> launchWifiDppConfiguratorQrCodeScanner());
WifiDppUtils.showLockScreenForWifiSharing(mContext,
() -> launchWifiDppConfiguratorQrCodeScanner());
return true; /* click is handled */
}

View File

@@ -980,7 +980,8 @@ public class WifiDetailPreferenceController2 extends AbstractPreferenceControlle
* Share the wifi network with QR code.
*/
private void shareNetwork() {
WifiDppUtils.showLockScreen(mContext, () -> launchWifiDppConfiguratorActivity());
WifiDppUtils.showLockScreenForWifiSharing(mContext,
() -> launchWifiDppConfiguratorActivity());
}
/**

View File

@@ -16,6 +16,8 @@
package com.android.settings.wifi.dpp;
import android.annotation.NonNull;
import android.annotation.SuppressLint;
import android.app.KeyguardManager;
import android.content.Context;
import android.content.Intent;
@@ -33,6 +35,9 @@ import android.os.Vibrator;
import android.security.keystore.KeyGenParameterSpec;
import android.security.keystore.KeyProperties;
import android.text.TextUtils;
import android.util.Log;
import androidx.annotation.VisibleForTesting;
import com.android.settings.R;
import com.android.settings.Utils;
@@ -58,6 +63,8 @@ import javax.crypto.SecretKey;
* @see WifiQrCode
*/
public class WifiDppUtils {
private static final String TAG = "WifiDppUtils";
/**
* The fragment tag specified to FragmentManager for container activities to manage fragments.
*/
@@ -109,7 +116,15 @@ public class WifiDppUtils {
private static final Duration VIBRATE_DURATION_QR_CODE_RECOGNITION = Duration.ofMillis(3);
private static final String AES_CBC_PKCS7_PADDING = "AES/CBC/PKCS7Padding";
/**
* Parameters to check whether the device has been locked recently
*/
@VisibleForTesting
public static final String AES_CBC_PKCS7_PADDING = "AES/CBC/PKCS7Padding";
@VisibleForTesting
public static final String WIFI_SHARING_KEY_ALIAS = "wifi_sharing_auth_key";
@VisibleForTesting
public static final int WIFI_SHARING_MAX_UNLOCK_SECONDS = 60;
/**
* Returns whether the device support WiFi DPP.
@@ -426,51 +441,75 @@ public class WifiDppUtils {
* Shows authentication screen to confirm credentials (pin, pattern or password) for the current
* user of the device.
*
* @param context The {@code Context} used to get {@code KeyguardManager} service
* @param context The {@code Context} used to get {@code KeyguardManager} service
* @param successRunnable The {@code Runnable} which will be executed if the user does not setup
* device security or if lock screen is unlocked
*/
public static void showLockScreen(Context context, Runnable successRunnable) {
final KeyguardManager keyguardManager = (KeyguardManager) context.getSystemService(
Context.KEYGUARD_SERVICE);
if (keyguardManager.isKeyguardSecure()) {
final BiometricPrompt.AuthenticationCallback authenticationCallback =
new BiometricPrompt.AuthenticationCallback() {
@Override
public void onAuthenticationSucceeded(
BiometricPrompt.AuthenticationResult result) {
successRunnable.run();
}
@Override
public void onAuthenticationError(int errorCode, CharSequence errString) {
//Do nothing
}
};
final int userId = UserHandle.myUserId();
final BiometricPrompt.Builder builder = new BiometricPrompt.Builder(context)
.setTitle(context.getText(R.string.wifi_dpp_lockscreen_title));
if (keyguardManager.isDeviceSecure()) {
builder.setDeviceCredentialAllowed(true);
builder.setTextForDeviceCredential(
null /* title */,
Utils.getConfirmCredentialStringForUser(
context, userId, Utils.getCredentialType(context, userId)),
null /* description */);
}
final BiometricPrompt bp = builder.build();
final Handler handler = new Handler(Looper.getMainLooper());
bp.authenticate(new CancellationSignal(),
runnable -> handler.post(runnable),
authenticationCallback);
} else {
public static void showLockScreen(@NonNull Context context, @NonNull Runnable successRunnable) {
KeyguardManager keyguardManager = context.getSystemService(KeyguardManager.class);
if (keyguardManager == null || !keyguardManager.isKeyguardSecure()) {
successRunnable.run();
return;
}
showLockScreen(context, successRunnable, keyguardManager);
}
/**
* Shows authentication screen to confirm credentials (pin, pattern or password) for the
* current user of the device. But if the device has been unlocked recently, the
* authentication screen will be skipped.
*
* @param context The {@code Context} used to get {@code KeyguardManager} service
* @param successRunnable The {@code Runnable} which will be executed if the user does not setup
* device security or if lock screen is unlocked
*/
public static void showLockScreenForWifiSharing(@NonNull Context context,
@NonNull Runnable successRunnable) {
KeyguardManager keyguardManager = context.getSystemService(KeyguardManager.class);
if (keyguardManager == null || !keyguardManager.isKeyguardSecure()) {
successRunnable.run();
return;
}
if (isUnlockedWithinSeconds(WIFI_SHARING_KEY_ALIAS, WIFI_SHARING_MAX_UNLOCK_SECONDS)) {
Log.d(TAG, "Bypassing the lock screen because the device was unlocked recently.");
successRunnable.run();
return;
}
showLockScreen(context, successRunnable, keyguardManager);
}
@SuppressLint("MissingPermission")
private static void showLockScreen(@NonNull Context context, @NonNull Runnable successRunnable,
@NonNull KeyguardManager keyguardManager) {
BiometricPrompt.AuthenticationCallback authenticationCallback =
new BiometricPrompt.AuthenticationCallback() {
@Override
public void onAuthenticationSucceeded(
BiometricPrompt.AuthenticationResult result) {
successRunnable.run();
}
@Override
public void onAuthenticationError(int errorCode, CharSequence errString) {
//Do nothing
}
};
int userId = UserHandle.myUserId();
BiometricPrompt.Builder builder = new BiometricPrompt.Builder(context)
.setTitle(context.getText(R.string.wifi_dpp_lockscreen_title));
if (keyguardManager.isDeviceSecure()) {
builder.setDeviceCredentialAllowed(true);
builder.setTextForDeviceCredential(
null /* title */,
Utils.getConfirmCredentialStringForUser(
context, userId, Utils.getCredentialType(context, userId)),
null /* description */);
}
BiometricPrompt bp = builder.build();
Handler handler = new Handler(Looper.getMainLooper());
bp.authenticate(new CancellationSignal(),
runnable -> handler.post(runnable),
authenticationCallback);
}
/**

View File

@@ -123,7 +123,7 @@ public class WifiTetherSSIDPreferenceController extends WifiTetherBasePreference
}
private void shareHotspotNetwork(Intent intent) {
WifiDppUtils.showLockScreen(mContext, () -> {
WifiDppUtils.showLockScreenForWifiSharing(mContext, () -> {
mMetricsFeatureProvider.action(SettingsEnums.PAGE_UNKNOWN,
SettingsEnums.ACTION_SETTINGS_SHARE_WIFI_HOTSPOT_QR_CODE,
SettingsEnums.SETTINGS_WIFI_DPP_CONFIGURATOR,

View File

@@ -0,0 +1,118 @@
/*
* Copyright (C) 2024 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.settings.spa.wifi.dpp
import android.app.KeyguardManager
import android.content.Context
import android.hardware.biometrics.BiometricPrompt
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.android.dx.mockito.inline.extended.ExtendedMockito
import com.android.settings.wifi.dpp.WifiDppUtils
import java.security.InvalidKeyException
import java.security.Key
import javax.crypto.Cipher
import org.junit.After
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mockito.any
import org.mockito.Mockito.anyInt
import org.mockito.Mockito.never
import org.mockito.Mockito.verify
import org.mockito.MockitoSession
import org.mockito.kotlin.doReturn
import org.mockito.kotlin.doThrow
import org.mockito.kotlin.mock
import org.mockito.kotlin.spy
import org.mockito.kotlin.stub
import org.mockito.kotlin.whenever
import org.mockito.quality.Strictness
@RunWith(AndroidJUnit4::class)
class WifiDppUtilsTest {
private lateinit var mockSession: MockitoSession
private val runnable = mock<Runnable>()
private val cipher = mock<Cipher>()
private var mockKeyguardManager = mock<KeyguardManager>()
private var context: Context =
spy(ApplicationProvider.getApplicationContext()) {
on { getSystemService(KeyguardManager::class.java) } doReturn mockKeyguardManager
}
@Before
fun setUp() {
mockSession =
ExtendedMockito.mockitoSession()
.initMocks(this)
.mockStatic(Cipher::class.java)
.mockStatic(BiometricPrompt::class.java)
.mockStatic(BiometricPrompt.Builder::class.java)
.strictness(Strictness.LENIENT)
.startMocking()
whenever(context.applicationContext).thenReturn(context)
}
@After
fun tearDown() {
mockSession.finishMocking()
}
@Test
fun showLockScreen_notKeyguardSecure_runRunnable() {
mockKeyguardManager.stub { on { isKeyguardSecure } doReturn false }
WifiDppUtils.showLockScreen(context, runnable)
verify(runnable).run()
}
@Test
fun showLockScreen_isKeyguardSecure_doNotRunRunnable() {
mockKeyguardManager.stub { on { isKeyguardSecure } doReturn true }
try {
WifiDppUtils.showLockScreen(context, runnable)
} catch (_: Exception) {}
verify(runnable, never()).run()
}
@Test
fun showLockScreenForWifiSharing_deviceUnlockedRecently_runRunnable() {
mockKeyguardManager.stub { on { isKeyguardSecure } doReturn true }
whenever(Cipher.getInstance(WifiDppUtils.AES_CBC_PKCS7_PADDING)).thenReturn(cipher)
WifiDppUtils.showLockScreenForWifiSharing(context, runnable)
verify(runnable).run()
}
@Test
fun showLockScreenForWifiSharing_deviceNotUnlockedRecently_doNotRunRunnable() {
mockKeyguardManager.stub { on { isKeyguardSecure } doReturn true }
whenever(Cipher.getInstance(WifiDppUtils.AES_CBC_PKCS7_PADDING)).thenReturn(cipher)
doThrow(InvalidKeyException()).whenever(cipher).init(anyInt(), any<Key>())
try {
WifiDppUtils.showLockScreenForWifiSharing(context, runnable)
} catch (_: Exception) {}
verify(runnable, never()).run()
}
}