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