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