blob: c50f0d56c992e09c623b2c816e20062ba7a36e77 [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.os.Environment;
import android.util.AtomicFile;
import android.util.Log;
import android.util.Xml;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.FastXmlSerializer;
import com.android.internal.util.HexDump;
import com.android.internal.util.XmlUtils;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
import org.xmlpull.v1.XmlSerializer;
import java.io.File;
import java.io.FileDescriptor;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
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;
/**
* A util class to do watchlist settings operations, like setting watchlist, query if a domain
* exists in watchlist.
*/
class WatchlistSettings {
private static final String TAG = "WatchlistSettings";
// Settings xml will be stored in /data/system/network_watchlist/watchlist_settings.xml
static final String SYSTEM_WATCHLIST_DIR = "network_watchlist";
private static final String WATCHLIST_XML_FILE = "watchlist_settings.xml";
private static class XmlTags {
private static final String WATCHLIST_SETTINGS = "watchlist-settings";
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 WatchlistSettings sInstance = new WatchlistSettings();
private final AtomicFile mXmlFile;
private final Object mLock = new Object();
private HarmfulDigests mCrc32DomainDigests = new HarmfulDigests(new ArrayList<>());
private HarmfulDigests mSha256DomainDigests = new HarmfulDigests(new ArrayList<>());
private HarmfulDigests mCrc32IpDigests = new HarmfulDigests(new ArrayList<>());
private HarmfulDigests mSha256IpDigests = new HarmfulDigests(new ArrayList<>());
public static synchronized WatchlistSettings getInstance() {
return sInstance;
}
private WatchlistSettings() {
this(getSystemWatchlistFile(WATCHLIST_XML_FILE));
}
@VisibleForTesting
protected WatchlistSettings(File xmlFile) {
mXmlFile = new AtomicFile(xmlFile);
readSettingsLocked();
}
static File getSystemWatchlistFile(String filename) {
final File dataSystemDir = Environment.getDataSystemDirectory();
final File systemWatchlistDir = new File(dataSystemDir, SYSTEM_WATCHLIST_DIR);
systemWatchlistDir.mkdirs();
return new File(systemWatchlistDir, filename);
}
private void readSettingsLocked() {
synchronized (mLock) {
FileInputStream stream;
try {
stream = mXmlFile.openRead();
} catch (FileNotFoundException e) {
Log.i(TAG, "No watchlist settings: " + mXmlFile.getBaseFile().getAbsolutePath());
return;
}
final List<byte[]> crc32DomainList = new ArrayList<>();
final List<byte[]> sha256DomainList = new ArrayList<>();
final List<byte[]> crc32IpList = new ArrayList<>();
final List<byte[]> sha256IpList = new ArrayList<>();
try {
XmlPullParser parser = Xml.newPullParser();
parser.setInput(stream, StandardCharsets.UTF_8.name());
parser.nextTag();
parser.require(XmlPullParser.START_TAG, null, XmlTags.WATCHLIST_SETTINGS);
while (parser.nextTag() == XmlPullParser.START_TAG) {
String tagName = parser.getName();
switch (tagName) {
case XmlTags.CRC32_DOMAIN:
parseHash(parser, tagName, crc32DomainList);
break;
case XmlTags.CRC32_IP:
parseHash(parser, tagName, crc32IpList);
break;
case XmlTags.SHA256_DOMAIN:
parseHash(parser, tagName, sha256DomainList);
break;
case XmlTags.SHA256_IP:
parseHash(parser, tagName, sha256IpList);
break;
default:
Log.w(TAG, "Unknown element: " + parser.getName());
XmlUtils.skipCurrentTag(parser);
}
}
parser.require(XmlPullParser.END_TAG, null, XmlTags.WATCHLIST_SETTINGS);
writeSettingsToMemory(crc32DomainList, sha256DomainList, crc32IpList, sha256IpList);
} catch (IllegalStateException | NullPointerException | NumberFormatException |
XmlPullParserException | IOException | IndexOutOfBoundsException e) {
Log.w(TAG, "Failed parsing " + e);
} finally {
try {
stream.close();
} catch (IOException e) {
}
}
}
}
private void parseHash(XmlPullParser parser, String tagName, List<byte[]> hashSet)
throws IOException, XmlPullParserException {
parser.require(XmlPullParser.START_TAG, null, tagName);
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);
hashSet.add(hash);
}
parser.require(XmlPullParser.END_TAG, null, tagName);
}
/**
* Write network watchlist settings to disk.
* Adb should not use it, should use writeSettingsToMemory directly instead.
*/
public void writeSettingsToDisk(List<byte[]> newCrc32DomainList,
List<byte[]> newSha256DomainList,
List<byte[]> newCrc32IpList,
List<byte[]> newSha256IpList) {
synchronized (mLock) {
FileOutputStream stream;
try {
stream = mXmlFile.startWrite();
} catch (IOException e) {
Log.w(TAG, "Failed to write display settings: " + e);
return;
}
try {
XmlSerializer out = new FastXmlSerializer();
out.setOutput(stream, StandardCharsets.UTF_8.name());
out.startDocument(null, true);
out.startTag(null, XmlTags.WATCHLIST_SETTINGS);
writeHashSetToXml(out, XmlTags.SHA256_DOMAIN, newSha256DomainList);
writeHashSetToXml(out, XmlTags.SHA256_IP, newSha256IpList);
writeHashSetToXml(out, XmlTags.CRC32_DOMAIN, newCrc32DomainList);
writeHashSetToXml(out, XmlTags.CRC32_IP, newCrc32IpList);
out.endTag(null, XmlTags.WATCHLIST_SETTINGS);
out.endDocument();
mXmlFile.finishWrite(stream);
writeSettingsToMemory(newCrc32DomainList, newSha256DomainList, newCrc32IpList,
newSha256IpList);
} catch (IOException e) {
Log.w(TAG, "Failed to write display settings, restoring backup.", e);
mXmlFile.failWrite(stream);
}
}
}
/**
* Write network watchlist settings to memory.
*/
public void writeSettingsToMemory(List<byte[]> newCrc32DomainList,
List<byte[]> newSha256DomainList,
List<byte[]> newCrc32IpList,
List<byte[]> newSha256IpList) {
synchronized (mLock) {
mCrc32DomainDigests = new HarmfulDigests(newCrc32DomainList);
mCrc32IpDigests = new HarmfulDigests(newCrc32IpList);
mSha256DomainDigests = new HarmfulDigests(newSha256DomainList);
mSha256IpDigests = new HarmfulDigests(newSha256IpList);
}
}
private static void writeHashSetToXml(XmlSerializer out, String tagName, List<byte[]> hashSet)
throws IOException {
out.startTag(null, tagName);
for (byte[] hash : hashSet) {
out.startTag(null, XmlTags.HASH);
out.text(HexDump.toHexString(hash));
out.endTag(null, XmlTags.HASH);
}
out.endTag(null, tagName);
}
public boolean containsDomain(String domain) {
// First it does a quick CRC32 check.
final byte[] crc32 = getCrc32(domain);
if (!mCrc32DomainDigests.contains(crc32)) {
return false;
}
// Now we do a slow SHA256 check.
final byte[] sha256 = getSha256(domain);
return mSha256DomainDigests.contains(sha256);
}
public boolean containsIp(String ip) {
// First it does a quick CRC32 check.
final byte[] crc32 = getCrc32(ip);
if (!mCrc32IpDigests.contains(crc32)) {
return false;
}
// Now we do a slow SHA256 check.
final byte[] sha256 = getSha256(ip);
return mSha256IpDigests.contains(sha256);
}
/** Get CRC32 of a string */
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 void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
pw.println("Domain CRC32 digest list:");
mCrc32DomainDigests.dump(fd, pw, args);
pw.println("Domain SHA256 digest list:");
mSha256DomainDigests.dump(fd, pw, args);
pw.println("Ip CRC32 digest list:");
mCrc32IpDigests.dump(fd, pw, args);
pw.println("Ip SHA256 digest list:");
mSha256IpDigests.dump(fd, pw, args);
}
}