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