1 /*
2  * Copyright (C) 2019 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 
17 package com.android.systemui.biometrics;
18 
19 import android.annotation.IntDef;
20 import android.annotation.NonNull;
21 import android.annotation.Nullable;
22 import android.app.AlertDialog;
23 import android.app.admin.DevicePolicyManager;
24 import android.content.Context;
25 import android.content.pm.UserInfo;
26 import android.graphics.drawable.Drawable;
27 import android.hardware.biometrics.BiometricPrompt;
28 import android.hardware.biometrics.PromptInfo;
29 import android.os.AsyncTask;
30 import android.os.CountDownTimer;
31 import android.os.Handler;
32 import android.os.Looper;
33 import android.os.SystemClock;
34 import android.os.UserManager;
35 import android.text.TextUtils;
36 import android.util.AttributeSet;
37 import android.view.View;
38 import android.view.WindowManager;
39 import android.view.accessibility.AccessibilityManager;
40 import android.widget.ImageView;
41 import android.widget.LinearLayout;
42 import android.widget.TextView;
43 
44 import androidx.annotation.StringRes;
45 
46 import com.android.internal.widget.LockPatternUtils;
47 import com.android.internal.widget.VerifyCredentialResponse;
48 import com.android.systemui.R;
49 import com.android.systemui.animation.Interpolators;
50 
51 import java.lang.annotation.Retention;
52 import java.lang.annotation.RetentionPolicy;
53 
54 /**
55  * Abstract base class for Pin, Pattern, or Password authentication, for
56  * {@link BiometricPrompt.Builder#setAllowedAuthenticators(int)}}
57  */
58 public abstract class AuthCredentialView extends LinearLayout {
59     private static final String TAG = "BiometricPrompt/AuthCredentialView";
60     private static final int ERROR_DURATION_MS = 3000;
61 
62     static final int USER_TYPE_PRIMARY = 1;
63     static final int USER_TYPE_MANAGED_PROFILE = 2;
64     static final int USER_TYPE_SECONDARY = 3;
65     @Retention(RetentionPolicy.SOURCE)
66     @IntDef({USER_TYPE_PRIMARY, USER_TYPE_MANAGED_PROFILE, USER_TYPE_SECONDARY})
67     private @interface UserType {}
68 
69     protected final Handler mHandler;
70     protected final LockPatternUtils mLockPatternUtils;
71 
72     private final AccessibilityManager mAccessibilityManager;
73     private final UserManager mUserManager;
74     private final DevicePolicyManager mDevicePolicyManager;
75 
76     private PromptInfo mPromptInfo;
77     private AuthPanelController mPanelController;
78     private boolean mShouldAnimatePanel;
79     private boolean mShouldAnimateContents;
80 
81     private TextView mTitleView;
82     private TextView mSubtitleView;
83     private TextView mDescriptionView;
84     private ImageView mIconView;
85     protected TextView mErrorView;
86 
87     protected @Utils.CredentialType int mCredentialType;
88     protected AuthContainerView mContainerView;
89     protected Callback mCallback;
90     protected AsyncTask<?, ?, ?> mPendingLockCheck;
91     protected int mUserId;
92     protected long mOperationId;
93     protected int mEffectiveUserId;
94     protected ErrorTimer mErrorTimer;
95 
96     interface Callback {
onCredentialMatched(byte[] attestation)97         void onCredentialMatched(byte[] attestation);
98     }
99 
100     protected static class ErrorTimer extends CountDownTimer {
101         private final TextView mErrorView;
102         private final Context mContext;
103 
104         /**
105          * @param millisInFuture    The number of millis in the future from the call
106          *                          to {@link #start()} until the countdown is done and {@link
107          *                          #onFinish()}
108          *                          is called.
109          * @param countDownInterval The interval along the way to receive
110          *                          {@link #onTick(long)} callbacks.
111          */
ErrorTimer(Context context, long millisInFuture, long countDownInterval, TextView errorView)112         public ErrorTimer(Context context, long millisInFuture, long countDownInterval,
113                 TextView errorView) {
114             super(millisInFuture, countDownInterval);
115             mErrorView = errorView;
116             mContext = context;
117         }
118 
119         @Override
onTick(long millisUntilFinished)120         public void onTick(long millisUntilFinished) {
121             final int secondsCountdown = (int) (millisUntilFinished / 1000);
122             mErrorView.setText(mContext.getString(
123                     R.string.biometric_dialog_credential_too_many_attempts, secondsCountdown));
124         }
125 
126         @Override
onFinish()127         public void onFinish() {
128             if (mErrorView != null) {
129                 mErrorView.setText("");
130             }
131         }
132     }
133 
134     protected final Runnable mClearErrorRunnable = new Runnable() {
135         @Override
136         public void run() {
137             if (mErrorView != null) {
138                 mErrorView.setText("");
139             }
140         }
141     };
142 
AuthCredentialView(Context context, AttributeSet attrs)143     public AuthCredentialView(Context context, AttributeSet attrs) {
144         super(context, attrs);
145 
146         mLockPatternUtils = new LockPatternUtils(mContext);
147         mHandler = new Handler(Looper.getMainLooper());
148         mAccessibilityManager = mContext.getSystemService(AccessibilityManager.class);
149         mUserManager = mContext.getSystemService(UserManager.class);
150         mDevicePolicyManager = mContext.getSystemService(DevicePolicyManager.class);
151     }
152 
showError(String error)153     protected void showError(String error) {
154         if (mHandler != null) {
155             mHandler.removeCallbacks(mClearErrorRunnable);
156             mHandler.postDelayed(mClearErrorRunnable, ERROR_DURATION_MS);
157         }
158         if (mErrorView != null) {
159             mErrorView.setText(error);
160         }
161     }
162 
setTextOrHide(TextView view, CharSequence text)163     private void setTextOrHide(TextView view, CharSequence text) {
164         if (TextUtils.isEmpty(text)) {
165             view.setVisibility(View.GONE);
166         } else {
167             view.setText(text);
168         }
169 
170         Utils.notifyAccessibilityContentChanged(mAccessibilityManager, this);
171     }
172 
setText(TextView view, CharSequence text)173     private void setText(TextView view, CharSequence text) {
174         view.setText(text);
175     }
176 
setUserId(int userId)177     void setUserId(int userId) {
178         mUserId = userId;
179     }
180 
setOperationId(long operationId)181     void setOperationId(long operationId) {
182         mOperationId = operationId;
183     }
184 
setEffectiveUserId(int effectiveUserId)185     void setEffectiveUserId(int effectiveUserId) {
186         mEffectiveUserId = effectiveUserId;
187     }
188 
setCredentialType(@tils.CredentialType int credentialType)189     void setCredentialType(@Utils.CredentialType int credentialType) {
190         mCredentialType = credentialType;
191     }
192 
setCallback(Callback callback)193     void setCallback(Callback callback) {
194         mCallback = callback;
195     }
196 
setPromptInfo(PromptInfo promptInfo)197     void setPromptInfo(PromptInfo promptInfo) {
198         mPromptInfo = promptInfo;
199     }
200 
setPanelController(AuthPanelController panelController, boolean animatePanel)201     void setPanelController(AuthPanelController panelController, boolean animatePanel) {
202         mPanelController = panelController;
203         mShouldAnimatePanel = animatePanel;
204     }
205 
setShouldAnimateContents(boolean animateContents)206     void setShouldAnimateContents(boolean animateContents) {
207         mShouldAnimateContents = animateContents;
208     }
209 
setContainerView(AuthContainerView containerView)210     void setContainerView(AuthContainerView containerView) {
211         mContainerView = containerView;
212     }
213 
214     @Override
onAttachedToWindow()215     protected void onAttachedToWindow() {
216         super.onAttachedToWindow();
217 
218         final CharSequence title = getTitle(mPromptInfo);
219         setText(mTitleView, title);
220         setTextOrHide(mSubtitleView, getSubtitle(mPromptInfo));
221         setTextOrHide(mDescriptionView, getDescription(mPromptInfo));
222         announceForAccessibility(title);
223 
224         if (mIconView != null) {
225             final boolean isManagedProfile = Utils.isManagedProfile(mContext, mEffectiveUserId);
226             final Drawable image;
227             if (isManagedProfile) {
228                 image = getResources().getDrawable(R.drawable.auth_dialog_enterprise,
229                         mContext.getTheme());
230             } else {
231                 image = getResources().getDrawable(R.drawable.auth_dialog_lock,
232                         mContext.getTheme());
233             }
234             mIconView.setImageDrawable(image);
235         }
236 
237         // Only animate this if we're transitioning from a biometric view.
238         if (mShouldAnimateContents) {
239             setTranslationY(getResources()
240                     .getDimension(R.dimen.biometric_dialog_credential_translation_offset));
241             setAlpha(0);
242 
243             postOnAnimation(() -> {
244                 animate().translationY(0)
245                         .setDuration(AuthDialog.ANIMATE_CREDENTIAL_INITIAL_DURATION_MS)
246                         .alpha(1.f)
247                         .setInterpolator(Interpolators.LINEAR_OUT_SLOW_IN)
248                         .withLayer()
249                         .start();
250             });
251         }
252     }
253 
254     @Override
onDetachedFromWindow()255     protected void onDetachedFromWindow() {
256         super.onDetachedFromWindow();
257         if (mErrorTimer != null) {
258             mErrorTimer.cancel();
259         }
260     }
261 
262     @Override
onFinishInflate()263     protected void onFinishInflate() {
264         super.onFinishInflate();
265         mTitleView = findViewById(R.id.title);
266         mSubtitleView = findViewById(R.id.subtitle);
267         mDescriptionView = findViewById(R.id.description);
268         mIconView = findViewById(R.id.icon);
269         mErrorView = findViewById(R.id.error);
270     }
271 
272     @Override
onLayout(boolean changed, int left, int top, int right, int bottom)273     protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
274         super.onLayout(changed, left, top, right, bottom);
275 
276         if (mShouldAnimatePanel) {
277             // Credential view is always full screen.
278             mPanelController.setUseFullScreen(true);
279             mPanelController.updateForContentDimensions(mPanelController.getContainerWidth(),
280                     mPanelController.getContainerHeight(), 0 /* animateDurationMs */);
281             mShouldAnimatePanel = false;
282         }
283     }
284 
onErrorTimeoutFinish()285     protected void onErrorTimeoutFinish() {}
286 
onCredentialVerified(@onNull VerifyCredentialResponse response, int timeoutMs)287     protected void onCredentialVerified(@NonNull VerifyCredentialResponse response, int timeoutMs) {
288         if (response.isMatched()) {
289             mClearErrorRunnable.run();
290             mLockPatternUtils.userPresent(mEffectiveUserId);
291 
292             // The response passed into this method contains the Gatekeeper Password. We still
293             // have to request Gatekeeper to create a Hardware Auth Token with the
294             // Gatekeeper Password and Challenge (keystore operationId in this case)
295             final long pwHandle = response.getGatekeeperPasswordHandle();
296             final VerifyCredentialResponse gkResponse = mLockPatternUtils
297                     .verifyGatekeeperPasswordHandle(pwHandle, mOperationId, mEffectiveUserId);
298 
299             mCallback.onCredentialMatched(gkResponse.getGatekeeperHAT());
300             mLockPatternUtils.removeGatekeeperPasswordHandle(pwHandle);
301         } else {
302             if (timeoutMs > 0) {
303                 mHandler.removeCallbacks(mClearErrorRunnable);
304                 long deadline = mLockPatternUtils.setLockoutAttemptDeadline(
305                         mEffectiveUserId, timeoutMs);
306                 mErrorTimer = new ErrorTimer(mContext,
307                         deadline - SystemClock.elapsedRealtime(),
308                         LockPatternUtils.FAILED_ATTEMPT_COUNTDOWN_INTERVAL_MS,
309                         mErrorView) {
310                     @Override
311                     public void onFinish() {
312                         onErrorTimeoutFinish();
313                         mClearErrorRunnable.run();
314                     }
315                 };
316                 mErrorTimer.start();
317             } else {
318                 final boolean didUpdateErrorText = reportFailedAttempt();
319                 if (!didUpdateErrorText) {
320                     final @StringRes int errorRes;
321                     switch (mCredentialType) {
322                         case Utils.CREDENTIAL_PIN:
323                             errorRes = R.string.biometric_dialog_wrong_pin;
324                             break;
325                         case Utils.CREDENTIAL_PATTERN:
326                             errorRes = R.string.biometric_dialog_wrong_pattern;
327                             break;
328                         case Utils.CREDENTIAL_PASSWORD:
329                         default:
330                             errorRes = R.string.biometric_dialog_wrong_password;
331                             break;
332                     }
333                     showError(getResources().getString(errorRes));
334                 }
335             }
336         }
337     }
338 
reportFailedAttempt()339     private boolean reportFailedAttempt() {
340         boolean result = updateErrorMessage(
341                 mLockPatternUtils.getCurrentFailedPasswordAttempts(mEffectiveUserId) + 1);
342         mLockPatternUtils.reportFailedPasswordAttempt(mEffectiveUserId);
343         return result;
344     }
345 
updateErrorMessage(int numAttempts)346     private boolean updateErrorMessage(int numAttempts) {
347         // Don't show any message if there's no maximum number of attempts.
348         final int maxAttempts = mLockPatternUtils.getMaximumFailedPasswordsForWipe(
349                 mEffectiveUserId);
350         if (maxAttempts <= 0 || numAttempts <= 0) {
351             return false;
352         }
353 
354         // Update the on-screen error string.
355         if (mErrorView != null) {
356             final String message = getResources().getString(
357                     R.string.biometric_dialog_credential_attempts_before_wipe,
358                     numAttempts,
359                     maxAttempts);
360             showError(message);
361         }
362 
363         // Only show dialog if <=1 attempts are left before wiping.
364         final int remainingAttempts = maxAttempts - numAttempts;
365         if (remainingAttempts == 1) {
366             showLastAttemptBeforeWipeDialog();
367         } else if (remainingAttempts <= 0) {
368             showNowWipingDialog();
369         }
370         return true;
371     }
372 
showLastAttemptBeforeWipeDialog()373     private void showLastAttemptBeforeWipeDialog() {
374         final AlertDialog alertDialog = new AlertDialog.Builder(mContext)
375                 .setTitle(R.string.biometric_dialog_last_attempt_before_wipe_dialog_title)
376                 .setMessage(
377                         getLastAttemptBeforeWipeMessageRes(getUserTypeForWipe(), mCredentialType))
378                 .setPositiveButton(android.R.string.ok, null)
379                 .create();
380         alertDialog.getWindow().setType(WindowManager.LayoutParams.TYPE_STATUS_BAR_SUB_PANEL);
381         alertDialog.show();
382     }
383 
showNowWipingDialog()384     private void showNowWipingDialog() {
385         final AlertDialog alertDialog = new AlertDialog.Builder(mContext)
386                 .setMessage(getNowWipingMessageRes(getUserTypeForWipe()))
387                 .setPositiveButton(R.string.biometric_dialog_now_wiping_dialog_dismiss, null)
388                 .setOnDismissListener(
389                         dialog -> mContainerView.animateAway(AuthDialogCallback.DISMISSED_ERROR))
390                 .create();
391         alertDialog.getWindow().setType(WindowManager.LayoutParams.TYPE_STATUS_BAR_SUB_PANEL);
392         alertDialog.show();
393     }
394 
getUserTypeForWipe()395     private @UserType int getUserTypeForWipe() {
396         final UserInfo userToBeWiped = mUserManager.getUserInfo(
397                 mDevicePolicyManager.getProfileWithMinimumFailedPasswordsForWipe(mEffectiveUserId));
398         if (userToBeWiped == null || userToBeWiped.isPrimary()) {
399             return USER_TYPE_PRIMARY;
400         } else if (userToBeWiped.isManagedProfile()) {
401             return USER_TYPE_MANAGED_PROFILE;
402         } else {
403             return USER_TYPE_SECONDARY;
404         }
405     }
406 
getLastAttemptBeforeWipeMessageRes( @serType int userType, @Utils.CredentialType int credentialType)407     private static @StringRes int getLastAttemptBeforeWipeMessageRes(
408             @UserType int userType, @Utils.CredentialType int credentialType) {
409         switch (userType) {
410             case USER_TYPE_PRIMARY:
411                 return getLastAttemptBeforeWipeDeviceMessageRes(credentialType);
412             case USER_TYPE_MANAGED_PROFILE:
413                 return getLastAttemptBeforeWipeProfileMessageRes(credentialType);
414             case USER_TYPE_SECONDARY:
415                 return getLastAttemptBeforeWipeUserMessageRes(credentialType);
416             default:
417                 throw new IllegalArgumentException("Unrecognized user type:" + userType);
418         }
419     }
420 
getLastAttemptBeforeWipeDeviceMessageRes( @tils.CredentialType int credentialType)421     private static @StringRes int getLastAttemptBeforeWipeDeviceMessageRes(
422             @Utils.CredentialType int credentialType) {
423         switch (credentialType) {
424             case Utils.CREDENTIAL_PIN:
425                 return R.string.biometric_dialog_last_pin_attempt_before_wipe_device;
426             case Utils.CREDENTIAL_PATTERN:
427                 return R.string.biometric_dialog_last_pattern_attempt_before_wipe_device;
428             case Utils.CREDENTIAL_PASSWORD:
429             default:
430                 return R.string.biometric_dialog_last_password_attempt_before_wipe_device;
431         }
432     }
433 
getLastAttemptBeforeWipeProfileMessageRes( @tils.CredentialType int credentialType)434     private static @StringRes int getLastAttemptBeforeWipeProfileMessageRes(
435             @Utils.CredentialType int credentialType) {
436         switch (credentialType) {
437             case Utils.CREDENTIAL_PIN:
438                 return R.string.biometric_dialog_last_pin_attempt_before_wipe_profile;
439             case Utils.CREDENTIAL_PATTERN:
440                 return R.string.biometric_dialog_last_pattern_attempt_before_wipe_profile;
441             case Utils.CREDENTIAL_PASSWORD:
442             default:
443                 return R.string.biometric_dialog_last_password_attempt_before_wipe_profile;
444         }
445     }
446 
getLastAttemptBeforeWipeUserMessageRes( @tils.CredentialType int credentialType)447     private static @StringRes int getLastAttemptBeforeWipeUserMessageRes(
448             @Utils.CredentialType int credentialType) {
449         switch (credentialType) {
450             case Utils.CREDENTIAL_PIN:
451                 return R.string.biometric_dialog_last_pin_attempt_before_wipe_user;
452             case Utils.CREDENTIAL_PATTERN:
453                 return R.string.biometric_dialog_last_pattern_attempt_before_wipe_user;
454             case Utils.CREDENTIAL_PASSWORD:
455             default:
456                 return R.string.biometric_dialog_last_password_attempt_before_wipe_user;
457         }
458     }
459 
getNowWipingMessageRes(@serType int userType)460     private static @StringRes int getNowWipingMessageRes(@UserType int userType) {
461         switch (userType) {
462             case USER_TYPE_PRIMARY:
463                 return R.string.biometric_dialog_failed_attempts_now_wiping_device;
464             case USER_TYPE_MANAGED_PROFILE:
465                 return R.string.biometric_dialog_failed_attempts_now_wiping_profile;
466             case USER_TYPE_SECONDARY:
467                 return R.string.biometric_dialog_failed_attempts_now_wiping_user;
468             default:
469                 throw new IllegalArgumentException("Unrecognized user type:" + userType);
470         }
471     }
472 
473     @Nullable
getTitle(@onNull PromptInfo promptInfo)474     private static CharSequence getTitle(@NonNull PromptInfo promptInfo) {
475         final CharSequence credentialTitle = promptInfo.getDeviceCredentialTitle();
476         return credentialTitle != null ? credentialTitle : promptInfo.getTitle();
477     }
478 
479     @Nullable
getSubtitle(@onNull PromptInfo promptInfo)480     private static CharSequence getSubtitle(@NonNull PromptInfo promptInfo) {
481         final CharSequence credentialSubtitle = promptInfo.getDeviceCredentialSubtitle();
482         return credentialSubtitle != null ? credentialSubtitle : promptInfo.getSubtitle();
483     }
484 
485     @Nullable
getDescription(@onNull PromptInfo promptInfo)486     private static CharSequence getDescription(@NonNull PromptInfo promptInfo) {
487         final CharSequence credentialDescription = promptInfo.getDeviceCredentialDescription();
488         return credentialDescription != null ? credentialDescription : promptInfo.getDescription();
489     }
490 }
491