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 com.android.server.biometrics.sensors;
18 
19 import android.annotation.NonNull;
20 import android.content.Context;
21 import android.hardware.biometrics.BiometricAuthenticator;
22 import android.os.AsyncTask;
23 import android.os.Environment;
24 import android.util.AtomicFile;
25 import android.util.Slog;
26 import android.util.Xml;
27 
28 import com.android.internal.annotations.GuardedBy;
29 import com.android.modules.utils.TypedXmlPullParser;
30 import com.android.modules.utils.TypedXmlSerializer;
31 
32 import libcore.io.IoUtils;
33 
34 import org.xmlpull.v1.XmlPullParser;
35 import org.xmlpull.v1.XmlPullParserException;
36 
37 import java.io.File;
38 import java.io.FileInputStream;
39 import java.io.FileNotFoundException;
40 import java.io.FileOutputStream;
41 import java.io.IOException;
42 import java.util.ArrayList;
43 import java.util.List;
44 
45 /**
46  * Abstract base class for managing biometrics per user across device reboots.
47  * @hide
48  */
49 public abstract class BiometricUserState<T extends BiometricAuthenticator.Identifier> {
50     private static final String TAG = "UserState";
51 
52     private static final String TAG_INVALIDATION = "authenticatorIdInvalidation_tag";
53     private static final String ATTR_INVALIDATION = "authenticatorIdInvalidation_attr";
54 
55     @GuardedBy("this")
56     protected final ArrayList<T> mBiometrics = new ArrayList<>();
57     protected boolean mInvalidationInProgress;
58     protected final Context mContext;
59     protected final File mFile;
60 
61     private final Runnable mWriteStateRunnable = this::doWriteStateInternal;
62 
63     /**
64      * @return The tag for the biometrics. There may be multiple instances of a biometric within.
65      */
getBiometricsTag()66     protected abstract String getBiometricsTag();
67 
68     /**
69      * @return The resource for the name template, this is used to generate the default name.
70      */
getNameTemplateResource()71     protected abstract int getNameTemplateResource();
72 
73     /**
74      * @return A copy of the list.
75      */
getCopy(ArrayList<T> array)76     protected abstract ArrayList<T> getCopy(ArrayList<T> array);
77 
doWriteState(@onNull TypedXmlSerializer serializer)78     protected abstract void doWriteState(@NonNull TypedXmlSerializer serializer) throws Exception;
79 
80     /**
81      * @Writes the cached data to persistent storage.
82      */
doWriteStateInternal()83     private void doWriteStateInternal() {
84         AtomicFile destination = new AtomicFile(mFile);
85 
86         FileOutputStream out = null;
87 
88         try {
89             out = destination.startWrite();
90             TypedXmlSerializer serializer = Xml.resolveSerializer(out);
91             serializer.setFeature("http://xmlpull.org/v1/doc/features.html#indent-output", true);
92             serializer.startDocument(null, true);
93 
94             // Store the authenticatorId
95             serializer.startTag(null, TAG_INVALIDATION);
96             serializer.attributeBoolean(null, ATTR_INVALIDATION, mInvalidationInProgress);
97             serializer.endTag(null, TAG_INVALIDATION);
98 
99             // Do any additional serialization that subclasses may require
100             doWriteState(serializer);
101 
102             serializer.endDocument();
103             destination.finishWrite(out);
104         } catch (Throwable t) {
105             Slog.wtf(TAG, "Failed to write settings, restoring backup", t);
106             destination.failWrite(out);
107             throw new IllegalStateException("Failed to write to file: " + mFile.toString(), t);
108         } finally {
109             IoUtils.closeQuietly(out);
110         }
111     }
112 
113     /**
114      * @return
115      */
parseBiometricsLocked(TypedXmlPullParser parser)116     protected abstract void parseBiometricsLocked(TypedXmlPullParser parser)
117             throws IOException, XmlPullParserException;
118 
119 
BiometricUserState(Context context, int userId, @NonNull String fileName)120     public BiometricUserState(Context context, int userId, @NonNull String fileName) {
121         mFile = getFileForUser(userId, fileName);
122         mContext = context;
123         synchronized (this) {
124             readStateSyncLocked();
125         }
126     }
127 
setInvalidationInProgress(boolean invalidationInProgress)128     public void setInvalidationInProgress(boolean invalidationInProgress) {
129         synchronized (this) {
130             mInvalidationInProgress = invalidationInProgress;
131             scheduleWriteStateLocked();
132         }
133     }
134 
isInvalidationInProgress()135     public boolean isInvalidationInProgress() {
136         synchronized (this) {
137             return mInvalidationInProgress;
138         }
139     }
140 
addBiometric(T identifier)141     public void addBiometric(T identifier) {
142         synchronized (this) {
143             mBiometrics.add(identifier);
144             scheduleWriteStateLocked();
145         }
146     }
147 
removeBiometric(int biometricId)148     public void removeBiometric(int biometricId) {
149         synchronized (this) {
150             for (int i = 0; i < mBiometrics.size(); i++) {
151                 if (mBiometrics.get(i).getBiometricId() == biometricId) {
152                     mBiometrics.remove(i);
153                     scheduleWriteStateLocked();
154                     break;
155                 }
156             }
157         }
158     }
159 
renameBiometric(int biometricId, CharSequence name)160     public void renameBiometric(int biometricId, CharSequence name) {
161         synchronized (this) {
162             for (int i = 0; i < mBiometrics.size(); i++) {
163                 if (mBiometrics.get(i).getBiometricId() == biometricId) {
164                     BiometricAuthenticator.Identifier identifier = mBiometrics.get(i);
165                     identifier.setName(name);
166                     scheduleWriteStateLocked();
167                     break;
168                 }
169             }
170         }
171     }
172 
getBiometrics()173     public List<T> getBiometrics() {
174         synchronized (this) {
175             return getCopy(mBiometrics);
176         }
177     }
178 
179     /**
180      * Finds a unique name for the given fingerprint
181      * @return unique name
182      */
getUniqueName()183     public String getUniqueName() {
184         int guess = 1;
185         while (true) {
186             // Not the most efficient algorithm in the world, but there shouldn't be more than 10
187             String name = mContext.getString(getNameTemplateResource(), guess);
188             if (isUnique(name)) {
189                 return name;
190             }
191             guess++;
192         }
193     }
194 
isUnique(String name)195     private boolean isUnique(String name) {
196         for (T identifier : mBiometrics) {
197             if (identifier.getName().equals(name)) {
198                 return false;
199             }
200         }
201         return true;
202     }
203 
getFileForUser(int userId, @NonNull String fileName)204     private File getFileForUser(int userId, @NonNull String fileName) {
205         return new File(Environment.getUserSystemDirectory(userId), fileName);
206     }
207 
scheduleWriteStateLocked()208     private void scheduleWriteStateLocked() {
209         AsyncTask.execute(mWriteStateRunnable);
210     }
211 
212     @GuardedBy("this")
readStateSyncLocked()213     private void readStateSyncLocked() {
214         FileInputStream in;
215         if (!mFile.exists()) {
216             return;
217         }
218         try {
219             in = new FileInputStream(mFile);
220         } catch (FileNotFoundException fnfe) {
221             Slog.i(TAG, "No fingerprint state");
222             return;
223         }
224         try {
225             TypedXmlPullParser parser = Xml.resolvePullParser(in);
226             parseStateLocked(parser);
227 
228         } catch (XmlPullParserException | IOException e) {
229             throw new IllegalStateException("Failed parsing settings file: "
230                     + mFile , e);
231         } finally {
232             IoUtils.closeQuietly(in);
233         }
234     }
235 
236     @GuardedBy("this")
parseStateLocked(TypedXmlPullParser parser)237     private void parseStateLocked(TypedXmlPullParser parser)
238             throws IOException, XmlPullParserException {
239         final int outerDepth = parser.getDepth();
240         int type;
241         while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
242                 && (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) {
243             if (type == XmlPullParser.END_TAG || type == XmlPullParser.TEXT) {
244                 continue;
245             }
246 
247             String tagName = parser.getName();
248             if (tagName.equals(getBiometricsTag())) {
249                 parseBiometricsLocked(parser);
250             } else if (tagName.equals(TAG_INVALIDATION)) {
251                 mInvalidationInProgress = parser.getAttributeBoolean(null, ATTR_INVALIDATION);
252             }
253         }
254     }
255 
256 }
257