Home | History | Annotate | Download | only in server
      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;
     18 
     19 import com.android.internal.annotations.VisibleForTesting;
     20 
     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;
     32 
     33 import java.io.File;
     34 import java.io.IOException;
     35 import java.io.RandomAccessFile;
     36 
     37 import static android.content.Context.USER_SERVICE;
     38 
     39 /**
     40  * Storage for the lock settings service.
     41  */
     42 class LockSettingsStorage {
     43 
     44     private static final String TAG = "LockSettingsStorage";
     45     private static final String TABLE = "locksettings";
     46 
     47     private static final String COLUMN_KEY = "name";
     48     private static final String COLUMN_USERID = "user";
     49     private static final String COLUMN_VALUE = "value";
     50 
     51     private static final String[] COLUMNS_FOR_QUERY = {
     52             COLUMN_VALUE
     53     };
     54     private static final String[] COLUMNS_FOR_PREFETCH = {
     55             COLUMN_KEY, COLUMN_VALUE
     56     };
     57 
     58     private static final String SYSTEM_DIRECTORY = "/system/";
     59     private static final String LOCK_PATTERN_FILE = "gatekeeper.pattern.key";
     60     private static final String BASE_ZERO_LOCK_PATTERN_FILE = "gatekeeper.gesture.key";
     61     private static final String LEGACY_LOCK_PATTERN_FILE = "gesture.key";
     62     private static final String LOCK_PASSWORD_FILE = "gatekeeper.password.key";
     63     private static final String LEGACY_LOCK_PASSWORD_FILE = "password.key";
     64 
     65     private static final Object DEFAULT = new Object();
     66 
     67     private final DatabaseHelper mOpenHelper;
     68     private final Context mContext;
     69     private final Cache mCache = new Cache();
     70     private final Object mFileWriteLock = new Object();
     71 
     72     private int mStoredCredentialType;
     73 
     74     class CredentialHash {
     75         static final int TYPE_NONE = -1;
     76         static final int TYPE_PATTERN = 1;
     77         static final int TYPE_PASSWORD = 2;
     78 
     79         static final int VERSION_LEGACY = 0;
     80         static final int VERSION_GATEKEEPER = 1;
     81 
     82         CredentialHash(byte[] hash, int version) {
     83             this.hash = hash;
     84             this.version = version;
     85             this.isBaseZeroPattern = false;
     86         }
     87 
     88         CredentialHash(byte[] hash, boolean isBaseZeroPattern) {
     89             this.hash = hash;
     90             this.version = VERSION_GATEKEEPER;
     91             this.isBaseZeroPattern = isBaseZeroPattern;
     92         }
     93 
     94         byte[] hash;
     95         int version;
     96         boolean isBaseZeroPattern;
     97     }
     98 
     99     public LockSettingsStorage(Context context, Callback callback) {
    100         mContext = context;
    101         mOpenHelper = new DatabaseHelper(context, callback);
    102     }
    103 
    104     public void writeKeyValue(String key, String value, int userId) {
    105         writeKeyValue(mOpenHelper.getWritableDatabase(), key, value, userId);
    106     }
    107 
    108     public void writeKeyValue(SQLiteDatabase db, String key, String value, int userId) {
    109         ContentValues cv = new ContentValues();
    110         cv.put(COLUMN_KEY, key);
    111         cv.put(COLUMN_USERID, userId);
    112         cv.put(COLUMN_VALUE, value);
    113 
    114         db.beginTransaction();
    115         try {
    116             db.delete(TABLE, COLUMN_KEY + "=? AND " + COLUMN_USERID + "=?",
    117                     new String[] {key, Integer.toString(userId)});
    118             db.insert(TABLE, null, cv);
    119             db.setTransactionSuccessful();
    120             mCache.putKeyValue(key, value, userId);
    121         } finally {
    122             db.endTransaction();
    123         }
    124 
    125     }
    126 
    127     public String readKeyValue(String key, String defaultValue, int userId) {
    128         int version;
    129         synchronized (mCache) {
    130             if (mCache.hasKeyValue(key, userId)) {
    131                 return mCache.peekKeyValue(key, defaultValue, userId);
    132             }
    133             version = mCache.getVersion();
    134         }
    135 
    136         Cursor cursor;
    137         Object result = DEFAULT;
    138         SQLiteDatabase db = mOpenHelper.getReadableDatabase();
    139         if ((cursor = db.query(TABLE, COLUMNS_FOR_QUERY,
    140                 COLUMN_USERID + "=? AND " + COLUMN_KEY + "=?",
    141                 new String[] { Integer.toString(userId), key },
    142                 null, null, null)) != null) {
    143             if (cursor.moveToFirst()) {
    144                 result = cursor.getString(0);
    145             }
    146             cursor.close();
    147         }
    148         mCache.putKeyValueIfUnchanged(key, result, userId, version);
    149         return result == DEFAULT ? defaultValue : (String) result;
    150     }
    151 
    152     public void prefetchUser(int userId) {
    153         int version;
    154         synchronized (mCache) {
    155             if (mCache.isFetched(userId)) {
    156                 return;
    157             }
    158             mCache.setFetched(userId);
    159             version = mCache.getVersion();
    160         }
    161 
    162         Cursor cursor;
    163         SQLiteDatabase db = mOpenHelper.getReadableDatabase();
    164         if ((cursor = db.query(TABLE, COLUMNS_FOR_PREFETCH,
    165                 COLUMN_USERID + "=?",
    166                 new String[] { Integer.toString(userId) },
    167                 null, null, null)) != null) {
    168             while (cursor.moveToNext()) {
    169                 String key = cursor.getString(0);
    170                 String value = cursor.getString(1);
    171                 mCache.putKeyValueIfUnchanged(key, value, userId, version);
    172             }
    173             cursor.close();
    174         }
    175 
    176         // Populate cache by reading the password and pattern files.
    177         readPasswordHash(userId);
    178         readPatternHash(userId);
    179     }
    180 
    181     public int getStoredCredentialType(int userId) {
    182         if (mStoredCredentialType != 0) {
    183             return mStoredCredentialType;
    184         }
    185 
    186         CredentialHash pattern = readPatternHash(userId);
    187         if (pattern == null) {
    188             if (readPasswordHash(userId) != null) {
    189                 mStoredCredentialType = CredentialHash.TYPE_PASSWORD;
    190             } else {
    191                 mStoredCredentialType = CredentialHash.TYPE_NONE;
    192             }
    193         } else {
    194             CredentialHash password = readPasswordHash(userId);
    195             if (password != null) {
    196                 // Both will never be GateKeeper
    197                 if (password.version == CredentialHash.VERSION_GATEKEEPER) {
    198                     mStoredCredentialType = CredentialHash.TYPE_PASSWORD;
    199                 } else {
    200                     mStoredCredentialType = CredentialHash.TYPE_PATTERN;
    201                 }
    202             } else {
    203                 mStoredCredentialType = CredentialHash.TYPE_PATTERN;
    204             }
    205         }
    206 
    207         return mStoredCredentialType;
    208     }
    209 
    210 
    211     public CredentialHash readPasswordHash(int userId) {
    212         byte[] stored = readFile(getLockPasswordFilename(userId));
    213         if (stored != null && stored.length > 0) {
    214             return new CredentialHash(stored, CredentialHash.VERSION_GATEKEEPER);
    215         }
    216 
    217         stored = readFile(getLegacyLockPasswordFilename(userId));
    218         if (stored != null && stored.length > 0) {
    219             return new CredentialHash(stored, CredentialHash.VERSION_LEGACY);
    220         }
    221 
    222         return null;
    223     }
    224 
    225     public CredentialHash readPatternHash(int userId) {
    226         byte[] stored = readFile(getLockPatternFilename(userId));
    227         if (stored != null && stored.length > 0) {
    228             return new CredentialHash(stored, CredentialHash.VERSION_GATEKEEPER);
    229         }
    230 
    231         stored = readFile(getBaseZeroLockPatternFilename(userId));
    232         if (stored != null && stored.length > 0) {
    233             return new CredentialHash(stored, true);
    234         }
    235 
    236         stored = readFile(getLegacyLockPatternFilename(userId));
    237         if (stored != null && stored.length > 0) {
    238             return new CredentialHash(stored, CredentialHash.VERSION_LEGACY);
    239         }
    240 
    241         return null;
    242     }
    243 
    244 
    245     public boolean hasPassword(int userId) {
    246         return hasFile(getLockPasswordFilename(userId)) ||
    247             hasFile(getLegacyLockPasswordFilename(userId));
    248     }
    249 
    250     public boolean hasPattern(int userId) {
    251         return hasFile(getLockPatternFilename(userId)) ||
    252             hasFile(getBaseZeroLockPatternFilename(userId)) ||
    253             hasFile(getLegacyLockPatternFilename(userId));
    254     }
    255 
    256     private boolean hasFile(String name) {
    257         byte[] contents = readFile(name);
    258         return contents != null && contents.length > 0;
    259     }
    260 
    261     private byte[] readFile(String name) {
    262         int version;
    263         synchronized (mCache) {
    264             if (mCache.hasFile(name)) {
    265                 return mCache.peekFile(name);
    266             }
    267             version = mCache.getVersion();
    268         }
    269 
    270         RandomAccessFile raf = null;
    271         byte[] stored = null;
    272         try {
    273             raf = new RandomAccessFile(name, "r");
    274             stored = new byte[(int) raf.length()];
    275             raf.readFully(stored, 0, stored.length);
    276             raf.close();
    277         } catch (IOException e) {
    278             Slog.e(TAG, "Cannot read file " + e);
    279         } finally {
    280             if (raf != null) {
    281                 try {
    282                     raf.close();
    283                 } catch (IOException e) {
    284                     Slog.e(TAG, "Error closing file " + e);
    285                 }
    286             }
    287         }
    288         mCache.putFileIfUnchanged(name, stored, version);
    289         return stored;
    290     }
    291 
    292     private void writeFile(String name, byte[] hash) {
    293         synchronized (mFileWriteLock) {
    294             RandomAccessFile raf = null;
    295             try {
    296                 // Write the hash to file
    297                 raf = new RandomAccessFile(name, "rw");
    298                 // Truncate the file if pattern is null, to clear the lock
    299                 if (hash == null || hash.length == 0) {
    300                     raf.setLength(0);
    301                 } else {
    302                     raf.write(hash, 0, hash.length);
    303                 }
    304                 raf.close();
    305             } catch (IOException e) {
    306                 Slog.e(TAG, "Error writing to file " + e);
    307             } finally {
    308                 if (raf != null) {
    309                     try {
    310                         raf.close();
    311                     } catch (IOException e) {
    312                         Slog.e(TAG, "Error closing file " + e);
    313                     }
    314                 }
    315             }
    316             mCache.putFile(name, hash);
    317         }
    318     }
    319 
    320     private void deleteFile(String name) {
    321         File f = new File(name);
    322         if (f != null) {
    323             f.delete();
    324         }
    325     }
    326 
    327     public void writePatternHash(byte[] hash, int userId) {
    328         mStoredCredentialType = hash == null
    329             ? CredentialHash.TYPE_NONE
    330             : CredentialHash.TYPE_PATTERN;
    331         writeFile(getLockPatternFilename(userId), hash);
    332         clearPasswordHash(userId);
    333     }
    334 
    335     private void clearPatternHash(int userId) {
    336         writeFile(getLockPatternFilename(userId), null);
    337     }
    338 
    339     public void writePasswordHash(byte[] hash, int userId) {
    340         mStoredCredentialType = hash == null
    341             ? CredentialHash.TYPE_NONE
    342             : CredentialHash.TYPE_PASSWORD;
    343         writeFile(getLockPasswordFilename(userId), hash);
    344         clearPatternHash(userId);
    345     }
    346 
    347     private void clearPasswordHash(int userId) {
    348         writeFile(getLockPasswordFilename(userId), null);
    349     }
    350 
    351     @VisibleForTesting
    352     String getLockPatternFilename(int userId) {
    353         return getLockCredentialFilePathForUser(userId, LOCK_PATTERN_FILE);
    354     }
    355 
    356     @VisibleForTesting
    357     String getLockPasswordFilename(int userId) {
    358         return getLockCredentialFilePathForUser(userId, LOCK_PASSWORD_FILE);
    359     }
    360 
    361     @VisibleForTesting
    362     String getLegacyLockPatternFilename(int userId) {
    363         return getLockCredentialFilePathForUser(userId, LEGACY_LOCK_PATTERN_FILE);
    364     }
    365 
    366     @VisibleForTesting
    367     String getLegacyLockPasswordFilename(int userId) {
    368         return getLockCredentialFilePathForUser(userId, LEGACY_LOCK_PASSWORD_FILE);
    369     }
    370 
    371     private String getBaseZeroLockPatternFilename(int userId) {
    372         return getLockCredentialFilePathForUser(userId, BASE_ZERO_LOCK_PATTERN_FILE);
    373     }
    374 
    375     private String getLockCredentialFilePathForUser(int userId, String basename) {
    376         userId = getUserParentOrSelfId(userId);
    377         String dataSystemDirectory =
    378                 android.os.Environment.getDataDirectory().getAbsolutePath() +
    379                         SYSTEM_DIRECTORY;
    380         if (userId == 0) {
    381             // Leave it in the same place for user 0
    382             return dataSystemDirectory + basename;
    383         } else {
    384             return new File(Environment.getUserSystemDirectory(userId), basename).getAbsolutePath();
    385         }
    386     }
    387 
    388     private int getUserParentOrSelfId(int userId) {
    389         if (userId != 0) {
    390             final UserManager um = (UserManager) mContext.getSystemService(USER_SERVICE);
    391             final UserInfo pi = um.getProfileParent(userId);
    392             if (pi != null) {
    393                 return pi.id;
    394             }
    395         }
    396         return userId;
    397     }
    398 
    399     public void removeUser(int userId) {
    400         SQLiteDatabase db = mOpenHelper.getWritableDatabase();
    401 
    402         final UserManager um = (UserManager) mContext.getSystemService(USER_SERVICE);
    403         final UserInfo parentInfo = um.getProfileParent(userId);
    404 
    405         if (parentInfo == null) {
    406             // This user owns its lock settings files - safe to delete them
    407             synchronized (mFileWriteLock) {
    408                 String name = getLockPasswordFilename(userId);
    409                 File file = new File(name);
    410                 if (file.exists()) {
    411                     file.delete();
    412                     mCache.putFile(name, null);
    413                 }
    414                 name = getLockPatternFilename(userId);
    415                 file = new File(name);
    416                 if (file.exists()) {
    417                     file.delete();
    418                     mCache.putFile(name, null);
    419                 }
    420             }
    421         }
    422 
    423         try {
    424             db.beginTransaction();
    425             db.delete(TABLE, COLUMN_USERID + "='" + userId + "'", null);
    426             db.setTransactionSuccessful();
    427             mCache.removeUser(userId);
    428         } finally {
    429             db.endTransaction();
    430         }
    431     }
    432 
    433     @VisibleForTesting
    434     void closeDatabase() {
    435         mOpenHelper.close();
    436     }
    437 
    438     @VisibleForTesting
    439     void clearCache() {
    440         mCache.clear();
    441     }
    442 
    443     public interface Callback {
    444         void initialize(SQLiteDatabase db);
    445     }
    446 
    447     class DatabaseHelper extends SQLiteOpenHelper {
    448         private static final String TAG = "LockSettingsDB";
    449         private static final String DATABASE_NAME = "locksettings.db";
    450 
    451         private static final int DATABASE_VERSION = 2;
    452 
    453         private final Callback mCallback;
    454 
    455         public DatabaseHelper(Context context, Callback callback) {
    456             super(context, DATABASE_NAME, null, DATABASE_VERSION);
    457             setWriteAheadLoggingEnabled(true);
    458             mCallback = callback;
    459         }
    460 
    461         private void createTable(SQLiteDatabase db) {
    462             db.execSQL("CREATE TABLE " + TABLE + " (" +
    463                     "_id INTEGER PRIMARY KEY AUTOINCREMENT," +
    464                     COLUMN_KEY + " TEXT," +
    465                     COLUMN_USERID + " INTEGER," +
    466                     COLUMN_VALUE + " TEXT" +
    467                     ");");
    468         }
    469 
    470         @Override
    471         public void onCreate(SQLiteDatabase db) {
    472             createTable(db);
    473             mCallback.initialize(db);
    474         }
    475 
    476         @Override
    477         public void onUpgrade(SQLiteDatabase db, int oldVersion, int currentVersion) {
    478             int upgradeVersion = oldVersion;
    479             if (upgradeVersion == 1) {
    480                 // Previously migrated lock screen widget settings. Now defunct.
    481                 upgradeVersion = 2;
    482             }
    483 
    484             if (upgradeVersion != DATABASE_VERSION) {
    485                 Log.w(TAG, "Failed to upgrade database!");
    486             }
    487         }
    488     }
    489 
    490     /**
    491      * Cache consistency model:
    492      * - Writes to storage write directly to the cache, but this MUST happen within the atomic
    493      *   section either provided by the database transaction or mWriteLock, such that writes to the
    494      *   cache and writes to the backing storage are guaranteed to occur in the same order
    495      *
    496      * - Reads can populate the cache, but because they are no strong ordering guarantees with
    497      *   respect to writes this precaution is taken:
    498      *   - The cache is assigned a version number that increases every time the cache is modified.
    499      *     Reads from backing storage can only populate the cache if the backing storage
    500      *     has not changed since the load operation has begun.
    501      *     This guarantees that no read operation can shadow a write to the cache that happens
    502      *     after it had begun.
    503      */
    504     private static class Cache {
    505         private final ArrayMap<CacheKey, Object> mCache = new ArrayMap<>();
    506         private final CacheKey mCacheKey = new CacheKey();
    507         private int mVersion = 0;
    508 
    509         String peekKeyValue(String key, String defaultValue, int userId) {
    510             Object cached = peek(CacheKey.TYPE_KEY_VALUE, key, userId);
    511             return cached == DEFAULT ? defaultValue : (String) cached;
    512         }
    513 
    514         boolean hasKeyValue(String key, int userId) {
    515             return contains(CacheKey.TYPE_KEY_VALUE, key, userId);
    516         }
    517 
    518         void putKeyValue(String key, String value, int userId) {
    519             put(CacheKey.TYPE_KEY_VALUE, key, value, userId);
    520         }
    521 
    522         void putKeyValueIfUnchanged(String key, Object value, int userId, int version) {
    523             putIfUnchanged(CacheKey.TYPE_KEY_VALUE, key, value, userId, version);
    524         }
    525 
    526         byte[] peekFile(String fileName) {
    527             return (byte[]) peek(CacheKey.TYPE_FILE, fileName, -1 /* userId */);
    528         }
    529 
    530         boolean hasFile(String fileName) {
    531             return contains(CacheKey.TYPE_FILE, fileName, -1 /* userId */);
    532         }
    533 
    534         void putFile(String key, byte[] value) {
    535             put(CacheKey.TYPE_FILE, key, value, -1 /* userId */);
    536         }
    537 
    538         void putFileIfUnchanged(String key, byte[] value, int version) {
    539             putIfUnchanged(CacheKey.TYPE_FILE, key, value, -1 /* userId */, version);
    540         }
    541 
    542         void setFetched(int userId) {
    543             put(CacheKey.TYPE_FETCHED, "isFetched", "true", userId);
    544         }
    545 
    546         boolean isFetched(int userId) {
    547             return contains(CacheKey.TYPE_FETCHED, "", userId);
    548         }
    549 
    550 
    551         private synchronized void put(int type, String key, Object value, int userId) {
    552             // Create a new CachKey here because it may be saved in the map if the key is absent.
    553             mCache.put(new CacheKey().set(type, key, userId), value);
    554             mVersion++;
    555         }
    556 
    557         private synchronized void putIfUnchanged(int type, String key, Object value, int userId,
    558                 int version) {
    559             if (!contains(type, key, userId) && mVersion == version) {
    560                 put(type, key, value, userId);
    561             }
    562         }
    563 
    564         private synchronized boolean contains(int type, String key, int userId) {
    565             return mCache.containsKey(mCacheKey.set(type, key, userId));
    566         }
    567 
    568         private synchronized Object peek(int type, String key, int userId) {
    569             return mCache.get(mCacheKey.set(type, key, userId));
    570         }
    571 
    572         private synchronized int getVersion() {
    573             return mVersion;
    574         }
    575 
    576         synchronized void removeUser(int userId) {
    577             for (int i = mCache.size() - 1; i >= 0; i--) {
    578                 if (mCache.keyAt(i).userId == userId) {
    579                     mCache.removeAt(i);
    580                 }
    581             }
    582 
    583             // Make sure in-flight loads can't write to cache.
    584             mVersion++;
    585         }
    586 
    587         synchronized void clear() {
    588             mCache.clear();
    589             mVersion++;
    590         }
    591 
    592         private static final class CacheKey {
    593             static final int TYPE_KEY_VALUE = 0;
    594             static final int TYPE_FILE = 1;
    595             static final int TYPE_FETCHED = 2;
    596 
    597             String key;
    598             int userId;
    599             int type;
    600 
    601             public CacheKey set(int type, String key, int userId) {
    602                 this.type = type;
    603                 this.key = key;
    604                 this.userId = userId;
    605                 return this;
    606             }
    607 
    608             @Override
    609             public boolean equals(Object obj) {
    610                 if (!(obj instanceof CacheKey))
    611                     return false;
    612                 CacheKey o = (CacheKey) obj;
    613                 return userId == o.userId && type == o.type && key.equals(o.key);
    614             }
    615 
    616             @Override
    617             public int hashCode() {
    618                 return key.hashCode() ^ userId ^ type;
    619             }
    620         }
    621     }
    622 }
    623