• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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.server.biometrics.util;
18 
19 import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
20 
21 import android.content.ComponentName;
22 import android.hardware.biometrics.BiometricManager;
23 import android.hardware.biometrics.BiometricPrompt;
24 import android.hardware.biometrics.Flags;
25 import android.hardware.biometrics.SensorProperties;
26 import android.os.ParcelFileDescriptor;
27 import android.os.Process;
28 import android.os.SystemProperties;
29 import android.security.keystore.KeyGenParameterSpec;
30 import android.security.keystore.KeyProperties;
31 import android.server.wm.Condition;
32 import android.util.Log;
33 
34 import androidx.annotation.NonNull;
35 import androidx.annotation.Nullable;
36 
37 import com.android.server.biometrics.nano.BiometricServiceStateProto;
38 
39 import java.io.ByteArrayOutputStream;
40 import java.io.FileInputStream;
41 import java.io.IOException;
42 import java.nio.charset.StandardCharsets;
43 import java.security.KeyStore;
44 import java.util.ArrayList;
45 import java.util.List;
46 import java.util.function.BooleanSupplier;
47 import java.util.function.Consumer;
48 import java.util.regex.Matcher;
49 import java.util.regex.Pattern;
50 
51 import javax.crypto.Cipher;
52 import javax.crypto.KeyGenerator;
53 import javax.crypto.SecretKey;
54 
55 public class Utils {
56 
57     private static final String TAG = "BiometricTestUtils";
58     private static final String KEYSTORE_PROVIDER = "AndroidKeyStore";
59 
60     /** adb command for dumping the biometric proto */
61     public static final String DUMPSYS_BIOMETRIC = "dumpsys biometric --proto";
62 
63     /**
64      * Retrieves the current SensorStates.
65      */
66     public interface SensorStatesSupplier {
getSensorStates()67         SensorStates getSensorStates() throws Exception;
68     }
69 
70     /**
71      * The Biometric {@link com.android.server.biometrics.SensorConfig}.
72      * Parsed sensor config. See core/res/res/values/config.xml config_biometric_sensors
73      */
74     public static class SensorConfig {
75         public final int id;
76         public final int modality;
77         public final int strength;
78 
SensorConfig(String config)79         public SensorConfig(String config) {
80             String[] elems = config.split(":");
81             id = Integer.parseInt(elems[0]);
82             modality = Integer.parseInt(elems[1]);
83             strength = Integer.parseInt(elems[2]);
84         }
85     }
86 
87     /** Waits for the service to become idle. */
waitForIdleService()88     public static void waitForIdleService() throws Exception {
89         waitForIdleService(() -> getBiometricServiceCurrentState().mSensorStates);
90     }
91 
92     /**
93      * Waits for the service to become idle
94      * @throws Exception
95      */
waitForIdleService(@onNull SensorStatesSupplier supplier)96     public static void waitForIdleService(@NonNull SensorStatesSupplier supplier) throws Exception {
97         for (int i = 0; i < 20; i++) {
98             if (!supplier.getSensorStates().areAllSensorsIdle()) {
99                 Log.d(TAG, "Not idle yet..");
100                 Thread.sleep(300);
101             } else {
102                 return;
103             }
104         }
105         Log.d(TAG, "Timed out waiting for idle");
106     }
107 
108 
109     /** Waits for the specified sensor to become non-idle. */
waitForBusySensor(int sensorId)110     public static void waitForBusySensor(int sensorId) throws Exception {
111         waitForBusySensor(sensorId, () -> getBiometricServiceCurrentState().mSensorStates);
112     }
113 
114     /**
115      * Waits for the specified sensor to become non-idle
116      */
waitForBusySensor(int sensorId, @NonNull SensorStatesSupplier supplier)117     public static void waitForBusySensor(int sensorId, @NonNull SensorStatesSupplier supplier)
118             throws Exception {
119         for (int i = 0; i < 10; i++) {
120             if (!supplier.getSensorStates().sensorStates.get(sensorId).isBusy()) {
121                 Log.d(TAG, "Not busy yet..");
122                 Thread.sleep(300);
123             } else {
124                 return;
125             }
126         }
127         Log.d(TAG, "Timed out waiting to become busy");
128     }
129 
waitFor(@onNull String message, @NonNull BooleanSupplier condition)130     public static void waitFor(@NonNull String message, @NonNull BooleanSupplier condition) {
131         waitFor(message, condition, null /* onFailure */);
132     }
133 
waitFor(@onNull String message, @NonNull BooleanSupplier condition, @Nullable Consumer<Object> onFailure)134     public static void waitFor(@NonNull String message, @NonNull BooleanSupplier condition,
135             @Nullable Consumer<Object> onFailure) {
136         Condition.waitFor(new Condition<>(message, condition)
137                 .setRetryIntervalMs(500)
138                 .setRetryLimit(20)
139                 .setOnFailure(onFailure));
140     }
141 
142     /**
143      * Retrieves the current states of all biometric sensor services (e.g. FingerprintService,
144      * FaceService, etc).
145      *
146      * Note that the states are retrieved from BiometricService, instead of individual services.
147      * This is because 1) BiometricService is the source of truth for all public API-facing things,
148      * and 2) This to include other information, such as UI states, etc as well.
149      */
150     @NonNull
getBiometricServiceCurrentState()151     public static BiometricServiceState getBiometricServiceCurrentState() throws Exception {
152         final byte[] dump = Utils.executeShellCommand(DUMPSYS_BIOMETRIC);
153         final BiometricServiceStateProto proto = BiometricServiceStateProto.parseFrom(dump);
154         return BiometricServiceState.parseFrom(proto);
155     }
156 
157     /**
158      * Runs a shell command, similar to running "adb shell ..." from the command line.
159      * @param cmd A command, without the preceding "adb shell" portion. For example,
160      *            passing in "dumpsys fingerprint" would be the equivalent of running
161      *            "adb shell dumpsys fingerprint" from the command line.
162      * @return The result of the command.
163      */
executeShellCommand(String cmd)164     public static byte[] executeShellCommand(String cmd) {
165         Log.d(TAG, "execute: " + cmd);
166         try {
167             ParcelFileDescriptor pfd = getInstrumentation().getUiAutomation()
168                     .executeShellCommand(cmd);
169             byte[] buf = new byte[512];
170             int bytesRead;
171             FileInputStream fis = new ParcelFileDescriptor.AutoCloseInputStream(pfd);
172             ByteArrayOutputStream stdout = new ByteArrayOutputStream();
173             while ((bytesRead = fis.read(buf)) != -1) {
174                 stdout.write(buf, 0, bytesRead);
175             }
176             fis.close();
177             return stdout.toByteArray();
178         } catch (IOException e) {
179             throw new RuntimeException(e);
180         }
181     }
182 
forceStopActivity(ComponentName componentName)183     public static void forceStopActivity(ComponentName componentName) {
184         executeShellCommand("am force-stop " + componentName.getPackageName()
185                 + " " + componentName.getShortClassName().replaceAll("\\.", ""));
186     }
187 
numberOfSpecifiedOperations(@onNull BiometricServiceState state, int sensorId, int operation)188     public static int numberOfSpecifiedOperations(@NonNull BiometricServiceState state,
189             int sensorId, int operation) {
190         int count = 0;
191         final List<Integer> recentOps = state.mSensorStates.sensorStates.get(sensorId)
192                 .getSchedulerState().getRecentOperations();
193         for (Integer i : recentOps) {
194             if (i == operation) {
195                 count++;
196             }
197         }
198         return count;
199     }
200 
createTimeBoundSecretKey_deprecated(String keyName, boolean useStrongBox)201     public static void createTimeBoundSecretKey_deprecated(String keyName, boolean useStrongBox)
202             throws Exception {
203         KeyStore keyStore = KeyStore.getInstance("AndroidKeyStore");
204         keyStore.load(null);
205         KeyGenerator keyGenerator = KeyGenerator.getInstance(
206                 KeyProperties.KEY_ALGORITHM_AES, "AndroidKeyStore");
207 
208         // Set the alias of the entry in Android KeyStore where the key will appear
209         // and the constrains (purposes) in the constructor of the Builder
210         keyGenerator.init(new KeyGenParameterSpec.Builder(keyName,
211                 KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT)
212                 .setBlockModes(KeyProperties.BLOCK_MODE_CBC)
213                 .setUserAuthenticationRequired(true)
214                 .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_PKCS7)
215                 .setIsStrongBoxBacked(useStrongBox)
216                 .setUserAuthenticationValidityDurationSeconds(5 /* seconds */)
217                 .build());
218         keyGenerator.generateKey();
219     }
220 
createTimeBoundSecretKey(String keyName, int authTypes, boolean useStrongBox)221     public static void createTimeBoundSecretKey(String keyName, int authTypes, boolean useStrongBox)
222             throws Exception {
223         KeyStore keyStore = KeyStore.getInstance("AndroidKeyStore");
224         keyStore.load(null);
225         KeyGenerator keyGenerator = KeyGenerator.getInstance(
226                 KeyProperties.KEY_ALGORITHM_AES, "AndroidKeyStore");
227 
228         // Set the alias of the entry in Android KeyStore where the key will appear
229         // and the constrains (purposes) in the constructor of the Builder
230         keyGenerator.init(new KeyGenParameterSpec.Builder(keyName,
231                 KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT)
232                 .setBlockModes(KeyProperties.BLOCK_MODE_CBC)
233                 .setUserAuthenticationRequired(true)
234                 .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_PKCS7)
235                 .setIsStrongBoxBacked(useStrongBox)
236                 .setUserAuthenticationParameters(1 /* seconds */, authTypes)
237                 .build());
238         keyGenerator.generateKey();
239     }
240 
generateBiometricBoundKey(String keyName, boolean useStrongBox)241     public static void generateBiometricBoundKey(String keyName, boolean useStrongBox)
242             throws Exception {
243         final KeyStore keystore = KeyStore.getInstance(KEYSTORE_PROVIDER);
244         keystore.load(null);
245         KeyGenParameterSpec.Builder builder = new KeyGenParameterSpec.Builder(
246                 keyName,
247                 KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT)
248                 .setBlockModes(KeyProperties.BLOCK_MODE_CBC)
249                 .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_PKCS7)
250                 .setUserAuthenticationRequired(true)
251                 .setInvalidatedByBiometricEnrollment(true)
252                 .setIsStrongBoxBacked(useStrongBox)
253                 .setUserAuthenticationParameters(0, KeyProperties.AUTH_BIOMETRIC_STRONG);
254 
255         KeyGenerator keyGenerator = KeyGenerator
256                 .getInstance(KeyProperties.KEY_ALGORITHM_AES, KEYSTORE_PROVIDER);
257         keyGenerator.init(builder.build());
258 
259         // Generates and stores the key in Android KeyStore under the keystoreAlias (keyName)
260         // specified in the builder.
261         keyGenerator.generateKey();
262     }
263 
initializeCryptoObject(String keyName)264     public static BiometricPrompt.CryptoObject initializeCryptoObject(String keyName)
265             throws Exception {
266         final KeyStore keystore = KeyStore.getInstance(KEYSTORE_PROVIDER);
267         keystore.load(null);
268         final SecretKey secretKey = (SecretKey) keystore.getKey(
269                 keyName, null /* password */);
270         final Cipher cipher = Cipher.getInstance(KeyProperties.KEY_ALGORITHM_AES + "/"
271                 + KeyProperties.BLOCK_MODE_CBC + "/"
272                 + KeyProperties.ENCRYPTION_PADDING_PKCS7);
273         cipher.init(Cipher.ENCRYPT_MODE, secretKey);
274 
275         final BiometricPrompt.CryptoObject cryptoObject =
276                 new BiometricPrompt.CryptoObject(cipher);
277         return cryptoObject;
278     }
279 
isPublicAuthenticatorConstant(int authenticator)280     public static boolean isPublicAuthenticatorConstant(int authenticator) {
281         switch (authenticator) {
282             case BiometricManager.Authenticators.BIOMETRIC_STRONG:
283             case BiometricManager.Authenticators.BIOMETRIC_WEAK:
284             case BiometricManager.Authenticators.DEVICE_CREDENTIAL:
285                 return true;
286             default:
287                 return false;
288         }
289     }
290 
testApiStrengthToAuthenticatorStrength(int testApiStrength)291     public static int testApiStrengthToAuthenticatorStrength(int testApiStrength) {
292         switch (testApiStrength) {
293             case SensorProperties.STRENGTH_STRONG:
294                 return BiometricManager.Authenticators.BIOMETRIC_STRONG;
295             case SensorProperties.STRENGTH_WEAK:
296                 return BiometricManager.Authenticators.BIOMETRIC_WEAK;
297             default:
298                 throw new IllegalArgumentException("Unable to convert testApiStrength: "
299                         + testApiStrength);
300         }
301     }
302 
isFirstApiLevel29orGreater()303     public static boolean isFirstApiLevel29orGreater() {
304         int firstApiLevel = SystemProperties.getInt("ro.product.first_api_level", 0);
305         if (firstApiLevel >= 29) {
306             return true;
307         }
308         return false;
309     }
310 
311     /** Find the sensor id of the AIDL fingerprint HAL, or -1 if not present. */
getAidlFingerprintSensorId()312     public static int getAidlFingerprintSensorId() {
313         return getAidlSensorId("dumpsys fingerprint", ", provider: FingerprintProvider");
314     }
315 
316     /** Find all of the sensor ids of the AIDL fingerprint HALs */
getAidlFingerprintSensorIds()317     public static List<Integer> getAidlFingerprintSensorIds() {
318         return getAidlSensorIds("dumpsys fingerprint", ", provider: FingerprintProvider");
319     }
320 
321     /** Find the sensor id of the AIDL face HAL, or -1 if not present. */
getAidlFaceSensorId()322     public static int getAidlFaceSensorId() {
323         return getAidlSensorId("dumpsys face", ", provider: FaceProvider");
324     }
325 
326     /** Find current user id. */
getUserId()327     public static int getUserId() {
328         return Process.myUserHandle().getIdentifier();
329     }
330 
getAidlSensorId(String adbCommand, String providerRegex)331     private static int getAidlSensorId(String adbCommand, String providerRegex) {
332         List<Integer> ids = getAidlSensorIds(adbCommand, providerRegex);
333 
334         if (ids.isEmpty()) {
335             return -1;
336         }
337 
338         return ids.get(0);
339     }
340 
getAidlSensorIds(String adbCommand, String providerRegex)341     private static List<Integer> getAidlSensorIds(String adbCommand, String providerRegex) {
342         final byte[] dump = executeShellCommand(adbCommand);
343         final String fpsDumpSys = new String(dump, StandardCharsets.UTF_8);
344         final Matcher matcher =
345                 Pattern.compile("sensorId: (\\d+)" + providerRegex).matcher(fpsDumpSys);
346 
347         final List<Integer> ids = new ArrayList<>();
348         while (matcher.find()) {
349             ids.add(Integer.parseInt(matcher.group(1)));
350         }
351 
352         return ids;
353     }
354 
355     /**
356      * @param isVerticalContentView Whether the content view is
357      *                              {@link
358      *                              android.hardware.biometrics.PromptVerticalListContentView}.
359      * @param isBiometricAllowed    Whether there is any biometric authenticator
360      *                              allowed.
361      * @return Whether biometric prompt without icon should show prior to credential view.
362      */
shouldShowBpWithoutIconForCredential( boolean isVerticalContentView, boolean isBiometricAllowed)363     public static boolean shouldShowBpWithoutIconForCredential(
364             boolean isVerticalContentView, boolean isBiometricAllowed) {
365         return Flags.customBiometricPrompt() && !isBiometricAllowed && isVerticalContentView;
366     }
367 }
368