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.os.FileUtils;
21 import android.util.Log;
22 import android.util.Slog;
23 import android.util.Xml;
24 
25 import com.android.internal.annotations.VisibleForTesting;
26 import com.android.internal.util.HexDump;
27 import com.android.internal.util.XmlUtils;
28 
29 import org.xmlpull.v1.XmlPullParser;
30 import org.xmlpull.v1.XmlPullParserException;
31 
32 import java.io.File;
33 import java.io.FileDescriptor;
34 import java.io.FileInputStream;
35 import java.io.IOException;
36 import java.io.InputStream;
37 import java.io.PrintWriter;
38 import java.nio.charset.StandardCharsets;
39 import java.security.MessageDigest;
40 import java.security.NoSuchAlgorithmException;
41 import java.util.ArrayList;
42 import java.util.List;
43 import java.util.zip.CRC32;
44 
45 /**
46  * Class for watchlist config operations, like setting watchlist, query if a domain
47  * exists in watchlist.
48  */
49 class WatchlistConfig {
50     private static final String TAG = "WatchlistConfig";
51 
52     // Watchlist config that pushed by ConfigUpdater.
53     private static final String NETWORK_WATCHLIST_DB_PATH =
54             "/data/misc/network_watchlist/network_watchlist.xml";
55     private static final String NETWORK_WATCHLIST_DB_FOR_TEST_PATH =
56             "/data/misc/network_watchlist/network_watchlist_for_test.xml";
57 
58     private static class XmlTags {
59         private static final String WATCHLIST_CONFIG = "watchlist-config";
60         private static final String SHA256_DOMAIN = "sha256-domain";
61         private static final String CRC32_DOMAIN = "crc32-domain";
62         private static final String SHA256_IP = "sha256-ip";
63         private static final String CRC32_IP = "crc32-ip";
64         private static final String HASH = "hash";
65     }
66 
67     private static class CrcShaDigests {
68         public final HarmfulCrcs crc32s;
69         public final HarmfulDigests sha256Digests;
70 
CrcShaDigests(HarmfulCrcs crc32s, HarmfulDigests sha256Digests)71         CrcShaDigests(HarmfulCrcs crc32s, HarmfulDigests sha256Digests) {
72             this.crc32s = crc32s;
73             this.sha256Digests = sha256Digests;
74         }
75     }
76 
77     /*
78      * This is always true unless watchlist is being set by adb command, then it will be false
79      * until next reboot.
80      */
81     private boolean mIsSecureConfig = true;
82 
83     private final static WatchlistConfig sInstance = new WatchlistConfig();
84     private File mXmlFile;
85 
86     private volatile CrcShaDigests mDomainDigests;
87     private volatile CrcShaDigests mIpDigests;
88 
getInstance()89     public static WatchlistConfig getInstance() {
90         return sInstance;
91     }
92 
WatchlistConfig()93     private WatchlistConfig() {
94         this(new File(NETWORK_WATCHLIST_DB_PATH));
95     }
96 
97     @VisibleForTesting
WatchlistConfig(File xmlFile)98     protected WatchlistConfig(File xmlFile) {
99         mXmlFile = xmlFile;
100         reloadConfig();
101     }
102 
103     /**
104      * Reload watchlist by reading config file.
105      */
reloadConfig()106     public void reloadConfig() {
107         if (!mXmlFile.exists()) {
108             // No config file
109             return;
110         }
111         try (FileInputStream stream = new FileInputStream(mXmlFile)){
112             final List<byte[]> crc32DomainList = new ArrayList<>();
113             final List<byte[]> sha256DomainList = new ArrayList<>();
114             final List<byte[]> crc32IpList = new ArrayList<>();
115             final List<byte[]> sha256IpList = new ArrayList<>();
116 
117             XmlPullParser parser = Xml.newPullParser();
118             parser.setInput(stream, StandardCharsets.UTF_8.name());
119             parser.nextTag();
120             parser.require(XmlPullParser.START_TAG, null, XmlTags.WATCHLIST_CONFIG);
121             while (parser.nextTag() == XmlPullParser.START_TAG) {
122                 String tagName = parser.getName();
123                 switch (tagName) {
124                     case XmlTags.CRC32_DOMAIN:
125                         parseHashes(parser, tagName, crc32DomainList);
126                         break;
127                     case XmlTags.CRC32_IP:
128                         parseHashes(parser, tagName, crc32IpList);
129                         break;
130                     case XmlTags.SHA256_DOMAIN:
131                         parseHashes(parser, tagName, sha256DomainList);
132                         break;
133                     case XmlTags.SHA256_IP:
134                         parseHashes(parser, tagName, sha256IpList);
135                         break;
136                     default:
137                         Log.w(TAG, "Unknown element: " + parser.getName());
138                         XmlUtils.skipCurrentTag(parser);
139                 }
140             }
141             parser.require(XmlPullParser.END_TAG, null, XmlTags.WATCHLIST_CONFIG);
142             mDomainDigests = new CrcShaDigests(new HarmfulCrcs(crc32DomainList),
143                     new HarmfulDigests(sha256DomainList));
144             mIpDigests = new CrcShaDigests(new HarmfulCrcs(crc32IpList),
145                     new HarmfulDigests(sha256IpList));
146             Log.i(TAG, "Reload watchlist done");
147         } catch (IllegalStateException | NullPointerException | NumberFormatException |
148                 XmlPullParserException | IOException | IndexOutOfBoundsException e) {
149             Slog.e(TAG, "Failed parsing xml", e);
150         }
151     }
152 
parseHashes(XmlPullParser parser, String tagName, List<byte[]> hashList)153     private void parseHashes(XmlPullParser parser, String tagName, List<byte[]> hashList)
154             throws IOException, XmlPullParserException {
155         parser.require(XmlPullParser.START_TAG, null, tagName);
156         // Get all the hashes for this tag
157         while (parser.nextTag() == XmlPullParser.START_TAG) {
158             parser.require(XmlPullParser.START_TAG, null, XmlTags.HASH);
159             byte[] hash = HexDump.hexStringToByteArray(parser.nextText());
160             parser.require(XmlPullParser.END_TAG, null, XmlTags.HASH);
161             hashList.add(hash);
162         }
163         parser.require(XmlPullParser.END_TAG, null, tagName);
164     }
165 
containsDomain(String domain)166     public boolean containsDomain(String domain) {
167         final CrcShaDigests domainDigests = mDomainDigests;
168         if (domainDigests == null) {
169             // mDomainDigests is not initialized
170             return false;
171         }
172         // First it does a quick CRC32 check.
173         final int crc32 = getCrc32(domain);
174         if (!domainDigests.crc32s.contains(crc32)) {
175             return false;
176         }
177         // Now we do a slow SHA256 check.
178         final byte[] sha256 = getSha256(domain);
179         return domainDigests.sha256Digests.contains(sha256);
180     }
181 
containsIp(String ip)182     public boolean containsIp(String ip) {
183         final CrcShaDigests ipDigests = mIpDigests;
184         if (ipDigests == null) {
185             // mIpDigests is not initialized
186             return false;
187         }
188         // First it does a quick CRC32 check.
189         final int crc32 = getCrc32(ip);
190         if (!ipDigests.crc32s.contains(crc32)) {
191             return false;
192         }
193         // Now we do a slow SHA256 check.
194         final byte[] sha256 = getSha256(ip);
195         return ipDigests.sha256Digests.contains(sha256);
196     }
197 
198 
199     /** Get CRC32 of a string
200      */
getCrc32(String str)201     private int getCrc32(String str) {
202         final CRC32 crc = new CRC32();
203         crc.update(str.getBytes());
204         return (int) crc.getValue();
205     }
206 
207     /** Get SHA256 of a string */
getSha256(String str)208     private byte[] getSha256(String str) {
209         MessageDigest messageDigest;
210         try {
211             messageDigest = MessageDigest.getInstance("SHA256");
212         } catch (NoSuchAlgorithmException e) {
213             /* can't happen */
214             return null;
215         }
216         messageDigest.update(str.getBytes());
217         return messageDigest.digest();
218     }
219 
isConfigSecure()220     public boolean isConfigSecure() {
221         return mIsSecureConfig;
222     }
223 
224     @Nullable
225     /**
226      * Get watchlist config SHA-256 digest.
227      * Return null if watchlist config does not exist.
228      */
getWatchlistConfigHash()229     public byte[] getWatchlistConfigHash() {
230         if (!mXmlFile.exists()) {
231             return null;
232         }
233         try {
234             return DigestUtils.getSha256Hash(mXmlFile);
235         } catch (IOException | NoSuchAlgorithmException e) {
236             Log.e(TAG, "Unable to get watchlist config hash", e);
237         }
238         return null;
239     }
240 
241     /**
242      * This method will copy temporary test config and temporary override network watchlist config
243      * in memory. When device is rebooted, temporary test config will be removed, and system will
244      * use back the original watchlist config.
245      * Also, as temporary network watchlist config is not secure, we will mark it as insecure
246      * config and will be applied to testOnly applications only.
247      */
setTestMode(InputStream testConfigInputStream)248     public void setTestMode(InputStream testConfigInputStream) throws IOException {
249         Log.i(TAG, "Setting watchlist testing config");
250         // Copy test config
251         FileUtils.copyToFileOrThrow(testConfigInputStream,
252                 new File(NETWORK_WATCHLIST_DB_FOR_TEST_PATH));
253         // Mark config as insecure, so it will be applied to testOnly applications only
254         mIsSecureConfig = false;
255         // Reload watchlist config using test config file
256         mXmlFile = new File(NETWORK_WATCHLIST_DB_FOR_TEST_PATH);
257         reloadConfig();
258     }
259 
removeTestModeConfig()260     public void removeTestModeConfig() {
261         try {
262             final File f = new File(NETWORK_WATCHLIST_DB_FOR_TEST_PATH);
263             if (f.exists()) {
264                 f.delete();
265             }
266         } catch (Exception e) {
267             Log.e(TAG, "Unable to delete test config");
268         }
269     }
270 
dump(FileDescriptor fd, PrintWriter pw, String[] args)271     public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
272         final byte[] hash = getWatchlistConfigHash();
273         pw.println("Watchlist config hash: " + (hash != null ? HexDump.toHexString(hash) : null));
274         pw.println("Domain CRC32 digest list:");
275         // mDomainDigests won't go from non-null to null so it's safe
276         if (mDomainDigests != null) {
277             mDomainDigests.crc32s.dump(fd, pw, args);
278         }
279         pw.println("Domain SHA256 digest list:");
280         if (mDomainDigests != null) {
281             mDomainDigests.sha256Digests.dump(fd, pw, args);
282         }
283         pw.println("Ip CRC32 digest list:");
284         // mIpDigests won't go from non-null to null so it's safe
285         if (mIpDigests != null) {
286             mIpDigests.crc32s.dump(fd, pw, args);
287         }
288         pw.println("Ip SHA256 digest list:");
289         if (mIpDigests != null) {
290             mIpDigests.sha256Digests.dump(fd, pw, args);
291         }
292     }
293 }
294