/* * Copyright (C) 2012 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.server; import android.content.ContentResolver; import android.content.ContentValues; import android.content.Context; import android.content.pm.PackageManager; import android.content.pm.UserInfo; import static android.content.Context.USER_SERVICE; import static android.Manifest.permission.READ_PROFILE; import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteOpenHelper; import android.os.Binder; import android.os.Environment; import android.os.RemoteException; import android.os.SystemProperties; import android.os.UserHandle; import android.os.UserManager; import android.provider.Settings; import android.provider.Settings.Secure; import android.provider.Settings.SettingNotFoundException; import android.text.TextUtils; import android.util.Slog; import com.android.internal.widget.ILockSettings; import com.android.internal.widget.LockPatternUtils; import java.io.File; import java.io.FileNotFoundException; import java.io.IOException; import java.io.RandomAccessFile; import java.util.Arrays; import java.util.List; /** * Keeps the lock pattern/password data and related settings for each user. * Used by LockPatternUtils. Needs to be a service because Settings app also needs * to be able to save lockscreen information for secondary users. * @hide */ public class LockSettingsService extends ILockSettings.Stub { private final DatabaseHelper mOpenHelper; private static final String TAG = "LockSettingsService"; private static final String TABLE = "locksettings"; private static final String COLUMN_KEY = "name"; private static final String COLUMN_USERID = "user"; private static final String COLUMN_VALUE = "value"; private static final String[] COLUMNS_FOR_QUERY = { COLUMN_VALUE }; private static final String SYSTEM_DIRECTORY = "/system/"; private static final String LOCK_PATTERN_FILE = "gesture.key"; private static final String LOCK_PASSWORD_FILE = "password.key"; private final Context mContext; public LockSettingsService(Context context) { mContext = context; // Open the database mOpenHelper = new DatabaseHelper(mContext); } public void systemReady() { migrateOldData(); } private void migrateOldData() { try { // These Settings moved before multi-user was enabled, so we only have to do it for the // root user. if (getString("migrated", null, 0) == null) { final ContentResolver cr = mContext.getContentResolver(); for (String validSetting : VALID_SETTINGS) { String value = Settings.Secure.getString(cr, validSetting); if (value != null) { setString(validSetting, value, 0); } } // No need to move the password / pattern files. They're already in the right place. setString("migrated", "true", 0); Slog.i(TAG, "Migrated lock settings to new location"); } // These Settings changed after multi-user was enabled, hence need to be moved per user. if (getString("migrated_user_specific", null, 0) == null) { final UserManager um = (UserManager) mContext.getSystemService(USER_SERVICE); final ContentResolver cr = mContext.getContentResolver(); List users = um.getUsers(); for (int user = 0; user < users.size(); user++) { // Migrate owner info final int userId = users.get(user).id; final String OWNER_INFO = Secure.LOCK_SCREEN_OWNER_INFO; String ownerInfo = Settings.Secure.getStringForUser(cr, OWNER_INFO, userId); if (ownerInfo != null) { setString(OWNER_INFO, ownerInfo, userId); Settings.Secure.putStringForUser(cr, ownerInfo, "", userId); } // Migrate owner info enabled. Note there was a bug where older platforms only // stored this value if the checkbox was toggled at least once. The code detects // this case by handling the exception. final String OWNER_INFO_ENABLED = Secure.LOCK_SCREEN_OWNER_INFO_ENABLED; boolean enabled; try { int ivalue = Settings.Secure.getIntForUser(cr, OWNER_INFO_ENABLED, userId); enabled = ivalue != 0; setLong(OWNER_INFO_ENABLED, enabled ? 1 : 0, userId); } catch (SettingNotFoundException e) { // Setting was never stored. Store it if the string is not empty. if (!TextUtils.isEmpty(ownerInfo)) { setLong(OWNER_INFO_ENABLED, 1, userId); } } Settings.Secure.putIntForUser(cr, OWNER_INFO_ENABLED, 0, userId); } // No need to move the password / pattern files. They're already in the right place. setString("migrated_user_specific", "true", 0); Slog.i(TAG, "Migrated per-user lock settings to new location"); } } catch (RemoteException re) { Slog.e(TAG, "Unable to migrate old data", re); } } private static final void checkWritePermission(int userId) { final int callingUid = Binder.getCallingUid(); if (UserHandle.getAppId(callingUid) != android.os.Process.SYSTEM_UID) { throw new SecurityException("uid=" + callingUid + " not authorized to write lock settings"); } } private static final void checkPasswordReadPermission(int userId) { final int callingUid = Binder.getCallingUid(); if (UserHandle.getAppId(callingUid) != android.os.Process.SYSTEM_UID) { throw new SecurityException("uid=" + callingUid + " not authorized to read lock password"); } } private final void checkReadPermission(String requestedKey, int userId) { final int callingUid = Binder.getCallingUid(); for (int i = 0; i < READ_PROFILE_PROTECTED_SETTINGS.length; i++) { String key = READ_PROFILE_PROTECTED_SETTINGS[i]; if (key.equals(requestedKey) && mContext.checkCallingOrSelfPermission(READ_PROFILE) != PackageManager.PERMISSION_GRANTED) { throw new SecurityException("uid=" + callingUid + " needs permission " + READ_PROFILE + " to read " + requestedKey + " for user " + userId); } } } @Override public void setBoolean(String key, boolean value, int userId) throws RemoteException { checkWritePermission(userId); writeToDb(key, value ? "1" : "0", userId); } @Override public void setLong(String key, long value, int userId) throws RemoteException { checkWritePermission(userId); writeToDb(key, Long.toString(value), userId); } @Override public void setString(String key, String value, int userId) throws RemoteException { checkWritePermission(userId); writeToDb(key, value, userId); } @Override public boolean getBoolean(String key, boolean defaultValue, int userId) throws RemoteException { checkReadPermission(key, userId); String value = readFromDb(key, null, userId); return TextUtils.isEmpty(value) ? defaultValue : (value.equals("1") || value.equals("true")); } @Override public long getLong(String key, long defaultValue, int userId) throws RemoteException { checkReadPermission(key, userId); String value = readFromDb(key, null, userId); return TextUtils.isEmpty(value) ? defaultValue : Long.parseLong(value); } @Override public String getString(String key, String defaultValue, int userId) throws RemoteException { checkReadPermission(key, userId); return readFromDb(key, defaultValue, userId); } private String getLockPatternFilename(int userId) { String dataSystemDirectory = android.os.Environment.getDataDirectory().getAbsolutePath() + SYSTEM_DIRECTORY; if (userId == 0) { // Leave it in the same place for user 0 return dataSystemDirectory + LOCK_PATTERN_FILE; } else { return new File(Environment.getUserSystemDirectory(userId), LOCK_PATTERN_FILE) .getAbsolutePath(); } } private String getLockPasswordFilename(int userId) { String dataSystemDirectory = android.os.Environment.getDataDirectory().getAbsolutePath() + SYSTEM_DIRECTORY; if (userId == 0) { // Leave it in the same place for user 0 return dataSystemDirectory + LOCK_PASSWORD_FILE; } else { return new File(Environment.getUserSystemDirectory(userId), LOCK_PASSWORD_FILE) .getAbsolutePath(); } } @Override public boolean havePassword(int userId) throws RemoteException { // Do we need a permissions check here? return new File(getLockPasswordFilename(userId)).length() > 0; } @Override public boolean havePattern(int userId) throws RemoteException { // Do we need a permissions check here? return new File(getLockPatternFilename(userId)).length() > 0; } @Override public void setLockPattern(byte[] hash, int userId) throws RemoteException { checkWritePermission(userId); writeFile(getLockPatternFilename(userId), hash); } @Override public boolean checkPattern(byte[] hash, int userId) throws RemoteException { checkPasswordReadPermission(userId); try { // Read all the bytes from the file RandomAccessFile raf = new RandomAccessFile(getLockPatternFilename(userId), "r"); final byte[] stored = new byte[(int) raf.length()]; int got = raf.read(stored, 0, stored.length); raf.close(); if (got <= 0) { return true; } // Compare the hash from the file with the entered pattern's hash return Arrays.equals(stored, hash); } catch (FileNotFoundException fnfe) { Slog.e(TAG, "Cannot read file " + fnfe); return true; } catch (IOException ioe) { Slog.e(TAG, "Cannot read file " + ioe); return true; } } @Override public void setLockPassword(byte[] hash, int userId) throws RemoteException { checkWritePermission(userId); writeFile(getLockPasswordFilename(userId), hash); } @Override public boolean checkPassword(byte[] hash, int userId) throws RemoteException { checkPasswordReadPermission(userId); try { // Read all the bytes from the file RandomAccessFile raf = new RandomAccessFile(getLockPasswordFilename(userId), "r"); final byte[] stored = new byte[(int) raf.length()]; int got = raf.read(stored, 0, stored.length); raf.close(); if (got <= 0) { return true; } // Compare the hash from the file with the entered password's hash return Arrays.equals(stored, hash); } catch (FileNotFoundException fnfe) { Slog.e(TAG, "Cannot read file " + fnfe); return true; } catch (IOException ioe) { Slog.e(TAG, "Cannot read file " + ioe); return true; } } @Override public void removeUser(int userId) { checkWritePermission(userId); SQLiteDatabase db = mOpenHelper.getWritableDatabase(); try { File file = new File(getLockPasswordFilename(userId)); if (file.exists()) { file.delete(); } file = new File(getLockPatternFilename(userId)); if (file.exists()) { file.delete(); } db.beginTransaction(); db.delete(TABLE, COLUMN_USERID + "='" + userId + "'", null); db.setTransactionSuccessful(); } finally { db.endTransaction(); } } private void writeFile(String name, byte[] hash) { try { // Write the hash to file RandomAccessFile raf = new RandomAccessFile(name, "rw"); // Truncate the file if pattern is null, to clear the lock if (hash == null || hash.length == 0) { raf.setLength(0); } else { raf.write(hash, 0, hash.length); } raf.close(); } catch (IOException ioe) { Slog.e(TAG, "Error writing to file " + ioe); } } private void writeToDb(String key, String value, int userId) { writeToDb(mOpenHelper.getWritableDatabase(), key, value, userId); } private void writeToDb(SQLiteDatabase db, String key, String value, int userId) { ContentValues cv = new ContentValues(); cv.put(COLUMN_KEY, key); cv.put(COLUMN_USERID, userId); cv.put(COLUMN_VALUE, value); db.beginTransaction(); try { db.delete(TABLE, COLUMN_KEY + "=? AND " + COLUMN_USERID + "=?", new String[] {key, Integer.toString(userId)}); db.insert(TABLE, null, cv); db.setTransactionSuccessful(); } finally { db.endTransaction(); } } private String readFromDb(String key, String defaultValue, int userId) { Cursor cursor; String result = defaultValue; SQLiteDatabase db = mOpenHelper.getReadableDatabase(); if ((cursor = db.query(TABLE, COLUMNS_FOR_QUERY, COLUMN_USERID + "=? AND " + COLUMN_KEY + "=?", new String[] { Integer.toString(userId), key }, null, null, null)) != null) { if (cursor.moveToFirst()) { result = cursor.getString(0); } cursor.close(); } return result; } class DatabaseHelper extends SQLiteOpenHelper { private static final String TAG = "LockSettingsDB"; private static final String DATABASE_NAME = "locksettings.db"; private static final int DATABASE_VERSION = 1; public DatabaseHelper(Context context) { super(context, DATABASE_NAME, null, DATABASE_VERSION); setWriteAheadLoggingEnabled(true); } private void createTable(SQLiteDatabase db) { db.execSQL("CREATE TABLE " + TABLE + " (" + "_id INTEGER PRIMARY KEY AUTOINCREMENT," + COLUMN_KEY + " TEXT," + COLUMN_USERID + " INTEGER," + COLUMN_VALUE + " TEXT" + ");"); } @Override public void onCreate(SQLiteDatabase db) { createTable(db); initializeDefaults(db); } private void initializeDefaults(SQLiteDatabase db) { // Get the lockscreen default from a system property, if available boolean lockScreenDisable = SystemProperties.getBoolean("ro.lockscreen.disable.default", false); if (lockScreenDisable) { writeToDb(db, LockPatternUtils.DISABLE_LOCKSCREEN_KEY, "1", 0); } } @Override public void onUpgrade(SQLiteDatabase db, int oldVersion, int currentVersion) { // Nothing yet } } private static final String[] VALID_SETTINGS = new String[] { LockPatternUtils.LOCKOUT_PERMANENT_KEY, LockPatternUtils.LOCKOUT_ATTEMPT_DEADLINE, LockPatternUtils.PATTERN_EVER_CHOSEN_KEY, LockPatternUtils.PASSWORD_TYPE_KEY, LockPatternUtils.PASSWORD_TYPE_ALTERNATE_KEY, LockPatternUtils.LOCK_PASSWORD_SALT_KEY, LockPatternUtils.DISABLE_LOCKSCREEN_KEY, LockPatternUtils.LOCKSCREEN_OPTIONS, LockPatternUtils.LOCKSCREEN_BIOMETRIC_WEAK_FALLBACK, LockPatternUtils.BIOMETRIC_WEAK_EVER_CHOSEN_KEY, LockPatternUtils.LOCKSCREEN_POWER_BUTTON_INSTANTLY_LOCKS, LockPatternUtils.PASSWORD_HISTORY_KEY, Secure.LOCK_PATTERN_ENABLED, Secure.LOCK_BIOMETRIC_WEAK_FLAGS, Secure.LOCK_PATTERN_VISIBLE, Secure.LOCK_PATTERN_TACTILE_FEEDBACK_ENABLED }; // These are protected with a read permission private static final String[] READ_PROFILE_PROTECTED_SETTINGS = new String[] { Secure.LOCK_SCREEN_OWNER_INFO_ENABLED, Secure.LOCK_SCREEN_OWNER_INFO }; }