1 /*
2  * Copyright (C) 2014 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.locksettings;
18 
19 import static android.content.Context.USER_SERVICE;
20 
21 import android.annotation.Nullable;
22 import android.app.admin.DevicePolicyManager;
23 import android.content.ContentValues;
24 import android.content.Context;
25 import android.content.pm.UserInfo;
26 import android.database.Cursor;
27 import android.database.sqlite.SQLiteDatabase;
28 import android.database.sqlite.SQLiteOpenHelper;
29 import android.os.Environment;
30 import android.os.UserHandle;
31 import android.os.UserManager;
32 import android.util.ArrayMap;
33 import android.util.Log;
34 import android.util.Slog;
35 
36 import com.android.internal.annotations.VisibleForTesting;
37 import com.android.internal.util.ArrayUtils;
38 import com.android.internal.util.Preconditions;
39 import com.android.internal.widget.LockPatternUtils;
40 import com.android.internal.widget.LockPatternUtils.CredentialType;
41 import com.android.server.LocalServices;
42 import com.android.server.PersistentDataBlockManagerInternal;
43 
44 import java.io.ByteArrayInputStream;
45 import java.io.ByteArrayOutputStream;
46 import java.io.DataInputStream;
47 import java.io.DataOutputStream;
48 import java.io.File;
49 import java.io.IOException;
50 import java.io.RandomAccessFile;
51 import java.nio.channels.FileChannel;
52 import java.nio.file.StandardOpenOption;
53 import java.util.ArrayList;
54 import java.util.Arrays;
55 import java.util.List;
56 import java.util.Map;
57 
58 /**
59  * Storage for the lock settings service.
60  */
61 class LockSettingsStorage {
62 
63     private static final String TAG = "LockSettingsStorage";
64     private static final String TABLE = "locksettings";
65     private static final boolean DEBUG = false;
66 
67     private static final String COLUMN_KEY = "name";
68     private static final String COLUMN_USERID = "user";
69     private static final String COLUMN_VALUE = "value";
70 
71     private static final String[] COLUMNS_FOR_QUERY = {
72             COLUMN_VALUE
73     };
74     private static final String[] COLUMNS_FOR_PREFETCH = {
75             COLUMN_KEY, COLUMN_VALUE
76     };
77 
78     private static final String SYSTEM_DIRECTORY = "/system/";
79     private static final String LOCK_PATTERN_FILE = "gatekeeper.pattern.key";
80     private static final String BASE_ZERO_LOCK_PATTERN_FILE = "gatekeeper.gesture.key";
81     private static final String LEGACY_LOCK_PATTERN_FILE = "gesture.key";
82     private static final String LOCK_PASSWORD_FILE = "gatekeeper.password.key";
83     private static final String LEGACY_LOCK_PASSWORD_FILE = "password.key";
84     private static final String CHILD_PROFILE_LOCK_FILE = "gatekeeper.profile.key";
85 
86     private static final String SYNTHETIC_PASSWORD_DIRECTORY = "spblob/";
87 
88     private static final Object DEFAULT = new Object();
89 
90     private final DatabaseHelper mOpenHelper;
91     private final Context mContext;
92     private final Cache mCache = new Cache();
93     private final Object mFileWriteLock = new Object();
94 
95     private PersistentDataBlockManagerInternal mPersistentDataBlockManagerInternal;
96 
97     @VisibleForTesting
98     public static class CredentialHash {
99         static final int VERSION_LEGACY = 0;
100         static final int VERSION_GATEKEEPER = 1;
101 
CredentialHash(byte[] hash, @CredentialType int type, int version)102         private CredentialHash(byte[] hash, @CredentialType int type, int version) {
103             this(hash, type, version, false /* isBaseZeroPattern */);
104         }
105 
CredentialHash( byte[] hash, @CredentialType int type, int version, boolean isBaseZeroPattern)106         private CredentialHash(
107                 byte[] hash, @CredentialType int type, int version, boolean isBaseZeroPattern) {
108             if (type != LockPatternUtils.CREDENTIAL_TYPE_NONE) {
109                 if (hash == null) {
110                     throw new RuntimeException("Empty hash for CredentialHash");
111                 }
112             } else /* type == LockPatternUtils.CREDENTIAL_TYPE_NONE */ {
113                 if (hash != null) {
114                     throw new RuntimeException("None type CredentialHash should not have hash");
115                 }
116             }
117             this.hash = hash;
118             this.type = type;
119             this.version = version;
120             this.isBaseZeroPattern = isBaseZeroPattern;
121         }
122 
createBaseZeroPattern(byte[] hash)123         private static CredentialHash createBaseZeroPattern(byte[] hash) {
124             return new CredentialHash(hash, LockPatternUtils.CREDENTIAL_TYPE_PATTERN,
125                     VERSION_GATEKEEPER, true /* isBaseZeroPattern */);
126         }
127 
create(byte[] hash, int type)128         static CredentialHash create(byte[] hash, int type) {
129             if (type == LockPatternUtils.CREDENTIAL_TYPE_NONE) {
130                 throw new RuntimeException("Bad type for CredentialHash");
131             }
132             return new CredentialHash(hash, type, VERSION_GATEKEEPER);
133         }
134 
createEmptyHash()135         static CredentialHash createEmptyHash() {
136             return new CredentialHash(null, LockPatternUtils.CREDENTIAL_TYPE_NONE,
137                     VERSION_GATEKEEPER);
138         }
139 
140         byte[] hash;
141         @CredentialType int type;
142         int version;
143         boolean isBaseZeroPattern;
144 
toBytes()145         public byte[] toBytes() {
146             Preconditions.checkState(!isBaseZeroPattern, "base zero patterns are not serializable");
147 
148             try {
149                 ByteArrayOutputStream os = new ByteArrayOutputStream();
150                 DataOutputStream dos = new DataOutputStream(os);
151                 dos.write(version);
152                 dos.write(type);
153                 if (hash != null && hash.length > 0) {
154                     dos.writeInt(hash.length);
155                     dos.write(hash);
156                 } else {
157                     dos.writeInt(0);
158                 }
159                 dos.close();
160                 return os.toByteArray();
161             } catch (IOException e) {
162                 throw new RuntimeException(e);
163             }
164         }
165 
fromBytes(byte[] bytes)166         public static CredentialHash fromBytes(byte[] bytes) {
167             try {
168                 DataInputStream is = new DataInputStream(new ByteArrayInputStream(bytes));
169                 int version = is.read();
170                 int type = is.read();
171                 int hashSize = is.readInt();
172                 byte[] hash = null;
173                 if (hashSize > 0) {
174                     hash = new byte[hashSize];
175                     is.readFully(hash);
176                 }
177                 return new CredentialHash(hash, type, version);
178             } catch (IOException e) {
179                 throw new RuntimeException(e);
180             }
181         }
182     }
183 
LockSettingsStorage(Context context)184     public LockSettingsStorage(Context context) {
185         mContext = context;
186         mOpenHelper = new DatabaseHelper(context);
187     }
188 
setDatabaseOnCreateCallback(Callback callback)189     public void setDatabaseOnCreateCallback(Callback callback) {
190         mOpenHelper.setCallback(callback);
191     }
192 
writeKeyValue(String key, String value, int userId)193     public void writeKeyValue(String key, String value, int userId) {
194         writeKeyValue(mOpenHelper.getWritableDatabase(), key, value, userId);
195     }
196 
writeKeyValue(SQLiteDatabase db, String key, String value, int userId)197     public void writeKeyValue(SQLiteDatabase db, String key, String value, int userId) {
198         ContentValues cv = new ContentValues();
199         cv.put(COLUMN_KEY, key);
200         cv.put(COLUMN_USERID, userId);
201         cv.put(COLUMN_VALUE, value);
202 
203         db.beginTransaction();
204         try {
205             db.delete(TABLE, COLUMN_KEY + "=? AND " + COLUMN_USERID + "=?",
206                     new String[] {key, Integer.toString(userId)});
207             db.insert(TABLE, null, cv);
208             db.setTransactionSuccessful();
209             mCache.putKeyValue(key, value, userId);
210         } finally {
211             db.endTransaction();
212         }
213 
214     }
215 
readKeyValue(String key, String defaultValue, int userId)216     public String readKeyValue(String key, String defaultValue, int userId) {
217         int version;
218         synchronized (mCache) {
219             if (mCache.hasKeyValue(key, userId)) {
220                 return mCache.peekKeyValue(key, defaultValue, userId);
221             }
222             version = mCache.getVersion();
223         }
224 
225         Cursor cursor;
226         Object result = DEFAULT;
227         SQLiteDatabase db = mOpenHelper.getReadableDatabase();
228         if ((cursor = db.query(TABLE, COLUMNS_FOR_QUERY,
229                 COLUMN_USERID + "=? AND " + COLUMN_KEY + "=?",
230                 new String[] { Integer.toString(userId), key },
231                 null, null, null)) != null) {
232             if (cursor.moveToFirst()) {
233                 result = cursor.getString(0);
234             }
235             cursor.close();
236         }
237         mCache.putKeyValueIfUnchanged(key, result, userId, version);
238         return result == DEFAULT ? defaultValue : (String) result;
239     }
240 
prefetchUser(int userId)241     public void prefetchUser(int userId) {
242         int version;
243         synchronized (mCache) {
244             if (mCache.isFetched(userId)) {
245                 return;
246             }
247             mCache.setFetched(userId);
248             version = mCache.getVersion();
249         }
250 
251         Cursor cursor;
252         SQLiteDatabase db = mOpenHelper.getReadableDatabase();
253         if ((cursor = db.query(TABLE, COLUMNS_FOR_PREFETCH,
254                 COLUMN_USERID + "=?",
255                 new String[] { Integer.toString(userId) },
256                 null, null, null)) != null) {
257             while (cursor.moveToNext()) {
258                 String key = cursor.getString(0);
259                 String value = cursor.getString(1);
260                 mCache.putKeyValueIfUnchanged(key, value, userId, version);
261             }
262             cursor.close();
263         }
264 
265         // Populate cache by reading the password and pattern files.
266         readCredentialHash(userId);
267     }
268 
readPasswordHashIfExists(int userId)269     private CredentialHash readPasswordHashIfExists(int userId) {
270         byte[] stored = readFile(getLockPasswordFilename(userId));
271         if (!ArrayUtils.isEmpty(stored)) {
272             return new CredentialHash(stored, LockPatternUtils.CREDENTIAL_TYPE_PASSWORD,
273                     CredentialHash.VERSION_GATEKEEPER);
274         }
275 
276         stored = readFile(getLegacyLockPasswordFilename(userId));
277         if (!ArrayUtils.isEmpty(stored)) {
278             return new CredentialHash(stored, LockPatternUtils.CREDENTIAL_TYPE_PASSWORD,
279                     CredentialHash.VERSION_LEGACY);
280         }
281         return null;
282     }
283 
readPatternHashIfExists(int userId)284     private CredentialHash readPatternHashIfExists(int userId) {
285         byte[] stored = readFile(getLockPatternFilename(userId));
286         if (!ArrayUtils.isEmpty(stored)) {
287             return new CredentialHash(stored, LockPatternUtils.CREDENTIAL_TYPE_PATTERN,
288                     CredentialHash.VERSION_GATEKEEPER);
289         }
290 
291         stored = readFile(getBaseZeroLockPatternFilename(userId));
292         if (!ArrayUtils.isEmpty(stored)) {
293             return CredentialHash.createBaseZeroPattern(stored);
294         }
295 
296         stored = readFile(getLegacyLockPatternFilename(userId));
297         if (!ArrayUtils.isEmpty(stored)) {
298             return new CredentialHash(stored, LockPatternUtils.CREDENTIAL_TYPE_PATTERN,
299                     CredentialHash.VERSION_LEGACY);
300         }
301         return null;
302     }
303 
readCredentialHash(int userId)304     public CredentialHash readCredentialHash(int userId) {
305         CredentialHash passwordHash = readPasswordHashIfExists(userId);
306         CredentialHash patternHash = readPatternHashIfExists(userId);
307         if (passwordHash != null && patternHash != null) {
308             if (passwordHash.version == CredentialHash.VERSION_GATEKEEPER) {
309                 return passwordHash;
310             } else {
311                 return patternHash;
312             }
313         } else if (passwordHash != null) {
314             return passwordHash;
315         } else if (patternHash != null) {
316             return patternHash;
317         } else {
318             return CredentialHash.createEmptyHash();
319         }
320     }
321 
removeChildProfileLock(int userId)322     public void removeChildProfileLock(int userId) {
323         if (DEBUG)
324             Slog.e(TAG, "Remove child profile lock for user: " + userId);
325         try {
326             deleteFile(getChildProfileLockFile(userId));
327         } catch (Exception e) {
328             e.printStackTrace();
329         }
330     }
331 
writeChildProfileLock(int userId, byte[] lock)332     public void writeChildProfileLock(int userId, byte[] lock) {
333         writeFile(getChildProfileLockFile(userId), lock);
334     }
335 
readChildProfileLock(int userId)336     public byte[] readChildProfileLock(int userId) {
337         return readFile(getChildProfileLockFile(userId));
338     }
339 
hasChildProfileLock(int userId)340     public boolean hasChildProfileLock(int userId) {
341         return hasFile(getChildProfileLockFile(userId));
342     }
343 
hasPassword(int userId)344     public boolean hasPassword(int userId) {
345         return hasFile(getLockPasswordFilename(userId)) ||
346             hasFile(getLegacyLockPasswordFilename(userId));
347     }
348 
hasPattern(int userId)349     public boolean hasPattern(int userId) {
350         return hasFile(getLockPatternFilename(userId)) ||
351             hasFile(getBaseZeroLockPatternFilename(userId)) ||
352             hasFile(getLegacyLockPatternFilename(userId));
353     }
354 
hasCredential(int userId)355     public boolean hasCredential(int userId) {
356         return hasPassword(userId) || hasPattern(userId);
357     }
358 
hasFile(String name)359     private boolean hasFile(String name) {
360         byte[] contents = readFile(name);
361         return contents != null && contents.length > 0;
362     }
363 
readFile(String name)364     private byte[] readFile(String name) {
365         int version;
366         synchronized (mCache) {
367             if (mCache.hasFile(name)) {
368                 return mCache.peekFile(name);
369             }
370             version = mCache.getVersion();
371         }
372 
373         RandomAccessFile raf = null;
374         byte[] stored = null;
375         try {
376             raf = new RandomAccessFile(name, "r");
377             stored = new byte[(int) raf.length()];
378             raf.readFully(stored, 0, stored.length);
379             raf.close();
380         } catch (IOException e) {
381             Slog.e(TAG, "Cannot read file " + e);
382         } finally {
383             if (raf != null) {
384                 try {
385                     raf.close();
386                 } catch (IOException e) {
387                     Slog.e(TAG, "Error closing file " + e);
388                 }
389             }
390         }
391         mCache.putFileIfUnchanged(name, stored, version);
392         return stored;
393     }
394 
fsyncDirectory(File directory)395     private void fsyncDirectory(File directory) {
396         try {
397             try (FileChannel file = FileChannel.open(directory.toPath(),
398                     StandardOpenOption.READ)) {
399                 file.force(true);
400             }
401         } catch (IOException e) {
402             Slog.e(TAG, "Error syncing directory: " + directory, e);
403         }
404     }
405 
writeFile(String name, byte[] hash)406     private void writeFile(String name, byte[] hash) {
407         synchronized (mFileWriteLock) {
408             RandomAccessFile raf = null;
409             try {
410                 // Write the hash to file, requiring each write to be synchronized to the
411                 // underlying storage device immediately to avoid data loss in case of power loss.
412                 // This also ensures future secdiscard operation on the file succeeds since the
413                 // file would have been allocated on flash.
414                 raf = new RandomAccessFile(name, "rws");
415                 // Truncate the file if pattern is null, to clear the lock
416                 if (hash == null || hash.length == 0) {
417                     raf.setLength(0);
418                 } else {
419                     raf.write(hash, 0, hash.length);
420                 }
421                 raf.close();
422                 fsyncDirectory((new File(name)).getAbsoluteFile().getParentFile());
423             } catch (IOException e) {
424                 Slog.e(TAG, "Error writing to file " + e);
425             } finally {
426                 if (raf != null) {
427                     try {
428                         raf.close();
429                     } catch (IOException e) {
430                         Slog.e(TAG, "Error closing file " + e);
431                     }
432                 }
433             }
434             mCache.putFile(name, hash);
435         }
436     }
437 
deleteFile(String name)438     private void deleteFile(String name) {
439         if (DEBUG) Slog.e(TAG, "Delete file " + name);
440         synchronized (mFileWriteLock) {
441             File file = new File(name);
442             if (file.exists()) {
443                 file.delete();
444                 mCache.putFile(name, null);
445             }
446         }
447     }
448 
writeCredentialHash(CredentialHash hash, int userId)449     public void writeCredentialHash(CredentialHash hash, int userId) {
450         byte[] patternHash = null;
451         byte[] passwordHash = null;
452 
453         if (hash.type == LockPatternUtils.CREDENTIAL_TYPE_PASSWORD) {
454             passwordHash = hash.hash;
455         } else if (hash.type == LockPatternUtils.CREDENTIAL_TYPE_PATTERN) {
456             patternHash = hash.hash;
457         }
458         writeFile(getLockPasswordFilename(userId), passwordHash);
459         writeFile(getLockPatternFilename(userId), patternHash);
460     }
461 
462     @VisibleForTesting
getLockPatternFilename(int userId)463     String getLockPatternFilename(int userId) {
464         return getLockCredentialFilePathForUser(userId, LOCK_PATTERN_FILE);
465     }
466 
467     @VisibleForTesting
getLockPasswordFilename(int userId)468     String getLockPasswordFilename(int userId) {
469         return getLockCredentialFilePathForUser(userId, LOCK_PASSWORD_FILE);
470     }
471 
472     @VisibleForTesting
getLegacyLockPatternFilename(int userId)473     String getLegacyLockPatternFilename(int userId) {
474         return getLockCredentialFilePathForUser(userId, LEGACY_LOCK_PATTERN_FILE);
475     }
476 
477     @VisibleForTesting
getLegacyLockPasswordFilename(int userId)478     String getLegacyLockPasswordFilename(int userId) {
479         return getLockCredentialFilePathForUser(userId, LEGACY_LOCK_PASSWORD_FILE);
480     }
481 
getBaseZeroLockPatternFilename(int userId)482     private String getBaseZeroLockPatternFilename(int userId) {
483         return getLockCredentialFilePathForUser(userId, BASE_ZERO_LOCK_PATTERN_FILE);
484     }
485 
486     @VisibleForTesting
getChildProfileLockFile(int userId)487     String getChildProfileLockFile(int userId) {
488         return getLockCredentialFilePathForUser(userId, CHILD_PROFILE_LOCK_FILE);
489     }
490 
getLockCredentialFilePathForUser(int userId, String basename)491     private String getLockCredentialFilePathForUser(int userId, String basename) {
492         String dataSystemDirectory = Environment.getDataDirectory().getAbsolutePath() +
493                         SYSTEM_DIRECTORY;
494         if (userId == 0) {
495             // Leave it in the same place for user 0
496             return dataSystemDirectory + basename;
497         } else {
498             return new File(Environment.getUserSystemDirectory(userId), basename).getAbsolutePath();
499         }
500     }
501 
writeSyntheticPasswordState(int userId, long handle, String name, byte[] data)502     public void writeSyntheticPasswordState(int userId, long handle, String name, byte[] data) {
503         ensureSyntheticPasswordDirectoryForUser(userId);
504         writeFile(getSynthenticPasswordStateFilePathForUser(userId, handle, name), data);
505     }
506 
readSyntheticPasswordState(int userId, long handle, String name)507     public byte[] readSyntheticPasswordState(int userId, long handle, String name) {
508         return readFile(getSynthenticPasswordStateFilePathForUser(userId, handle, name));
509     }
510 
deleteSyntheticPasswordState(int userId, long handle, String name)511     public void deleteSyntheticPasswordState(int userId, long handle, String name) {
512         String path = getSynthenticPasswordStateFilePathForUser(userId, handle, name);
513         File file = new File(path);
514         if (file.exists()) {
515             try (RandomAccessFile raf = new RandomAccessFile(path, "rws")) {
516                 final int fileSize = (int) raf.length();
517                 raf.write(new byte[fileSize]);
518             } catch (Exception e) {
519                 Slog.w(TAG, "Failed to zeroize " + path, e);
520             } finally {
521                 file.delete();
522             }
523             mCache.putFile(path, null);
524         }
525     }
526 
listSyntheticPasswordHandlesForAllUsers(String stateName)527     public Map<Integer, List<Long>> listSyntheticPasswordHandlesForAllUsers(String stateName) {
528         Map<Integer, List<Long>> result = new ArrayMap<>();
529         final UserManager um = UserManager.get(mContext);
530         for (UserInfo user : um.getUsers(false)) {
531             result.put(user.id, listSyntheticPasswordHandlesForUser(stateName, user.id));
532         }
533         return result;
534     }
535 
listSyntheticPasswordHandlesForUser(String stateName, int userId)536     public List<Long> listSyntheticPasswordHandlesForUser(String stateName, int userId) {
537         File baseDir = getSyntheticPasswordDirectoryForUser(userId);
538         List<Long> result = new ArrayList<>();
539         File[] files = baseDir.listFiles();
540         if (files == null) {
541             return result;
542         }
543         for (File file : files) {
544             String[] parts = file.getName().split("\\.");
545             if (parts.length == 2 && parts[1].equals(stateName)) {
546                 try {
547                     result.add(Long.parseUnsignedLong(parts[0], 16));
548                 } catch (NumberFormatException e) {
549                     Slog.e(TAG, "Failed to parse handle " + parts[0]);
550                 }
551             }
552         }
553         return result;
554     }
555 
556     @VisibleForTesting
getSyntheticPasswordDirectoryForUser(int userId)557     protected File getSyntheticPasswordDirectoryForUser(int userId) {
558         return new File(Environment.getDataSystemDeDirectory(userId) ,SYNTHETIC_PASSWORD_DIRECTORY);
559     }
560 
561     /** Ensure per-user directory for synthetic password state exists */
ensureSyntheticPasswordDirectoryForUser(int userId)562     private void ensureSyntheticPasswordDirectoryForUser(int userId) {
563         File baseDir = getSyntheticPasswordDirectoryForUser(userId);
564         if (!baseDir.exists()) {
565             baseDir.mkdir();
566         }
567     }
568 
569     @VisibleForTesting
getSynthenticPasswordStateFilePathForUser(int userId, long handle, String name)570     protected String getSynthenticPasswordStateFilePathForUser(int userId, long handle,
571             String name) {
572         final File baseDir = getSyntheticPasswordDirectoryForUser(userId);
573         final String baseName = String.format("%016x.%s", handle, name);
574         return new File(baseDir, baseName).getAbsolutePath();
575     }
576 
removeUser(int userId)577     public void removeUser(int userId) {
578         SQLiteDatabase db = mOpenHelper.getWritableDatabase();
579 
580         final UserManager um = (UserManager) mContext.getSystemService(USER_SERVICE);
581         final UserInfo parentInfo = um.getProfileParent(userId);
582 
583         if (parentInfo == null) {
584             // This user owns its lock settings files - safe to delete them
585             synchronized (mFileWriteLock) {
586                 String name = getLockPasswordFilename(userId);
587                 File file = new File(name);
588                 if (file.exists()) {
589                     file.delete();
590                     mCache.putFile(name, null);
591                 }
592                 name = getLockPatternFilename(userId);
593                 file = new File(name);
594                 if (file.exists()) {
595                     file.delete();
596                     mCache.putFile(name, null);
597                 }
598             }
599         } else {
600             // Managed profile
601             removeChildProfileLock(userId);
602         }
603 
604         File spStateDir = getSyntheticPasswordDirectoryForUser(userId);
605         try {
606             db.beginTransaction();
607             db.delete(TABLE, COLUMN_USERID + "='" + userId + "'", null);
608             db.setTransactionSuccessful();
609             mCache.removeUser(userId);
610             // The directory itself will be deleted as part of user deletion operation by the
611             // framework, so only need to purge cache here.
612             //TODO: (b/34600579) invoke secdiscardable
613             mCache.purgePath(spStateDir.getAbsolutePath());
614         } finally {
615             db.endTransaction();
616         }
617     }
618 
619     @VisibleForTesting
closeDatabase()620     void closeDatabase() {
621         mOpenHelper.close();
622     }
623 
624     @VisibleForTesting
clearCache()625     void clearCache() {
626         mCache.clear();
627     }
628 
629     @Nullable
getPersistentDataBlock()630     public PersistentDataBlockManagerInternal getPersistentDataBlock() {
631         if (mPersistentDataBlockManagerInternal == null) {
632             mPersistentDataBlockManagerInternal =
633                     LocalServices.getService(PersistentDataBlockManagerInternal.class);
634         }
635         return mPersistentDataBlockManagerInternal;
636     }
637 
writePersistentDataBlock(int persistentType, int userId, int qualityForUi, byte[] payload)638     public void writePersistentDataBlock(int persistentType, int userId, int qualityForUi,
639             byte[] payload) {
640         PersistentDataBlockManagerInternal persistentDataBlock = getPersistentDataBlock();
641         if (persistentDataBlock == null) {
642             return;
643         }
644         persistentDataBlock.setFrpCredentialHandle(PersistentData.toBytes(
645                 persistentType, userId, qualityForUi, payload));
646     }
647 
readPersistentDataBlock()648     public PersistentData readPersistentDataBlock() {
649         PersistentDataBlockManagerInternal persistentDataBlock = getPersistentDataBlock();
650         if (persistentDataBlock == null) {
651             return PersistentData.NONE;
652         }
653         try {
654             return PersistentData.fromBytes(persistentDataBlock.getFrpCredentialHandle());
655         } catch (IllegalStateException e) {
656             Slog.e(TAG, "Error reading persistent data block", e);
657             return PersistentData.NONE;
658         }
659     }
660 
661     public static class PersistentData {
662         static final byte VERSION_1 = 1;
663         static final int VERSION_1_HEADER_SIZE = 1 + 1 + 4 + 4;
664 
665         public static final int TYPE_NONE = 0;
666         public static final int TYPE_SP = 1;
667         public static final int TYPE_SP_WEAVER = 2;
668 
669         public static final PersistentData NONE = new PersistentData(TYPE_NONE,
670                 UserHandle.USER_NULL, DevicePolicyManager.PASSWORD_QUALITY_UNSPECIFIED, null);
671 
672         final int type;
673         final int userId;
674         final int qualityForUi;
675         final byte[] payload;
676 
PersistentData(int type, int userId, int qualityForUi, byte[] payload)677         private PersistentData(int type, int userId, int qualityForUi, byte[] payload) {
678             this.type = type;
679             this.userId = userId;
680             this.qualityForUi = qualityForUi;
681             this.payload = payload;
682         }
683 
fromBytes(byte[] frpData)684         public static PersistentData fromBytes(byte[] frpData) {
685             if (frpData == null || frpData.length == 0) {
686                 return NONE;
687             }
688 
689             DataInputStream is = new DataInputStream(new ByteArrayInputStream(frpData));
690             try {
691                 byte version = is.readByte();
692                 if (version == PersistentData.VERSION_1) {
693                     int type = is.readByte() & 0xFF;
694                     int userId = is.readInt();
695                     int qualityForUi = is.readInt();
696                     byte[] payload = new byte[frpData.length - VERSION_1_HEADER_SIZE];
697                     System.arraycopy(frpData, VERSION_1_HEADER_SIZE, payload, 0, payload.length);
698                     return new PersistentData(type, userId, qualityForUi, payload);
699                 } else {
700                     Slog.wtf(TAG, "Unknown PersistentData version code: " + version);
701                     return NONE;
702                 }
703             } catch (IOException e) {
704                 Slog.wtf(TAG, "Could not parse PersistentData", e);
705                 return NONE;
706             }
707         }
708 
toBytes(int persistentType, int userId, int qualityForUi, byte[] payload)709         public static byte[] toBytes(int persistentType, int userId, int qualityForUi,
710                 byte[] payload) {
711             if (persistentType == PersistentData.TYPE_NONE) {
712                 Preconditions.checkArgument(payload == null,
713                         "TYPE_NONE must have empty payload");
714                 return null;
715             }
716             Preconditions.checkArgument(payload != null && payload.length > 0,
717                     "empty payload must only be used with TYPE_NONE");
718 
719             ByteArrayOutputStream os = new ByteArrayOutputStream(
720                     VERSION_1_HEADER_SIZE + payload.length);
721             DataOutputStream dos = new DataOutputStream(os);
722             try {
723                 dos.writeByte(PersistentData.VERSION_1);
724                 dos.writeByte(persistentType);
725                 dos.writeInt(userId);
726                 dos.writeInt(qualityForUi);
727                 dos.write(payload);
728             } catch (IOException e) {
729                 throw new RuntimeException("ByteArrayOutputStream cannot throw IOException");
730             }
731             return os.toByteArray();
732         }
733     }
734 
735     public interface Callback {
initialize(SQLiteDatabase db)736         void initialize(SQLiteDatabase db);
737     }
738 
739     static class DatabaseHelper extends SQLiteOpenHelper {
740         private static final String TAG = "LockSettingsDB";
741         private static final String DATABASE_NAME = "locksettings.db";
742 
743         private static final int DATABASE_VERSION = 2;
744         private static final int IDLE_CONNECTION_TIMEOUT_MS = 30000;
745 
746         private Callback mCallback;
747 
DatabaseHelper(Context context)748         public DatabaseHelper(Context context) {
749             super(context, DATABASE_NAME, null, DATABASE_VERSION);
750             setWriteAheadLoggingEnabled(true);
751             // Memory optimization - close idle connections after 30s of inactivity
752             setIdleConnectionTimeout(IDLE_CONNECTION_TIMEOUT_MS);
753         }
754 
setCallback(Callback callback)755         public void setCallback(Callback callback) {
756             mCallback = callback;
757         }
758 
createTable(SQLiteDatabase db)759         private void createTable(SQLiteDatabase db) {
760             db.execSQL("CREATE TABLE " + TABLE + " (" +
761                     "_id INTEGER PRIMARY KEY AUTOINCREMENT," +
762                     COLUMN_KEY + " TEXT," +
763                     COLUMN_USERID + " INTEGER," +
764                     COLUMN_VALUE + " TEXT" +
765                     ");");
766         }
767 
768         @Override
onCreate(SQLiteDatabase db)769         public void onCreate(SQLiteDatabase db) {
770             createTable(db);
771             if (mCallback != null) {
772                 mCallback.initialize(db);
773             }
774         }
775 
776         @Override
onUpgrade(SQLiteDatabase db, int oldVersion, int currentVersion)777         public void onUpgrade(SQLiteDatabase db, int oldVersion, int currentVersion) {
778             int upgradeVersion = oldVersion;
779             if (upgradeVersion == 1) {
780                 // Previously migrated lock screen widget settings. Now defunct.
781                 upgradeVersion = 2;
782             }
783 
784             if (upgradeVersion != DATABASE_VERSION) {
785                 Log.w(TAG, "Failed to upgrade database!");
786             }
787         }
788     }
789 
790     /**
791      * Cache consistency model:
792      * - Writes to storage write directly to the cache, but this MUST happen within the atomic
793      *   section either provided by the database transaction or mWriteLock, such that writes to the
794      *   cache and writes to the backing storage are guaranteed to occur in the same order
795      *
796      * - Reads can populate the cache, but because they are no strong ordering guarantees with
797      *   respect to writes this precaution is taken:
798      *   - The cache is assigned a version number that increases every time the cache is modified.
799      *     Reads from backing storage can only populate the cache if the backing storage
800      *     has not changed since the load operation has begun.
801      *     This guarantees that no read operation can shadow a write to the cache that happens
802      *     after it had begun.
803      */
804     private static class Cache {
805         private final ArrayMap<CacheKey, Object> mCache = new ArrayMap<>();
806         private final CacheKey mCacheKey = new CacheKey();
807         private int mVersion = 0;
808 
peekKeyValue(String key, String defaultValue, int userId)809         String peekKeyValue(String key, String defaultValue, int userId) {
810             Object cached = peek(CacheKey.TYPE_KEY_VALUE, key, userId);
811             return cached == DEFAULT ? defaultValue : (String) cached;
812         }
813 
hasKeyValue(String key, int userId)814         boolean hasKeyValue(String key, int userId) {
815             return contains(CacheKey.TYPE_KEY_VALUE, key, userId);
816         }
817 
putKeyValue(String key, String value, int userId)818         void putKeyValue(String key, String value, int userId) {
819             put(CacheKey.TYPE_KEY_VALUE, key, value, userId);
820         }
821 
putKeyValueIfUnchanged(String key, Object value, int userId, int version)822         void putKeyValueIfUnchanged(String key, Object value, int userId, int version) {
823             putIfUnchanged(CacheKey.TYPE_KEY_VALUE, key, value, userId, version);
824         }
825 
peekFile(String fileName)826         byte[] peekFile(String fileName) {
827             return copyOf((byte[]) peek(CacheKey.TYPE_FILE, fileName, -1 /* userId */));
828         }
829 
hasFile(String fileName)830         boolean hasFile(String fileName) {
831             return contains(CacheKey.TYPE_FILE, fileName, -1 /* userId */);
832         }
833 
putFile(String key, byte[] value)834         void putFile(String key, byte[] value) {
835             put(CacheKey.TYPE_FILE, key, copyOf(value), -1 /* userId */);
836         }
837 
putFileIfUnchanged(String key, byte[] value, int version)838         void putFileIfUnchanged(String key, byte[] value, int version) {
839             putIfUnchanged(CacheKey.TYPE_FILE, key, copyOf(value), -1 /* userId */, version);
840         }
841 
setFetched(int userId)842         void setFetched(int userId) {
843             put(CacheKey.TYPE_FETCHED, "isFetched", "true", userId);
844         }
845 
isFetched(int userId)846         boolean isFetched(int userId) {
847             return contains(CacheKey.TYPE_FETCHED, "", userId);
848         }
849 
850 
put(int type, String key, Object value, int userId)851         private synchronized void put(int type, String key, Object value, int userId) {
852             // Create a new CachKey here because it may be saved in the map if the key is absent.
853             mCache.put(new CacheKey().set(type, key, userId), value);
854             mVersion++;
855         }
856 
putIfUnchanged(int type, String key, Object value, int userId, int version)857         private synchronized void putIfUnchanged(int type, String key, Object value, int userId,
858                 int version) {
859             if (!contains(type, key, userId) && mVersion == version) {
860                 put(type, key, value, userId);
861             }
862         }
863 
contains(int type, String key, int userId)864         private synchronized boolean contains(int type, String key, int userId) {
865             return mCache.containsKey(mCacheKey.set(type, key, userId));
866         }
867 
peek(int type, String key, int userId)868         private synchronized Object peek(int type, String key, int userId) {
869             return mCache.get(mCacheKey.set(type, key, userId));
870         }
871 
getVersion()872         private synchronized int getVersion() {
873             return mVersion;
874         }
875 
removeUser(int userId)876         synchronized void removeUser(int userId) {
877             for (int i = mCache.size() - 1; i >= 0; i--) {
878                 if (mCache.keyAt(i).userId == userId) {
879                     mCache.removeAt(i);
880                 }
881             }
882 
883             // Make sure in-flight loads can't write to cache.
884             mVersion++;
885         }
886 
copyOf(byte[] data)887         private byte[] copyOf(byte[] data) {
888             return data != null ? Arrays.copyOf(data, data.length) : null;
889         }
890 
purgePath(String path)891         synchronized void purgePath(String path) {
892             for (int i = mCache.size() - 1; i >= 0; i--) {
893                 CacheKey entry = mCache.keyAt(i);
894                 if (entry.type == CacheKey.TYPE_FILE && entry.key.startsWith(path)) {
895                     mCache.removeAt(i);
896                 }
897             }
898             mVersion++;
899         }
900 
clear()901         synchronized void clear() {
902             mCache.clear();
903             mVersion++;
904         }
905 
906         private static final class CacheKey {
907             static final int TYPE_KEY_VALUE = 0;
908             static final int TYPE_FILE = 1;
909             static final int TYPE_FETCHED = 2;
910 
911             String key;
912             int userId;
913             int type;
914 
set(int type, String key, int userId)915             public CacheKey set(int type, String key, int userId) {
916                 this.type = type;
917                 this.key = key;
918                 this.userId = userId;
919                 return this;
920             }
921 
922             @Override
equals(Object obj)923             public boolean equals(Object obj) {
924                 if (!(obj instanceof CacheKey))
925                     return false;
926                 CacheKey o = (CacheKey) obj;
927                 return userId == o.userId && type == o.type && key.equals(o.key);
928             }
929 
930             @Override
hashCode()931             public int hashCode() {
932                 return key.hashCode() ^ userId ^ type;
933             }
934         }
935     }
936 
937 }
938