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 private boolean mIsInvalidBiometricState = false; 61 62 private final Runnable mWriteStateRunnable = this::doWriteStateInternal; 63 64 /** 65 * @return The tag for the biometrics. There may be multiple instances of a biometric within. 66 */ getBiometricsTag()67 protected abstract String getBiometricsTag(); 68 69 /** 70 * @return The resource for the name template, this is used to generate the default name. 71 */ getNameTemplateResource()72 protected abstract int getNameTemplateResource(); 73 74 /** 75 * @return A copy of the list. 76 */ getCopy(ArrayList<T> array)77 protected abstract ArrayList<T> getCopy(ArrayList<T> array); 78 doWriteState(@onNull TypedXmlSerializer serializer)79 protected abstract void doWriteState(@NonNull TypedXmlSerializer serializer) throws Exception; 80 81 /** 82 * @Writes the cached data to persistent storage. 83 */ doWriteStateInternal()84 private void doWriteStateInternal() { 85 AtomicFile destination = new AtomicFile(mFile); 86 87 FileOutputStream out = null; 88 89 try { 90 out = destination.startWrite(); 91 TypedXmlSerializer serializer = Xml.resolveSerializer(out); 92 serializer.setFeature("http://xmlpull.org/v1/doc/features.html#indent-output", true); 93 serializer.startDocument(null, true); 94 95 // Store the authenticatorId 96 serializer.startTag(null, TAG_INVALIDATION); 97 serializer.attributeBoolean(null, ATTR_INVALIDATION, mInvalidationInProgress); 98 serializer.endTag(null, TAG_INVALIDATION); 99 100 // Do any additional serialization that subclasses may require 101 doWriteState(serializer); 102 103 serializer.endDocument(); 104 destination.finishWrite(out); 105 } catch (Throwable t) { 106 Slog.e(TAG, "Failed to write settings, restoring backup", t); 107 destination.failWrite(out); 108 throw new IllegalStateException("Failed to write to file: " + mFile.toString(), t); 109 } finally { 110 IoUtils.closeQuietly(out); 111 } 112 } 113 114 /** 115 * @return 116 */ parseBiometricsLocked(TypedXmlPullParser parser)117 protected abstract void parseBiometricsLocked(TypedXmlPullParser parser) 118 throws IOException, XmlPullParserException; 119 120 BiometricUserState(Context context, int userId, @NonNull String fileName)121 public BiometricUserState(Context context, int userId, @NonNull String fileName) { 122 mFile = getFileForUser(userId, fileName); 123 mContext = context; 124 synchronized (this) { 125 readStateSyncLocked(); 126 } 127 } 128 setInvalidationInProgress(boolean invalidationInProgress)129 public void setInvalidationInProgress(boolean invalidationInProgress) { 130 synchronized (this) { 131 mInvalidationInProgress = invalidationInProgress; 132 scheduleWriteStateLocked(); 133 } 134 } 135 isInvalidationInProgress()136 public boolean isInvalidationInProgress() { 137 synchronized (this) { 138 return mInvalidationInProgress; 139 } 140 } 141 addBiometric(T identifier)142 public void addBiometric(T identifier) { 143 synchronized (this) { 144 mBiometrics.add(identifier); 145 scheduleWriteStateLocked(); 146 } 147 } 148 removeBiometric(int biometricId)149 public void removeBiometric(int biometricId) { 150 synchronized (this) { 151 for (int i = 0; i < mBiometrics.size(); i++) { 152 if (mBiometrics.get(i).getBiometricId() == biometricId) { 153 mBiometrics.remove(i); 154 scheduleWriteStateLocked(); 155 break; 156 } 157 } 158 } 159 } 160 renameBiometric(int biometricId, CharSequence name)161 public void renameBiometric(int biometricId, CharSequence name) { 162 synchronized (this) { 163 for (int i = 0; i < mBiometrics.size(); i++) { 164 if (mBiometrics.get(i).getBiometricId() == biometricId) { 165 BiometricAuthenticator.Identifier identifier = mBiometrics.get(i); 166 identifier.setName(name); 167 scheduleWriteStateLocked(); 168 break; 169 } 170 } 171 } 172 } 173 getBiometrics()174 public List<T> getBiometrics() { 175 synchronized (this) { 176 return getCopy(mBiometrics); 177 } 178 } 179 180 /** 181 * Finds a unique name for the given fingerprint 182 * @return unique name 183 */ getUniqueName()184 public String getUniqueName() { 185 int guess = 1; 186 while (true) { 187 // Not the most efficient algorithm in the world, but there shouldn't be more than 10 188 String name = mContext.getString(getNameTemplateResource(), guess); 189 if (isUnique(name)) { 190 return name; 191 } 192 guess++; 193 } 194 } 195 196 /** 197 * Return true if the biometric file is correctly read. Otherwise return false. 198 */ isInvalidBiometricState()199 public boolean isInvalidBiometricState() { 200 return mIsInvalidBiometricState; 201 } 202 203 /** 204 * Delete the file of the biometric state. 205 */ deleteBiometricFile()206 public void deleteBiometricFile() { 207 synchronized (this) { 208 if (!mFile.exists()) { 209 return; 210 } 211 if (mFile.delete()) { 212 Slog.i(TAG, mFile + " is deleted successfully"); 213 } else { 214 Slog.i(TAG, "Failed to delete " + mFile); 215 } 216 } 217 } 218 isUnique(String name)219 private boolean isUnique(String name) { 220 for (T identifier : mBiometrics) { 221 if (identifier.getName().equals(name)) { 222 return false; 223 } 224 } 225 return true; 226 } 227 getFileForUser(int userId, @NonNull String fileName)228 private File getFileForUser(int userId, @NonNull String fileName) { 229 return new File(Environment.getUserSystemDirectory(userId), fileName); 230 } 231 scheduleWriteStateLocked()232 private void scheduleWriteStateLocked() { 233 AsyncTask.execute(mWriteStateRunnable); 234 } 235 236 @GuardedBy("this") readStateSyncLocked()237 private void readStateSyncLocked() { 238 FileInputStream in; 239 if (!mFile.exists()) { 240 return; 241 } 242 try { 243 in = new FileInputStream(mFile); 244 } catch (FileNotFoundException fnfe) { 245 Slog.i(TAG, "No fingerprint state", fnfe); 246 mIsInvalidBiometricState = true; 247 return; 248 } 249 try { 250 TypedXmlPullParser parser = Xml.resolvePullParser(in); 251 parseStateLocked(parser); 252 253 } catch (XmlPullParserException | IOException e) { 254 throw new IllegalStateException("Failed parsing settings file: " 255 + mFile , e); 256 } finally { 257 IoUtils.closeQuietly(in); 258 } 259 } 260 261 @GuardedBy("this") parseStateLocked(TypedXmlPullParser parser)262 private void parseStateLocked(TypedXmlPullParser parser) 263 throws IOException, XmlPullParserException { 264 final int outerDepth = parser.getDepth(); 265 int type; 266 while ((type = parser.next()) != XmlPullParser.END_DOCUMENT 267 && (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) { 268 if (type == XmlPullParser.END_TAG || type == XmlPullParser.TEXT) { 269 continue; 270 } 271 272 String tagName = parser.getName(); 273 if (tagName.equals(getBiometricsTag())) { 274 parseBiometricsLocked(parser); 275 } else if (tagName.equals(TAG_INVALIDATION)) { 276 mInvalidationInProgress = parser.getAttributeBoolean(null, ATTR_INVALIDATION); 277 } 278 } 279 } 280 281 } 282