blob: c03bb585a94f2c9f4a4f15454ad1bb2d723bef3a [file] [log] [blame]
Adrian Roos261d5ab2014-10-29 14:42:38 +01001/*
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
Adrian Roose5424992014-11-07 21:47:17 +010019import com.android.internal.annotations.VisibleForTesting;
20
Adrian Roos261d5ab2014-10-29 14:42:38 +010021import 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 };
Adrian Roos3dcae682014-10-29 14:43:56 +010054 private static final String[] COLUMNS_FOR_PREFETCH = {
55 COLUMN_KEY, COLUMN_VALUE
56 };
Adrian Roos261d5ab2014-10-29 14:42:38 +010057
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
Adrian Roos3dcae682014-10-29 14:43:56 +010062 private static final Object DEFAULT = new Object();
63
Adrian Roos261d5ab2014-10-29 14:42:38 +010064 private final DatabaseHelper mOpenHelper;
65 private final Context mContext;
Adrian Roos3dcae682014-10-29 14:43:56 +010066 private final Cache mCache = new Cache();
Adrian Roos261d5ab2014-10-29 14:42:38 +010067 private final Object mFileWriteLock = new Object();
68
Adrian Roos3dcae682014-10-29 14:43:56 +010069 public LockSettingsStorage(Context context, Callback callback) {
Adrian Roos261d5ab2014-10-29 14:42:38 +010070 mContext = context;
71 mOpenHelper = new DatabaseHelper(context, callback);
72 }
73
Adrian Roos3dcae682014-10-29 14:43:56 +010074 public void writeKeyValue(String key, String value, int userId) {
Adrian Roos261d5ab2014-10-29 14:42:38 +010075 writeKeyValue(mOpenHelper.getWritableDatabase(), key, value, userId);
76 }
77
Adrian Roos3dcae682014-10-29 14:43:56 +010078 public void writeKeyValue(SQLiteDatabase db, String key, String value, int userId) {
Adrian Roos261d5ab2014-10-29 14:42:38 +010079 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();
Adrian Roos3dcae682014-10-29 14:43:56 +010090 mCache.putKeyValue(key, value, userId);
Adrian Roos261d5ab2014-10-29 14:42:38 +010091 } finally {
92 db.endTransaction();
93 }
94
95 }
96
Adrian Roos3dcae682014-10-29 14:43:56 +010097 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
Adrian Roos261d5ab2014-10-29 14:42:38 +0100106 Cursor cursor;
Adrian Roos3dcae682014-10-29 14:43:56 +0100107 Object result = DEFAULT;
Adrian Roos261d5ab2014-10-29 14:42:38 +0100108 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 }
Adrian Roos3dcae682014-10-29 14:43:56 +0100118 mCache.putKeyValueIfUnchanged(key, result, userId, version);
119 return result == DEFAULT ? defaultValue : (String) result;
Adrian Roos261d5ab2014-10-29 14:42:38 +0100120 }
121
Adrian Roos3dcae682014-10-29 14:43:56 +0100122 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));
Adrian Roos261d5ab2014-10-29 14:42:38 +0100153 if (stored != null && stored.length > 0) {
154 return stored;
155 }
156 return null;
157 }
158
Adrian Roos3dcae682014-10-29 14:43:56 +0100159 public byte[] readPatternHash(int userId) {
160 final byte[] stored = readFile(getLockPatternFilename(userId));
Adrian Roos261d5ab2014-10-29 14:42:38 +0100161 if (stored != null && stored.length > 0) {
162 return stored;
163 }
164 return null;
165 }
166
Adrian Roos3dcae682014-10-29 14:43:56 +0100167 public boolean hasPassword(int userId) {
168 return hasFile(getLockPasswordFilename(userId));
Adrian Roos261d5ab2014-10-29 14:42:38 +0100169 }
170
Adrian Roos3dcae682014-10-29 14:43:56 +0100171 public boolean hasPattern(int userId) {
172 return hasFile(getLockPatternFilename(userId));
Adrian Roos261d5ab2014-10-29 14:42:38 +0100173 }
174
Adrian Roos3dcae682014-10-29 14:43:56 +0100175 private boolean hasFile(String name) {
176 byte[] contents = readFile(name);
Adrian Roos261d5ab2014-10-29 14:42:38 +0100177 return contents != null && contents.length > 0;
178 }
179
Adrian Roos3dcae682014-10-29 14:43:56 +0100180 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
Adrian Roos261d5ab2014-10-29 14:42:38 +0100189 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 }
Adrian Roos3dcae682014-10-29 14:43:56 +0100207 mCache.putFileIfUnchanged(name, stored, version);
Adrian Roos261d5ab2014-10-29 14:42:38 +0100208 return stored;
209 }
210
Adrian Roos3dcae682014-10-29 14:43:56 +0100211 private void writeFile(String name, byte[] hash) {
Adrian Roos261d5ab2014-10-29 14:42:38 +0100212 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 }
Adrian Roos3dcae682014-10-29 14:43:56 +0100235 mCache.putFile(name, hash);
Adrian Roos261d5ab2014-10-29 14:42:38 +0100236 }
237 }
238
239 public void writePatternHash(byte[] hash, int userId) {
Adrian Roos3dcae682014-10-29 14:43:56 +0100240 writeFile(getLockPatternFilename(userId), hash);
Adrian Roos261d5ab2014-10-29 14:42:38 +0100241 }
242
243 public void writePasswordHash(byte[] hash, int userId) {
Adrian Roos3dcae682014-10-29 14:43:56 +0100244 writeFile(getLockPasswordFilename(userId), hash);
Adrian Roos261d5ab2014-10-29 14:42:38 +0100245 }
246
247
Adrian Roose5424992014-11-07 21:47:17 +0100248 @VisibleForTesting
249 String getLockPatternFilename(int userId) {
Adrian Roos3dcae682014-10-29 14:43:56 +0100250 return getLockCredentialFilePathForUser(userId, LOCK_PATTERN_FILE);
Adrian Roos261d5ab2014-10-29 14:42:38 +0100251 }
252
Adrian Roose5424992014-11-07 21:47:17 +0100253 @VisibleForTesting
254 String getLockPasswordFilename(int userId) {
Adrian Roos3dcae682014-10-29 14:43:56 +0100255 return getLockCredentialFilePathForUser(userId, LOCK_PASSWORD_FILE);
256 }
257
258 private String getLockCredentialFilePathForUser(int userId, String basename) {
Adrian Roos261d5ab2014-10-29 14:42:38 +0100259 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
Adrian Roos3dcae682014-10-29 14:43:56 +0100265 return dataSystemDirectory + basename;
Adrian Roos261d5ab2014-10-29 14:42:38 +0100266 } else {
Adrian Roos3dcae682014-10-29 14:43:56 +0100267 return new File(Environment.getUserSystemDirectory(userId), basename).getAbsolutePath();
Adrian Roos261d5ab2014-10-29 14:42:38 +0100268 }
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
Adrian Roos3dcae682014-10-29 14:43:56 +0100292 String name = getLockPasswordFilename(userId);
293 File file = new File(name);
Adrian Roos261d5ab2014-10-29 14:42:38 +0100294 if (file.exists()) {
295 file.delete();
Adrian Roos3dcae682014-10-29 14:43:56 +0100296 mCache.putFile(name, null);
Adrian Roos261d5ab2014-10-29 14:42:38 +0100297 }
Adrian Roos3dcae682014-10-29 14:43:56 +0100298 name = getLockPatternFilename(userId);
299 file = new File(name);
Adrian Roos261d5ab2014-10-29 14:42:38 +0100300 if (file.exists()) {
301 file.delete();
Adrian Roos3dcae682014-10-29 14:43:56 +0100302 mCache.putFile(name, null);
Adrian Roos261d5ab2014-10-29 14:42:38 +0100303 }
304 }
305 }
306
307 try {
308 db.beginTransaction();
309 db.delete(TABLE, COLUMN_USERID + "='" + userId + "'", null);
310 db.setTransactionSuccessful();
Adrian Roos3dcae682014-10-29 14:43:56 +0100311 mCache.removeUser(userId);
Adrian Roos261d5ab2014-10-29 14:42:38 +0100312 } finally {
313 db.endTransaction();
314 }
315 }
316
Adrian Roose5424992014-11-07 21:47:17 +0100317 @VisibleForTesting
318 void closeDatabase() {
319 mOpenHelper.close();
320 }
321
322 @VisibleForTesting
323 void clearCache() {
324 mCache.clear();
325 }
Adrian Roos261d5ab2014-10-29 14:42:38 +0100326
Adrian Roos3dcae682014-10-29 14:43:56 +0100327 public interface Callback {
Adrian Roos261d5ab2014-10-29 14:42:38 +0100328 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 }
Adrian Roos3dcae682014-10-29 14:43:56 +0100373
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
Adrian Roose5424992014-11-07 21:47:17 +0100471 synchronized void clear() {
472 mCache.clear();
473 mVersion++;
474 }
Adrian Roos3dcae682014-10-29 14:43:56 +0100475
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 }
Adrian Roos261d5ab2014-10-29 14:42:38 +0100506}