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.content.ContentResolver;
  import android.content.Context;
  import android.content.pm.ApplicationInfo;
  import android.content.pm.PackageManager;
  import android.content.pm.PackageManager.NameNotFoundException;
  import android.content.pm.UserInfo;
  import android.os.Bundle;
  import android.os.DropBoxManager;
  import android.os.Handler;
  import android.os.Looper;
  import android.os.Message;
  import android.os.UserHandle;
  import android.os.UserManager;
  import android.provider.Settings;
  import android.text.TextUtils;
  import android.util.Slog;
  
  import com.android.internal.annotations.GuardedBy;
  import com.android.internal.annotations.VisibleForTesting;
  import com.android.internal.util.ArrayUtils;
  import com.android.internal.util.HexDump;
  
  import java.io.File;
  import java.io.IOException;
  import java.security.NoSuchAlgorithmException;
  import java.util.ArrayList;
  import java.util.GregorianCalendar;
  import java.util.HashMap;
  import java.util.HashSet;
  import java.util.List;
  import java.util.concurrent.ConcurrentHashMap;
  import java.util.concurrent.TimeUnit;
  
  /**
   * A Handler class for network watchlist logging on a background thread.
   */
  class WatchlistLoggingHandler extends Handler {
  
      private static final String TAG = WatchlistLoggingHandler.class.getSimpleName();
      private static final boolean DEBUG = NetworkWatchlistService.DEBUG;
  
      @VisibleForTesting
      static final int LOG_WATCHLIST_EVENT_MSG = 1;
      @VisibleForTesting
      static final int REPORT_RECORDS_IF_NECESSARY_MSG = 2;
      @VisibleForTesting
      static final int FORCE_REPORT_RECORDS_NOW_FOR_TEST_MSG = 3;
  
      private static final long ONE_DAY_MS = TimeUnit.DAYS.toMillis(1);
      private static final String DROPBOX_TAG = "network_watchlist_report";
  
      private final Context mContext;
      private final @Nullable DropBoxManager mDropBoxManager;
      private final ContentResolver mResolver;
      private final PackageManager mPm;
      private final WatchlistReportDbHelper mDbHelper;
      private final WatchlistConfig mConfig;
      private final WatchlistSettings mSettings;
      private int mPrimaryUserId = -1;
      // A cache for uid and apk digest mapping.
      // As uid won't be reused until reboot, it's safe to assume uid is unique per signature and app.
      // TODO: Use more efficient data structure.
      private final ConcurrentHashMap<Integer, byte[]> mCachedUidDigestMap =
              new ConcurrentHashMap<>();
  
      private interface WatchlistEventKeys {
          String HOST = "host";
          String IP_ADDRESSES = "ipAddresses";
          String UID = "uid";
          String TIMESTAMP = "timestamp";
      }
  
      WatchlistLoggingHandler(Context context, Looper looper) {
          super(looper);
          mContext = context;
          mPm = mContext.getPackageManager();
          mResolver = mContext.getContentResolver();
          mDbHelper = WatchlistReportDbHelper.getInstance(context);
          mConfig = WatchlistConfig.getInstance();
          mSettings = WatchlistSettings.getInstance();
          mDropBoxManager = mContext.getSystemService(DropBoxManager.class);
          mPrimaryUserId = getPrimaryUserId();
      }
  
      @Override
      public void handleMessage(Message msg) {
          switch (msg.what) {
              case LOG_WATCHLIST_EVENT_MSG: {
                  final Bundle data = msg.getData();
                  handleNetworkEvent(
                          data.getString(WatchlistEventKeys.HOST),
                          data.getStringArray(WatchlistEventKeys.IP_ADDRESSES),
                          data.getInt(WatchlistEventKeys.UID),
                          data.getLong(WatchlistEventKeys.TIMESTAMP)
                  );
                  break;
              }
              case REPORT_RECORDS_IF_NECESSARY_MSG:
                  tryAggregateRecords(getLastMidnightTime());
                  break;
              case FORCE_REPORT_RECORDS_NOW_FOR_TEST_MSG:
                  if (msg.obj instanceof Long) {
                      long lastRecordTime = (Long) msg.obj;
                      tryAggregateRecords(lastRecordTime);
                  } else {
                      Slog.e(TAG, "Msg.obj needs to be a Long object.");
                  }
                  break;
              default: {
                  Slog.d(TAG, "WatchlistLoggingHandler received an unknown of message.");
                  break;
              }
          }
      }
  
      /**
       * Get primary user id.
       * @return Primary user id. -1 if primary user not found.
       */
      private int getPrimaryUserId() {
          final UserInfo primaryUserInfo = ((UserManager) mContext.getSystemService(
                  Context.USER_SERVICE)).getPrimaryUser();
          if (primaryUserInfo != null) {
              return primaryUserInfo.id;
          }
          return -1;
      }
  
      /**
       * Return if a given package has testOnly is true.
       */
      private boolean isPackageTestOnly(int uid) {
          final ApplicationInfo ai;
          try {
              final String[] packageNames = mPm.getPackagesForUid(uid);
              if (packageNames == null || packageNames.length == 0) {
                  Slog.e(TAG, "Couldn't find package: " + packageNames);
                  return false;
              }
              ai = mPm.getApplicationInfo(packageNames[0], 0);
          } catch (NameNotFoundException e) {
              // Should not happen.
              return false;
          }
          return (ai.flags & ApplicationInfo.FLAG_TEST_ONLY) != 0;
      }
  
      /**
       * Report network watchlist records if we collected enough data.
       */
      public void reportWatchlistIfNecessary() {
          final Message msg = obtainMessage(REPORT_RECORDS_IF_NECESSARY_MSG);
          sendMessage(msg);
      }
  
      public void forceReportWatchlistForTest(long lastReportTime) {
          final Message msg = obtainMessage(FORCE_REPORT_RECORDS_NOW_FOR_TEST_MSG);
          msg.obj = lastReportTime;
          sendMessage(msg);
      }
  
      /**
       * Insert network traffic event to watchlist async queue processor.
       */
      public void asyncNetworkEvent(String host, String[] ipAddresses, int uid) {
          final Message msg = obtainMessage(LOG_WATCHLIST_EVENT_MSG);
          final Bundle bundle = new Bundle();
          bundle.putString(WatchlistEventKeys.HOST, host);
          bundle.putStringArray(WatchlistEventKeys.IP_ADDRESSES, ipAddresses);
          bundle.putInt(WatchlistEventKeys.UID, uid);
          bundle.putLong(WatchlistEventKeys.TIMESTAMP, System.currentTimeMillis());
          msg.setData(bundle);
          sendMessage(msg);
      }
  
      private void handleNetworkEvent(String hostname, String[] ipAddresses,
              int uid, long timestamp) {
          if (DEBUG) {
              Slog.i(TAG, "handleNetworkEvent with host: " + hostname + ", uid: " + uid);
          }
          // Update primary user id if necessary
          if (mPrimaryUserId == -1) {
              mPrimaryUserId = getPrimaryUserId();
          }
  
          // Only process primary user data
          if (UserHandle.getUserId(uid) != mPrimaryUserId) {
              if (DEBUG) {
                  Slog.i(TAG, "Do not log non-system user records");
              }
              return;
          }
          final String cncDomain = searchAllSubDomainsInWatchlist(hostname);
          if (cncDomain != null) {
              insertRecord(uid, cncDomain, timestamp);
          } else {
              final String cncIp = searchIpInWatchlist(ipAddresses);
              if (cncIp != null) {
                  insertRecord(uid, cncIp, timestamp);
              }
          }
      }
  
      private boolean insertRecord(int uid, String cncHost, long timestamp) {
          if (DEBUG) {
              Slog.i(TAG, "trying to insert record with host: " + cncHost + ", uid: " + uid);
          }
          if (!mConfig.isConfigSecure() && !isPackageTestOnly(uid)) {
              // Skip package if config is not secure and package is not TestOnly app.
              if (DEBUG) {
                  Slog.i(TAG, "uid: " + uid + " is not test only package");
              }
              return true;
          }
          final byte[] digest = getDigestFromUid(uid);
          if (digest == null) {
              Slog.e(TAG, "Cannot get digest from uid: " + uid);
              return false;
          }
          final boolean result = mDbHelper.insertNewRecord(digest, cncHost, timestamp);
          return result;
      }
  
      private boolean shouldReportNetworkWatchlist(long lastRecordTime) {
          final long lastReportTime = Settings.Global.getLong(mResolver,
                  Settings.Global.NETWORK_WATCHLIST_LAST_REPORT_TIME, 0L);
          if (lastRecordTime < lastReportTime) {
              Slog.i(TAG, "Last report time is larger than current time, reset report");
              mDbHelper.cleanup(lastReportTime);
              return false;
          }
          return lastRecordTime >= lastReportTime + ONE_DAY_MS;
      }
  
      private void tryAggregateRecords(long lastRecordTime) {
          long startTime = System.currentTimeMillis();
          try {
              // Check if it's necessary to generate watchlist report now.
              if (!shouldReportNetworkWatchlist(lastRecordTime)) {
                  Slog.i(TAG, "No need to aggregate record yet.");
                  return;
              }
              Slog.i(TAG, "Start aggregating watchlist records.");
              if (mDropBoxManager != null && mDropBoxManager.isTagEnabled(DROPBOX_TAG)) {
                  Settings.Global.putLong(mResolver,
                          Settings.Global.NETWORK_WATCHLIST_LAST_REPORT_TIME,
                          lastRecordTime);
                  final WatchlistReportDbHelper.AggregatedResult aggregatedResult =
                          mDbHelper.getAggregatedRecords(lastRecordTime);
                  if (aggregatedResult == null) {
                      Slog.i(TAG, "Cannot get result from database");
                      return;
                  }
                  // Get all digests for watchlist report, it should include all installed
                  // application digests and previously recorded app digests.
                  final List<String> digestsForReport = getAllDigestsForReport(aggregatedResult);
                  final byte[] secretKey = mSettings.getPrivacySecretKey();
                  final byte[] encodedResult = ReportEncoder.encodeWatchlistReport(mConfig,
                          secretKey, digestsForReport, aggregatedResult);
                  if (encodedResult != null) {
                      addEncodedReportToDropBox(encodedResult);
                  }
              } else {
                  Slog.w(TAG, "Network Watchlist dropbox tag is not enabled");
              }
              mDbHelper.cleanup(lastRecordTime);
          } finally {
              long endTime = System.currentTimeMillis();
              Slog.i(TAG, "Milliseconds spent on tryAggregateRecords(): " + (endTime - startTime));
          }
      }
  
      /**
       * Get all digests for watchlist report.
       * It should include:
       * (1) All installed app digests. We need this because we need to ensure after DP we don't know
       * if an app is really visited C&C site.
       * (2) App digests that previously recorded in database.
       */
      @VisibleForTesting
      List<String> getAllDigestsForReport(WatchlistReportDbHelper.AggregatedResult record) {
          // Step 1: Get all installed application digests.
          final List<ApplicationInfo> apps = mContext.getPackageManager().getInstalledApplications(
                  PackageManager.MATCH_ALL);
          final HashSet<String> result = new HashSet<>(apps.size() + record.appDigestCNCList.size());
          final int size = apps.size();
          for (int i = 0; i < size; i++) {
              byte[] digest = getDigestFromUid(apps.get(i).uid);
              if (digest != null) {
                  result.add(HexDump.toHexString(digest));
              } else {
                  Slog.e(TAG, "Cannot get digest from uid: " + apps.get(i).uid
                          + ",pkg: " + apps.get(i).packageName);
              }
          }
          // Step 2: Add all digests from records
          result.addAll(record.appDigestCNCList.keySet());
          return new ArrayList<>(result);
      }
  
      private void addEncodedReportToDropBox(byte[] encodedReport) {
          mDropBoxManager.addData(DROPBOX_TAG, encodedReport, 0);
      }
  
      /**
       * Get app digest from app uid.
       * Return null if system cannot get digest from uid.
       */
      @Nullable
      private byte[] getDigestFromUid(int uid) {
          return mCachedUidDigestMap.computeIfAbsent(uid, key -> {
              final String[] packageNames = mPm.getPackagesForUid(key);
              final int userId = UserHandle.getUserId(uid);
              if (!ArrayUtils.isEmpty(packageNames)) {
                  for (String packageName : packageNames) {
                      try {
                          final String apkPath = mPm.getPackageInfoAsUser(packageName,
                                  PackageManager.MATCH_DIRECT_BOOT_AWARE
                                          | PackageManager.MATCH_DIRECT_BOOT_UNAWARE, userId)
                                  .applicationInfo.publicSourceDir;
                          if (TextUtils.isEmpty(apkPath)) {
                              Slog.w(TAG, "Cannot find apkPath for " + packageName);
                              continue;
                          }
                          return DigestUtils.getSha256Hash(new File(apkPath));
                      } catch (NameNotFoundException | NoSuchAlgorithmException | IOException e) {
                          Slog.e(TAG, "Should not happen", e);
                          return null;
                      }
                  }
              }
              // Not able to find a package name for this uid, possibly the package is installed on
              // another user.
              return null;
          });
      }
  
      /**
       * Search if any ip addresses are in watchlist.
       *
       * @param ipAddresses Ip address that you want to search in watchlist.
       * @return Ip address that exists in watchlist, null if it does not match anything.
       */
      @Nullable
      private String searchIpInWatchlist(String[] ipAddresses) {
          for (String ipAddress : ipAddresses) {
              if (isIpInWatchlist(ipAddress)) {
                  return ipAddress;
              }
          }
          return null;
      }
  
      /** Search if the ip is in watchlist */
      private boolean isIpInWatchlist(String ipAddr) {
          if (ipAddr == null) {
              return false;
          }
          return mConfig.containsIp(ipAddr);
      }
  
      /** Search if the host is in watchlist */
      private boolean isHostInWatchlist(String host) {
          if (host == null) {
              return false;
          }
          return mConfig.containsDomain(host);
      }
  
      /**
       * Search if any sub-domain in host is in watchlist.
       *
       * @param host Host that we want to search.
       * @return Domain that exists in watchlist, null if it does not match anything.
       */
      @Nullable
      private String searchAllSubDomainsInWatchlist(String host) {
          if (host == null) {
              return null;
          }
          final String[] subDomains = getAllSubDomains(host);
          for (String subDomain : subDomains) {
              if (isHostInWatchlist(subDomain)) {
                  return subDomain;
              }
          }
          return null;
      }
  
      /** Get all sub-domains in a host */
      @VisibleForTesting
      @Nullable
      static String[] getAllSubDomains(String host) {
          if (host == null) {
              return null;
          }
          final ArrayList<String> subDomainList = new ArrayList<>();
          subDomainList.add(host);
          int index = host.indexOf(".");
          while (index != -1) {
              host = host.substring(index + 1);
              if (!TextUtils.isEmpty(host)) {
                  subDomainList.add(host);
              }
              index = host.indexOf(".");
          }
          return subDomainList.toArray(new String[0]);
      }
  
      static long getLastMidnightTime() {
          return getMidnightTimestamp(0);
      }
  
      static long getMidnightTimestamp(int daysBefore) {
          java.util.Calendar date = new GregorianCalendar();
          // reset hour, minutes, seconds and millis
          date.set(java.util.Calendar.HOUR_OF_DAY, 0);
          date.set(java.util.Calendar.MINUTE, 0);
          date.set(java.util.Calendar.SECOND, 0);
          date.set(java.util.Calendar.MILLISECOND, 0);
          date.add(java.util.Calendar.DAY_OF_MONTH, -daysBefore);
          return date.getTimeInMillis();
      }
  }
  