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