| /* |
| * 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); |
| } |
| } |
| } |