1 /* 2 * 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 17 package com.android.server.biometrics; 18 19 import android.annotation.NonNull; 20 import android.content.Context; 21 import android.content.SharedPreferences; 22 import android.hardware.biometrics.BiometricsProtoEnums; 23 import android.os.Environment; 24 import android.os.UserHandle; 25 import android.util.Slog; 26 27 import org.json.JSONException; 28 import org.json.JSONObject; 29 30 import java.io.File; 31 import java.util.ArrayList; 32 import java.util.HashSet; 33 import java.util.Iterator; 34 import java.util.List; 35 import java.util.Set; 36 37 /** 38 * Persists and retrieves stats for Biometric Authentication. 39 * Authentication stats include userId, total attempts, rejected attempts, 40 * and the number of sent enrollment notifications. 41 * Data are stored in SharedPreferences in a form of a set of JSON objects, 42 * where it's one element per user. 43 */ 44 public class AuthenticationStatsPersister { 45 46 private static final String TAG = "AuthenticationStatsPersister"; 47 private static final String FILE_NAME = "authentication_stats"; 48 private static final String USER_ID = "user_id"; 49 private static final String FACE_ATTEMPTS = "face_attempts"; 50 private static final String FACE_REJECTIONS = "face_rejections"; 51 private static final String FINGERPRINT_ATTEMPTS = "fingerprint_attempts"; 52 private static final String FINGERPRINT_REJECTIONS = "fingerprint_rejections"; 53 private static final String ENROLLMENT_NOTIFICATIONS = "enrollment_notifications"; 54 private static final String KEY = "frr_stats"; 55 private static final String THRESHOLD_KEY = "frr_threshold"; 56 57 @NonNull private final SharedPreferences mSharedPreferences; 58 AuthenticationStatsPersister(@onNull Context context)59 AuthenticationStatsPersister(@NonNull Context context) { 60 // The package info in the context isn't initialized in the way it is for normal apps, 61 // so the standard, name-based context.getSharedPreferences doesn't work. Instead, we 62 // build the path manually below using the same policy that appears in ContextImpl. 63 final File prefsFile = new File(Environment.getDataSystemDirectory(), FILE_NAME); 64 mSharedPreferences = context.getSharedPreferences(prefsFile, Context.MODE_PRIVATE); 65 } 66 67 /** 68 * Get all frr data from SharedPreference. 69 */ getAllFrrStats(int modality)70 public List<AuthenticationStats> getAllFrrStats(int modality) { 71 List<AuthenticationStats> authenticationStatsList = new ArrayList<>(); 72 for (String frrStats : readFrrStats()) { 73 try { 74 JSONObject frrStatsJson = new JSONObject(frrStats); 75 if (modality == BiometricsProtoEnums.MODALITY_FACE) { 76 authenticationStatsList.add(new AuthenticationStats( 77 getIntValue(frrStatsJson, USER_ID, 78 UserHandle.USER_NULL /* defaultValue */), 79 getIntValue(frrStatsJson, FACE_ATTEMPTS), 80 getIntValue(frrStatsJson, FACE_REJECTIONS), 81 getIntValue(frrStatsJson, ENROLLMENT_NOTIFICATIONS), 82 modality)); 83 } else if (modality == BiometricsProtoEnums.MODALITY_FINGERPRINT) { 84 authenticationStatsList.add(new AuthenticationStats( 85 getIntValue(frrStatsJson, USER_ID, 86 UserHandle.USER_NULL /* defaultValue */), 87 getIntValue(frrStatsJson, FINGERPRINT_ATTEMPTS), 88 getIntValue(frrStatsJson, FINGERPRINT_REJECTIONS), 89 getIntValue(frrStatsJson, ENROLLMENT_NOTIFICATIONS), 90 modality)); 91 } 92 } catch (JSONException e) { 93 Slog.w(TAG, String.format("Unable to resolve authentication stats JSON: %s", 94 frrStats)); 95 } 96 } 97 return authenticationStatsList; 98 } 99 100 /** 101 * Remove frr data for a specific user. 102 */ removeFrrStats(int userId)103 public void removeFrrStats(int userId) { 104 try { 105 // Copy into a new HashSet to allow modification. 106 Set<String> frrStatsSet = new HashSet<>(readFrrStats()); 107 108 // Remove the old authentication stat for the user if it exists. 109 for (Iterator<String> iterator = frrStatsSet.iterator(); iterator.hasNext();) { 110 String frrStats = iterator.next(); 111 JSONObject frrStatJson = new JSONObject(frrStats); 112 if (getValue(frrStatJson, USER_ID).equals(String.valueOf(userId))) { 113 iterator.remove(); 114 break; 115 } 116 } 117 118 mSharedPreferences.edit().putStringSet(KEY, frrStatsSet).apply(); 119 } catch (JSONException ignored) { 120 } 121 } 122 123 /** 124 * Persist frr data for a specific user. 125 */ persistFrrStats(int userId, int totalAttempts, int rejectedAttempts, int enrollmentNotifications, int modality)126 public void persistFrrStats(int userId, int totalAttempts, int rejectedAttempts, 127 int enrollmentNotifications, int modality) { 128 try { 129 // Copy into a new HashSet to allow modification. 130 Set<String> frrStatsSet = new HashSet<>(readFrrStats()); 131 132 // Remove the old authentication stat for the user if it exists. 133 JSONObject frrStatJson = null; 134 for (Iterator<String> iterator = frrStatsSet.iterator(); iterator.hasNext();) { 135 String frrStats = iterator.next(); 136 frrStatJson = new JSONObject(frrStats); 137 if (getValue(frrStatJson, USER_ID).equals(String.valueOf(userId))) { 138 iterator.remove(); 139 break; 140 } 141 // Reset frrStatJson when user doesn't exist. 142 frrStatJson = null; 143 } 144 145 // Checks if this is a new user and there's no JSON for this user in the storage. 146 if (frrStatJson == null) { 147 frrStatJson = new JSONObject().put(USER_ID, userId); 148 } 149 frrStatsSet.add(buildFrrStats(frrStatJson, totalAttempts, rejectedAttempts, 150 enrollmentNotifications, modality)); 151 152 Slog.d(TAG, "frrStatsSet to persist: " + frrStatsSet); 153 154 mSharedPreferences.edit().putStringSet(KEY, frrStatsSet).apply(); 155 156 } catch (JSONException e) { 157 Slog.e(TAG, "Unable to persist authentication stats"); 158 } 159 } 160 161 /** 162 * Persist frr threshold. 163 */ persistFrrThreshold(float frrThreshold)164 public void persistFrrThreshold(float frrThreshold) { 165 mSharedPreferences.edit().putFloat(THRESHOLD_KEY, frrThreshold).apply(); 166 } 167 readFrrStats()168 private Set<String> readFrrStats() { 169 return mSharedPreferences.getStringSet(KEY, Set.of()); 170 } 171 172 // Update frr stats for existing frrStats JSONObject and build the new string. buildFrrStats(JSONObject frrStats, int totalAttempts, int rejectedAttempts, int enrollmentNotifications, int modality)173 private String buildFrrStats(JSONObject frrStats, int totalAttempts, int rejectedAttempts, 174 int enrollmentNotifications, int modality) throws JSONException { 175 if (modality == BiometricsProtoEnums.MODALITY_FACE) { 176 return frrStats 177 .put(FACE_ATTEMPTS, totalAttempts) 178 .put(FACE_REJECTIONS, rejectedAttempts) 179 .put(ENROLLMENT_NOTIFICATIONS, enrollmentNotifications) 180 .toString(); 181 } else if (modality == BiometricsProtoEnums.MODALITY_FINGERPRINT) { 182 return frrStats 183 .put(FINGERPRINT_ATTEMPTS, totalAttempts) 184 .put(FINGERPRINT_REJECTIONS, rejectedAttempts) 185 .put(ENROLLMENT_NOTIFICATIONS, enrollmentNotifications) 186 .toString(); 187 } else { 188 return frrStats.toString(); 189 } 190 } 191 getValue(JSONObject jsonObject, String key)192 private String getValue(JSONObject jsonObject, String key) throws JSONException { 193 return jsonObject.has(key) ? jsonObject.getString(key) : ""; 194 } 195 getIntValue(JSONObject jsonObject, String key)196 private int getIntValue(JSONObject jsonObject, String key) throws JSONException { 197 return getIntValue(jsonObject, key, 0 /* defaultValue */); 198 } 199 getIntValue(JSONObject jsonObject, String key, int defaultValue)200 private int getIntValue(JSONObject jsonObject, String key, int defaultValue) 201 throws JSONException { 202 return jsonObject.has(key) ? jsonObject.getInt(key) : defaultValue; 203 } 204 } 205