1 /*
<lambda>null2  * Copyright (C) 2023 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package com.android.settings.biometrics2.ui.viewmodel
17 
18 import android.app.Activity
19 import android.app.Application
20 import android.app.admin.DevicePolicyManager.PASSWORD_QUALITY_UNSPECIFIED
21 import android.content.Context
22 import android.content.Intent
23 import android.os.Bundle
24 import android.util.Log
25 import androidx.activity.result.ActivityResult
26 import androidx.lifecycle.AndroidViewModel
27 import com.android.internal.widget.LockPatternUtils
28 import com.android.settings.biometrics.BiometricEnrollBase
29 import com.android.settings.biometrics.BiometricUtils
30 import com.android.settings.biometrics.BiometricUtils.GatekeeperCredentialNotMatchException
31 import com.android.settings.biometrics2.data.repository.FingerprintRepository
32 import com.android.settings.biometrics2.ui.model.CredentialModel
33 import com.android.settings.password.ChooseLockGeneric
34 import com.android.settings.password.ChooseLockPattern
35 import com.android.settings.password.ChooseLockSettingsHelper
36 import kotlinx.coroutines.CoroutineScope
37 import kotlinx.coroutines.flow.MutableSharedFlow
38 import kotlinx.coroutines.flow.SharedFlow
39 import kotlinx.coroutines.flow.asSharedFlow
40 import kotlinx.coroutines.launch
41 
42 /**
43  * AutoCredentialViewModel which uses CredentialModel to determine next actions for activity, like
44  * start ChooseLockActivity, start ConfirmLockActivity, GenerateCredential, or do nothing.
45  */
46 class AutoCredentialViewModel(
47     application: Application,
48     private val lockPatternUtils: LockPatternUtils,
49     private val challengeGenerator: ChallengeGenerator,
50     private val credentialModel: CredentialModel
51 ) : AndroidViewModel(application) {
52 
53     /**
54      * Generic callback for FingerprintManager#generateChallenge or FaceManager#generateChallenge
55      */
56     interface GenerateChallengeCallback {
57         /** Generic generateChallenge method for FingerprintManager or FaceManager */
58         fun onChallengeGenerated(sensorId: Int, userId: Int, challenge: Long)
59     }
60 
61     /**
62      * A generic interface class for calling different generateChallenge from FingerprintManager or
63      * FaceManager
64      */
65     interface ChallengeGenerator {
66 
67         /** Callback that will be called later after challenge generated */
68         var callback: GenerateChallengeCallback?
69 
70         /** Method for generating challenge from FingerprintManager or FaceManager */
71         fun generateChallenge(userId: Int)
72     }
73 
74     /** Used to generate challenge through FingerprintRepository */
75     class FingerprintChallengeGenerator(
76         private val fingerprintRepository: FingerprintRepository
77     ) : ChallengeGenerator {
78 
79         override var callback: GenerateChallengeCallback? = null
80 
81         override fun generateChallenge(userId: Int) {
82             callback?.let {
83                 fingerprintRepository.generateChallenge(userId) {
84                         sensorId: Int, uid: Int, challenge: Long ->
85                     it.onChallengeGenerated(sensorId, uid, challenge)
86                 }
87             } ?:run {
88                 Log.e(TAG, "generateChallenge, null callback")
89             }
90         }
91 
92         companion object {
93             private const val TAG = "FingerprintChallengeGenerator"
94         }
95     }
96 
97     private val _generateChallengeFailedFlow = MutableSharedFlow<Boolean>()
98     val generateChallengeFailedFlow: SharedFlow<Boolean>
99         get() = _generateChallengeFailedFlow.asSharedFlow()
100 
101 
102     // flag if token is generating through checkCredential()'s generateChallenge()
103     private var isGeneratingChallengeDuringCheckingCredential = false
104 
105     /** Get bundle which passing back to FingerprintSettings for late generateChallenge() */
106     fun createGeneratingChallengeExtras(): Bundle? {
107         if (!isGeneratingChallengeDuringCheckingCredential
108             || !credentialModel.isValidToken
109             || !credentialModel.isValidChallenge
110         ) {
111             return null
112         }
113         val bundle = Bundle()
114         bundle.putByteArray(
115             ChooseLockSettingsHelper.EXTRA_KEY_CHALLENGE_TOKEN,
116             credentialModel.token
117         )
118         bundle.putLong(BiometricEnrollBase.EXTRA_KEY_CHALLENGE, credentialModel.challenge)
119         return bundle
120     }
121 
122     /** Check credential status for biometric enrollment. */
123     fun checkCredential(scope: CoroutineScope): CredentialAction {
124         return if (isValidCredential) {
125             CredentialAction.CREDENTIAL_VALID
126         } else if (isUnspecifiedPassword) {
127             CredentialAction.FAIL_NEED_TO_CHOOSE_LOCK
128         } else if (credentialModel.isValidGkPwHandle) {
129             val gkPwHandle = credentialModel.gkPwHandle
130             credentialModel.clearGkPwHandle()
131             // GkPwHandle is got through caller activity, we shall not revoke it after
132             // generateChallenge(). Let caller activity to make decision.
133             generateChallenge(gkPwHandle, false, scope)
134             isGeneratingChallengeDuringCheckingCredential = true
135             CredentialAction.IS_GENERATING_CHALLENGE
136         } else {
137             CredentialAction.FAIL_NEED_TO_CONFIRM_LOCK
138         }
139     }
140 
141     private fun generateChallenge(
142         gkPwHandle: Long,
143         revokeGkPwHandle: Boolean,
144         scope: CoroutineScope
145     ) {
146         challengeGenerator.callback = object : GenerateChallengeCallback {
147             override fun onChallengeGenerated(sensorId: Int, userId: Int, challenge: Long) {
148                 var illegalStateExceptionCaught = false
149                 try {
150                     val newToken = requestGatekeeperHat(gkPwHandle, challenge, userId)
151                     credentialModel.challenge = challenge
152                     credentialModel.token = newToken
153                 } catch (e: IllegalStateException) {
154                     Log.e(TAG, "generateChallenge, IllegalStateException", e)
155                     illegalStateExceptionCaught = true
156                 } finally {
157                     if (revokeGkPwHandle) {
158                         lockPatternUtils.removeGatekeeperPasswordHandle(gkPwHandle)
159                     }
160                     Log.d(
161                         TAG,
162                         "generateChallenge(), model:$credentialModel"
163                                 + ", revokeGkPwHandle:$revokeGkPwHandle"
164                     )
165                     // Check credential again
166                     if (!isValidCredential || illegalStateExceptionCaught) {
167                         Log.w(TAG, "generateChallenge, invalid Credential or IllegalStateException")
168                         scope.launch {
169                             _generateChallengeFailedFlow.emit(true)
170                         }
171                     }
172                 }
173             }
174         }
175         challengeGenerator.generateChallenge(userId)
176     }
177 
178     private val isValidCredential: Boolean
179         get() = !isUnspecifiedPassword && credentialModel.isValidToken
180 
181     private val isUnspecifiedPassword: Boolean
182         get() = lockPatternUtils.getActivePasswordQuality(userId) == PASSWORD_QUALITY_UNSPECIFIED
183 
184     /**
185      * Handle activity result from ChooseLockGeneric, ConfirmLockPassword, or ConfirmLockPattern
186      * @param isChooseLock true if result is coming from ChooseLockGeneric. False if result is
187      * coming from ConfirmLockPassword or ConfirmLockPattern
188      * @param result activity result
189      * @return if it is a valid result and viewModel is generating challenge
190      */
191     fun generateChallengeAsCredentialActivityResult(
192         isChooseLock: Boolean,
193         result: ActivityResult,
194         scope: CoroutineScope
195     ): Boolean {
196         if ((isChooseLock && result.resultCode == ChooseLockPattern.RESULT_FINISHED) ||
197             (!isChooseLock && result.resultCode == Activity.RESULT_OK)) {
198             result.data?.let {
199                 val gkPwHandle = it.getLongExtra(
200                     ChooseLockSettingsHelper.EXTRA_KEY_GK_PW_HANDLE,
201                     CredentialModel.INVALID_GK_PW_HANDLE
202                 )
203                 // Revoke self requested GkPwHandle because it shall only used once inside this
204                 // activity lifecycle.
205                 generateChallenge(gkPwHandle, true, scope)
206                 return true
207             }
208         }
209         return false
210     }
211 
212     val userId: Int
213         get() = credentialModel.userId
214 
215     val token: ByteArray?
216         get() = credentialModel.token
217 
218     @Throws(IllegalStateException::class)
219     private fun requestGatekeeperHat(gkPwHandle: Long, challenge: Long, userId: Int): ByteArray? {
220         val response = lockPatternUtils
221             .verifyGatekeeperPasswordHandle(gkPwHandle, challenge, userId)
222         if (!response.isMatched) {
223             throw GatekeeperCredentialNotMatchException("Unable to request Gatekeeper HAT")
224         }
225         return response.gatekeeperHAT
226     }
227 
228     /** Create Intent for choosing lock */
229     fun createChooseLockIntent(
230         context: Context, isSuw: Boolean,
231         suwExtras: Bundle
232     ): Intent {
233         val intent = BiometricUtils.getChooseLockIntent(
234             context, isSuw,
235             suwExtras
236         )
237         intent.putExtra(
238             ChooseLockGeneric.ChooseLockGenericFragment.HIDE_INSECURE_OPTIONS,
239             true
240         )
241         intent.putExtra(ChooseLockSettingsHelper.EXTRA_KEY_REQUEST_GK_PW_HANDLE, true)
242         intent.putExtra(ChooseLockSettingsHelper.EXTRA_KEY_FOR_FINGERPRINT, true)
243         if (credentialModel.isValidUserId) {
244             intent.putExtra(Intent.EXTRA_USER_ID, credentialModel.userId)
245         }
246         return intent
247     }
248 
249     /** Create ConfirmLockLauncher */
250     fun createConfirmLockLauncher(
251         activity: Activity,
252         requestCode: Int, title: String
253     ): ChooseLockSettingsHelper {
254         val builder = ChooseLockSettingsHelper.Builder(activity)
255         builder.setRequestCode(requestCode)
256             .setTitle(title)
257             .setRequestGatekeeperPasswordHandle(true)
258             .setForegroundOnly(true)
259             .setReturnCredentials(true)
260         if (credentialModel.isValidUserId) {
261             builder.setUserId(credentialModel.userId)
262         }
263         return builder.build()
264     }
265 
266     companion object {
267         private const val TAG = "AutoCredentialViewModel"
268     }
269 }
270 
271 enum class CredentialAction {
272 
273     CREDENTIAL_VALID,
274 
275     /** Valid credential, activity does nothing. */
276     IS_GENERATING_CHALLENGE,
277 
278     /** This credential looks good, but still need to run generateChallenge(). */
279     FAIL_NEED_TO_CHOOSE_LOCK,
280 
281     /** Need activity to run confirm lock */
282     FAIL_NEED_TO_CONFIRM_LOCK
283 }
284