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