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