1/*
2 * Copyright (C) 2017 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.keychain.internal;
18
19import android.content.ContentValues;
20import android.content.Context;
21import android.content.pm.PackageManager;
22import android.database.Cursor;
23import android.database.DatabaseUtils;
24import android.database.sqlite.SQLiteDatabase;
25import android.database.sqlite.SQLiteOpenHelper;
26import android.util.Log;
27
28public class GrantsDatabase {
29    private static final String TAG = "KeyChain";
30
31    private static final String DATABASE_NAME = "grants.db";
32    private static final int DATABASE_VERSION = 2;
33    private static final String TABLE_GRANTS = "grants";
34    private static final String GRANTS_ALIAS = "alias";
35    private static final String GRANTS_GRANTEE_UID = "uid";
36
37    private static final String SELECTION_COUNT_OF_MATCHING_GRANTS =
38            "SELECT COUNT(*) FROM "
39                    + TABLE_GRANTS
40                    + " WHERE "
41                    + GRANTS_GRANTEE_UID
42                    + "=? AND "
43                    + GRANTS_ALIAS
44                    + "=?";
45
46    private static final String SELECT_GRANTS_BY_UID_AND_ALIAS =
47            GRANTS_GRANTEE_UID + "=? AND " + GRANTS_ALIAS + "=?";
48
49    private static final String SELECTION_GRANTS_BY_UID = GRANTS_GRANTEE_UID + "=?";
50
51    private static final String SELECTION_GRANTS_BY_ALIAS = GRANTS_ALIAS + "=?";
52
53    private static final String TABLE_SELECTABLE = "userselectable";
54    private static final String SELECTABLE_IS_SELECTABLE = "is_selectable";
55    private static final String COUNT_SELECTABILITY_FOR_ALIAS =
56            "SELECT COUNT(*) FROM " + TABLE_SELECTABLE + " WHERE " + GRANTS_ALIAS + "=?";
57
58    public DatabaseHelper mDatabaseHelper;
59
60    private class DatabaseHelper extends SQLiteOpenHelper {
61        public DatabaseHelper(Context context) {
62            super(context, DATABASE_NAME, null /* CursorFactory */, DATABASE_VERSION);
63        }
64
65        void createSelectableTable(final SQLiteDatabase db) {
66            // There are some broken V1 databases that actually have the 'userselectable'
67            // already created. Only create it if it does not exist.
68            db.execSQL(
69                    "CREATE TABLE IF NOT EXISTS "
70                            + TABLE_SELECTABLE
71                            + " (  "
72                            + GRANTS_ALIAS
73                            + " STRING NOT NULL,  "
74                            + SELECTABLE_IS_SELECTABLE
75                            + " STRING NOT NULL,  "
76                            + "UNIQUE ("
77                            + GRANTS_ALIAS
78                            + "))");
79        }
80
81        @Override
82        public void onCreate(final SQLiteDatabase db) {
83            db.execSQL(
84                    "CREATE TABLE "
85                            + TABLE_GRANTS
86                            + " (  "
87                            + GRANTS_ALIAS
88                            + " STRING NOT NULL,  "
89                            + GRANTS_GRANTEE_UID
90                            + " INTEGER NOT NULL,  "
91                            + "UNIQUE ("
92                            + GRANTS_ALIAS
93                            + ","
94                            + GRANTS_GRANTEE_UID
95                            + "))");
96
97            createSelectableTable(db);
98        }
99
100        private boolean hasEntryInUserSelectableTable(final SQLiteDatabase db, final String alias) {
101            final long numMatches =
102                    DatabaseUtils.longForQuery(
103                            db,
104                            COUNT_SELECTABILITY_FOR_ALIAS,
105                            new String[] {alias});
106            return numMatches > 0;
107        }
108
109        @Override
110        public void onUpgrade(final SQLiteDatabase db, int oldVersion, final int newVersion) {
111            Log.w(TAG, "upgrade from version " + oldVersion + " to version " + newVersion);
112
113            if (oldVersion == 1) {
114                // Version 1 of the database does not have the 'userselectable' table, meaning
115                // upgraded keys could not be selected by users.
116                // The upgrade from version 1 to 2 consists of creating the 'userselectable'
117                // table and adding all existing keys as user-selectable ones into that table.
118                oldVersion++;
119                createSelectableTable(db);
120
121                try (Cursor cursor =
122                        db.query(
123                                TABLE_GRANTS,
124                                new String[] {GRANTS_ALIAS},
125                                null,
126                                null,
127                                GRANTS_ALIAS,
128                                null,
129                                null)) {
130
131                    while ((cursor != null) && (cursor.moveToNext())) {
132                        final String alias = cursor.getString(0);
133                        if (!hasEntryInUserSelectableTable(db, alias)) {
134                            final ContentValues values = new ContentValues();
135                            values.put(GRANTS_ALIAS, alias);
136                            values.put(SELECTABLE_IS_SELECTABLE, Boolean.toString(true));
137                            db.replace(TABLE_SELECTABLE, null, values);
138                        }
139                    }
140                }
141            }
142        }
143    }
144
145    public GrantsDatabase(Context context) {
146        mDatabaseHelper = new DatabaseHelper(context);
147    }
148
149    public void destroy() {
150        mDatabaseHelper.close();
151        mDatabaseHelper = null;
152    }
153
154    boolean hasGrantInternal(final SQLiteDatabase db, final int uid, final String alias) {
155        final long numMatches =
156                DatabaseUtils.longForQuery(
157                        db,
158                        SELECTION_COUNT_OF_MATCHING_GRANTS,
159                        new String[] {String.valueOf(uid), alias});
160        return numMatches > 0;
161    }
162
163    public boolean hasGrant(final int uid, final String alias) {
164        final SQLiteDatabase db = mDatabaseHelper.getWritableDatabase();
165        return hasGrantInternal(db, uid, alias);
166    }
167
168    public void setGrant(final int uid, final String alias, final boolean value) {
169        final SQLiteDatabase db = mDatabaseHelper.getWritableDatabase();
170        if (value) {
171            if (!hasGrantInternal(db, uid, alias)) {
172                final ContentValues values = new ContentValues();
173                values.put(GRANTS_ALIAS, alias);
174                values.put(GRANTS_GRANTEE_UID, uid);
175                db.insert(TABLE_GRANTS, GRANTS_ALIAS, values);
176            }
177        } else {
178            db.delete(
179                    TABLE_GRANTS,
180                    SELECT_GRANTS_BY_UID_AND_ALIAS,
181                    new String[] {String.valueOf(uid), alias});
182        }
183    }
184
185    public void removeAliasInformation(String alias) {
186        final SQLiteDatabase db = mDatabaseHelper.getWritableDatabase();
187        db.delete(TABLE_GRANTS, SELECTION_GRANTS_BY_ALIAS, new String[] {alias});
188        db.delete(TABLE_SELECTABLE, SELECTION_GRANTS_BY_ALIAS, new String[] {alias});
189    }
190
191    public void removeAllAliasesInformation() {
192        final SQLiteDatabase db = mDatabaseHelper.getWritableDatabase();
193        db.delete(TABLE_GRANTS, null /* whereClause */, null /* whereArgs */);
194        db.delete(TABLE_SELECTABLE, null /* whereClause */, null /* whereArgs */);
195    }
196
197    public void purgeOldGrants(PackageManager pm) {
198        final SQLiteDatabase db = mDatabaseHelper.getWritableDatabase();
199        db.beginTransaction();
200        try (Cursor cursor = db.query(
201                TABLE_GRANTS,
202                new String[] {GRANTS_GRANTEE_UID}, null, null, GRANTS_GRANTEE_UID, null, null)) {
203            while ((cursor != null) && (cursor.moveToNext())) {
204                final int uid = cursor.getInt(0);
205                final boolean packageExists = pm.getPackagesForUid(uid) != null;
206                if (packageExists) {
207                    continue;
208                }
209                Log.d(TAG, String.format(
210                        "deleting grants for UID %d because its package is no longer installed",
211                        uid));
212                db.delete(
213                        TABLE_GRANTS,
214                        SELECTION_GRANTS_BY_UID,
215                        new String[] {Integer.toString(uid)});
216            }
217            db.setTransactionSuccessful();
218        }
219
220        db.endTransaction();
221    }
222
223    public void setIsUserSelectable(final String alias, final boolean userSelectable) {
224        final SQLiteDatabase db = mDatabaseHelper.getWritableDatabase();
225        final ContentValues values = new ContentValues();
226        values.put(GRANTS_ALIAS, alias);
227        values.put(SELECTABLE_IS_SELECTABLE, Boolean.toString(userSelectable));
228
229        db.replace(TABLE_SELECTABLE, null, values);
230    }
231
232    public boolean isUserSelectable(final String alias) {
233        final SQLiteDatabase db = mDatabaseHelper.getWritableDatabase();
234        try (Cursor res =
235                db.query(
236                        TABLE_SELECTABLE,
237                        new String[] {SELECTABLE_IS_SELECTABLE},
238                        SELECTION_GRANTS_BY_ALIAS,
239                        new String[] {alias},
240                        null /* group by */,
241                        null /* having */,
242                        null /* order by */)) {
243            if (res == null || !res.moveToNext()) {
244                return false;
245            }
246
247            boolean isSelectable = Boolean.parseBoolean(res.getString(0));
248            if (res.getCount() > 1) {
249                // BUG! Should not have more than one result for any given alias.
250                Log.w(TAG, String.format("Have more than one result for alias %s", alias));
251            }
252            return isSelectable;
253        }
254    }
255}
256