/* * Copyright (C) 2014 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License */ package com.android.server; import static android.content.Context.USER_SERVICE; import android.content.ContentValues; import android.content.Context; import android.content.pm.UserInfo; import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteOpenHelper; import android.os.Environment; import android.os.UserManager; import android.os.storage.StorageManager; import android.util.ArrayMap; import android.util.Log; import android.util.Slog; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.util.ArrayUtils; import com.android.internal.widget.LockPatternUtils; import java.io.File; import java.io.IOException; import java.io.RandomAccessFile; /** * Storage for the lock settings service. */ class LockSettingsStorage { private static final String TAG = "LockSettingsStorage"; private static final String TABLE = "locksettings"; private static final boolean DEBUG = false; private static final String COLUMN_KEY = "name"; private static final String COLUMN_USERID = "user"; private static final String COLUMN_VALUE = "value"; private static final String[] COLUMNS_FOR_QUERY = { COLUMN_VALUE }; private static final String[] COLUMNS_FOR_PREFETCH = { COLUMN_KEY, COLUMN_VALUE }; private static final String SYSTEM_DIRECTORY = "/system/"; private static final String LOCK_PATTERN_FILE = "gatekeeper.pattern.key"; private static final String BASE_ZERO_LOCK_PATTERN_FILE = "gatekeeper.gesture.key"; private static final String LEGACY_LOCK_PATTERN_FILE = "gesture.key"; private static final String LOCK_PASSWORD_FILE = "gatekeeper.password.key"; private static final String LEGACY_LOCK_PASSWORD_FILE = "password.key"; private static final String CHILD_PROFILE_LOCK_FILE = "gatekeeper.profile.key"; private static final String SYNTHETIC_PASSWORD_DIRECTORY = "spblob/"; private static final Object DEFAULT = new Object(); private final DatabaseHelper mOpenHelper; private final Context mContext; private final Cache mCache = new Cache(); private final Object mFileWriteLock = new Object(); @VisibleForTesting public static class CredentialHash { static final int VERSION_LEGACY = 0; static final int VERSION_GATEKEEPER = 1; private CredentialHash(byte[] hash, int type, int version) { if (type != LockPatternUtils.CREDENTIAL_TYPE_NONE) { if (hash == null) { throw new RuntimeException("Empty hash for CredentialHash"); } } else /* type == LockPatternUtils.CREDENTIAL_TYPE_NONE */ { if (hash != null) { throw new RuntimeException("None type CredentialHash should not have hash"); } } this.hash = hash; this.type = type; this.version = version; this.isBaseZeroPattern = false; } private CredentialHash(byte[] hash, boolean isBaseZeroPattern) { this.hash = hash; this.type = LockPatternUtils.CREDENTIAL_TYPE_PATTERN; this.version = VERSION_GATEKEEPER; this.isBaseZeroPattern = isBaseZeroPattern; } static CredentialHash create(byte[] hash, int type) { if (type == LockPatternUtils.CREDENTIAL_TYPE_NONE) { throw new RuntimeException("Bad type for CredentialHash"); } return new CredentialHash(hash, type, VERSION_GATEKEEPER); } static CredentialHash createEmptyHash() { return new CredentialHash(null, LockPatternUtils.CREDENTIAL_TYPE_NONE, VERSION_GATEKEEPER); } byte[] hash; int type; int version; boolean isBaseZeroPattern; } public LockSettingsStorage(Context context) { mContext = context; mOpenHelper = new DatabaseHelper(context); } public void setDatabaseOnCreateCallback(Callback callback) { mOpenHelper.setCallback(callback); } public void writeKeyValue(String key, String value, int userId) { writeKeyValue(mOpenHelper.getWritableDatabase(), key, value, userId); } public void writeKeyValue(SQLiteDatabase db, String key, String value, int userId) { ContentValues cv = new ContentValues(); cv.put(COLUMN_KEY, key); cv.put(COLUMN_USERID, userId); cv.put(COLUMN_VALUE, value); db.beginTransaction(); try { db.delete(TABLE, COLUMN_KEY + "=? AND " + COLUMN_USERID + "=?", new String[] {key, Integer.toString(userId)}); db.insert(TABLE, null, cv); db.setTransactionSuccessful(); mCache.putKeyValue(key, value, userId); } finally { db.endTransaction(); } } public String readKeyValue(String key, String defaultValue, int userId) { int version; synchronized (mCache) { if (mCache.hasKeyValue(key, userId)) { return mCache.peekKeyValue(key, defaultValue, userId); } version = mCache.getVersion(); } Cursor cursor; Object result = DEFAULT; SQLiteDatabase db = mOpenHelper.getReadableDatabase(); if ((cursor = db.query(TABLE, COLUMNS_FOR_QUERY, COLUMN_USERID + "=? AND " + COLUMN_KEY + "=?", new String[] { Integer.toString(userId), key }, null, null, null)) != null) { if (cursor.moveToFirst()) { result = cursor.getString(0); } cursor.close(); } mCache.putKeyValueIfUnchanged(key, result, userId, version); return result == DEFAULT ? defaultValue : (String) result; } public void prefetchUser(int userId) { int version; synchronized (mCache) { if (mCache.isFetched(userId)) { return; } mCache.setFetched(userId); version = mCache.getVersion(); } Cursor cursor; SQLiteDatabase db = mOpenHelper.getReadableDatabase(); if ((cursor = db.query(TABLE, COLUMNS_FOR_PREFETCH, COLUMN_USERID + "=?", new String[] { Integer.toString(userId) }, null, null, null)) != null) { while (cursor.moveToNext()) { String key = cursor.getString(0); String value = cursor.getString(1); mCache.putKeyValueIfUnchanged(key, value, userId, version); } cursor.close(); } // Populate cache by reading the password and pattern files. readCredentialHash(userId); } private CredentialHash readPasswordHashIfExists(int userId) { byte[] stored = readFile(getLockPasswordFilename(userId)); if (!ArrayUtils.isEmpty(stored)) { return new CredentialHash(stored, LockPatternUtils.CREDENTIAL_TYPE_PASSWORD, CredentialHash.VERSION_GATEKEEPER); } stored = readFile(getLegacyLockPasswordFilename(userId)); if (!ArrayUtils.isEmpty(stored)) { return new CredentialHash(stored, LockPatternUtils.CREDENTIAL_TYPE_PASSWORD, CredentialHash.VERSION_LEGACY); } return null; } private CredentialHash readPatternHashIfExists(int userId) { byte[] stored = readFile(getLockPatternFilename(userId)); if (!ArrayUtils.isEmpty(stored)) { return new CredentialHash(stored, LockPatternUtils.CREDENTIAL_TYPE_PATTERN, CredentialHash.VERSION_GATEKEEPER); } stored = readFile(getBaseZeroLockPatternFilename(userId)); if (!ArrayUtils.isEmpty(stored)) { return new CredentialHash(stored, true); } stored = readFile(getLegacyLockPatternFilename(userId)); if (!ArrayUtils.isEmpty(stored)) { return new CredentialHash(stored, LockPatternUtils.CREDENTIAL_TYPE_PATTERN, CredentialHash.VERSION_LEGACY); } return null; } public CredentialHash readCredentialHash(int userId) { CredentialHash passwordHash = readPasswordHashIfExists(userId); CredentialHash patternHash = readPatternHashIfExists(userId); if (passwordHash != null && patternHash != null) { if (passwordHash.version == CredentialHash.VERSION_GATEKEEPER) { return passwordHash; } else { return patternHash; } } else if (passwordHash != null) { return passwordHash; } else if (patternHash != null) { return patternHash; } else { return CredentialHash.createEmptyHash(); } } public void removeChildProfileLock(int userId) { if (DEBUG) Slog.e(TAG, "Remove child profile lock for user: " + userId); try { deleteFile(getChildProfileLockFile(userId)); } catch (Exception e) { e.printStackTrace(); } } public void writeChildProfileLock(int userId, byte[] lock) { writeFile(getChildProfileLockFile(userId), lock); } public byte[] readChildProfileLock(int userId) { return readFile(getChildProfileLockFile(userId)); } public boolean hasChildProfileLock(int userId) { return hasFile(getChildProfileLockFile(userId)); } public boolean hasPassword(int userId) { return hasFile(getLockPasswordFilename(userId)) || hasFile(getLegacyLockPasswordFilename(userId)); } public boolean hasPattern(int userId) { return hasFile(getLockPatternFilename(userId)) || hasFile(getBaseZeroLockPatternFilename(userId)) || hasFile(getLegacyLockPatternFilename(userId)); } public boolean hasCredential(int userId) { return hasPassword(userId) || hasPattern(userId); } private boolean hasFile(String name) { byte[] contents = readFile(name); return contents != null && contents.length > 0; } private byte[] readFile(String name) { int version; synchronized (mCache) { if (mCache.hasFile(name)) { return mCache.peekFile(name); } version = mCache.getVersion(); } RandomAccessFile raf = null; byte[] stored = null; try { raf = new RandomAccessFile(name, "r"); stored = new byte[(int) raf.length()]; raf.readFully(stored, 0, stored.length); raf.close(); } catch (IOException e) { Slog.e(TAG, "Cannot read file " + e); } finally { if (raf != null) { try { raf.close(); } catch (IOException e) { Slog.e(TAG, "Error closing file " + e); } } } mCache.putFileIfUnchanged(name, stored, version); return stored; } private void writeFile(String name, byte[] hash) { synchronized (mFileWriteLock) { RandomAccessFile raf = null; try { // Write the hash to file, requiring each write to be synchronized to the // underlying storage device immediately to avoid data loss in case of power loss. // This also ensures future secdiscard operation on the file succeeds since the // file would have been allocated on flash. raf = new RandomAccessFile(name, "rws"); // Truncate the file if pattern is null, to clear the lock if (hash == null || hash.length == 0) { raf.setLength(0); } else { raf.write(hash, 0, hash.length); } raf.close(); } catch (IOException e) { Slog.e(TAG, "Error writing to file " + e); } finally { if (raf != null) { try { raf.close(); } catch (IOException e) { Slog.e(TAG, "Error closing file " + e); } } } mCache.putFile(name, hash); } } private void deleteFile(String name) { if (DEBUG) Slog.e(TAG, "Delete file " + name); synchronized (mFileWriteLock) { File file = new File(name); if (file.exists()) { file.delete(); mCache.putFile(name, null); } } } public void writeCredentialHash(CredentialHash hash, int userId) { byte[] patternHash = null; byte[] passwordHash = null; if (hash.type == LockPatternUtils.CREDENTIAL_TYPE_PASSWORD) { passwordHash = hash.hash; } else if (hash.type == LockPatternUtils.CREDENTIAL_TYPE_PATTERN) { patternHash = hash.hash; } writeFile(getLockPasswordFilename(userId), passwordHash); writeFile(getLockPatternFilename(userId), patternHash); } @VisibleForTesting String getLockPatternFilename(int userId) { return getLockCredentialFilePathForUser(userId, LOCK_PATTERN_FILE); } @VisibleForTesting String getLockPasswordFilename(int userId) { return getLockCredentialFilePathForUser(userId, LOCK_PASSWORD_FILE); } @VisibleForTesting String getLegacyLockPatternFilename(int userId) { return getLockCredentialFilePathForUser(userId, LEGACY_LOCK_PATTERN_FILE); } @VisibleForTesting String getLegacyLockPasswordFilename(int userId) { return getLockCredentialFilePathForUser(userId, LEGACY_LOCK_PASSWORD_FILE); } private String getBaseZeroLockPatternFilename(int userId) { return getLockCredentialFilePathForUser(userId, BASE_ZERO_LOCK_PATTERN_FILE); } @VisibleForTesting String getChildProfileLockFile(int userId) { return getLockCredentialFilePathForUser(userId, CHILD_PROFILE_LOCK_FILE); } private String getLockCredentialFilePathForUser(int userId, String basename) { String dataSystemDirectory = Environment.getDataDirectory().getAbsolutePath() + SYSTEM_DIRECTORY; if (userId == 0) { // Leave it in the same place for user 0 return dataSystemDirectory + basename; } else { return new File(Environment.getUserSystemDirectory(userId), basename).getAbsolutePath(); } } public void writeSyntheticPasswordState(int userId, long handle, String name, byte[] data) { writeFile(getSynthenticPasswordStateFilePathForUser(userId, handle, name), data); } public byte[] readSyntheticPasswordState(int userId, long handle, String name) { return readFile(getSynthenticPasswordStateFilePathForUser(userId, handle, name)); } public void deleteSyntheticPasswordState(int userId, long handle, String name) { String path = getSynthenticPasswordStateFilePathForUser(userId, handle, name); File file = new File(path); if (file.exists()) { try { mContext.getSystemService(StorageManager.class).secdiscard(file.getAbsolutePath()); } catch (Exception e) { Slog.w(TAG, "Failed to secdiscard " + path, e); } finally { file.delete(); } mCache.putFile(path, null); } } @VisibleForTesting protected File getSyntheticPasswordDirectoryForUser(int userId) { return new File(Environment.getDataSystemDeDirectory(userId) ,SYNTHETIC_PASSWORD_DIRECTORY); } @VisibleForTesting protected String getSynthenticPasswordStateFilePathForUser(int userId, long handle, String name) { File baseDir = getSyntheticPasswordDirectoryForUser(userId); String baseName = String.format("%016x.%s", handle, name); if (!baseDir.exists()) { baseDir.mkdir(); } return new File(baseDir, baseName).getAbsolutePath(); } public void removeUser(int userId) { SQLiteDatabase db = mOpenHelper.getWritableDatabase(); final UserManager um = (UserManager) mContext.getSystemService(USER_SERVICE); final UserInfo parentInfo = um.getProfileParent(userId); if (parentInfo == null) { // This user owns its lock settings files - safe to delete them synchronized (mFileWriteLock) { String name = getLockPasswordFilename(userId); File file = new File(name); if (file.exists()) { file.delete(); mCache.putFile(name, null); } name = getLockPatternFilename(userId); file = new File(name); if (file.exists()) { file.delete(); mCache.putFile(name, null); } } } else { // Managed profile removeChildProfileLock(userId); } File spStateDir = getSyntheticPasswordDirectoryForUser(userId); try { db.beginTransaction(); db.delete(TABLE, COLUMN_USERID + "='" + userId + "'", null); db.setTransactionSuccessful(); mCache.removeUser(userId); // The directory itself will be deleted as part of user deletion operation by the // framework, so only need to purge cache here. //TODO: (b/34600579) invoke secdiscardable mCache.purgePath(spStateDir.getAbsolutePath()); } finally { db.endTransaction(); } } @VisibleForTesting void closeDatabase() { mOpenHelper.close(); } @VisibleForTesting void clearCache() { mCache.clear(); } public interface Callback { void initialize(SQLiteDatabase db); } class DatabaseHelper extends SQLiteOpenHelper { private static final String TAG = "LockSettingsDB"; private static final String DATABASE_NAME = "locksettings.db"; private static final int DATABASE_VERSION = 2; private Callback mCallback; public DatabaseHelper(Context context) { super(context, DATABASE_NAME, null, DATABASE_VERSION); setWriteAheadLoggingEnabled(true); } public void setCallback(Callback callback) { mCallback = callback; } private void createTable(SQLiteDatabase db) { db.execSQL("CREATE TABLE " + TABLE + " (" + "_id INTEGER PRIMARY KEY AUTOINCREMENT," + COLUMN_KEY + " TEXT," + COLUMN_USERID + " INTEGER," + COLUMN_VALUE + " TEXT" + ");"); } @Override public void onCreate(SQLiteDatabase db) { createTable(db); if (mCallback != null) { mCallback.initialize(db); } } @Override public void onUpgrade(SQLiteDatabase db, int oldVersion, int currentVersion) { int upgradeVersion = oldVersion; if (upgradeVersion == 1) { // Previously migrated lock screen widget settings. Now defunct. upgradeVersion = 2; } if (upgradeVersion != DATABASE_VERSION) { Log.w(TAG, "Failed to upgrade database!"); } } } /** * Cache consistency model: * - Writes to storage write directly to the cache, but this MUST happen within the atomic * section either provided by the database transaction or mWriteLock, such that writes to the * cache and writes to the backing storage are guaranteed to occur in the same order * * - Reads can populate the cache, but because they are no strong ordering guarantees with * respect to writes this precaution is taken: * - The cache is assigned a version number that increases every time the cache is modified. * Reads from backing storage can only populate the cache if the backing storage * has not changed since the load operation has begun. * This guarantees that no read operation can shadow a write to the cache that happens * after it had begun. */ private static class Cache { private final ArrayMap mCache = new ArrayMap<>(); private final CacheKey mCacheKey = new CacheKey(); private int mVersion = 0; String peekKeyValue(String key, String defaultValue, int userId) { Object cached = peek(CacheKey.TYPE_KEY_VALUE, key, userId); return cached == DEFAULT ? defaultValue : (String) cached; } boolean hasKeyValue(String key, int userId) { return contains(CacheKey.TYPE_KEY_VALUE, key, userId); } void putKeyValue(String key, String value, int userId) { put(CacheKey.TYPE_KEY_VALUE, key, value, userId); } void putKeyValueIfUnchanged(String key, Object value, int userId, int version) { putIfUnchanged(CacheKey.TYPE_KEY_VALUE, key, value, userId, version); } byte[] peekFile(String fileName) { return (byte[]) peek(CacheKey.TYPE_FILE, fileName, -1 /* userId */); } boolean hasFile(String fileName) { return contains(CacheKey.TYPE_FILE, fileName, -1 /* userId */); } void putFile(String key, byte[] value) { put(CacheKey.TYPE_FILE, key, value, -1 /* userId */); } void putFileIfUnchanged(String key, byte[] value, int version) { putIfUnchanged(CacheKey.TYPE_FILE, key, value, -1 /* userId */, version); } void setFetched(int userId) { put(CacheKey.TYPE_FETCHED, "isFetched", "true", userId); } boolean isFetched(int userId) { return contains(CacheKey.TYPE_FETCHED, "", userId); } private synchronized void put(int type, String key, Object value, int userId) { // Create a new CachKey here because it may be saved in the map if the key is absent. mCache.put(new CacheKey().set(type, key, userId), value); mVersion++; } private synchronized void putIfUnchanged(int type, String key, Object value, int userId, int version) { if (!contains(type, key, userId) && mVersion == version) { put(type, key, value, userId); } } private synchronized boolean contains(int type, String key, int userId) { return mCache.containsKey(mCacheKey.set(type, key, userId)); } private synchronized Object peek(int type, String key, int userId) { return mCache.get(mCacheKey.set(type, key, userId)); } private synchronized int getVersion() { return mVersion; } synchronized void removeUser(int userId) { for (int i = mCache.size() - 1; i >= 0; i--) { if (mCache.keyAt(i).userId == userId) { mCache.removeAt(i); } } // Make sure in-flight loads can't write to cache. mVersion++; } synchronized void purgePath(String path) { for (int i = mCache.size() - 1; i >= 0; i--) { CacheKey entry = mCache.keyAt(i); if (entry.type == CacheKey.TYPE_FILE && entry.key.startsWith(path)) { mCache.removeAt(i); } } mVersion++; } synchronized void clear() { mCache.clear(); mVersion++; } private static final class CacheKey { static final int TYPE_KEY_VALUE = 0; static final int TYPE_FILE = 1; static final int TYPE_FETCHED = 2; String key; int userId; int type; public CacheKey set(int type, String key, int userId) { this.type = type; this.key = key; this.userId = userId; return this; } @Override public boolean equals(Object obj) { if (!(obj instanceof CacheKey)) return false; CacheKey o = (CacheKey) obj; return userId == o.userId && type == o.type && key.equals(o.key); } @Override public int hashCode() { return key.hashCode() ^ userId ^ type; } } } }