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