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