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.internal.widget;
18
19import android.content.ContentResolver;
20import android.content.ContentValues;
21import android.content.Context;
22import android.database.Cursor;
23import android.database.sqlite.SQLiteDatabase;
24import android.database.sqlite.SQLiteOpenHelper;
25import android.os.Binder;
26import android.os.Environment;
27import android.os.RemoteException;
28import android.os.SystemProperties;
29import android.os.UserHandle;
30import android.provider.Settings;
31import android.provider.Settings.Secure;
32import android.text.TextUtils;
33import android.util.Slog;
34
35import java.io.File;
36import java.io.FileNotFoundException;
37import java.io.IOException;
38import java.io.RandomAccessFile;
39import java.util.Arrays;
40
41/**
42 * Keeps the lock pattern/password data and related settings for each user.
43 * Used by LockPatternUtils. Needs to be a service because Settings app also needs
44 * to be able to save lockscreen information for secondary users.
45 * @hide
46 */
47public class LockSettingsService extends ILockSettings.Stub {
48
49    private final DatabaseHelper mOpenHelper;
50    private static final String TAG = "LockSettingsService";
51
52    private static final String TABLE = "locksettings";
53    private static final String COLUMN_KEY = "name";
54    private static final String COLUMN_USERID = "user";
55    private static final String COLUMN_VALUE = "value";
56
57    private static final String[] COLUMNS_FOR_QUERY = {
58        COLUMN_VALUE
59    };
60
61    private static final String SYSTEM_DIRECTORY = "/system/";
62    private static final String LOCK_PATTERN_FILE = "gesture.key";
63    private static final String LOCK_PASSWORD_FILE = "password.key";
64
65    private final Context mContext;
66
67    public LockSettingsService(Context context) {
68        mContext = context;
69        // Open the database
70        mOpenHelper = new DatabaseHelper(mContext);
71    }
72
73    public void systemReady() {
74        migrateOldData();
75    }
76
77    private void migrateOldData() {
78        try {
79            if (getString("migrated", null, 0) != null) {
80                // Already migrated
81                return;
82            }
83
84            final ContentResolver cr = mContext.getContentResolver();
85            for (String validSetting : VALID_SETTINGS) {
86                String value = Settings.Secure.getString(cr, validSetting);
87                if (value != null) {
88                    setString(validSetting, value, 0);
89                }
90            }
91            // No need to move the password / pattern files. They're already in the right place.
92            setString("migrated", "true", 0);
93            Slog.i(TAG, "Migrated lock settings to new location");
94        } catch (RemoteException re) {
95            Slog.e(TAG, "Unable to migrate old data");
96        }
97    }
98
99    private static final void checkWritePermission(int userId) {
100        final int callingUid = Binder.getCallingUid();
101        if (UserHandle.getAppId(callingUid) != android.os.Process.SYSTEM_UID) {
102            throw new SecurityException("uid=" + callingUid
103                    + " not authorized to write lock settings");
104        }
105    }
106
107    private static final void checkPasswordReadPermission(int userId) {
108        final int callingUid = Binder.getCallingUid();
109        if (UserHandle.getAppId(callingUid) != android.os.Process.SYSTEM_UID) {
110            throw new SecurityException("uid=" + callingUid
111                    + " not authorized to read lock password");
112        }
113    }
114
115    private static final void checkReadPermission(int userId) {
116        final int callingUid = Binder.getCallingUid();
117        if (UserHandle.getAppId(callingUid) != android.os.Process.SYSTEM_UID
118                && UserHandle.getUserId(callingUid) != userId) {
119            throw new SecurityException("uid=" + callingUid
120                    + " not authorized to read settings of user " + userId);
121        }
122    }
123
124    @Override
125    public void setBoolean(String key, boolean value, int userId) throws RemoteException {
126        checkWritePermission(userId);
127
128        writeToDb(key, value ? "1" : "0", userId);
129    }
130
131    @Override
132    public void setLong(String key, long value, int userId) throws RemoteException {
133        checkWritePermission(userId);
134
135        writeToDb(key, Long.toString(value), userId);
136    }
137
138    @Override
139    public void setString(String key, String value, int userId) throws RemoteException {
140        checkWritePermission(userId);
141
142        writeToDb(key, value, userId);
143    }
144
145    @Override
146    public boolean getBoolean(String key, boolean defaultValue, int userId) throws RemoteException {
147        //checkReadPermission(userId);
148
149        String value = readFromDb(key, null, userId);
150        return TextUtils.isEmpty(value) ?
151                defaultValue : (value.equals("1") || value.equals("true"));
152    }
153
154    @Override
155    public long getLong(String key, long defaultValue, int userId) throws RemoteException {
156        //checkReadPermission(userId);
157
158        String value = readFromDb(key, null, userId);
159        return TextUtils.isEmpty(value) ? defaultValue : Long.parseLong(value);
160    }
161
162    @Override
163    public String getString(String key, String defaultValue, int userId) throws RemoteException {
164        //checkReadPermission(userId);
165
166        return readFromDb(key, defaultValue, userId);
167    }
168
169    private String getLockPatternFilename(int userId) {
170        String dataSystemDirectory =
171                android.os.Environment.getDataDirectory().getAbsolutePath() +
172                SYSTEM_DIRECTORY;
173        if (userId == 0) {
174            // Leave it in the same place for user 0
175            return dataSystemDirectory + LOCK_PATTERN_FILE;
176        } else {
177            return  new File(Environment.getUserSystemDirectory(userId), LOCK_PATTERN_FILE)
178                    .getAbsolutePath();
179        }
180    }
181
182    private String getLockPasswordFilename(int userId) {
183        String dataSystemDirectory =
184                android.os.Environment.getDataDirectory().getAbsolutePath() +
185                SYSTEM_DIRECTORY;
186        if (userId == 0) {
187            // Leave it in the same place for user 0
188            return dataSystemDirectory + LOCK_PASSWORD_FILE;
189        } else {
190            return  new File(Environment.getUserSystemDirectory(userId), LOCK_PASSWORD_FILE)
191                    .getAbsolutePath();
192        }
193    }
194
195    @Override
196    public boolean havePassword(int userId) throws RemoteException {
197        // Do we need a permissions check here?
198
199        return new File(getLockPasswordFilename(userId)).length() > 0;
200    }
201
202    @Override
203    public boolean havePattern(int userId) throws RemoteException {
204        // Do we need a permissions check here?
205
206        return new File(getLockPatternFilename(userId)).length() > 0;
207    }
208
209    @Override
210    public void setLockPattern(byte[] hash, int userId) throws RemoteException {
211        checkWritePermission(userId);
212
213        writeFile(getLockPatternFilename(userId), hash);
214    }
215
216    @Override
217    public boolean checkPattern(byte[] hash, int userId) throws RemoteException {
218        checkPasswordReadPermission(userId);
219        try {
220            // Read all the bytes from the file
221            RandomAccessFile raf = new RandomAccessFile(getLockPatternFilename(userId), "r");
222            final byte[] stored = new byte[(int) raf.length()];
223            int got = raf.read(stored, 0, stored.length);
224            raf.close();
225            if (got <= 0) {
226                return true;
227            }
228            // Compare the hash from the file with the entered pattern's hash
229            return Arrays.equals(stored, hash);
230        } catch (FileNotFoundException fnfe) {
231            Slog.e(TAG, "Cannot read file " + fnfe);
232            return true;
233        } catch (IOException ioe) {
234            Slog.e(TAG, "Cannot read file " + ioe);
235            return true;
236        }
237    }
238
239    @Override
240    public void setLockPassword(byte[] hash, int userId) throws RemoteException {
241        checkWritePermission(userId);
242
243        writeFile(getLockPasswordFilename(userId), hash);
244    }
245
246    @Override
247    public boolean checkPassword(byte[] hash, int userId) throws RemoteException {
248        checkPasswordReadPermission(userId);
249
250        try {
251            // Read all the bytes from the file
252            RandomAccessFile raf = new RandomAccessFile(getLockPasswordFilename(userId), "r");
253            final byte[] stored = new byte[(int) raf.length()];
254            int got = raf.read(stored, 0, stored.length);
255            raf.close();
256            if (got <= 0) {
257                return true;
258            }
259            // Compare the hash from the file with the entered password's hash
260            return Arrays.equals(stored, hash);
261        } catch (FileNotFoundException fnfe) {
262            Slog.e(TAG, "Cannot read file " + fnfe);
263            return true;
264        } catch (IOException ioe) {
265            Slog.e(TAG, "Cannot read file " + ioe);
266            return true;
267        }
268    }
269
270    @Override
271    public void removeUser(int userId) {
272        checkWritePermission(userId);
273
274        SQLiteDatabase db = mOpenHelper.getWritableDatabase();
275        try {
276            File file = new File(getLockPasswordFilename(userId));
277            if (file.exists()) {
278                file.delete();
279            }
280            file = new File(getLockPatternFilename(userId));
281            if (file.exists()) {
282                file.delete();
283            }
284
285            db.beginTransaction();
286            db.delete(TABLE, COLUMN_USERID + "='" + userId + "'", null);
287            db.setTransactionSuccessful();
288        } finally {
289            db.endTransaction();
290        }
291    }
292
293    private void writeFile(String name, byte[] hash) {
294        try {
295            // Write the hash to file
296            RandomAccessFile raf = new RandomAccessFile(name, "rw");
297            // Truncate the file if pattern is null, to clear the lock
298            if (hash == null || hash.length == 0) {
299                raf.setLength(0);
300            } else {
301                raf.write(hash, 0, hash.length);
302            }
303            raf.close();
304        } catch (IOException ioe) {
305            Slog.e(TAG, "Error writing to file " + ioe);
306        }
307    }
308
309    private void writeToDb(String key, String value, int userId) {
310        writeToDb(mOpenHelper.getWritableDatabase(), key, value, userId);
311    }
312
313    private void writeToDb(SQLiteDatabase db, String key, String value, int userId) {
314        ContentValues cv = new ContentValues();
315        cv.put(COLUMN_KEY, key);
316        cv.put(COLUMN_USERID, userId);
317        cv.put(COLUMN_VALUE, value);
318
319        db.beginTransaction();
320        try {
321            db.delete(TABLE, COLUMN_KEY + "=? AND " + COLUMN_USERID + "=?",
322                    new String[] {key, Integer.toString(userId)});
323            db.insert(TABLE, null, cv);
324            db.setTransactionSuccessful();
325        } finally {
326            db.endTransaction();
327        }
328    }
329
330    private String readFromDb(String key, String defaultValue, int userId) {
331        Cursor cursor;
332        String result = defaultValue;
333        SQLiteDatabase db = mOpenHelper.getReadableDatabase();
334        if ((cursor = db.query(TABLE, COLUMNS_FOR_QUERY,
335                COLUMN_USERID + "=? AND " + COLUMN_KEY + "=?",
336                new String[] { Integer.toString(userId), key },
337                null, null, null)) != null) {
338            if (cursor.moveToFirst()) {
339                result = cursor.getString(0);
340            }
341            cursor.close();
342        }
343        return result;
344    }
345
346    class DatabaseHelper extends SQLiteOpenHelper {
347        private static final String TAG = "LockSettingsDB";
348        private static final String DATABASE_NAME = "locksettings.db";
349
350        private static final int DATABASE_VERSION = 1;
351
352        public DatabaseHelper(Context context) {
353            super(context, DATABASE_NAME, null, DATABASE_VERSION);
354            setWriteAheadLoggingEnabled(true);
355        }
356
357        private void createTable(SQLiteDatabase db) {
358            db.execSQL("CREATE TABLE " + TABLE + " (" +
359                    "_id INTEGER PRIMARY KEY AUTOINCREMENT," +
360                    COLUMN_KEY + " TEXT," +
361                    COLUMN_USERID + " INTEGER," +
362                    COLUMN_VALUE + " TEXT" +
363                    ");");
364        }
365
366        @Override
367        public void onCreate(SQLiteDatabase db) {
368            createTable(db);
369            initializeDefaults(db);
370        }
371
372        private void initializeDefaults(SQLiteDatabase db) {
373            // Get the lockscreen default from a system property, if available
374            boolean lockScreenDisable = SystemProperties.getBoolean("ro.lockscreen.disable.default",
375                    false);
376            if (lockScreenDisable) {
377                writeToDb(db, LockPatternUtils.DISABLE_LOCKSCREEN_KEY, "1", 0);
378            }
379        }
380
381        @Override
382        public void onUpgrade(SQLiteDatabase db, int oldVersion, int currentVersion) {
383            // Nothing yet
384        }
385    }
386
387    private static final String[] VALID_SETTINGS = new String[] {
388        LockPatternUtils.LOCKOUT_PERMANENT_KEY,
389        LockPatternUtils.LOCKOUT_ATTEMPT_DEADLINE,
390        LockPatternUtils.PATTERN_EVER_CHOSEN_KEY,
391        LockPatternUtils.PASSWORD_TYPE_KEY,
392        LockPatternUtils.PASSWORD_TYPE_ALTERNATE_KEY,
393        LockPatternUtils.LOCK_PASSWORD_SALT_KEY,
394        LockPatternUtils.DISABLE_LOCKSCREEN_KEY,
395        LockPatternUtils.LOCKSCREEN_OPTIONS,
396        LockPatternUtils.LOCKSCREEN_BIOMETRIC_WEAK_FALLBACK,
397        LockPatternUtils.BIOMETRIC_WEAK_EVER_CHOSEN_KEY,
398        LockPatternUtils.LOCKSCREEN_POWER_BUTTON_INSTANTLY_LOCKS,
399        LockPatternUtils.PASSWORD_HISTORY_KEY,
400        Secure.LOCK_PATTERN_ENABLED,
401        Secure.LOCK_BIOMETRIC_WEAK_FLAGS,
402        Secure.LOCK_PATTERN_VISIBLE,
403        Secure.LOCK_PATTERN_TACTILE_FEEDBACK_ENABLED
404        };
405}
406