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