content: /*
   * Copyright (C) 2017 The Android Open Source Project
   *
   * Licensed under the Apache License, Version 2.0 (the "License");
   * you may not use this file except in compliance with the License.
   * You may obtain a copy of the License at
   *
   *      http://www.apache.org/licenses/LICENSE-2.0
   *
   * Unless required by applicable law or agreed to in writing, software
   * distributed under the License is distributed on an "AS IS" BASIS,
   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   * See the License for the specific language governing permissions and
   * limitations under the License.
   */
  
  package com.android.server.net.watchlist;
  
  import android.annotation.Nullable;
  import android.os.FileUtils;
  import android.util.AtomicFile;
  import android.util.Log;
  import android.util.Slog;
  import android.util.Xml;
  
  import com.android.internal.annotations.VisibleForTesting;
  import com.android.internal.util.HexDump;
  import com.android.internal.util.XmlUtils;
  
  import org.xmlpull.v1.XmlPullParser;
  import org.xmlpull.v1.XmlPullParserException;
  
  import java.io.File;
  import java.io.FileDescriptor;
  import java.io.FileInputStream;
  import java.io.IOException;
  import java.io.InputStream;
  import java.io.PrintWriter;
  import java.nio.charset.StandardCharsets;
  import java.security.MessageDigest;
  import java.security.NoSuchAlgorithmException;
  import java.util.ArrayList;
  import java.util.List;
  import java.util.zip.CRC32;
  
  /**
   * Class for watchlist config operations, like setting watchlist, query if a domain
   * exists in watchlist.
   */
  class WatchlistConfig {
      private static final String TAG = "WatchlistConfig";
  
      // Watchlist config that pushed by ConfigUpdater.
      private static final String NETWORK_WATCHLIST_DB_PATH =
              "/data/misc/network_watchlist/network_watchlist.xml";
      private static final String NETWORK_WATCHLIST_DB_FOR_TEST_PATH =
              "/data/misc/network_watchlist/network_watchlist_for_test.xml";
  
      private static class XmlTags {
          private static final String WATCHLIST_CONFIG = "watchlist-config";
          private static final String SHA256_DOMAIN = "sha256-domain";
          private static final String CRC32_DOMAIN = "crc32-domain";
          private static final String SHA256_IP = "sha256-ip";
          private static final String CRC32_IP = "crc32-ip";
          private static final String HASH = "hash";
      }
  
      private static class CrcShaDigests {
          final HarmfulDigests crc32Digests;
          final HarmfulDigests sha256Digests;
  
          public CrcShaDigests(HarmfulDigests crc32Digests, HarmfulDigests sha256Digests) {
              this.crc32Digests = crc32Digests;
              this.sha256Digests = sha256Digests;
          }
      }
  
      /*
       * This is always true unless watchlist is being set by adb command, then it will be false
       * until next reboot.
       */
      private boolean mIsSecureConfig = true;
  
      private final static WatchlistConfig sInstance = new WatchlistConfig();
      private File mXmlFile;
  
      private volatile CrcShaDigests mDomainDigests;
      private volatile CrcShaDigests mIpDigests;
  
      public static WatchlistConfig getInstance() {
          return sInstance;
      }
  
      private WatchlistConfig() {
          this(new File(NETWORK_WATCHLIST_DB_PATH));
      }
  
      @VisibleForTesting
      protected WatchlistConfig(File xmlFile) {
          mXmlFile = xmlFile;
          reloadConfig();
      }
  
      /**
       * Reload watchlist by reading config file.
       */
      public void reloadConfig() {
          if (!mXmlFile.exists()) {
              // No config file
              return;
          }
          try (FileInputStream stream = new FileInputStream(mXmlFile)){
              final List<byte[]> crc32DomainList = new ArrayList<>();
              final List<byte[]> sha256DomainList = new ArrayList<>();
              final List<byte[]> crc32IpList = new ArrayList<>();
              final List<byte[]> sha256IpList = new ArrayList<>();
  
              XmlPullParser parser = Xml.newPullParser();
              parser.setInput(stream, StandardCharsets.UTF_8.name());
              parser.nextTag();
              parser.require(XmlPullParser.START_TAG, null, XmlTags.WATCHLIST_CONFIG);
              while (parser.nextTag() == XmlPullParser.START_TAG) {
                  String tagName = parser.getName();
                  switch (tagName) {
                      case XmlTags.CRC32_DOMAIN:
                          parseHashes(parser, tagName, crc32DomainList);
                          break;
                      case XmlTags.CRC32_IP:
                          parseHashes(parser, tagName, crc32IpList);
                          break;
                      case XmlTags.SHA256_DOMAIN:
                          parseHashes(parser, tagName, sha256DomainList);
                          break;
                      case XmlTags.SHA256_IP:
                          parseHashes(parser, tagName, sha256IpList);
                          break;
                      default:
                          Log.w(TAG, "Unknown element: " + parser.getName());
                          XmlUtils.skipCurrentTag(parser);
                  }
              }
              parser.require(XmlPullParser.END_TAG, null, XmlTags.WATCHLIST_CONFIG);
              mDomainDigests = new CrcShaDigests(new HarmfulDigests(crc32DomainList),
                      new HarmfulDigests(sha256DomainList));
              mIpDigests = new CrcShaDigests(new HarmfulDigests(crc32IpList),
                      new HarmfulDigests(sha256IpList));
              Log.i(TAG, "Reload watchlist done");
          } catch (IllegalStateException | NullPointerException | NumberFormatException |
                  XmlPullParserException | IOException | IndexOutOfBoundsException e) {
              Slog.e(TAG, "Failed parsing xml", e);
          }
      }
  
      private void parseHashes(XmlPullParser parser, String tagName, List<byte[]> hashList)
              throws IOException, XmlPullParserException {
          parser.require(XmlPullParser.START_TAG, null, tagName);
          // Get all the hashes for this tag
          while (parser.nextTag() == XmlPullParser.START_TAG) {
              parser.require(XmlPullParser.START_TAG, null, XmlTags.HASH);
              byte[] hash = HexDump.hexStringToByteArray(parser.nextText());
              parser.require(XmlPullParser.END_TAG, null, XmlTags.HASH);
              hashList.add(hash);
          }
          parser.require(XmlPullParser.END_TAG, null, tagName);
      }
  
      public boolean containsDomain(String domain) {
          final CrcShaDigests domainDigests = mDomainDigests;
          if (domainDigests == null) {
              // mDomainDigests is not initialized
              return false;
          }
          // First it does a quick CRC32 check.
          final byte[] crc32 = getCrc32(domain);
          if (!domainDigests.crc32Digests.contains(crc32)) {
              return false;
          }
          // Now we do a slow SHA256 check.
          final byte[] sha256 = getSha256(domain);
          return domainDigests.sha256Digests.contains(sha256);
      }
  
      public boolean containsIp(String ip) {
          final CrcShaDigests ipDigests = mIpDigests;
          if (ipDigests == null) {
              // mIpDigests is not initialized
              return false;
          }
          // First it does a quick CRC32 check.
          final byte[] crc32 = getCrc32(ip);
          if (!ipDigests.crc32Digests.contains(crc32)) {
              return false;
          }
          // Now we do a slow SHA256 check.
          final byte[] sha256 = getSha256(ip);
          return ipDigests.sha256Digests.contains(sha256);
      }
  
  
      /** Get CRC32 of a string
       *
       * TODO: Review if we should use CRC32 or other algorithms
       */
      private byte[] getCrc32(String str) {
          final CRC32 crc = new CRC32();
          crc.update(str.getBytes());
          final long tmp = crc.getValue();
          return new byte[]{(byte) (tmp >> 24 & 255), (byte) (tmp >> 16 & 255),
                  (byte) (tmp >> 8 & 255), (byte) (tmp & 255)};
      }
  
      /** Get SHA256 of a string */
      private byte[] getSha256(String str) {
          MessageDigest messageDigest;
          try {
              messageDigest = MessageDigest.getInstance("SHA256");
          } catch (NoSuchAlgorithmException e) {
              /* can't happen */
              return null;
          }
          messageDigest.update(str.getBytes());
          return messageDigest.digest();
      }
  
      public boolean isConfigSecure() {
          return mIsSecureConfig;
      }
  
      @Nullable
      /**
       * Get watchlist config SHA-256 digest.
       * Return null if watchlist config does not exist.
       */
      public byte[] getWatchlistConfigHash() {
          if (!mXmlFile.exists()) {
              return null;
          }
          try {
              return DigestUtils.getSha256Hash(mXmlFile);
          } catch (IOException | NoSuchAlgorithmException e) {
              Log.e(TAG, "Unable to get watchlist config hash", e);
          }
          return null;
      }
  
      /**
       * This method will copy temporary test config and temporary override network watchlist config
       * in memory. When device is rebooted, temporary test config will be removed, and system will
       * use back the original watchlist config.
       * Also, as temporary network watchlist config is not secure, we will mark it as insecure
       * config and will be applied to testOnly applications only.
       */
      public void setTestMode(InputStream testConfigInputStream) throws IOException {
          Log.i(TAG, "Setting watchlist testing config");
          // Copy test config
          FileUtils.copyToFileOrThrow(testConfigInputStream,
                  new File(NETWORK_WATCHLIST_DB_FOR_TEST_PATH));
          // Mark config as insecure, so it will be applied to testOnly applications only
          mIsSecureConfig = false;
          // Reload watchlist config using test config file
          mXmlFile = new File(NETWORK_WATCHLIST_DB_FOR_TEST_PATH);
          reloadConfig();
      }
  
      public void removeTestModeConfig() {
          try {
              final File f = new File(NETWORK_WATCHLIST_DB_FOR_TEST_PATH);
              if (f.exists()) {
                  f.delete();
              }
          } catch (Exception e) {
              Log.e(TAG, "Unable to delete test config");
          }
      }
  
      public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
          final byte[] hash = getWatchlistConfigHash();
          pw.println("Watchlist config hash: " + (hash != null ? HexDump.toHexString(hash) : null));
          pw.println("Domain CRC32 digest list:");
          // mDomainDigests won't go from non-null to null so it's safe
          if (mDomainDigests != null) {
              mDomainDigests.crc32Digests.dump(fd, pw, args);
          }
          pw.println("Domain SHA256 digest list:");
          if (mDomainDigests != null) {
              mDomainDigests.sha256Digests.dump(fd, pw, args);
          }
          pw.println("Ip CRC32 digest list:");
          // mIpDigests won't go from non-null to null so it's safe
          if (mIpDigests != null) {
              mIpDigests.crc32Digests.dump(fd, pw, args);
          }
          pw.println("Ip SHA256 digest list:");
          if (mIpDigests != null) {
              mIpDigests.sha256Digests.dump(fd, pw, args);
          }
      }
  }
  