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