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