blob: 8352ca60e3620622d9a9f2fda80ae6e2f4fcd6b2 [file] [log] [blame]
/*
* 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);
}
}
}