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