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