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