blob: ac6d79541bc1078ad7dc302e31de256f35a298c4 [file] [log] [blame]
/*
* Copyright (C) 2020 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.pm.verify.domain;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.UserIdInt;
import android.content.pm.Signature;
import android.content.pm.verify.domain.DomainVerificationState;
import android.os.UserHandle;
import android.text.TextUtils;
import android.util.ArrayMap;
import android.util.ArraySet;
import android.util.PackageUtils;
import android.util.SparseArray;
import com.android.modules.utils.TypedXmlPullParser;
import com.android.modules.utils.TypedXmlSerializer;
import com.android.server.pm.SettingsXml;
import com.android.server.pm.verify.domain.models.DomainVerificationInternalUserState;
import com.android.server.pm.verify.domain.models.DomainVerificationPkgState;
import com.android.server.pm.verify.domain.models.DomainVerificationStateMap;
import org.xmlpull.v1.XmlPullParserException;
import java.io.IOException;
import java.util.Collection;
import java.util.UUID;
import java.util.function.Function;
public class DomainVerificationPersistence {
private static final String TAG = "DomainVerificationPersistence";
public static final String TAG_DOMAIN_VERIFICATIONS = "domain-verifications";
public static final String TAG_ACTIVE = "active";
public static final String TAG_RESTORED = "restored";
public static final String TAG_PACKAGE_STATE = "package-state";
private static final String ATTR_PACKAGE_NAME = "packageName";
private static final String ATTR_ID = "id";
private static final String ATTR_HAS_AUTO_VERIFY_DOMAINS = "hasAutoVerifyDomains";
private static final String ATTR_SIGNATURE = "signature";
private static final String TAG_USER_STATES = "user-states";
public static final String TAG_USER_STATE = "user-state";
public static final String ATTR_USER_ID = "userId";
public static final String ATTR_ALLOW_LINK_HANDLING = "allowLinkHandling";
public static final String TAG_ENABLED_HOSTS = "enabled-hosts";
public static final String TAG_HOST = "host";
private static final String TAG_STATE = "state";
public static final String TAG_DOMAIN = "domain";
public static final String ATTR_NAME = "name";
public static final String ATTR_STATE = "state";
/**
* @param pkgNameToSignature Converts package name to a string representation of its signature.
* Usually this is the SHA-256 hash from
* {@link PackageUtils#computeSignaturesSha256Digest(Signature[])},
* but can be an arbitrary string for testing purposes. Pass non-null
* to write out signatures, or null to ignore.
*/
public static void writeToXml(@NonNull TypedXmlSerializer xmlSerializer,
@NonNull DomainVerificationStateMap<DomainVerificationPkgState> attached,
@NonNull ArrayMap<String, DomainVerificationPkgState> pending,
@NonNull ArrayMap<String, DomainVerificationPkgState> restored,
@UserIdInt int userId, @Nullable Function<String, String> pkgNameToSignature)
throws IOException {
try (SettingsXml.Serializer serializer = SettingsXml.serializer(xmlSerializer)) {
try (SettingsXml.WriteSection ignored = serializer.startSection(
TAG_DOMAIN_VERIFICATIONS)) {
// Both attached and pending states are written to the active set, since both
// should be restored when the device reboots or runs a backup. They're merged into
// the same list because at read time the distinction isn't relevant. The pending
// list should generally be empty at this point anyways.
ArraySet<DomainVerificationPkgState> active = new ArraySet<>();
int attachedSize = attached.size();
for (int attachedIndex = 0; attachedIndex < attachedSize; attachedIndex++) {
active.add(attached.valueAt(attachedIndex));
}
int pendingSize = pending.size();
for (int pendingIndex = 0; pendingIndex < pendingSize; pendingIndex++) {
active.add(pending.valueAt(pendingIndex));
}
try (SettingsXml.WriteSection activeSection = serializer.startSection(TAG_ACTIVE)) {
writePackageStates(activeSection, active, userId, pkgNameToSignature);
}
try (SettingsXml.WriteSection restoredSection = serializer.startSection(
TAG_RESTORED)) {
writePackageStates(restoredSection, restored.values(), userId,
pkgNameToSignature);
}
}
}
}
private static void writePackageStates(@NonNull SettingsXml.WriteSection section,
@NonNull Collection<DomainVerificationPkgState> states, int userId,
@Nullable Function<String, String> pkgNameToSignature) throws IOException {
if (states.isEmpty()) {
return;
}
for (DomainVerificationPkgState state : states) {
writePkgStateToXml(section, state, userId, pkgNameToSignature);
}
}
@NonNull
public static ReadResult readFromXml(@NonNull TypedXmlPullParser parentParser)
throws IOException, XmlPullParserException {
ArrayMap<String, DomainVerificationPkgState> active = new ArrayMap<>();
ArrayMap<String, DomainVerificationPkgState> restored = new ArrayMap<>();
SettingsXml.ChildSection child = SettingsXml.parser(parentParser).children();
while (child.moveToNext()) {
switch (child.getName()) {
case TAG_ACTIVE:
readPackageStates(child, active);
break;
case TAG_RESTORED:
readPackageStates(child, restored);
break;
}
}
return new ReadResult(active, restored);
}
private static void readPackageStates(@NonNull SettingsXml.ReadSection section,
@NonNull ArrayMap<String, DomainVerificationPkgState> map) {
SettingsXml.ChildSection child = section.children();
while (child.moveToNext(TAG_PACKAGE_STATE)) {
DomainVerificationPkgState pkgState = createPkgStateFromXml(child);
if (pkgState != null) {
// State is unique by package name
map.put(pkgState.getPackageName(), pkgState);
}
}
}
/**
* Reads a package state from XML. Assumes the starting {@link #TAG_PACKAGE_STATE} has already
* been entered.
*/
@Nullable
private static DomainVerificationPkgState createPkgStateFromXml(
@NonNull SettingsXml.ReadSection section) {
String packageName = section.getString(ATTR_PACKAGE_NAME);
String idString = section.getString(ATTR_ID);
boolean hasAutoVerifyDomains = section.getBoolean(ATTR_HAS_AUTO_VERIFY_DOMAINS);
String signature = section.getString(ATTR_SIGNATURE);
if (TextUtils.isEmpty(packageName) || TextUtils.isEmpty(idString)) {
return null;
}
UUID id = UUID.fromString(idString);
final ArrayMap<String, Integer> stateMap = new ArrayMap<>();
final SparseArray<DomainVerificationInternalUserState> userStates = new SparseArray<>();
SettingsXml.ChildSection child = section.children();
while (child.moveToNext()) {
switch (child.getName()) {
case TAG_STATE:
readDomainStates(child, stateMap);
break;
case TAG_USER_STATES:
readUserStates(child, userStates);
break;
}
}
return new DomainVerificationPkgState(packageName, id, hasAutoVerifyDomains, stateMap,
userStates, signature);
}
private static void readUserStates(@NonNull SettingsXml.ReadSection section,
@NonNull SparseArray<DomainVerificationInternalUserState> userStates) {
SettingsXml.ChildSection child = section.children();
while (child.moveToNext(TAG_USER_STATE)) {
DomainVerificationInternalUserState userState = createUserStateFromXml(child);
if (userState != null) {
userStates.put(userState.getUserId(), userState);
}
}
}
private static void readDomainStates(@NonNull SettingsXml.ReadSection stateSection,
@NonNull ArrayMap<String, Integer> stateMap) {
SettingsXml.ChildSection child = stateSection.children();
while (child.moveToNext(TAG_DOMAIN)) {
String name = child.getString(ATTR_NAME);
int state = child.getInt(ATTR_STATE, DomainVerificationState.STATE_NO_RESPONSE);
stateMap.put(name, state);
}
}
private static void writePkgStateToXml(@NonNull SettingsXml.WriteSection parentSection,
@NonNull DomainVerificationPkgState pkgState, @UserIdInt int userId,
@Nullable Function<String, String> pkgNameToSignature) throws IOException {
String packageName = pkgState.getPackageName();
String signature = pkgNameToSignature == null
? null : pkgNameToSignature.apply(packageName);
if (signature == null) {
// If a package isn't available to get its signature, fallback to the previously stored
// result, which can occur if the package has been marked for restore but hasn't
// been installed on the new device yet.
signature = pkgState.getBackupSignatureHash();
}
try (SettingsXml.WriteSection ignored =
parentSection.startSection(TAG_PACKAGE_STATE)
.attribute(ATTR_PACKAGE_NAME, packageName)
.attribute(ATTR_ID, pkgState.getId().toString())
.attribute(ATTR_HAS_AUTO_VERIFY_DOMAINS,
pkgState.isHasAutoVerifyDomains())
.attribute(ATTR_SIGNATURE, signature)) {
writeStateMap(parentSection, pkgState.getStateMap());
writeUserStates(parentSection, userId, pkgState.getUserStates());
}
}
private static void writeUserStates(@NonNull SettingsXml.WriteSection parentSection,
@UserIdInt int userId,
@NonNull SparseArray<DomainVerificationInternalUserState> states) throws IOException {
int size = states.size();
if (size == 0) {
return;
}
try (SettingsXml.WriteSection section = parentSection.startSection(TAG_USER_STATES)) {
if (userId == UserHandle.USER_ALL) {
for (int index = 0; index < size; index++) {
writeUserStateToXml(section, states.valueAt(index));
}
} else {
DomainVerificationInternalUserState userState = states.get(userId);
if (userState != null) {
writeUserStateToXml(section, userState);
}
}
}
}
private static void writeStateMap(@NonNull SettingsXml.WriteSection parentSection,
@NonNull ArrayMap<String, Integer> stateMap) throws IOException {
if (stateMap.isEmpty()) {
return;
}
try (SettingsXml.WriteSection stateSection = parentSection.startSection(TAG_STATE)) {
int size = stateMap.size();
for (int index = 0; index < size; index++) {
stateSection.startSection(TAG_DOMAIN)
.attribute(ATTR_NAME, stateMap.keyAt(index))
.attribute(ATTR_STATE, stateMap.valueAt(index))
.finish();
}
}
}
/**
* Reads a user state from XML. Assumes the starting {@link #TAG_USER_STATE} has already been
* entered.
*/
@Nullable
private static DomainVerificationInternalUserState createUserStateFromXml(
@NonNull SettingsXml.ReadSection section) {
int userId = section.getInt(ATTR_USER_ID);
if (userId == -1) {
return null;
}
boolean allowLinkHandling = section.getBoolean(ATTR_ALLOW_LINK_HANDLING, false);
ArraySet<String> enabledHosts = new ArraySet<>();
SettingsXml.ChildSection child = section.children();
while (child.moveToNext(TAG_ENABLED_HOSTS)) {
readEnabledHosts(child, enabledHosts);
}
return new DomainVerificationInternalUserState(userId, enabledHosts, allowLinkHandling);
}
private static void readEnabledHosts(@NonNull SettingsXml.ReadSection section,
@NonNull ArraySet<String> enabledHosts) {
SettingsXml.ChildSection child = section.children();
while (child.moveToNext(TAG_HOST)) {
String hostName = child.getString(ATTR_NAME);
if (!TextUtils.isEmpty(hostName)) {
enabledHosts.add(hostName);
}
}
}
private static void writeUserStateToXml(@NonNull SettingsXml.WriteSection parentSection,
@NonNull DomainVerificationInternalUserState userState) throws IOException {
try (SettingsXml.WriteSection section =
parentSection.startSection(TAG_USER_STATE)
.attribute(ATTR_USER_ID, userState.getUserId())
.attribute(ATTR_ALLOW_LINK_HANDLING,
userState.isLinkHandlingAllowed())) {
ArraySet<String> enabledHosts = userState.getEnabledHosts();
if (!enabledHosts.isEmpty()) {
try (SettingsXml.WriteSection enabledHostsSection =
section.startSection(TAG_ENABLED_HOSTS)) {
int size = enabledHosts.size();
for (int index = 0; index < size; index++) {
enabledHostsSection.startSection(TAG_HOST)
.attribute(ATTR_NAME, enabledHosts.valueAt(index))
.finish();
}
}
}
}
}
public static class ReadResult {
@NonNull
public final ArrayMap<String, DomainVerificationPkgState> active;
@NonNull
public final ArrayMap<String, DomainVerificationPkgState> restored;
public ReadResult(@NonNull ArrayMap<String, DomainVerificationPkgState> active,
@NonNull ArrayMap<String, DomainVerificationPkgState> restored) {
this.active = active;
this.restored = restored;
}
}
}