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 17 package com.android.server.locksettings; 18 19 import static android.content.Context.USER_SERVICE; 20 21 import android.annotation.Nullable; 22 import android.app.admin.DevicePolicyManager; 23 import android.content.ContentValues; 24 import android.content.Context; 25 import android.content.pm.UserInfo; 26 import android.database.Cursor; 27 import android.database.sqlite.SQLiteDatabase; 28 import android.database.sqlite.SQLiteOpenHelper; 29 import android.os.Environment; 30 import android.os.UserHandle; 31 import android.os.UserManager; 32 import android.os.storage.StorageManager; 33 import android.util.ArrayMap; 34 import android.util.Log; 35 import android.util.Slog; 36 37 import com.android.internal.annotations.VisibleForTesting; 38 import com.android.internal.util.ArrayUtils; 39 import com.android.internal.util.Preconditions; 40 import com.android.internal.widget.LockPatternUtils; 41 import com.android.server.LocalServices; 42 import com.android.server.PersistentDataBlockManagerInternal; 43 44 import java.io.ByteArrayInputStream; 45 import java.io.ByteArrayOutputStream; 46 import java.io.DataInputStream; 47 import java.io.DataOutputStream; 48 import java.io.File; 49 import java.io.IOException; 50 import java.io.RandomAccessFile; 51 import java.util.ArrayList; 52 import java.util.List; 53 import java.util.Map; 54 55 /** 56 * Storage for the lock settings service. 57 */ 58 class LockSettingsStorage { 59 60 private static final String TAG = "LockSettingsStorage"; 61 private static final String TABLE = "locksettings"; 62 private static final boolean DEBUG = false; 63 64 private static final String COLUMN_KEY = "name"; 65 private static final String COLUMN_USERID = "user"; 66 private static final String COLUMN_VALUE = "value"; 67 68 private static final String[] COLUMNS_FOR_QUERY = { 69 COLUMN_VALUE 70 }; 71 private static final String[] COLUMNS_FOR_PREFETCH = { 72 COLUMN_KEY, COLUMN_VALUE 73 }; 74 75 private static final String SYSTEM_DIRECTORY = "/system/"; 76 private static final String LOCK_PATTERN_FILE = "gatekeeper.pattern.key"; 77 private static final String BASE_ZERO_LOCK_PATTERN_FILE = "gatekeeper.gesture.key"; 78 private static final String LEGACY_LOCK_PATTERN_FILE = "gesture.key"; 79 private static final String LOCK_PASSWORD_FILE = "gatekeeper.password.key"; 80 private static final String LEGACY_LOCK_PASSWORD_FILE = "password.key"; 81 private static final String CHILD_PROFILE_LOCK_FILE = "gatekeeper.profile.key"; 82 83 private static final String SYNTHETIC_PASSWORD_DIRECTORY = "spblob/"; 84 85 private static final Object DEFAULT = new Object(); 86 87 private final DatabaseHelper mOpenHelper; 88 private final Context mContext; 89 private final Cache mCache = new Cache(); 90 private final Object mFileWriteLock = new Object(); 91 92 private PersistentDataBlockManagerInternal mPersistentDataBlockManagerInternal; 93 94 @VisibleForTesting 95 public static class CredentialHash { 96 static final int VERSION_LEGACY = 0; 97 static final int VERSION_GATEKEEPER = 1; 98 CredentialHash(byte[] hash, int type, int version)99 private CredentialHash(byte[] hash, int type, int version) { 100 this(hash, type, version, false /* isBaseZeroPattern */); 101 } 102 CredentialHash(byte[] hash, int type, int version, boolean isBaseZeroPattern)103 private CredentialHash(byte[] hash, int type, int version, boolean isBaseZeroPattern) { 104 if (type != LockPatternUtils.CREDENTIAL_TYPE_NONE) { 105 if (hash == null) { 106 throw new RuntimeException("Empty hash for CredentialHash"); 107 } 108 } else /* type == LockPatternUtils.CREDENTIAL_TYPE_NONE */ { 109 if (hash != null) { 110 throw new RuntimeException("None type CredentialHash should not have hash"); 111 } 112 } 113 this.hash = hash; 114 this.type = type; 115 this.version = version; 116 this.isBaseZeroPattern = isBaseZeroPattern; 117 } 118 createBaseZeroPattern(byte[] hash)119 private static CredentialHash createBaseZeroPattern(byte[] hash) { 120 return new CredentialHash(hash, LockPatternUtils.CREDENTIAL_TYPE_PATTERN, 121 VERSION_GATEKEEPER, true /* isBaseZeroPattern */); 122 } 123 create(byte[] hash, int type)124 static CredentialHash create(byte[] hash, int type) { 125 if (type == LockPatternUtils.CREDENTIAL_TYPE_NONE) { 126 throw new RuntimeException("Bad type for CredentialHash"); 127 } 128 return new CredentialHash(hash, type, VERSION_GATEKEEPER); 129 } 130 createEmptyHash()131 static CredentialHash createEmptyHash() { 132 return new CredentialHash(null, LockPatternUtils.CREDENTIAL_TYPE_NONE, 133 VERSION_GATEKEEPER); 134 } 135 136 byte[] hash; 137 int type; 138 int version; 139 boolean isBaseZeroPattern; 140 toBytes()141 public byte[] toBytes() { 142 Preconditions.checkState(!isBaseZeroPattern, "base zero patterns are not serializable"); 143 144 try { 145 ByteArrayOutputStream os = new ByteArrayOutputStream(); 146 DataOutputStream dos = new DataOutputStream(os); 147 dos.write(version); 148 dos.write(type); 149 if (hash != null && hash.length > 0) { 150 dos.writeInt(hash.length); 151 dos.write(hash); 152 } else { 153 dos.writeInt(0); 154 } 155 dos.close(); 156 return os.toByteArray(); 157 } catch (IOException e) { 158 throw new RuntimeException(e); 159 } 160 } 161 fromBytes(byte[] bytes)162 public static CredentialHash fromBytes(byte[] bytes) { 163 try { 164 DataInputStream is = new DataInputStream(new ByteArrayInputStream(bytes)); 165 int version = is.read(); 166 int type = is.read(); 167 int hashSize = is.readInt(); 168 byte[] hash = null; 169 if (hashSize > 0) { 170 hash = new byte[hashSize]; 171 is.readFully(hash); 172 } 173 return new CredentialHash(hash, type, version); 174 } catch (IOException e) { 175 throw new RuntimeException(e); 176 } 177 } 178 } 179 LockSettingsStorage(Context context)180 public LockSettingsStorage(Context context) { 181 mContext = context; 182 mOpenHelper = new DatabaseHelper(context); 183 } 184 setDatabaseOnCreateCallback(Callback callback)185 public void setDatabaseOnCreateCallback(Callback callback) { 186 mOpenHelper.setCallback(callback); 187 } 188 writeKeyValue(String key, String value, int userId)189 public void writeKeyValue(String key, String value, int userId) { 190 writeKeyValue(mOpenHelper.getWritableDatabase(), key, value, userId); 191 } 192 writeKeyValue(SQLiteDatabase db, String key, String value, int userId)193 public void writeKeyValue(SQLiteDatabase db, String key, String value, int userId) { 194 ContentValues cv = new ContentValues(); 195 cv.put(COLUMN_KEY, key); 196 cv.put(COLUMN_USERID, userId); 197 cv.put(COLUMN_VALUE, value); 198 199 db.beginTransaction(); 200 try { 201 db.delete(TABLE, COLUMN_KEY + "=? AND " + COLUMN_USERID + "=?", 202 new String[] {key, Integer.toString(userId)}); 203 db.insert(TABLE, null, cv); 204 db.setTransactionSuccessful(); 205 mCache.putKeyValue(key, value, userId); 206 } finally { 207 db.endTransaction(); 208 } 209 210 } 211 readKeyValue(String key, String defaultValue, int userId)212 public String readKeyValue(String key, String defaultValue, int userId) { 213 int version; 214 synchronized (mCache) { 215 if (mCache.hasKeyValue(key, userId)) { 216 return mCache.peekKeyValue(key, defaultValue, userId); 217 } 218 version = mCache.getVersion(); 219 } 220 221 Cursor cursor; 222 Object result = DEFAULT; 223 SQLiteDatabase db = mOpenHelper.getReadableDatabase(); 224 if ((cursor = db.query(TABLE, COLUMNS_FOR_QUERY, 225 COLUMN_USERID + "=? AND " + COLUMN_KEY + "=?", 226 new String[] { Integer.toString(userId), key }, 227 null, null, null)) != null) { 228 if (cursor.moveToFirst()) { 229 result = cursor.getString(0); 230 } 231 cursor.close(); 232 } 233 mCache.putKeyValueIfUnchanged(key, result, userId, version); 234 return result == DEFAULT ? defaultValue : (String) result; 235 } 236 prefetchUser(int userId)237 public void prefetchUser(int userId) { 238 int version; 239 synchronized (mCache) { 240 if (mCache.isFetched(userId)) { 241 return; 242 } 243 mCache.setFetched(userId); 244 version = mCache.getVersion(); 245 } 246 247 Cursor cursor; 248 SQLiteDatabase db = mOpenHelper.getReadableDatabase(); 249 if ((cursor = db.query(TABLE, COLUMNS_FOR_PREFETCH, 250 COLUMN_USERID + "=?", 251 new String[] { Integer.toString(userId) }, 252 null, null, null)) != null) { 253 while (cursor.moveToNext()) { 254 String key = cursor.getString(0); 255 String value = cursor.getString(1); 256 mCache.putKeyValueIfUnchanged(key, value, userId, version); 257 } 258 cursor.close(); 259 } 260 261 // Populate cache by reading the password and pattern files. 262 readCredentialHash(userId); 263 } 264 readPasswordHashIfExists(int userId)265 private CredentialHash readPasswordHashIfExists(int userId) { 266 byte[] stored = readFile(getLockPasswordFilename(userId)); 267 if (!ArrayUtils.isEmpty(stored)) { 268 return new CredentialHash(stored, LockPatternUtils.CREDENTIAL_TYPE_PASSWORD, 269 CredentialHash.VERSION_GATEKEEPER); 270 } 271 272 stored = readFile(getLegacyLockPasswordFilename(userId)); 273 if (!ArrayUtils.isEmpty(stored)) { 274 return new CredentialHash(stored, LockPatternUtils.CREDENTIAL_TYPE_PASSWORD, 275 CredentialHash.VERSION_LEGACY); 276 } 277 return null; 278 } 279 readPatternHashIfExists(int userId)280 private CredentialHash readPatternHashIfExists(int userId) { 281 byte[] stored = readFile(getLockPatternFilename(userId)); 282 if (!ArrayUtils.isEmpty(stored)) { 283 return new CredentialHash(stored, LockPatternUtils.CREDENTIAL_TYPE_PATTERN, 284 CredentialHash.VERSION_GATEKEEPER); 285 } 286 287 stored = readFile(getBaseZeroLockPatternFilename(userId)); 288 if (!ArrayUtils.isEmpty(stored)) { 289 return CredentialHash.createBaseZeroPattern(stored); 290 } 291 292 stored = readFile(getLegacyLockPatternFilename(userId)); 293 if (!ArrayUtils.isEmpty(stored)) { 294 return new CredentialHash(stored, LockPatternUtils.CREDENTIAL_TYPE_PATTERN, 295 CredentialHash.VERSION_LEGACY); 296 } 297 return null; 298 } 299 readCredentialHash(int userId)300 public CredentialHash readCredentialHash(int userId) { 301 CredentialHash passwordHash = readPasswordHashIfExists(userId); 302 CredentialHash patternHash = readPatternHashIfExists(userId); 303 if (passwordHash != null && patternHash != null) { 304 if (passwordHash.version == CredentialHash.VERSION_GATEKEEPER) { 305 return passwordHash; 306 } else { 307 return patternHash; 308 } 309 } else if (passwordHash != null) { 310 return passwordHash; 311 } else if (patternHash != null) { 312 return patternHash; 313 } else { 314 return CredentialHash.createEmptyHash(); 315 } 316 } 317 removeChildProfileLock(int userId)318 public void removeChildProfileLock(int userId) { 319 if (DEBUG) 320 Slog.e(TAG, "Remove child profile lock for user: " + userId); 321 try { 322 deleteFile(getChildProfileLockFile(userId)); 323 } catch (Exception e) { 324 e.printStackTrace(); 325 } 326 } 327 writeChildProfileLock(int userId, byte[] lock)328 public void writeChildProfileLock(int userId, byte[] lock) { 329 writeFile(getChildProfileLockFile(userId), lock); 330 } 331 readChildProfileLock(int userId)332 public byte[] readChildProfileLock(int userId) { 333 return readFile(getChildProfileLockFile(userId)); 334 } 335 hasChildProfileLock(int userId)336 public boolean hasChildProfileLock(int userId) { 337 return hasFile(getChildProfileLockFile(userId)); 338 } 339 hasPassword(int userId)340 public boolean hasPassword(int userId) { 341 return hasFile(getLockPasswordFilename(userId)) || 342 hasFile(getLegacyLockPasswordFilename(userId)); 343 } 344 hasPattern(int userId)345 public boolean hasPattern(int userId) { 346 return hasFile(getLockPatternFilename(userId)) || 347 hasFile(getBaseZeroLockPatternFilename(userId)) || 348 hasFile(getLegacyLockPatternFilename(userId)); 349 } 350 hasCredential(int userId)351 public boolean hasCredential(int userId) { 352 return hasPassword(userId) || hasPattern(userId); 353 } 354 hasFile(String name)355 private boolean hasFile(String name) { 356 byte[] contents = readFile(name); 357 return contents != null && contents.length > 0; 358 } 359 readFile(String name)360 private byte[] readFile(String name) { 361 int version; 362 synchronized (mCache) { 363 if (mCache.hasFile(name)) { 364 return mCache.peekFile(name); 365 } 366 version = mCache.getVersion(); 367 } 368 369 RandomAccessFile raf = null; 370 byte[] stored = null; 371 try { 372 raf = new RandomAccessFile(name, "r"); 373 stored = new byte[(int) raf.length()]; 374 raf.readFully(stored, 0, stored.length); 375 raf.close(); 376 } catch (IOException e) { 377 Slog.e(TAG, "Cannot read file " + e); 378 } finally { 379 if (raf != null) { 380 try { 381 raf.close(); 382 } catch (IOException e) { 383 Slog.e(TAG, "Error closing file " + e); 384 } 385 } 386 } 387 mCache.putFileIfUnchanged(name, stored, version); 388 return stored; 389 } 390 writeFile(String name, byte[] hash)391 private void writeFile(String name, byte[] hash) { 392 synchronized (mFileWriteLock) { 393 RandomAccessFile raf = null; 394 try { 395 // Write the hash to file, requiring each write to be synchronized to the 396 // underlying storage device immediately to avoid data loss in case of power loss. 397 // This also ensures future secdiscard operation on the file succeeds since the 398 // file would have been allocated on flash. 399 raf = new RandomAccessFile(name, "rws"); 400 // Truncate the file if pattern is null, to clear the lock 401 if (hash == null || hash.length == 0) { 402 raf.setLength(0); 403 } else { 404 raf.write(hash, 0, hash.length); 405 } 406 raf.close(); 407 } catch (IOException e) { 408 Slog.e(TAG, "Error writing to file " + e); 409 } finally { 410 if (raf != null) { 411 try { 412 raf.close(); 413 } catch (IOException e) { 414 Slog.e(TAG, "Error closing file " + e); 415 } 416 } 417 } 418 mCache.putFile(name, hash); 419 } 420 } 421 deleteFile(String name)422 private void deleteFile(String name) { 423 if (DEBUG) Slog.e(TAG, "Delete file " + name); 424 synchronized (mFileWriteLock) { 425 File file = new File(name); 426 if (file.exists()) { 427 file.delete(); 428 mCache.putFile(name, null); 429 } 430 } 431 } 432 writeCredentialHash(CredentialHash hash, int userId)433 public void writeCredentialHash(CredentialHash hash, int userId) { 434 byte[] patternHash = null; 435 byte[] passwordHash = null; 436 437 if (hash.type == LockPatternUtils.CREDENTIAL_TYPE_PASSWORD) { 438 passwordHash = hash.hash; 439 } else if (hash.type == LockPatternUtils.CREDENTIAL_TYPE_PATTERN) { 440 patternHash = hash.hash; 441 } 442 writeFile(getLockPasswordFilename(userId), passwordHash); 443 writeFile(getLockPatternFilename(userId), patternHash); 444 } 445 446 @VisibleForTesting getLockPatternFilename(int userId)447 String getLockPatternFilename(int userId) { 448 return getLockCredentialFilePathForUser(userId, LOCK_PATTERN_FILE); 449 } 450 451 @VisibleForTesting getLockPasswordFilename(int userId)452 String getLockPasswordFilename(int userId) { 453 return getLockCredentialFilePathForUser(userId, LOCK_PASSWORD_FILE); 454 } 455 456 @VisibleForTesting getLegacyLockPatternFilename(int userId)457 String getLegacyLockPatternFilename(int userId) { 458 return getLockCredentialFilePathForUser(userId, LEGACY_LOCK_PATTERN_FILE); 459 } 460 461 @VisibleForTesting getLegacyLockPasswordFilename(int userId)462 String getLegacyLockPasswordFilename(int userId) { 463 return getLockCredentialFilePathForUser(userId, LEGACY_LOCK_PASSWORD_FILE); 464 } 465 getBaseZeroLockPatternFilename(int userId)466 private String getBaseZeroLockPatternFilename(int userId) { 467 return getLockCredentialFilePathForUser(userId, BASE_ZERO_LOCK_PATTERN_FILE); 468 } 469 470 @VisibleForTesting getChildProfileLockFile(int userId)471 String getChildProfileLockFile(int userId) { 472 return getLockCredentialFilePathForUser(userId, CHILD_PROFILE_LOCK_FILE); 473 } 474 getLockCredentialFilePathForUser(int userId, String basename)475 private String getLockCredentialFilePathForUser(int userId, String basename) { 476 String dataSystemDirectory = Environment.getDataDirectory().getAbsolutePath() + 477 SYSTEM_DIRECTORY; 478 if (userId == 0) { 479 // Leave it in the same place for user 0 480 return dataSystemDirectory + basename; 481 } else { 482 return new File(Environment.getUserSystemDirectory(userId), basename).getAbsolutePath(); 483 } 484 } 485 writeSyntheticPasswordState(int userId, long handle, String name, byte[] data)486 public void writeSyntheticPasswordState(int userId, long handle, String name, byte[] data) { 487 ensureSyntheticPasswordDirectoryForUser(userId); 488 writeFile(getSynthenticPasswordStateFilePathForUser(userId, handle, name), data); 489 } 490 readSyntheticPasswordState(int userId, long handle, String name)491 public byte[] readSyntheticPasswordState(int userId, long handle, String name) { 492 return readFile(getSynthenticPasswordStateFilePathForUser(userId, handle, name)); 493 } 494 deleteSyntheticPasswordState(int userId, long handle, String name)495 public void deleteSyntheticPasswordState(int userId, long handle, String name) { 496 String path = getSynthenticPasswordStateFilePathForUser(userId, handle, name); 497 File file = new File(path); 498 if (file.exists()) { 499 try (RandomAccessFile raf = new RandomAccessFile(path, "rws")) { 500 final int fileSize = (int) raf.length(); 501 raf.write(new byte[fileSize]); 502 } catch (Exception e) { 503 Slog.w(TAG, "Failed to zeroize " + path, e); 504 } finally { 505 file.delete(); 506 } 507 mCache.putFile(path, null); 508 } 509 } 510 listSyntheticPasswordHandlesForAllUsers(String stateName)511 public Map<Integer, List<Long>> listSyntheticPasswordHandlesForAllUsers(String stateName) { 512 Map<Integer, List<Long>> result = new ArrayMap<>(); 513 final UserManager um = UserManager.get(mContext); 514 for (UserInfo user : um.getUsers(false)) { 515 result.put(user.id, listSyntheticPasswordHandlesForUser(stateName, user.id)); 516 } 517 return result; 518 } 519 listSyntheticPasswordHandlesForUser(String stateName, int userId)520 public List<Long> listSyntheticPasswordHandlesForUser(String stateName, int userId) { 521 File baseDir = getSyntheticPasswordDirectoryForUser(userId); 522 List<Long> result = new ArrayList<>(); 523 File[] files = baseDir.listFiles(); 524 if (files == null) { 525 return result; 526 } 527 for (File file : files) { 528 String[] parts = file.getName().split("\\."); 529 if (parts.length == 2 && parts[1].equals(stateName)) { 530 try { 531 result.add(Long.parseUnsignedLong(parts[0], 16)); 532 } catch (NumberFormatException e) { 533 Slog.e(TAG, "Failed to parse handle " + parts[0]); 534 } 535 } 536 } 537 return result; 538 } 539 540 @VisibleForTesting getSyntheticPasswordDirectoryForUser(int userId)541 protected File getSyntheticPasswordDirectoryForUser(int userId) { 542 return new File(Environment.getDataSystemDeDirectory(userId) ,SYNTHETIC_PASSWORD_DIRECTORY); 543 } 544 545 /** Ensure per-user directory for synthetic password state exists */ ensureSyntheticPasswordDirectoryForUser(int userId)546 private void ensureSyntheticPasswordDirectoryForUser(int userId) { 547 File baseDir = getSyntheticPasswordDirectoryForUser(userId); 548 if (!baseDir.exists()) { 549 baseDir.mkdir(); 550 } 551 } 552 553 @VisibleForTesting getSynthenticPasswordStateFilePathForUser(int userId, long handle, String name)554 protected String getSynthenticPasswordStateFilePathForUser(int userId, long handle, 555 String name) { 556 final File baseDir = getSyntheticPasswordDirectoryForUser(userId); 557 final String baseName = String.format("%016x.%s", handle, name); 558 return new File(baseDir, baseName).getAbsolutePath(); 559 } 560 removeUser(int userId)561 public void removeUser(int userId) { 562 SQLiteDatabase db = mOpenHelper.getWritableDatabase(); 563 564 final UserManager um = (UserManager) mContext.getSystemService(USER_SERVICE); 565 final UserInfo parentInfo = um.getProfileParent(userId); 566 567 if (parentInfo == null) { 568 // This user owns its lock settings files - safe to delete them 569 synchronized (mFileWriteLock) { 570 String name = getLockPasswordFilename(userId); 571 File file = new File(name); 572 if (file.exists()) { 573 file.delete(); 574 mCache.putFile(name, null); 575 } 576 name = getLockPatternFilename(userId); 577 file = new File(name); 578 if (file.exists()) { 579 file.delete(); 580 mCache.putFile(name, null); 581 } 582 } 583 } else { 584 // Managed profile 585 removeChildProfileLock(userId); 586 } 587 588 File spStateDir = getSyntheticPasswordDirectoryForUser(userId); 589 try { 590 db.beginTransaction(); 591 db.delete(TABLE, COLUMN_USERID + "='" + userId + "'", null); 592 db.setTransactionSuccessful(); 593 mCache.removeUser(userId); 594 // The directory itself will be deleted as part of user deletion operation by the 595 // framework, so only need to purge cache here. 596 //TODO: (b/34600579) invoke secdiscardable 597 mCache.purgePath(spStateDir.getAbsolutePath()); 598 } finally { 599 db.endTransaction(); 600 } 601 } 602 603 @VisibleForTesting closeDatabase()604 void closeDatabase() { 605 mOpenHelper.close(); 606 } 607 608 @VisibleForTesting clearCache()609 void clearCache() { 610 mCache.clear(); 611 } 612 613 @Nullable getPersistentDataBlock()614 public PersistentDataBlockManagerInternal getPersistentDataBlock() { 615 if (mPersistentDataBlockManagerInternal == null) { 616 mPersistentDataBlockManagerInternal = 617 LocalServices.getService(PersistentDataBlockManagerInternal.class); 618 } 619 return mPersistentDataBlockManagerInternal; 620 } 621 writePersistentDataBlock(int persistentType, int userId, int qualityForUi, byte[] payload)622 public void writePersistentDataBlock(int persistentType, int userId, int qualityForUi, 623 byte[] payload) { 624 PersistentDataBlockManagerInternal persistentDataBlock = getPersistentDataBlock(); 625 if (persistentDataBlock == null) { 626 return; 627 } 628 persistentDataBlock.setFrpCredentialHandle(PersistentData.toBytes( 629 persistentType, userId, qualityForUi, payload)); 630 } 631 readPersistentDataBlock()632 public PersistentData readPersistentDataBlock() { 633 PersistentDataBlockManagerInternal persistentDataBlock = getPersistentDataBlock(); 634 if (persistentDataBlock == null) { 635 return PersistentData.NONE; 636 } 637 try { 638 return PersistentData.fromBytes(persistentDataBlock.getFrpCredentialHandle()); 639 } catch (IllegalStateException e) { 640 Slog.e(TAG, "Error reading persistent data block", e); 641 return PersistentData.NONE; 642 } 643 } 644 645 public static class PersistentData { 646 static final byte VERSION_1 = 1; 647 static final int VERSION_1_HEADER_SIZE = 1 + 1 + 4 + 4; 648 649 public static final int TYPE_NONE = 0; 650 public static final int TYPE_SP = 1; 651 public static final int TYPE_SP_WEAVER = 2; 652 653 public static final PersistentData NONE = new PersistentData(TYPE_NONE, 654 UserHandle.USER_NULL, DevicePolicyManager.PASSWORD_QUALITY_UNSPECIFIED, null); 655 656 final int type; 657 final int userId; 658 final int qualityForUi; 659 final byte[] payload; 660 PersistentData(int type, int userId, int qualityForUi, byte[] payload)661 private PersistentData(int type, int userId, int qualityForUi, byte[] payload) { 662 this.type = type; 663 this.userId = userId; 664 this.qualityForUi = qualityForUi; 665 this.payload = payload; 666 } 667 fromBytes(byte[] frpData)668 public static PersistentData fromBytes(byte[] frpData) { 669 if (frpData == null || frpData.length == 0) { 670 return NONE; 671 } 672 673 DataInputStream is = new DataInputStream(new ByteArrayInputStream(frpData)); 674 try { 675 byte version = is.readByte(); 676 if (version == PersistentData.VERSION_1) { 677 int type = is.readByte() & 0xFF; 678 int userId = is.readInt(); 679 int qualityForUi = is.readInt(); 680 byte[] payload = new byte[frpData.length - VERSION_1_HEADER_SIZE]; 681 System.arraycopy(frpData, VERSION_1_HEADER_SIZE, payload, 0, payload.length); 682 return new PersistentData(type, userId, qualityForUi, payload); 683 } else { 684 Slog.wtf(TAG, "Unknown PersistentData version code: " + version); 685 return NONE; 686 } 687 } catch (IOException e) { 688 Slog.wtf(TAG, "Could not parse PersistentData", e); 689 return NONE; 690 } 691 } 692 toBytes(int persistentType, int userId, int qualityForUi, byte[] payload)693 public static byte[] toBytes(int persistentType, int userId, int qualityForUi, 694 byte[] payload) { 695 if (persistentType == PersistentData.TYPE_NONE) { 696 Preconditions.checkArgument(payload == null, 697 "TYPE_NONE must have empty payload"); 698 return null; 699 } 700 Preconditions.checkArgument(payload != null && payload.length > 0, 701 "empty payload must only be used with TYPE_NONE"); 702 703 ByteArrayOutputStream os = new ByteArrayOutputStream( 704 VERSION_1_HEADER_SIZE + payload.length); 705 DataOutputStream dos = new DataOutputStream(os); 706 try { 707 dos.writeByte(PersistentData.VERSION_1); 708 dos.writeByte(persistentType); 709 dos.writeInt(userId); 710 dos.writeInt(qualityForUi); 711 dos.write(payload); 712 } catch (IOException e) { 713 throw new RuntimeException("ByteArrayOutputStream cannot throw IOException"); 714 } 715 return os.toByteArray(); 716 } 717 } 718 719 public interface Callback { initialize(SQLiteDatabase db)720 void initialize(SQLiteDatabase db); 721 } 722 723 static class DatabaseHelper extends SQLiteOpenHelper { 724 private static final String TAG = "LockSettingsDB"; 725 private static final String DATABASE_NAME = "locksettings.db"; 726 727 private static final int DATABASE_VERSION = 2; 728 private static final int IDLE_CONNECTION_TIMEOUT_MS = 30000; 729 730 private Callback mCallback; 731 DatabaseHelper(Context context)732 public DatabaseHelper(Context context) { 733 super(context, DATABASE_NAME, null, DATABASE_VERSION); 734 setWriteAheadLoggingEnabled(true); 735 // Memory optimization - close idle connections after 30s of inactivity 736 setIdleConnectionTimeout(IDLE_CONNECTION_TIMEOUT_MS); 737 } 738 setCallback(Callback callback)739 public void setCallback(Callback callback) { 740 mCallback = callback; 741 } 742 createTable(SQLiteDatabase db)743 private void createTable(SQLiteDatabase db) { 744 db.execSQL("CREATE TABLE " + TABLE + " (" + 745 "_id INTEGER PRIMARY KEY AUTOINCREMENT," + 746 COLUMN_KEY + " TEXT," + 747 COLUMN_USERID + " INTEGER," + 748 COLUMN_VALUE + " TEXT" + 749 ");"); 750 } 751 752 @Override onCreate(SQLiteDatabase db)753 public void onCreate(SQLiteDatabase db) { 754 createTable(db); 755 if (mCallback != null) { 756 mCallback.initialize(db); 757 } 758 } 759 760 @Override onUpgrade(SQLiteDatabase db, int oldVersion, int currentVersion)761 public void onUpgrade(SQLiteDatabase db, int oldVersion, int currentVersion) { 762 int upgradeVersion = oldVersion; 763 if (upgradeVersion == 1) { 764 // Previously migrated lock screen widget settings. Now defunct. 765 upgradeVersion = 2; 766 } 767 768 if (upgradeVersion != DATABASE_VERSION) { 769 Log.w(TAG, "Failed to upgrade database!"); 770 } 771 } 772 } 773 774 /** 775 * Cache consistency model: 776 * - Writes to storage write directly to the cache, but this MUST happen within the atomic 777 * section either provided by the database transaction or mWriteLock, such that writes to the 778 * cache and writes to the backing storage are guaranteed to occur in the same order 779 * 780 * - Reads can populate the cache, but because they are no strong ordering guarantees with 781 * respect to writes this precaution is taken: 782 * - The cache is assigned a version number that increases every time the cache is modified. 783 * Reads from backing storage can only populate the cache if the backing storage 784 * has not changed since the load operation has begun. 785 * This guarantees that no read operation can shadow a write to the cache that happens 786 * after it had begun. 787 */ 788 private static class Cache { 789 private final ArrayMap<CacheKey, Object> mCache = new ArrayMap<>(); 790 private final CacheKey mCacheKey = new CacheKey(); 791 private int mVersion = 0; 792 peekKeyValue(String key, String defaultValue, int userId)793 String peekKeyValue(String key, String defaultValue, int userId) { 794 Object cached = peek(CacheKey.TYPE_KEY_VALUE, key, userId); 795 return cached == DEFAULT ? defaultValue : (String) cached; 796 } 797 hasKeyValue(String key, int userId)798 boolean hasKeyValue(String key, int userId) { 799 return contains(CacheKey.TYPE_KEY_VALUE, key, userId); 800 } 801 putKeyValue(String key, String value, int userId)802 void putKeyValue(String key, String value, int userId) { 803 put(CacheKey.TYPE_KEY_VALUE, key, value, userId); 804 } 805 putKeyValueIfUnchanged(String key, Object value, int userId, int version)806 void putKeyValueIfUnchanged(String key, Object value, int userId, int version) { 807 putIfUnchanged(CacheKey.TYPE_KEY_VALUE, key, value, userId, version); 808 } 809 peekFile(String fileName)810 byte[] peekFile(String fileName) { 811 return (byte[]) peek(CacheKey.TYPE_FILE, fileName, -1 /* userId */); 812 } 813 hasFile(String fileName)814 boolean hasFile(String fileName) { 815 return contains(CacheKey.TYPE_FILE, fileName, -1 /* userId */); 816 } 817 putFile(String key, byte[] value)818 void putFile(String key, byte[] value) { 819 put(CacheKey.TYPE_FILE, key, value, -1 /* userId */); 820 } 821 putFileIfUnchanged(String key, byte[] value, int version)822 void putFileIfUnchanged(String key, byte[] value, int version) { 823 putIfUnchanged(CacheKey.TYPE_FILE, key, value, -1 /* userId */, version); 824 } 825 setFetched(int userId)826 void setFetched(int userId) { 827 put(CacheKey.TYPE_FETCHED, "isFetched", "true", userId); 828 } 829 isFetched(int userId)830 boolean isFetched(int userId) { 831 return contains(CacheKey.TYPE_FETCHED, "", userId); 832 } 833 834 put(int type, String key, Object value, int userId)835 private synchronized void put(int type, String key, Object value, int userId) { 836 // Create a new CachKey here because it may be saved in the map if the key is absent. 837 mCache.put(new CacheKey().set(type, key, userId), value); 838 mVersion++; 839 } 840 putIfUnchanged(int type, String key, Object value, int userId, int version)841 private synchronized void putIfUnchanged(int type, String key, Object value, int userId, 842 int version) { 843 if (!contains(type, key, userId) && mVersion == version) { 844 put(type, key, value, userId); 845 } 846 } 847 contains(int type, String key, int userId)848 private synchronized boolean contains(int type, String key, int userId) { 849 return mCache.containsKey(mCacheKey.set(type, key, userId)); 850 } 851 peek(int type, String key, int userId)852 private synchronized Object peek(int type, String key, int userId) { 853 return mCache.get(mCacheKey.set(type, key, userId)); 854 } 855 getVersion()856 private synchronized int getVersion() { 857 return mVersion; 858 } 859 removeUser(int userId)860 synchronized void removeUser(int userId) { 861 for (int i = mCache.size() - 1; i >= 0; i--) { 862 if (mCache.keyAt(i).userId == userId) { 863 mCache.removeAt(i); 864 } 865 } 866 867 // Make sure in-flight loads can't write to cache. 868 mVersion++; 869 } 870 purgePath(String path)871 synchronized void purgePath(String path) { 872 for (int i = mCache.size() - 1; i >= 0; i--) { 873 CacheKey entry = mCache.keyAt(i); 874 if (entry.type == CacheKey.TYPE_FILE && entry.key.startsWith(path)) { 875 mCache.removeAt(i); 876 } 877 } 878 mVersion++; 879 } 880 clear()881 synchronized void clear() { 882 mCache.clear(); 883 mVersion++; 884 } 885 886 private static final class CacheKey { 887 static final int TYPE_KEY_VALUE = 0; 888 static final int TYPE_FILE = 1; 889 static final int TYPE_FETCHED = 2; 890 891 String key; 892 int userId; 893 int type; 894 set(int type, String key, int userId)895 public CacheKey set(int type, String key, int userId) { 896 this.type = type; 897 this.key = key; 898 this.userId = userId; 899 return this; 900 } 901 902 @Override equals(Object obj)903 public boolean equals(Object obj) { 904 if (!(obj instanceof CacheKey)) 905 return false; 906 CacheKey o = (CacheKey) obj; 907 return userId == o.userId && type == o.type && key.equals(o.key); 908 } 909 910 @Override hashCode()911 public int hashCode() { 912 return key.hashCode() ^ userId ^ type; 913 } 914 } 915 } 916 917 } 918