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