1 /*
2  * Copyright (C) 2022 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.server.biometrics.sensors;
18 
19 import static com.android.server.biometrics.sensors.AuthResultCoordinator.AUTHENTICATOR_PERMANENT_LOCKED;
20 import static com.android.server.biometrics.sensors.AuthResultCoordinator.AUTHENTICATOR_TIMED_LOCKED;
21 import static com.android.server.biometrics.sensors.AuthResultCoordinator.AUTHENTICATOR_UNLOCKED;
22 
23 import android.hardware.biometrics.BiometricManager.Authenticators;
24 import android.os.SystemClock;
25 import android.util.Slog;
26 
27 import com.android.internal.annotations.VisibleForTesting;
28 
29 import java.time.Clock;
30 import java.util.Arrays;
31 import java.util.HashSet;
32 import java.util.Map;
33 import java.util.Set;
34 
35 /**
36  * Coordinates lockout counter enforcement for all types of biometric strengths across all users.
37  *
38  * This class is not thread-safe. In general, all calls to this class should be made on the same
39  * handler to ensure no collisions.
40  */
41 public class AuthSessionCoordinator implements AuthSessionListener {
42     private static final String TAG = "AuthSessionCoordinator";
43 
44     private final Set<Integer> mAuthOperations;
45     private final MultiBiometricLockoutState mMultiBiometricLockoutState;
46     private final RingBuffer mRingBuffer;
47 
48     private int mUserId;
49     private boolean mIsAuthenticating;
50     private AuthResultCoordinator mAuthResultCoordinator;
51 
AuthSessionCoordinator()52     public AuthSessionCoordinator() {
53         this(SystemClock.elapsedRealtimeClock());
54     }
55 
56     @VisibleForTesting
AuthSessionCoordinator(Clock clock)57     AuthSessionCoordinator(Clock clock) {
58         mAuthOperations = new HashSet<>();
59         mAuthResultCoordinator = new AuthResultCoordinator();
60         mMultiBiometricLockoutState = new MultiBiometricLockoutState(clock);
61         mRingBuffer = new RingBuffer(100);
62     }
63 
64     /**
65      * A Call indicating that an auth session has started
66      */
onAuthSessionStarted(int userId)67     void onAuthSessionStarted(int userId) {
68         mAuthOperations.clear();
69         mUserId = userId;
70         mIsAuthenticating = true;
71         mAuthResultCoordinator = new AuthResultCoordinator();
72         mRingBuffer.addApiCall("internal : onAuthSessionStarted(" + userId + ")");
73     }
74 
75     /**
76      * Ends the current auth session and updates the lockout state.
77      *
78      * This can happen two ways.
79      * 1. Manually calling this API
80      * 2. If authStartedFor() was called, and any authentication attempts finish.
81      */
endAuthSession()82     void endAuthSession() {
83         // User unlocks can also unlock timed lockout Authenticator.Types
84         final Map<Integer, Integer> result = mAuthResultCoordinator.getResult();
85         for (int authenticator : Arrays.asList(Authenticators.BIOMETRIC_CONVENIENCE,
86                 Authenticators.BIOMETRIC_WEAK, Authenticators.BIOMETRIC_STRONG)) {
87             final Integer value = result.get(authenticator);
88             if ((value & AUTHENTICATOR_UNLOCKED) == AUTHENTICATOR_UNLOCKED) {
89                 mMultiBiometricLockoutState.clearPermanentLockOut(mUserId, authenticator);
90                 mMultiBiometricLockoutState.clearTimedLockout(mUserId, authenticator);
91             } else if ((value & AUTHENTICATOR_PERMANENT_LOCKED) == AUTHENTICATOR_PERMANENT_LOCKED) {
92                 mMultiBiometricLockoutState.setPermanentLockOut(mUserId, authenticator);
93             } else if ((value & AUTHENTICATOR_TIMED_LOCKED) == AUTHENTICATOR_TIMED_LOCKED) {
94                 mMultiBiometricLockoutState.setTimedLockout(mUserId, authenticator);
95             }
96         }
97 
98         if (mAuthOperations.isEmpty()) {
99             mRingBuffer.addApiCall("internal : onAuthSessionEnded(" + mUserId + ")");
100             clearSession();
101         }
102     }
103 
clearSession()104     private void clearSession() {
105         mIsAuthenticating = false;
106         mAuthOperations.clear();
107     }
108 
109     /**
110      * Returns the current lockout state for a given user/strength.
111      */
112     @LockoutTracker.LockoutMode
getLockoutStateFor(int userId, @Authenticators.Types int strength)113     public int getLockoutStateFor(int userId, @Authenticators.Types int strength) {
114         return mMultiBiometricLockoutState.getLockoutState(userId, strength);
115     }
116 
117     @Override
authStartedFor(int userId, int sensorId, long requestId)118     public void authStartedFor(int userId, int sensorId, long requestId) {
119         mRingBuffer.addApiCall(
120                 "authStartedFor(userId=" + userId + ", sensorId=" + sensorId + ", requestId="
121                         + requestId + ")");
122         if (!mIsAuthenticating) {
123             onAuthSessionStarted(userId);
124         }
125 
126         if (mAuthOperations.contains(sensorId)) {
127             Slog.e(TAG, "Error, authStartedFor(" + sensorId + ") without being finished");
128             return;
129         }
130 
131         if (mUserId != userId) {
132             Slog.e(TAG, "Error authStartedFor(" + userId + ") Incorrect userId, expected" + mUserId
133                     + ", ignoring...");
134             return;
135         }
136 
137         mAuthOperations.add(sensorId);
138     }
139 
140     @Override
lockedOutFor(int userId, @Authenticators.Types int biometricStrength, int sensorId, long requestId)141     public void lockedOutFor(int userId, @Authenticators.Types int biometricStrength, int sensorId,
142             long requestId) {
143         final String lockedOutStr =
144                 "lockOutFor(userId=" + userId + ", biometricStrength=" + biometricStrength
145                         + ", sensorId=" + sensorId + ", requestId=" + requestId + ")";
146         mRingBuffer.addApiCall(lockedOutStr);
147         mAuthResultCoordinator.lockedOutFor(biometricStrength);
148         attemptToFinish(userId, sensorId, lockedOutStr);
149     }
150 
151     @Override
lockOutTimed(int userId, @Authenticators.Types int biometricStrength, int sensorId, long time, long requestId)152     public void lockOutTimed(int userId, @Authenticators.Types int biometricStrength, int sensorId,
153             long time, long requestId) {
154         final String lockedOutStr =
155                 "lockOutTimedFor(userId=" + userId + ", biometricStrength=" + biometricStrength
156                         + ", sensorId=" + sensorId + "time=" + time + ", requestId=" + requestId
157                         + ")";
158         mRingBuffer.addApiCall(lockedOutStr);
159         mAuthResultCoordinator.lockOutTimed(biometricStrength);
160         attemptToFinish(userId, sensorId, lockedOutStr);
161     }
162 
163     @Override
authEndedFor(int userId, @Authenticators.Types int biometricStrength, int sensorId, long requestId, boolean wasSuccessful)164     public void authEndedFor(int userId, @Authenticators.Types int biometricStrength, int sensorId,
165             long requestId, boolean wasSuccessful) {
166         final String authEndedStr =
167                 "authEndedFor(userId=" + userId + " ,biometricStrength=" + biometricStrength
168                         + ", sensorId=" + sensorId + ", requestId=" + requestId + ", wasSuccessful="
169                         + wasSuccessful + ")";
170         mRingBuffer.addApiCall(authEndedStr);
171         if (wasSuccessful) {
172             mAuthResultCoordinator.authenticatedFor(biometricStrength);
173         }
174         attemptToFinish(userId, sensorId, authEndedStr);
175     }
176 
177     @Override
resetLockoutFor(int userId, @Authenticators.Types int biometricStrength, long requestId)178     public void resetLockoutFor(int userId, @Authenticators.Types int biometricStrength,
179             long requestId) {
180         final String resetLockStr =
181                 "resetLockoutFor(userId=" + userId + " ,biometricStrength=" + biometricStrength
182                         + ", requestId=" + requestId + ")";
183         mRingBuffer.addApiCall(resetLockStr);
184         if (biometricStrength == Authenticators.BIOMETRIC_STRONG) {
185             clearSession();
186         } else {
187             // Lockouts cannot be reset by non-strong biometrics
188             return;
189         }
190         mMultiBiometricLockoutState.clearPermanentLockOut(userId, biometricStrength);
191         mMultiBiometricLockoutState.clearTimedLockout(userId, biometricStrength);
192     }
193 
attemptToFinish(int userId, int sensorId, String description)194     private void attemptToFinish(int userId, int sensorId, String description) {
195         boolean didFail = false;
196         if (!mAuthOperations.contains(sensorId)) {
197             Slog.e(TAG, "Error unable to find auth operation : " + description);
198             didFail = true;
199         }
200         if (userId != mUserId) {
201             Slog.e(TAG, "Error mismatched userId, expected=" + mUserId + " for " + description);
202             didFail = true;
203         }
204         if (didFail) {
205             return;
206         }
207         mAuthOperations.remove(sensorId);
208         if (mIsAuthenticating) {
209             endAuthSession();
210         }
211     }
212 
213     /**
214      * Returns a string representation of the past N API calls as well as the
215      * permanent and timed lockout states for each user's authenticators.
216      */
217     @Override
toString()218     public String toString() {
219         return mRingBuffer + "\n" + mMultiBiometricLockoutState;
220     }
221 
222     private static class RingBuffer {
223         private final String[] mApiCalls;
224         private final int mSize;
225         private int mCurr;
226         private int mApiCallNumber;
227 
RingBuffer(int size)228         RingBuffer(int size) {
229             if (size <= 0) {
230                 Slog.wtf(TAG, "Cannot initialize ring buffer of size: " + size);
231             }
232             mApiCalls = new String[size];
233             mCurr = 0;
234             mSize = size;
235             mApiCallNumber = 0;
236         }
237 
addApiCall(String str)238         void addApiCall(String str) {
239             mApiCalls[mCurr] = str;
240             mCurr++;
241             mCurr %= mSize;
242             mApiCallNumber++;
243         }
244 
245         @Override
toString()246         public String toString() {
247             String buffer = "";
248             int apiCall = mApiCallNumber > mSize ? mApiCallNumber - mSize : 0;
249             for (int i = 0; i < mSize; i++) {
250                 final int location = (mCurr + i) % mSize;
251                 if (mApiCalls[location] != null) {
252                     buffer += String.format("#%-5d %s\n", apiCall++, mApiCalls[location]);
253                 }
254             }
255             return buffer;
256         }
257     }
258 }
259