Home | History | Annotate | Download | only in watchlist
      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