2 * Copyright (C) 2014 The Android Open Source Project
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
8 * http://www.apache.org/licenses/LICENSE-2.0
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
17 package com.android.server;
19 import static android.content.Context.USER_SERVICE;
21 import android.content.ContentValues;
22 import android.content.Context;
23 import android.content.pm.UserInfo;
24 import android.database.Cursor;
25 import android.database.sqlite.SQLiteDatabase;
26 import android.database.sqlite.SQLiteOpenHelper;
27 import android.os.Environment;
28 import android.os.UserManager;
29 import android.util.ArrayMap;
30 import android.util.Log;
31 import android.util.Slog;
33 import com.android.internal.annotations.VisibleForTesting;
34 import com.android.internal.util.ArrayUtils;
35 import com.android.internal.widget.LockPatternUtils;
38 import java.io.IOException;
39 import java.io.RandomAccessFile;
42 * Storage for the lock settings service.
44 class LockSettingsStorage {
46 private static final String TAG = "LockSettingsStorage";
47 private static final String TABLE = "locksettings";
48 private static final boolean DEBUG = false;
50 private static final String COLUMN_KEY = "name";
51 private static final String COLUMN_USERID = "user";
52 private static final String COLUMN_VALUE = "value";
54 private static final String[] COLUMNS_FOR_QUERY = {
57 private static final String[] COLUMNS_FOR_PREFETCH = {
58 COLUMN_KEY, COLUMN_VALUE
61 private static final String SYSTEM_DIRECTORY = "/system/";
62 private static final String LOCK_PATTERN_FILE = "gatekeeper.pattern.key";
63 private static final String BASE_ZERO_LOCK_PATTERN_FILE = "gatekeeper.gesture.key";
64 private static final String LEGACY_LOCK_PATTERN_FILE = "gesture.key";
65 private static final String LOCK_PASSWORD_FILE = "gatekeeper.password.key";
66 private static final String LEGACY_LOCK_PASSWORD_FILE = "password.key";
67 private static final String CHILD_PROFILE_LOCK_FILE = "gatekeeper.profile.key";
69 private static final String SYNTHETIC_PASSWORD_DIRECTORY = "spblob/";
71 private static final Object DEFAULT = new Object();
73 private final DatabaseHelper mOpenHelper;
74 private final Context mContext;
75 private final Cache mCache = new Cache();
76 private final Object mFileWriteLock = new Object();
79 public static class CredentialHash {
80 static final int VERSION_LEGACY = 0;
81 static final int VERSION_GATEKEEPER = 1;
83 private CredentialHash(byte[] hash, int type, int version) {
84 if (type != LockPatternUtils.CREDENTIAL_TYPE_NONE) {
86 throw new RuntimeException("Empty hash for CredentialHash");
88 } else /* type == LockPatternUtils.CREDENTIAL_TYPE_NONE */ {
90 throw new RuntimeException("None type CredentialHash should not have hash");
95 this.version = version;
96 this.isBaseZeroPattern = false;
99 private CredentialHash(byte[] hash, boolean isBaseZeroPattern) {
101 this.type = LockPatternUtils.CREDENTIAL_TYPE_PATTERN;
102 this.version = VERSION_GATEKEEPER;
103 this.isBaseZeroPattern = isBaseZeroPattern;
106 static CredentialHash create(byte[] hash, int type) {
107 if (type == LockPatternUtils.CREDENTIAL_TYPE_NONE) {
108 throw new RuntimeException("Bad type for CredentialHash");
110 return new CredentialHash(hash, type, VERSION_GATEKEEPER);
113 static CredentialHash createEmptyHash() {
114 return new CredentialHash(null, LockPatternUtils.CREDENTIAL_TYPE_NONE,
121 boolean isBaseZeroPattern;
124 public LockSettingsStorage(Context context) {
126 mOpenHelper = new DatabaseHelper(context);
129 public void setDatabaseOnCreateCallback(Callback callback) {
130 mOpenHelper.setCallback(callback);
133 public void writeKeyValue(String key, String value, int userId) {
134 writeKeyValue(mOpenHelper.getWritableDatabase(), key, value, userId);
137 public void writeKeyValue(SQLiteDatabase db, String key, String value, int userId) {
138 ContentValues cv = new ContentValues();
139 cv.put(COLUMN_KEY, key);
140 cv.put(COLUMN_USERID, userId);
141 cv.put(COLUMN_VALUE, value);
143 db.beginTransaction();
145 db.delete(TABLE, COLUMN_KEY + "=? AND " + COLUMN_USERID + "=?",
146 new String[] {key, Integer.toString(userId)});
147 db.insert(TABLE, null, cv);
148 db.setTransactionSuccessful();
149 mCache.putKeyValue(key, value, userId);
156 public String readKeyValue(String key, String defaultValue, int userId) {
158 synchronized (mCache) {
159 if (mCache.hasKeyValue(key, userId)) {
160 return mCache.peekKeyValue(key, defaultValue, userId);
162 version = mCache.getVersion();
166 Object result = DEFAULT;
167 SQLiteDatabase db = mOpenHelper.getReadableDatabase();
168 if ((cursor = db.query(TABLE, COLUMNS_FOR_QUERY,
169 COLUMN_USERID + "=? AND " + COLUMN_KEY + "=?",
170 new String[] { Integer.toString(userId), key },
171 null, null, null)) != null) {
172 if (cursor.moveToFirst()) {
173 result = cursor.getString(0);
177 mCache.putKeyValueIfUnchanged(key, result, userId, version);
178 return result == DEFAULT ? defaultValue : (String) result;
181 public void prefetchUser(int userId) {
183 synchronized (mCache) {
184 if (mCache.isFetched(userId)) {
187 mCache.setFetched(userId);
188 version = mCache.getVersion();
192 SQLiteDatabase db = mOpenHelper.getReadableDatabase();
193 if ((cursor = db.query(TABLE, COLUMNS_FOR_PREFETCH,
194 COLUMN_USERID + "=?",
195 new String[] { Integer.toString(userId) },
196 null, null, null)) != null) {
197 while (cursor.moveToNext()) {
198 String key = cursor.getString(0);
199 String value = cursor.getString(1);
200 mCache.putKeyValueIfUnchanged(key, value, userId, version);
205 // Populate cache by reading the password and pattern files.
206 readCredentialHash(userId);
209 private CredentialHash readPasswordHashIfExists(int userId) {
210 byte[] stored = readFile(getLockPasswordFilename(userId));
211 if (!ArrayUtils.isEmpty(stored)) {
212 return new CredentialHash(stored, LockPatternUtils.CREDENTIAL_TYPE_PASSWORD,
213 CredentialHash.VERSION_GATEKEEPER);
216 stored = readFile(getLegacyLockPasswordFilename(userId));
217 if (!ArrayUtils.isEmpty(stored)) {
218 return new CredentialHash(stored, LockPatternUtils.CREDENTIAL_TYPE_PASSWORD,
219 CredentialHash.VERSION_LEGACY);
224 private CredentialHash readPatternHashIfExists(int userId) {
225 byte[] stored = readFile(getLockPatternFilename(userId));
226 if (!ArrayUtils.isEmpty(stored)) {
227 return new CredentialHash(stored, LockPatternUtils.CREDENTIAL_TYPE_PATTERN,
228 CredentialHash.VERSION_GATEKEEPER);
231 stored = readFile(getBaseZeroLockPatternFilename(userId));
232 if (!ArrayUtils.isEmpty(stored)) {
233 return new CredentialHash(stored, true);
236 stored = readFile(getLegacyLockPatternFilename(userId));
237 if (!ArrayUtils.isEmpty(stored)) {
238 return new CredentialHash(stored, LockPatternUtils.CREDENTIAL_TYPE_PATTERN,
239 CredentialHash.VERSION_LEGACY);
244 public CredentialHash readCredentialHash(int userId) {
245 CredentialHash passwordHash = readPasswordHashIfExists(userId);
246 CredentialHash patternHash = readPatternHashIfExists(userId);
247 if (passwordHash != null && patternHash != null) {
248 if (passwordHash.version == CredentialHash.VERSION_GATEKEEPER) {
253 } else if (passwordHash != null) {
255 } else if (patternHash != null) {
258 return CredentialHash.createEmptyHash();
262 public void removeChildProfileLock(int userId) {
264 Slog.e(TAG, "Remove child profile lock for user: " + userId);
266 deleteFile(getChildProfileLockFile(userId));
267 } catch (Exception e) {
272 public void writeChildProfileLock(int userId, byte[] lock) {
273 writeFile(getChildProfileLockFile(userId), lock);
276 public byte[] readChildProfileLock(int userId) {
277 return readFile(getChildProfileLockFile(userId));
280 public boolean hasChildProfileLock(int userId) {
281 return hasFile(getChildProfileLockFile(userId));
284 public boolean hasPassword(int userId) {
285 return hasFile(getLockPasswordFilename(userId)) ||
286 hasFile(getLegacyLockPasswordFilename(userId));
289 public boolean hasPattern(int userId) {
290 return hasFile(getLockPatternFilename(userId)) ||
291 hasFile(getBaseZeroLockPatternFilename(userId)) ||
292 hasFile(getLegacyLockPatternFilename(userId));
295 public boolean hasCredential(int userId) {
296 return hasPassword(userId) || hasPattern(userId);
299 private boolean hasFile(String name) {
300 byte[] contents = readFile(name);
301 return contents != null && contents.length > 0;
304 private byte[] readFile(String name) {
306 synchronized (mCache) {
307 if (mCache.hasFile(name)) {
308 return mCache.peekFile(name);
310 version = mCache.getVersion();
313 RandomAccessFile raf = null;
314 byte[] stored = null;
316 raf = new RandomAccessFile(name, "r");
317 stored = new byte[(int) raf.length()];
318 raf.readFully(stored, 0, stored.length);
320 } catch (IOException e) {
321 Slog.e(TAG, "Cannot read file " + e);
326 } catch (IOException e) {
327 Slog.e(TAG, "Error closing file " + e);
331 mCache.putFileIfUnchanged(name, stored, version);
335 private void writeFile(String name, byte[] hash) {
336 synchronized (mFileWriteLock) {
337 RandomAccessFile raf = null;
339 // Write the hash to file
340 raf = new RandomAccessFile(name, "rw");
341 // Truncate the file if pattern is null, to clear the lock
342 if (hash == null || hash.length == 0) {
345 raf.write(hash, 0, hash.length);
348 } catch (IOException e) {
349 Slog.e(TAG, "Error writing to file " + e);
354 } catch (IOException e) {
355 Slog.e(TAG, "Error closing file " + e);
359 mCache.putFile(name, hash);
363 private void deleteFile(String name) {
364 if (DEBUG) Slog.e(TAG, "Delete file " + name);
365 synchronized (mFileWriteLock) {
366 File file = new File(name);
369 mCache.putFile(name, null);
374 public void writeCredentialHash(CredentialHash hash, int userId) {
375 byte[] patternHash = null;
376 byte[] passwordHash = null;
378 if (hash.type == LockPatternUtils.CREDENTIAL_TYPE_PASSWORD) {
379 passwordHash = hash.hash;
380 } else if (hash.type == LockPatternUtils.CREDENTIAL_TYPE_PATTERN) {
381 patternHash = hash.hash;
383 writeFile(getLockPasswordFilename(userId), passwordHash);
384 writeFile(getLockPatternFilename(userId), patternHash);
388 String getLockPatternFilename(int userId) {
389 return getLockCredentialFilePathForUser(userId, LOCK_PATTERN_FILE);
393 String getLockPasswordFilename(int userId) {
394 return getLockCredentialFilePathForUser(userId, LOCK_PASSWORD_FILE);
398 String getLegacyLockPatternFilename(int userId) {
399 return getLockCredentialFilePathForUser(userId, LEGACY_LOCK_PATTERN_FILE);
403 String getLegacyLockPasswordFilename(int userId) {
404 return getLockCredentialFilePathForUser(userId, LEGACY_LOCK_PASSWORD_FILE);
407 private String getBaseZeroLockPatternFilename(int userId) {
408 return getLockCredentialFilePathForUser(userId, BASE_ZERO_LOCK_PATTERN_FILE);
412 String getChildProfileLockFile(int userId) {
413 return getLockCredentialFilePathForUser(userId, CHILD_PROFILE_LOCK_FILE);
416 private String getLockCredentialFilePathForUser(int userId, String basename) {
417 String dataSystemDirectory = Environment.getDataDirectory().getAbsolutePath() +
420 // Leave it in the same place for user 0
421 return dataSystemDirectory + basename;
423 return new File(Environment.getUserSystemDirectory(userId), basename).getAbsolutePath();
427 public void writeSyntheticPasswordState(int userId, long handle, String name, byte[] data) {
428 writeFile(getSynthenticPasswordStateFilePathForUser(userId, handle, name), data);
431 public byte[] readSyntheticPasswordState(int userId, long handle, String name) {
432 return readFile(getSynthenticPasswordStateFilePathForUser(userId, handle, name));
435 public void deleteSyntheticPasswordState(int userId, long handle, String name, boolean secure) {
436 String path = getSynthenticPasswordStateFilePathForUser(userId, handle, name);
437 File file = new File(path);
439 //TODO: (b/34600579) invoke secdiscardable
441 mCache.putFile(path, null);
446 protected File getSyntheticPasswordDirectoryForUser(int userId) {
447 return new File(Environment.getDataSystemDeDirectory(userId) ,SYNTHETIC_PASSWORD_DIRECTORY);
451 protected String getSynthenticPasswordStateFilePathForUser(int userId, long handle,
453 File baseDir = getSyntheticPasswordDirectoryForUser(userId);
454 String baseName = String.format("%016x.%s", handle, name);
455 if (!baseDir.exists()) {
458 return new File(baseDir, baseName).getAbsolutePath();
461 public void removeUser(int userId) {
462 SQLiteDatabase db = mOpenHelper.getWritableDatabase();
464 final UserManager um = (UserManager) mContext.getSystemService(USER_SERVICE);
465 final UserInfo parentInfo = um.getProfileParent(userId);
467 if (parentInfo == null) {
468 // This user owns its lock settings files - safe to delete them
469 synchronized (mFileWriteLock) {
470 String name = getLockPasswordFilename(userId);
471 File file = new File(name);
474 mCache.putFile(name, null);
476 name = getLockPatternFilename(userId);
477 file = new File(name);
480 mCache.putFile(name, null);
485 removeChildProfileLock(userId);
488 File spStateDir = getSyntheticPasswordDirectoryForUser(userId);
490 db.beginTransaction();
491 db.delete(TABLE, COLUMN_USERID + "='" + userId + "'", null);
492 db.setTransactionSuccessful();
493 mCache.removeUser(userId);
494 // The directory itself will be deleted as part of user deletion operation by the
495 // framework, so only need to purge cache here.
496 //TODO: (b/34600579) invoke secdiscardable
497 mCache.purgePath(spStateDir.getAbsolutePath());
504 void closeDatabase() {
513 public interface Callback {
514 void initialize(SQLiteDatabase db);
517 class DatabaseHelper extends SQLiteOpenHelper {
518 private static final String TAG = "LockSettingsDB";
519 private static final String DATABASE_NAME = "locksettings.db";
521 private static final int DATABASE_VERSION = 2;
523 private Callback mCallback;
525 public DatabaseHelper(Context context) {
526 super(context, DATABASE_NAME, null, DATABASE_VERSION);
527 setWriteAheadLoggingEnabled(true);
530 public void setCallback(Callback callback) {
531 mCallback = callback;
534 private void createTable(SQLiteDatabase db) {
535 db.execSQL("CREATE TABLE " + TABLE + " (" +
536 "_id INTEGER PRIMARY KEY AUTOINCREMENT," +
537 COLUMN_KEY + " TEXT," +
538 COLUMN_USERID + " INTEGER," +
539 COLUMN_VALUE + " TEXT" +
544 public void onCreate(SQLiteDatabase db) {
546 if (mCallback != null) {
547 mCallback.initialize(db);
552 public void onUpgrade(SQLiteDatabase db, int oldVersion, int currentVersion) {
553 int upgradeVersion = oldVersion;
554 if (upgradeVersion == 1) {
555 // Previously migrated lock screen widget settings. Now defunct.
559 if (upgradeVersion != DATABASE_VERSION) {
560 Log.w(TAG, "Failed to upgrade database!");
566 * Cache consistency model:
567 * - Writes to storage write directly to the cache, but this MUST happen within the atomic
568 * section either provided by the database transaction or mWriteLock, such that writes to the
569 * cache and writes to the backing storage are guaranteed to occur in the same order
571 * - Reads can populate the cache, but because they are no strong ordering guarantees with
572 * respect to writes this precaution is taken:
573 * - The cache is assigned a version number that increases every time the cache is modified.
574 * Reads from backing storage can only populate the cache if the backing storage
575 * has not changed since the load operation has begun.
576 * This guarantees that no read operation can shadow a write to the cache that happens
577 * after it had begun.
579 private static class Cache {
580 private final ArrayMap<CacheKey, Object> mCache = new ArrayMap<>();
581 private final CacheKey mCacheKey = new CacheKey();
582 private int mVersion = 0;
584 String peekKeyValue(String key, String defaultValue, int userId) {
585 Object cached = peek(CacheKey.TYPE_KEY_VALUE, key, userId);
586 return cached == DEFAULT ? defaultValue : (String) cached;
589 boolean hasKeyValue(String key, int userId) {
590 return contains(CacheKey.TYPE_KEY_VALUE, key, userId);
593 void putKeyValue(String key, String value, int userId) {
594 put(CacheKey.TYPE_KEY_VALUE, key, value, userId);
597 void putKeyValueIfUnchanged(String key, Object value, int userId, int version) {
598 putIfUnchanged(CacheKey.TYPE_KEY_VALUE, key, value, userId, version);
601 byte[] peekFile(String fileName) {
602 return (byte[]) peek(CacheKey.TYPE_FILE, fileName, -1 /* userId */);
605 boolean hasFile(String fileName) {
606 return contains(CacheKey.TYPE_FILE, fileName, -1 /* userId */);
609 void putFile(String key, byte[] value) {
610 put(CacheKey.TYPE_FILE, key, value, -1 /* userId */);
613 void putFileIfUnchanged(String key, byte[] value, int version) {
614 putIfUnchanged(CacheKey.TYPE_FILE, key, value, -1 /* userId */, version);
617 void setFetched(int userId) {
618 put(CacheKey.TYPE_FETCHED, "isFetched", "true", userId);
621 boolean isFetched(int userId) {
622 return contains(CacheKey.TYPE_FETCHED, "", userId);
626 private synchronized void put(int type, String key, Object value, int userId) {
627 // Create a new CachKey here because it may be saved in the map if the key is absent.
628 mCache.put(new CacheKey().set(type, key, userId), value);
632 private synchronized void putIfUnchanged(int type, String key, Object value, int userId,
634 if (!contains(type, key, userId) && mVersion == version) {
635 put(type, key, value, userId);
639 private synchronized boolean contains(int type, String key, int userId) {
640 return mCache.containsKey(mCacheKey.set(type, key, userId));
643 private synchronized Object peek(int type, String key, int userId) {
644 return mCache.get(mCacheKey.set(type, key, userId));
647 private synchronized int getVersion() {
651 synchronized void removeUser(int userId) {
652 for (int i = mCache.size() - 1; i >= 0; i--) {
653 if (mCache.keyAt(i).userId == userId) {
658 // Make sure in-flight loads can't write to cache.
662 synchronized void purgePath(String path) {
663 for (int i = mCache.size() - 1; i >= 0; i--) {
664 CacheKey entry = mCache.keyAt(i);
665 if (entry.type == CacheKey.TYPE_FILE && entry.key.startsWith(path)) {
672 synchronized void clear() {
677 private static final class CacheKey {
678 static final int TYPE_KEY_VALUE = 0;
679 static final int TYPE_FILE = 1;
680 static final int TYPE_FETCHED = 2;
686 public CacheKey set(int type, String key, int userId) {
689 this.userId = userId;
694 public boolean equals(Object obj) {
695 if (!(obj instanceof CacheKey))
697 CacheKey o = (CacheKey) obj;
698 return userId == o.userId && type == o.type && key.equals(o.key);
702 public int hashCode() {
703 return key.hashCode() ^ userId ^ type;