LockSettingsService.java revision 6ee7d25010d4f23b44a151f3953225ba253de8af
1/*
2 * Copyright (C) 2012 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 android.content.ContentResolver;
20import android.content.ContentValues;
21import android.content.Context;
22import android.content.pm.PackageManager;
23import android.content.pm.UserInfo;
24
25import static android.content.Context.USER_SERVICE;
26import static android.Manifest.permission.READ_PROFILE;
27import android.database.Cursor;
28import android.database.sqlite.SQLiteDatabase;
29import android.database.sqlite.SQLiteOpenHelper;
30import android.database.sqlite.SQLiteStatement;
31import android.os.Binder;
32import android.os.Environment;
33import android.os.RemoteException;
34import android.os.SystemProperties;
35import android.os.UserHandle;
36import android.os.UserManager;
37import android.provider.Settings;
38import android.provider.Settings.Secure;
39import android.provider.Settings.SettingNotFoundException;
40import android.security.KeyStore;
41import android.text.TextUtils;
42import android.util.Log;
43import android.util.Slog;
44
45import com.android.internal.widget.ILockSettings;
46import com.android.internal.widget.LockPatternUtils;
47
48import java.io.File;
49import java.io.FileNotFoundException;
50import java.io.IOException;
51import java.io.RandomAccessFile;
52import java.util.Arrays;
53import java.util.List;
54
55/**
56 * Keeps the lock pattern/password data and related settings for each user.
57 * Used by LockPatternUtils. Needs to be a service because Settings app also needs
58 * to be able to save lockscreen information for secondary users.
59 * @hide
60 */
61public class LockSettingsService extends ILockSettings.Stub {
62
63    private static final String PERMISSION = "android.permission.ACCESS_KEYGUARD_SECURE_STORAGE";
64    private final DatabaseHelper mOpenHelper;
65    private static final String TAG = "LockSettingsService";
66
67    private static final String TABLE = "locksettings";
68    private static final String COLUMN_KEY = "name";
69    private static final String COLUMN_USERID = "user";
70    private static final String COLUMN_VALUE = "value";
71
72    private static final String[] COLUMNS_FOR_QUERY = {
73        COLUMN_VALUE
74    };
75
76    private static final String SYSTEM_DIRECTORY = "/system/";
77    private static final String LOCK_PATTERN_FILE = "gesture.key";
78    private static final String LOCK_PASSWORD_FILE = "password.key";
79
80    private final Context mContext;
81    private LockPatternUtils mLockPatternUtils;
82
83    public LockSettingsService(Context context) {
84        mContext = context;
85        // Open the database
86        mOpenHelper = new DatabaseHelper(mContext);
87
88        mLockPatternUtils = new LockPatternUtils(context);
89    }
90
91    public void systemReady() {
92        migrateOldData();
93    }
94
95    private void migrateOldData() {
96        try {
97            // These Settings moved before multi-user was enabled, so we only have to do it for the
98            // root user.
99            if (getString("migrated", null, 0) == null) {
100                final ContentResolver cr = mContext.getContentResolver();
101                for (String validSetting : VALID_SETTINGS) {
102                    String value = Settings.Secure.getString(cr, validSetting);
103                    if (value != null) {
104                        setString(validSetting, value, 0);
105                    }
106                }
107                // No need to move the password / pattern files. They're already in the right place.
108                setString("migrated", "true", 0);
109                Slog.i(TAG, "Migrated lock settings to new location");
110            }
111
112            // These Settings changed after multi-user was enabled, hence need to be moved per user.
113            if (getString("migrated_user_specific", null, 0) == null) {
114                final UserManager um = (UserManager) mContext.getSystemService(USER_SERVICE);
115                final ContentResolver cr = mContext.getContentResolver();
116                List<UserInfo> users = um.getUsers();
117                for (int user = 0; user < users.size(); user++) {
118                    // Migrate owner info
119                    final int userId = users.get(user).id;
120                    final String OWNER_INFO = Secure.LOCK_SCREEN_OWNER_INFO;
121                    String ownerInfo = Settings.Secure.getStringForUser(cr, OWNER_INFO, userId);
122                    if (ownerInfo != null) {
123                        setString(OWNER_INFO, ownerInfo, userId);
124                        Settings.Secure.putStringForUser(cr, ownerInfo, "", userId);
125                    }
126
127                    // Migrate owner info enabled.  Note there was a bug where older platforms only
128                    // stored this value if the checkbox was toggled at least once. The code detects
129                    // this case by handling the exception.
130                    final String OWNER_INFO_ENABLED = Secure.LOCK_SCREEN_OWNER_INFO_ENABLED;
131                    boolean enabled;
132                    try {
133                        int ivalue = Settings.Secure.getIntForUser(cr, OWNER_INFO_ENABLED, userId);
134                        enabled = ivalue != 0;
135                        setLong(OWNER_INFO_ENABLED, enabled ? 1 : 0, userId);
136                    } catch (SettingNotFoundException e) {
137                        // Setting was never stored. Store it if the string is not empty.
138                        if (!TextUtils.isEmpty(ownerInfo)) {
139                            setLong(OWNER_INFO_ENABLED, 1, userId);
140                        }
141                    }
142                    Settings.Secure.putIntForUser(cr, OWNER_INFO_ENABLED, 0, userId);
143                }
144                // No need to move the password / pattern files. They're already in the right place.
145                setString("migrated_user_specific", "true", 0);
146                Slog.i(TAG, "Migrated per-user lock settings to new location");
147            }
148        } catch (RemoteException re) {
149            Slog.e(TAG, "Unable to migrate old data", re);
150        }
151    }
152
153    private final void checkWritePermission(int userId) {
154        mContext.enforceCallingOrSelfPermission(PERMISSION, "LockSettingsWrite");
155    }
156
157    private final void checkPasswordReadPermission(int userId) {
158        mContext.enforceCallingOrSelfPermission(PERMISSION, "LockSettingsRead");
159    }
160
161    private final void checkReadPermission(String requestedKey, int userId) {
162        final int callingUid = Binder.getCallingUid();
163        for (int i = 0; i < READ_PROFILE_PROTECTED_SETTINGS.length; i++) {
164            String key = READ_PROFILE_PROTECTED_SETTINGS[i];
165            if (key.equals(requestedKey) && mContext.checkCallingOrSelfPermission(READ_PROFILE)
166                    != PackageManager.PERMISSION_GRANTED) {
167                throw new SecurityException("uid=" + callingUid
168                        + " needs permission " + READ_PROFILE + " to read "
169                        + requestedKey + " for user " + userId);
170            }
171        }
172    }
173
174    @Override
175    public void setBoolean(String key, boolean value, int userId) throws RemoteException {
176        checkWritePermission(userId);
177
178        writeToDb(key, value ? "1" : "0", userId);
179    }
180
181    @Override
182    public void setLong(String key, long value, int userId) throws RemoteException {
183        checkWritePermission(userId);
184
185        writeToDb(key, Long.toString(value), userId);
186    }
187
188    @Override
189    public void setString(String key, String value, int userId) throws RemoteException {
190        checkWritePermission(userId);
191
192        writeToDb(key, value, userId);
193    }
194
195    @Override
196    public boolean getBoolean(String key, boolean defaultValue, int userId) throws RemoteException {
197        checkReadPermission(key, userId);
198
199        String value = readFromDb(key, null, userId);
200        return TextUtils.isEmpty(value) ?
201                defaultValue : (value.equals("1") || value.equals("true"));
202    }
203
204    @Override
205    public long getLong(String key, long defaultValue, int userId) throws RemoteException {
206        checkReadPermission(key, userId);
207
208        String value = readFromDb(key, null, userId);
209        return TextUtils.isEmpty(value) ? defaultValue : Long.parseLong(value);
210    }
211
212    @Override
213    public String getString(String key, String defaultValue, int userId) throws RemoteException {
214        checkReadPermission(key, userId);
215
216        return readFromDb(key, defaultValue, userId);
217    }
218
219    private String getLockPatternFilename(int userId) {
220        String dataSystemDirectory =
221                android.os.Environment.getDataDirectory().getAbsolutePath() +
222                SYSTEM_DIRECTORY;
223        if (userId == 0) {
224            // Leave it in the same place for user 0
225            return dataSystemDirectory + LOCK_PATTERN_FILE;
226        } else {
227            return  new File(Environment.getUserSystemDirectory(userId), LOCK_PATTERN_FILE)
228                    .getAbsolutePath();
229        }
230    }
231
232    private String getLockPasswordFilename(int userId) {
233        String dataSystemDirectory =
234                android.os.Environment.getDataDirectory().getAbsolutePath() +
235                SYSTEM_DIRECTORY;
236        if (userId == 0) {
237            // Leave it in the same place for user 0
238            return dataSystemDirectory + LOCK_PASSWORD_FILE;
239        } else {
240            return  new File(Environment.getUserSystemDirectory(userId), LOCK_PASSWORD_FILE)
241                    .getAbsolutePath();
242        }
243    }
244
245    @Override
246    public boolean havePassword(int userId) throws RemoteException {
247        // Do we need a permissions check here?
248
249        return new File(getLockPasswordFilename(userId)).length() > 0;
250    }
251
252    @Override
253    public boolean havePattern(int userId) throws RemoteException {
254        // Do we need a permissions check here?
255
256        return new File(getLockPatternFilename(userId)).length() > 0;
257    }
258
259    private void maybeUpdateKeystore(String password, int userId) {
260        if (userId == UserHandle.USER_OWNER) {
261            final KeyStore keyStore = KeyStore.getInstance();
262            // Conditionally reset the keystore if empty. If non-empty, we are just
263            // switching key guard type
264            if (TextUtils.isEmpty(password) && keyStore.isEmpty()) {
265                keyStore.reset();
266            } else {
267                // Update the keystore password
268                keyStore.password(password);
269            }
270        }
271    }
272
273    @Override
274    public void setLockPattern(String pattern, int userId) throws RemoteException {
275        checkWritePermission(userId);
276
277        maybeUpdateKeystore(pattern, userId);
278
279        final byte[] hash = LockPatternUtils.patternToHash(
280                LockPatternUtils.stringToPattern(pattern));
281        writeFile(getLockPatternFilename(userId), hash);
282    }
283
284    @Override
285    public void setLockPassword(String password, int userId) throws RemoteException {
286        checkWritePermission(userId);
287
288        maybeUpdateKeystore(password, userId);
289
290        writeFile(getLockPasswordFilename(userId), mLockPatternUtils.passwordToHash(password));
291    }
292
293    @Override
294    public boolean checkPattern(String pattern, int userId) throws RemoteException {
295        checkPasswordReadPermission(userId);
296        try {
297            // Read all the bytes from the file
298            RandomAccessFile raf = new RandomAccessFile(getLockPatternFilename(userId), "r");
299            final byte[] stored = new byte[(int) raf.length()];
300            int got = raf.read(stored, 0, stored.length);
301            raf.close();
302            if (got <= 0) {
303                return true;
304            }
305            // Compare the hash from the file with the entered pattern's hash
306            final byte[] hash = LockPatternUtils.patternToHash(
307                    LockPatternUtils.stringToPattern(pattern));
308            final boolean matched = Arrays.equals(stored, hash);
309            if (matched && !TextUtils.isEmpty(pattern)) {
310                maybeUpdateKeystore(pattern, userId);
311            }
312            return matched;
313        } catch (FileNotFoundException fnfe) {
314            Slog.e(TAG, "Cannot read file " + fnfe);
315        } catch (IOException ioe) {
316            Slog.e(TAG, "Cannot read file " + ioe);
317        }
318        return true;
319    }
320
321    @Override
322    public boolean checkPassword(String password, int userId) throws RemoteException {
323        checkPasswordReadPermission(userId);
324
325        try {
326            // Read all the bytes from the file
327            RandomAccessFile raf = new RandomAccessFile(getLockPasswordFilename(userId), "r");
328            final byte[] stored = new byte[(int) raf.length()];
329            int got = raf.read(stored, 0, stored.length);
330            raf.close();
331            if (got <= 0) {
332                return true;
333            }
334            // Compare the hash from the file with the entered password's hash
335            final byte[] hash = mLockPatternUtils.passwordToHash(password);
336            final boolean matched = Arrays.equals(stored, hash);
337            if (matched && !TextUtils.isEmpty(password)) {
338                maybeUpdateKeystore(password, userId);
339            }
340            return matched;
341        } catch (FileNotFoundException fnfe) {
342            Slog.e(TAG, "Cannot read file " + fnfe);
343        } catch (IOException ioe) {
344            Slog.e(TAG, "Cannot read file " + ioe);
345        }
346        return true;
347    }
348
349    @Override
350    public void removeUser(int userId) {
351        checkWritePermission(userId);
352
353        SQLiteDatabase db = mOpenHelper.getWritableDatabase();
354        try {
355            File file = new File(getLockPasswordFilename(userId));
356            if (file.exists()) {
357                file.delete();
358            }
359            file = new File(getLockPatternFilename(userId));
360            if (file.exists()) {
361                file.delete();
362            }
363
364            db.beginTransaction();
365            db.delete(TABLE, COLUMN_USERID + "='" + userId + "'", null);
366            db.setTransactionSuccessful();
367        } finally {
368            db.endTransaction();
369        }
370    }
371
372    private void writeFile(String name, byte[] hash) {
373        try {
374            // Write the hash to file
375            RandomAccessFile raf = new RandomAccessFile(name, "rw");
376            // Truncate the file if pattern is null, to clear the lock
377            if (hash == null || hash.length == 0) {
378                raf.setLength(0);
379            } else {
380                raf.write(hash, 0, hash.length);
381            }
382            raf.close();
383        } catch (IOException ioe) {
384            Slog.e(TAG, "Error writing to file " + ioe);
385        }
386    }
387
388    private void writeToDb(String key, String value, int userId) {
389        writeToDb(mOpenHelper.getWritableDatabase(), key, value, userId);
390    }
391
392    private void writeToDb(SQLiteDatabase db, String key, String value, int userId) {
393        ContentValues cv = new ContentValues();
394        cv.put(COLUMN_KEY, key);
395        cv.put(COLUMN_USERID, userId);
396        cv.put(COLUMN_VALUE, value);
397
398        db.beginTransaction();
399        try {
400            db.delete(TABLE, COLUMN_KEY + "=? AND " + COLUMN_USERID + "=?",
401                    new String[] {key, Integer.toString(userId)});
402            db.insert(TABLE, null, cv);
403            db.setTransactionSuccessful();
404        } finally {
405            db.endTransaction();
406        }
407    }
408
409    private String readFromDb(String key, String defaultValue, int userId) {
410        Cursor cursor;
411        String result = defaultValue;
412        SQLiteDatabase db = mOpenHelper.getReadableDatabase();
413        if ((cursor = db.query(TABLE, COLUMNS_FOR_QUERY,
414                COLUMN_USERID + "=? AND " + COLUMN_KEY + "=?",
415                new String[] { Integer.toString(userId), key },
416                null, null, null)) != null) {
417            if (cursor.moveToFirst()) {
418                result = cursor.getString(0);
419            }
420            cursor.close();
421        }
422        return result;
423    }
424
425    class DatabaseHelper extends SQLiteOpenHelper {
426        private static final String TAG = "LockSettingsDB";
427        private static final String DATABASE_NAME = "locksettings.db";
428
429        private static final int DATABASE_VERSION = 2;
430
431        public DatabaseHelper(Context context) {
432            super(context, DATABASE_NAME, null, DATABASE_VERSION);
433            setWriteAheadLoggingEnabled(true);
434        }
435
436        private void createTable(SQLiteDatabase db) {
437            db.execSQL("CREATE TABLE " + TABLE + " (" +
438                    "_id INTEGER PRIMARY KEY AUTOINCREMENT," +
439                    COLUMN_KEY + " TEXT," +
440                    COLUMN_USERID + " INTEGER," +
441                    COLUMN_VALUE + " TEXT" +
442                    ");");
443        }
444
445        @Override
446        public void onCreate(SQLiteDatabase db) {
447            createTable(db);
448            initializeDefaults(db);
449        }
450
451        private void initializeDefaults(SQLiteDatabase db) {
452            // Get the lockscreen default from a system property, if available
453            boolean lockScreenDisable = SystemProperties.getBoolean("ro.lockscreen.disable.default",
454                    false);
455            if (lockScreenDisable) {
456                writeToDb(db, LockPatternUtils.DISABLE_LOCKSCREEN_KEY, "1", 0);
457            }
458        }
459
460        @Override
461        public void onUpgrade(SQLiteDatabase db, int oldVersion, int currentVersion) {
462            int upgradeVersion = oldVersion;
463            if (upgradeVersion == 1) {
464                // Set the initial value for {@link LockPatternUtils#LOCKSCREEN_WIDGETS_ENABLED}
465                // during upgrade based on whether each user previously had widgets in keyguard.
466                maybeEnableWidgetSettingForUsers(db);
467                upgradeVersion = 2;
468            }
469
470            if (upgradeVersion != DATABASE_VERSION) {
471                Log.w(TAG, "Failed to upgrade database!");
472            }
473        }
474
475        private void maybeEnableWidgetSettingForUsers(SQLiteDatabase db) {
476            final UserManager um = (UserManager) mContext.getSystemService(USER_SERVICE);
477            final ContentResolver cr = mContext.getContentResolver();
478            final List<UserInfo> users = um.getUsers();
479            for (int i = 0; i < users.size(); i++) {
480                final int userId = users.get(i).id;
481                final boolean enabled = mLockPatternUtils.hasWidgetsEnabledInKeyguard(userId);
482                Log.v(TAG, "Widget upgrade uid=" + userId + ", enabled="
483                        + enabled + ", w[]=" + mLockPatternUtils.getAppWidgets());
484                loadSetting(db, LockPatternUtils.LOCKSCREEN_WIDGETS_ENABLED, userId, enabled);
485            }
486        }
487
488        private void loadSetting(SQLiteDatabase db, String key, int userId, boolean value) {
489            SQLiteStatement stmt = null;
490            try {
491                stmt = db.compileStatement(
492                        "INSERT OR REPLACE INTO locksettings(name,user,value) VALUES(?,?,?);");
493                stmt.bindString(1, key);
494                stmt.bindLong(2, userId);
495                stmt.bindLong(3, value ? 1 : 0);
496                stmt.execute();
497            } finally {
498                if (stmt != null) stmt.close();
499            }
500        }
501    }
502
503    private static final String[] VALID_SETTINGS = new String[] {
504        LockPatternUtils.LOCKOUT_PERMANENT_KEY,
505        LockPatternUtils.LOCKOUT_ATTEMPT_DEADLINE,
506        LockPatternUtils.PATTERN_EVER_CHOSEN_KEY,
507        LockPatternUtils.PASSWORD_TYPE_KEY,
508        LockPatternUtils.PASSWORD_TYPE_ALTERNATE_KEY,
509        LockPatternUtils.LOCK_PASSWORD_SALT_KEY,
510        LockPatternUtils.DISABLE_LOCKSCREEN_KEY,
511        LockPatternUtils.LOCKSCREEN_OPTIONS,
512        LockPatternUtils.LOCKSCREEN_BIOMETRIC_WEAK_FALLBACK,
513        LockPatternUtils.BIOMETRIC_WEAK_EVER_CHOSEN_KEY,
514        LockPatternUtils.LOCKSCREEN_POWER_BUTTON_INSTANTLY_LOCKS,
515        LockPatternUtils.PASSWORD_HISTORY_KEY,
516        Secure.LOCK_PATTERN_ENABLED,
517        Secure.LOCK_BIOMETRIC_WEAK_FLAGS,
518        Secure.LOCK_PATTERN_VISIBLE,
519        Secure.LOCK_PATTERN_TACTILE_FEEDBACK_ENABLED
520    };
521
522    // These are protected with a read permission
523    private static final String[] READ_PROFILE_PROTECTED_SETTINGS = new String[] {
524        Secure.LOCK_SCREEN_OWNER_INFO_ENABLED,
525        Secure.LOCK_SCREEN_OWNER_INFO
526    };
527}
528