1 /*
2  * Copyright (C) 2020 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 android.hardware.biometrics;
18 
19 import static android.Manifest.permission.TEST_BIOMETRIC;
20 
21 import android.annotation.NonNull;
22 import android.annotation.Nullable;
23 import android.annotation.RequiresPermission;
24 import android.annotation.TestApi;
25 import android.content.Context;
26 import android.os.RemoteException;
27 import android.util.ArraySet;
28 import android.util.Log;
29 
30 import java.util.concurrent.CountDownLatch;
31 import java.util.concurrent.TimeUnit;
32 
33 /**
34  * Common set of interfaces to test biometric-related APIs, including {@link BiometricPrompt} and
35  * {@link android.hardware.fingerprint.FingerprintManager}.
36  * @hide
37  */
38 @TestApi
39 public class BiometricTestSession implements AutoCloseable {
40     private static final String BASE_TAG = "BiometricTestSession";
41 
42     /**
43      * @hide
44      */
45     public interface TestSessionProvider {
46         @NonNull
createTestSession(@onNull Context context, int sensorId, @NonNull ITestSessionCallback callback)47         ITestSession createTestSession(@NonNull Context context, int sensorId,
48                 @NonNull ITestSessionCallback callback) throws RemoteException;
49     }
50 
51     private final Context mContext;
52     private final int mSensorId;
53     private final ITestSession mTestSession;
54 
55     // Keep track of users that were tested, which need to be cleaned up when finishing.
56     @NonNull private final ArraySet<Integer> mTestedUsers;
57 
58     // Track the users currently cleaning up, and provide a latch that gets notified when all
59     // users have finished cleaning up. This is an imperfect system, as there can technically be
60     // multiple cleanups per user. Theoretically we should track the cleanup's BaseClientMonitor's
61     // unique ID, but it's complicated to plumb it through. This should be fine for now.
62     @Nullable private CountDownLatch mCloseLatch;
63     @NonNull private final ArraySet<Integer> mUsersCleaningUp;
64 
65     private final ITestSessionCallback mCallback = new ITestSessionCallback.Stub() {
66         @Override
67         public void onCleanupStarted(int userId) {
68             Log.d(getTag(), "onCleanupStarted, sensor: " + mSensorId + ", userId: " + userId);
69         }
70 
71         @Override
72         public void onCleanupFinished(int userId) {
73             Log.d(getTag(), "onCleanupFinished, sensor: " + mSensorId
74                     + ", userId: " + userId
75                     + ", remaining users: " + mUsersCleaningUp.size());
76             mUsersCleaningUp.remove(userId);
77 
78             if (mUsersCleaningUp.isEmpty() && mCloseLatch != null) {
79                 mCloseLatch.countDown();
80             }
81         }
82     };
83 
84     /**
85      * @hide
86      */
BiometricTestSession(@onNull Context context, int sensorId, @NonNull TestSessionProvider testSessionProvider)87     public BiometricTestSession(@NonNull Context context, int sensorId,
88             @NonNull TestSessionProvider testSessionProvider) throws RemoteException {
89         mContext = context;
90         mSensorId = sensorId;
91         mTestSession = testSessionProvider.createTestSession(context, sensorId, mCallback);
92         mTestedUsers = new ArraySet<>();
93         mUsersCleaningUp = new ArraySet<>();
94         setTestHalEnabled(true);
95     }
96 
97     /**
98      * Switches the specified sensor to use a test HAL. In this mode, the framework will not invoke
99      * any methods on the real HAL implementation. This allows the framework to test a substantial
100      * portion of the framework code that would otherwise require human interaction. Note that
101      * secure pathways such as HAT/Keystore are not testable, since they depend on the TEE or its
102      * equivalent for the secret key.
103      *
104      * @param enabled If true, enable testing with a fake HAL instead of the real HAL.
105      */
106     @RequiresPermission(TEST_BIOMETRIC)
setTestHalEnabled(boolean enabled)107     private void setTestHalEnabled(boolean enabled) {
108         try {
109             Log.w(getTag(), "setTestHalEnabled, sensor: " + mSensorId + " enabled: " + enabled);
110             mTestSession.setTestHalEnabled(enabled);
111         } catch (RemoteException e) {
112             throw e.rethrowFromSystemServer();
113         }
114     }
115 
116     /**
117      * Starts the enrollment process. This should generally be used when the test HAL is enabled.
118      *
119      * @param userId User that this command applies to.
120      */
121     @RequiresPermission(TEST_BIOMETRIC)
startEnroll(int userId)122     public void startEnroll(int userId) {
123         try {
124             mTestedUsers.add(userId);
125             mTestSession.startEnroll(userId);
126         } catch (RemoteException e) {
127             throw e.rethrowFromSystemServer();
128         }
129     }
130 
131     /**
132      * Finishes the enrollment process. Simulates the HAL's callback.
133      *
134      * @param userId User that this command applies to.
135      */
136     @RequiresPermission(TEST_BIOMETRIC)
finishEnroll(int userId)137     public void finishEnroll(int userId) {
138         try {
139             mTestedUsers.add(userId);
140             mTestSession.finishEnroll(userId);
141         } catch (RemoteException e) {
142             throw e.rethrowFromSystemServer();
143         }
144     }
145 
146     /**
147      * Simulates a successful authentication, but does not provide a valid HAT.
148      *
149      * @param userId User that this command applies to.
150      */
151     @RequiresPermission(TEST_BIOMETRIC)
acceptAuthentication(int userId)152     public void acceptAuthentication(int userId) {
153         try {
154             mTestSession.acceptAuthentication(userId);
155         } catch (RemoteException e) {
156             throw e.rethrowFromSystemServer();
157         }
158     }
159 
160     /**
161      * Simulates a rejected attempt.
162      *
163      * @param userId User that this command applies to.
164      */
165     @RequiresPermission(TEST_BIOMETRIC)
rejectAuthentication(int userId)166     public void rejectAuthentication(int userId) {
167         try {
168             mTestSession.rejectAuthentication(userId);
169         } catch (RemoteException e) {
170             throw e.rethrowFromSystemServer();
171         }
172     }
173 
174     /**
175      * Simulates an acquired message from the HAL.
176      *
177      * @param userId User that this command applies to.
178      * @param acquireInfo See
179      * {@link BiometricPrompt.AuthenticationCallback#onAuthenticationAcquired(int)} and
180      * {@link FingerprintManager.AuthenticationCallback#onAuthenticationAcquired(int)}
181      */
182     @RequiresPermission(TEST_BIOMETRIC)
notifyAcquired(int userId, int acquireInfo)183     public void notifyAcquired(int userId, int acquireInfo) {
184         try {
185             mTestSession.notifyAcquired(userId, acquireInfo);
186         } catch (RemoteException e) {
187             throw e.rethrowFromSystemServer();
188         }
189     }
190 
191     /**
192      * Simulates an error message from the HAL.
193      *
194      * @param userId User that this command applies to.
195      * @param errorCode See
196      * {@link BiometricPrompt.AuthenticationCallback#onAuthenticationError(int, CharSequence)} and
197      * {@link FingerprintManager.AuthenticationCallback#onAuthenticationError(int, CharSequence)}
198      */
199     @RequiresPermission(TEST_BIOMETRIC)
notifyError(int userId, int errorCode)200     public void notifyError(int userId, int errorCode) {
201         try {
202             mTestSession.notifyError(userId, errorCode);
203         } catch (RemoteException e) {
204             throw e.rethrowFromSystemServer();
205         }
206     }
207 
208     /**
209      * Matches the framework's cached enrollments against the HAL's enrollments. Any enrollment
210      * that isn't known by both sides are deleted. This should generally be used when the test
211      * HAL is disabled (e.g. to clean up after a test).
212      *
213      * @param userId User that this command applies to.
214      */
215     @RequiresPermission(TEST_BIOMETRIC)
cleanupInternalState(int userId)216     public void cleanupInternalState(int userId) {
217         try {
218             if (mUsersCleaningUp.contains(userId)) {
219                 Log.w(getTag(), "Cleanup already in progress for user: " + userId);
220             }
221 
222             mUsersCleaningUp.add(userId);
223             mTestSession.cleanupInternalState(userId);
224         } catch (RemoteException e) {
225             throw e.rethrowFromSystemServer();
226         }
227     }
228 
229     @Override
230     @RequiresPermission(TEST_BIOMETRIC)
close()231     public void close() {
232         Log.d(getTag(), "Close, mTestedUsers size; " + mTestedUsers.size());
233         // Cleanup can be performed using the test HAL, since it always responds to enumerate with
234         // zero enrollments.
235         if (!mTestedUsers.isEmpty()) {
236             mCloseLatch = new CountDownLatch(1);
237             for (int user : mTestedUsers) {
238                 cleanupInternalState(user);
239             }
240 
241             try {
242                 Log.d(getTag(), "Awaiting latch...");
243                 mCloseLatch.await(3, TimeUnit.SECONDS);
244                 Log.d(getTag(), "Finished awaiting");
245             } catch (InterruptedException e) {
246                 Log.e(getTag(), "Latch interrupted", e);
247             }
248         }
249 
250         if (!mUsersCleaningUp.isEmpty()) {
251             // TODO(b/186600837): this seems common on multi sensor devices
252             Log.e(getTag(), "Cleanup not finished before shutdown - pending: "
253                     + mUsersCleaningUp.size());
254         }
255 
256         // Disable the test HAL after the sensor becomes idle.
257         setTestHalEnabled(false);
258     }
259 
getTag()260     private String getTag() {
261         return BASE_TAG + "_" + mSensorId;
262     }
263 }
264