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.server.net.watchlist; 18 19 import android.annotation.Nullable; 20 import android.content.ContentValues; 21 import android.content.Context; 22 import android.database.Cursor; 23 import android.database.sqlite.SQLiteDatabase; 24 import android.database.sqlite.SQLiteDatabaseCorruptException; 25 import android.database.sqlite.SQLiteException; 26 import android.database.sqlite.SQLiteOpenHelper; 27 import android.os.Environment; 28 import android.util.Slog; 29 30 import com.android.internal.util.HexDump; 31 32 import java.io.File; 33 import java.util.HashMap; 34 import java.util.HashSet; 35 import java.util.Set; 36 37 /** 38 * Helper class to process watchlist read / save watchlist reports. 39 */ 40 class WatchlistReportDbHelper extends SQLiteOpenHelper { 41 42 private static final String TAG = "WatchlistReportDbHelper"; 43 44 private static final String NAME = "watchlist_report.db"; 45 private static final int VERSION = 2; 46 47 private static final int IDLE_CONNECTION_TIMEOUT_MS = 30000; 48 49 private static class WhiteListReportContract { 50 private static final String TABLE = "records"; 51 private static final String APP_DIGEST = "app_digest"; 52 private static final String CNC_DOMAIN = "cnc_domain"; 53 private static final String TIMESTAMP = "timestamp"; 54 } 55 56 private static final String CREATE_TABLE_MODEL = "CREATE TABLE " 57 + WhiteListReportContract.TABLE + "(" 58 + WhiteListReportContract.APP_DIGEST + " BLOB," 59 + WhiteListReportContract.CNC_DOMAIN + " TEXT," 60 + WhiteListReportContract.TIMESTAMP + " INTEGER DEFAULT 0" + " )"; 61 62 private static final int INDEX_DIGEST = 0; 63 private static final int INDEX_CNC_DOMAIN = 1; 64 private static final int INDEX_TIMESTAMP = 2; 65 66 private static final String[] DIGEST_DOMAIN_PROJECTION = 67 new String[] { 68 WhiteListReportContract.APP_DIGEST, 69 WhiteListReportContract.CNC_DOMAIN 70 }; 71 72 private static WatchlistReportDbHelper sInstance; 73 74 /** 75 * Aggregated watchlist records. 76 */ 77 public static class AggregatedResult { 78 // A list of digests that visited c&c domain or ip before. 79 final Set<String> appDigestList; 80 81 // The c&c domain or ip visited before. 82 @Nullable final String cncDomainVisited; 83 84 // A list of app digests and c&c domain visited. 85 final HashMap<String, String> appDigestCNCList; 86 AggregatedResult(Set<String> appDigestList, String cncDomainVisited, HashMap<String, String> appDigestCNCList)87 public AggregatedResult(Set<String> appDigestList, String cncDomainVisited, 88 HashMap<String, String> appDigestCNCList) { 89 this.appDigestList = appDigestList; 90 this.cncDomainVisited = cncDomainVisited; 91 this.appDigestCNCList = appDigestCNCList; 92 } 93 } 94 getSystemWatchlistDbFile()95 static File getSystemWatchlistDbFile() { 96 return new File(Environment.getDataSystemDirectory(), NAME); 97 } 98 WatchlistReportDbHelper(Context context)99 private WatchlistReportDbHelper(Context context) { 100 super(context, getSystemWatchlistDbFile().getAbsolutePath(), null, VERSION); 101 // Memory optimization - close idle connections after 30s of inactivity 102 setIdleConnectionTimeout(IDLE_CONNECTION_TIMEOUT_MS); 103 } 104 getInstance(Context context)105 public static synchronized WatchlistReportDbHelper getInstance(Context context) { 106 if (sInstance != null) { 107 return sInstance; 108 } 109 sInstance = new WatchlistReportDbHelper(context); 110 return sInstance; 111 } 112 113 @Override onCreate(SQLiteDatabase db)114 public void onCreate(SQLiteDatabase db) { 115 db.execSQL(CREATE_TABLE_MODEL); 116 } 117 118 @Override onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion)119 public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { 120 // TODO: For now, drop older tables and recreate new ones. 121 db.execSQL("DROP TABLE IF EXISTS " + WhiteListReportContract.TABLE); 122 onCreate(db); 123 } 124 125 /** 126 * Insert new watchlist record. 127 * 128 * @param appDigest The digest of an app. 129 * @param cncDomain C&C domain that app visited. 130 * @return True if success. 131 */ insertNewRecord(byte[] appDigest, String cncDomain, long timestamp)132 public boolean insertNewRecord(byte[] appDigest, String cncDomain, 133 long timestamp) { 134 final SQLiteDatabase db; 135 try { 136 db = getWritableDatabase(); 137 } catch (SQLiteException e) { 138 Slog.e(TAG, "Error opening the database to insert a new record", e); 139 return false; 140 } 141 final ContentValues values = new ContentValues(); 142 values.put(WhiteListReportContract.APP_DIGEST, appDigest); 143 values.put(WhiteListReportContract.CNC_DOMAIN, cncDomain); 144 values.put(WhiteListReportContract.TIMESTAMP, timestamp); 145 return db.insert(WhiteListReportContract.TABLE, null, values) != -1; 146 } 147 148 /** 149 * Aggregate all records in database before input timestamp, and return a 150 * rappor encoded result. 151 */ 152 @Nullable getAggregatedRecords(long untilTimestamp)153 public AggregatedResult getAggregatedRecords(long untilTimestamp) { 154 final String selectStatement = WhiteListReportContract.TIMESTAMP + " < ?"; 155 156 final SQLiteDatabase db; 157 try { 158 db = getReadableDatabase(); 159 } catch (SQLiteException e) { 160 Slog.e(TAG, "Error opening the database", e); 161 return null; 162 } 163 Cursor c = null; 164 try { 165 c = db.query(true /* distinct */, 166 WhiteListReportContract.TABLE, DIGEST_DOMAIN_PROJECTION, selectStatement, 167 new String[]{Long.toString(untilTimestamp)}, null, null, 168 null, null); 169 if (c == null) { 170 return null; 171 } 172 final HashSet<String> appDigestList = new HashSet<>(); 173 final HashMap<String, String> appDigestCNCList = new HashMap<>(); 174 String cncDomainVisited = null; 175 while (c.moveToNext()) { 176 // We use hex string here as byte[] cannot be a key in HashMap. 177 String digestHexStr = HexDump.toHexString(c.getBlob(INDEX_DIGEST)); 178 String cncDomain = c.getString(INDEX_CNC_DOMAIN); 179 180 appDigestList.add(digestHexStr); 181 if (cncDomainVisited != null) { 182 cncDomainVisited = cncDomain; 183 } 184 appDigestCNCList.put(digestHexStr, cncDomain); 185 } 186 return new AggregatedResult(appDigestList, cncDomainVisited, appDigestCNCList); 187 } finally { 188 if (c != null) { 189 c.close(); 190 } 191 } 192 } 193 194 /** 195 * Remove all the records before input timestamp. 196 * 197 * @return True if success. 198 */ cleanup(long untilTimestamp)199 public boolean cleanup(long untilTimestamp) { 200 final SQLiteDatabase db; 201 try { 202 db = getWritableDatabase(); 203 } catch (SQLiteException e) { 204 Slog.e(TAG, "Error opening the database to cleanup", e); 205 return false; 206 } 207 final String clause = WhiteListReportContract.TIMESTAMP + "< " + untilTimestamp; 208 try { 209 return db.delete(WhiteListReportContract.TABLE, clause, null) != 0; 210 } catch (SQLiteDatabaseCorruptException e) { 211 Slog.e(TAG, "Error deleting records", e); 212 return false; 213 } 214 } 215 } 216