Merge "Add key-value mappers for improved safety."

This commit is contained in:
Mathew Inwood
2018-12-11 16:11:34 +00:00
committed by Android (Google) Code Review
3 changed files with 184 additions and 35 deletions

View File

@@ -48,7 +48,7 @@ public class SignedConfig {
private static final String CONFIG_KEY_VALUE = "value";
/**
* Represents config values targetting to an SDK range.
* Represents config values targeting an SDK range.
*/
public static class PerSdkConfig {
public final int minSdk;
@@ -90,11 +90,24 @@ public class SignedConfig {
/**
* Parse configuration from an APK.
*
* @param config config as read from the APK metadata.
* @param config Config string as read from the APK metadata.
* @param allowedKeys Set of allowed keys in the config. Any key/value mapping for a key not in
* this set will result in an {@link InvalidConfigException} being thrown.
* @param keyValueMappers Mappings for values per key. The keys in the top level map should be
* a subset of {@code allowedKeys}. The keys in the inner map indicate
* the set of allowed values for that keys value. This map will be
* applied to the value in the configuration. This is intended to allow
* enum-like values to be encoded as strings in the configuration, and
* mapped back to integers when the configuration is parsed.
*
* <p>Any config key with a value that does not appear in the
* corresponding map will result in an {@link InvalidConfigException}
* being thrown.
* @return Parsed configuration.
* @throws InvalidConfigException If there's a problem parsing the config.
*/
public static SignedConfig parse(String config, Set<String> allowedKeys)
public static SignedConfig parse(String config, Set<String> allowedKeys,
Map<String, Map<String, String>> keyValueMappers)
throws InvalidConfigException {
try {
JSONObject json = new JSONObject(config);
@@ -103,7 +116,8 @@ public class SignedConfig {
JSONArray perSdkConfig = json.getJSONArray(KEY_CONFIG);
List<PerSdkConfig> parsedConfigs = new ArrayList<>();
for (int i = 0; i < perSdkConfig.length(); ++i) {
parsedConfigs.add(parsePerSdkConfig(perSdkConfig.getJSONObject(i), allowedKeys));
parsedConfigs.add(parsePerSdkConfig(perSdkConfig.getJSONObject(i), allowedKeys,
keyValueMappers));
}
return new SignedConfig(version, parsedConfigs);
@@ -113,8 +127,17 @@ public class SignedConfig {
}
private static CharSequence quoted(Object s) {
if (s == null) {
return "null";
} else {
return "\"" + s + "\"";
}
}
@VisibleForTesting
static PerSdkConfig parsePerSdkConfig(JSONObject json, Set<String> allowedKeys)
static PerSdkConfig parsePerSdkConfig(JSONObject json, Set<String> allowedKeys,
Map<String, Map<String, String>> keyValueMappers)
throws JSONException, InvalidConfigException {
int minSdk = json.getInt(CONFIG_KEY_MIN_SDK);
int maxSdk = json.getInt(CONFIG_KEY_MAX_SDK);
@@ -123,12 +146,23 @@ public class SignedConfig {
for (int i = 0; i < valueArray.length(); ++i) {
JSONObject keyValuePair = valueArray.getJSONObject(i);
String key = keyValuePair.getString(CONFIG_KEY_KEY);
String value = keyValuePair.has(CONFIG_KEY_VALUE)
? keyValuePair.getString(CONFIG_KEY_VALUE)
Object valueObject = keyValuePair.has(CONFIG_KEY_VALUE)
? keyValuePair.get(CONFIG_KEY_VALUE)
: null;
String value = valueObject == JSONObject.NULL || valueObject == null
? null
: valueObject.toString();
if (!allowedKeys.contains(key)) {
throw new InvalidConfigException("Config key " + key + " is not allowed");
}
if (keyValueMappers.containsKey(key)) {
Map<String, String> mapper = keyValueMappers.get(key);
if (!mapper.containsKey(value)) {
throw new InvalidConfigException(
"Config key " + key + " contains unsupported value " + quoted(value));
}
value = mapper.get(value);
}
values.put(key, value);
}
return new PerSdkConfig(minSdk, maxSdk, values);

View File

@@ -17,8 +17,10 @@
package com.android.server.signedconfig;
import android.content.Context;
import android.content.pm.ApplicationInfo;
import android.os.Build;
import android.provider.Settings;
import android.util.ArrayMap;
import android.util.ArraySet;
import android.util.Slog;
@@ -37,6 +39,30 @@ class SignedConfigApplicator {
Settings.Global.HIDDEN_API_BLACKLIST_EXEMPTIONS
)));
private static final Map<String, String> HIDDEN_API_POLICY_KEY_MAP = makeMap(
"DEFAULT", String.valueOf(ApplicationInfo.HIDDEN_API_ENFORCEMENT_DEFAULT),
"DISABLED", String.valueOf(ApplicationInfo.HIDDEN_API_ENFORCEMENT_DISABLED),
"JUST_WARN", String.valueOf(ApplicationInfo.HIDDEN_API_ENFORCEMENT_JUST_WARN),
"ENABLED", String.valueOf(ApplicationInfo.HIDDEN_API_ENFORCEMENT_ENABLED)
);
private static final Map<String, Map<String, String>> KEY_VALUE_MAPPERS = makeMap(
Settings.Global.HIDDEN_API_POLICY, HIDDEN_API_POLICY_KEY_MAP
);
private static <K, V> Map<K, V> makeMap(Object... keyValuePairs) {
if (keyValuePairs.length % 2 != 0) {
throw new IllegalArgumentException();
}
final int len = keyValuePairs.length / 2;
ArrayMap<K, V> m = new ArrayMap<>(len);
for (int i = 0; i < len; ++i) {
m.put((K) keyValuePairs[i * 2], (V) keyValuePairs[(i * 2) + 1]);
}
return Collections.unmodifiableMap(m);
}
private final Context mContext;
private final String mSourcePackage;
@@ -75,7 +101,7 @@ class SignedConfigApplicator {
}
SignedConfig config;
try {
config = SignedConfig.parse(configStr, ALLOWED_KEYS);
config = SignedConfig.parse(configStr, ALLOWED_KEYS, KEY_VALUE_MAPPERS);
} catch (InvalidConfigException e) {
Slog.e(TAG, "Failed to parse config from package " + mSourcePackage, e);
return;

View File

@@ -20,8 +20,11 @@ import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.fail;
import static java.util.Collections.emptyMap;
import static java.util.Collections.emptySet;
import android.util.ArrayMap;
import androidx.test.runner.AndroidJUnit4;
import com.google.common.collect.Sets;
@@ -33,6 +36,7 @@ import org.junit.runner.RunWith;
import java.util.Arrays;
import java.util.Collections;
import java.util.Map;
import java.util.Set;
@@ -46,10 +50,25 @@ public class SignedConfigTest {
return Sets.newHashSet(values);
}
private static <K, V> Map<K, V> mapOf(Object... keyValuePairs) {
if (keyValuePairs.length % 2 != 0) {
throw new IllegalArgumentException();
}
final int len = keyValuePairs.length / 2;
ArrayMap<K, V> m = new ArrayMap<>(len);
for (int i = 0; i < len; ++i) {
m.put((K) keyValuePairs[i * 2], (V) keyValuePairs[(i * 2) + 1]);
}
return Collections.unmodifiableMap(m);
}
@Test
public void testParsePerSdkConfigSdkMinMax() throws JSONException, InvalidConfigException {
JSONObject json = new JSONObject("{\"minSdk\":2, \"maxSdk\": 3, \"values\": []}");
SignedConfig.PerSdkConfig config = SignedConfig.parsePerSdkConfig(json, emptySet());
SignedConfig.PerSdkConfig config = SignedConfig.parsePerSdkConfig(json, emptySet(),
emptyMap());
assertThat(config.minSdk).isEqualTo(2);
assertThat(config.maxSdk).isEqualTo(3);
}
@@ -58,7 +77,7 @@ public class SignedConfigTest {
public void testParsePerSdkConfigNoMinSdk() throws JSONException {
JSONObject json = new JSONObject("{\"maxSdk\": 3, \"values\": []}");
try {
SignedConfig.parsePerSdkConfig(json, emptySet());
SignedConfig.parsePerSdkConfig(json, emptySet(), emptyMap());
fail("Expected InvalidConfigException or JSONException");
} catch (JSONException | InvalidConfigException e) {
// expected
@@ -69,7 +88,7 @@ public class SignedConfigTest {
public void testParsePerSdkConfigNoMaxSdk() throws JSONException {
JSONObject json = new JSONObject("{\"minSdk\": 1, \"values\": []}");
try {
SignedConfig.parsePerSdkConfig(json, emptySet());
SignedConfig.parsePerSdkConfig(json, emptySet(), emptyMap());
fail("Expected InvalidConfigException or JSONException");
} catch (JSONException | InvalidConfigException e) {
// expected
@@ -80,7 +99,7 @@ public class SignedConfigTest {
public void testParsePerSdkConfigNoValues() throws JSONException {
JSONObject json = new JSONObject("{\"minSdk\": 1, \"maxSdk\": 3}");
try {
SignedConfig.parsePerSdkConfig(json, emptySet());
SignedConfig.parsePerSdkConfig(json, emptySet(), emptyMap());
fail("Expected InvalidConfigException or JSONException");
} catch (JSONException | InvalidConfigException e) {
// expected
@@ -88,10 +107,10 @@ public class SignedConfigTest {
}
@Test
public void testParsePerSdkConfigSdkNullMinSdk() throws JSONException, InvalidConfigException {
public void testParsePerSdkConfigSdkNullMinSdk() throws JSONException {
JSONObject json = new JSONObject("{\"minSdk\":null, \"maxSdk\": 3, \"values\": []}");
try {
SignedConfig.parsePerSdkConfig(json, emptySet());
SignedConfig.parsePerSdkConfig(json, emptySet(), emptyMap());
fail("Expected InvalidConfigException or JSONException");
} catch (JSONException | InvalidConfigException e) {
// expected
@@ -99,10 +118,10 @@ public class SignedConfigTest {
}
@Test
public void testParsePerSdkConfigSdkNullMaxSdk() throws JSONException, InvalidConfigException {
public void testParsePerSdkConfigSdkNullMaxSdk() throws JSONException {
JSONObject json = new JSONObject("{\"minSdk\":1, \"maxSdk\": null, \"values\": []}");
try {
SignedConfig.parsePerSdkConfig(json, emptySet());
SignedConfig.parsePerSdkConfig(json, emptySet(), emptyMap());
fail("Expected InvalidConfigException or JSONException");
} catch (JSONException | InvalidConfigException e) {
// expected
@@ -113,7 +132,7 @@ public class SignedConfigTest {
public void testParsePerSdkConfigNullValues() throws JSONException {
JSONObject json = new JSONObject("{\"minSdk\": 1, \"maxSdk\": 3, \"values\": null}");
try {
SignedConfig.parsePerSdkConfig(json, emptySet());
SignedConfig.parsePerSdkConfig(json, emptySet(), emptyMap());
fail("Expected InvalidConfigException or JSONException");
} catch (JSONException | InvalidConfigException e) {
// expected
@@ -124,7 +143,8 @@ public class SignedConfigTest {
public void testParsePerSdkConfigZeroValues()
throws JSONException, InvalidConfigException {
JSONObject json = new JSONObject("{\"minSdk\": 1, \"maxSdk\": 3, \"values\": []}");
SignedConfig.PerSdkConfig config = SignedConfig.parsePerSdkConfig(json, setOf("a", "b"));
SignedConfig.PerSdkConfig config = SignedConfig.parsePerSdkConfig(json, setOf("a", "b"),
emptyMap());
assertThat(config.values).hasSize(0);
}
@@ -133,10 +153,22 @@ public class SignedConfigTest {
throws JSONException, InvalidConfigException {
JSONObject json = new JSONObject(
"{\"minSdk\": 1, \"maxSdk\": 1, \"values\": [{\"key\":\"a\", \"value\": \"1\"}]}");
SignedConfig.PerSdkConfig config = SignedConfig.parsePerSdkConfig(json, setOf("a", "b"));
SignedConfig.PerSdkConfig config = SignedConfig.parsePerSdkConfig(json, setOf("a", "b"),
emptyMap());
assertThat(config.values).containsExactly("a", "1");
}
@Test
public void testParsePerSdkConfigSingleKeyNullValue()
throws JSONException, InvalidConfigException {
JSONObject json = new JSONObject(
"{\"minSdk\": 1, \"maxSdk\": 1, \"values\": [{\"key\":\"a\", \"value\": null}]}");
SignedConfig.PerSdkConfig config = SignedConfig.parsePerSdkConfig(json, setOf("a", "b"),
emptyMap());
assertThat(config.values.keySet()).containsExactly("a");
assertThat(config.values.get("a")).isNull();
}
@Test
public void testParsePerSdkConfigMultiKeys()
throws JSONException, InvalidConfigException {
@@ -144,7 +176,7 @@ public class SignedConfigTest {
"{\"minSdk\": 1, \"maxSdk\": 1, \"values\": [{\"key\":\"a\", \"value\": \"1\"}, "
+ "{\"key\":\"c\", \"value\": \"2\"}]}");
SignedConfig.PerSdkConfig config = SignedConfig.parsePerSdkConfig(
json, setOf("a", "b", "c"));
json, setOf("a", "b", "c"), emptyMap());
assertThat(config.values).containsExactly("a", "1", "c", "2");
}
@@ -153,19 +185,75 @@ public class SignedConfigTest {
JSONObject json = new JSONObject(
"{\"minSdk\": 1, \"maxSdk\": 1, \"values\": [{\"key\":\"a\", \"value\": \"1\"}]}");
try {
SignedConfig.parsePerSdkConfig(json, setOf("b"));
SignedConfig.parsePerSdkConfig(json, setOf("b"), emptyMap());
fail("Expected InvalidConfigException or JSONException");
} catch (JSONException | InvalidConfigException e) {
// expected
}
}
@Test
public void testParsePerSdkConfigSingleKeyWithMap()
throws JSONException, InvalidConfigException {
JSONObject json = new JSONObject(
"{\"minSdk\": 1, \"maxSdk\": 1, \"values\": [{\"key\":\"a\", \"value\": \"1\"}]}");
SignedConfig.PerSdkConfig config = SignedConfig.parsePerSdkConfig(json, setOf("a"),
mapOf("a", mapOf("1", "one")));
assertThat(config.values).containsExactly("a", "one");
}
@Test
public void testParsePerSdkConfigSingleKeyWithMapInvalidValue() throws JSONException {
JSONObject json = new JSONObject(
"{\"minSdk\": 1, \"maxSdk\": 1, \"values\": [{\"key\":\"a\", \"value\": \"2\"}]}");
try {
SignedConfig.parsePerSdkConfig(json, setOf("b"), mapOf("a", mapOf("1", "one")));
fail("Expected InvalidConfigException");
} catch (InvalidConfigException e) {
// expected
}
}
@Test
public void testParsePerSdkConfigMultiKeysWithMap()
throws JSONException, InvalidConfigException {
JSONObject json = new JSONObject(
"{\"minSdk\": 1, \"maxSdk\": 1, \"values\": [{\"key\":\"a\", \"value\": \"1\"},"
+ "{\"key\":\"b\", \"value\": \"1\"}]}");
SignedConfig.PerSdkConfig config = SignedConfig.parsePerSdkConfig(json, setOf("a", "b"),
mapOf("a", mapOf("1", "one")));
assertThat(config.values).containsExactly("a", "one", "b", "1");
}
@Test
public void testParsePerSdkConfigSingleKeyWithMapToNull()
throws JSONException, InvalidConfigException {
JSONObject json = new JSONObject(
"{\"minSdk\": 1, \"maxSdk\": 1, \"values\": [{\"key\":\"a\", \"value\": \"1\"}]}");
SignedConfig.PerSdkConfig config = SignedConfig.parsePerSdkConfig(json, setOf("a"),
mapOf("a", mapOf("1", null)));
assertThat(config.values).containsExactly("a", null);
}
@Test
public void testParsePerSdkConfigSingleKeyWithMapFromNull()
throws JSONException, InvalidConfigException {
assertThat(mapOf(null, "allitnil")).containsExactly(null, "allitnil");
assertThat(mapOf(null, "allitnil").containsKey(null)).isTrue();
JSONObject json = new JSONObject(
"{\"minSdk\": 1, \"maxSdk\": 1, \"values\": [{\"key\":\"a\", \"value\": null}]}");
SignedConfig.PerSdkConfig config = SignedConfig.parsePerSdkConfig(json, setOf("a"),
mapOf("a", mapOf(null, "allitnil")));
assertThat(config.values).containsExactly("a", "allitnil");
}
@Test
public void testParsePerSdkConfigSingleKeyNoValue()
throws JSONException, InvalidConfigException {
JSONObject json = new JSONObject(
"{\"minSdk\": 1, \"maxSdk\": 1, \"values\": [{\"key\":\"a\"}]}");
SignedConfig.PerSdkConfig config = SignedConfig.parsePerSdkConfig(json, setOf("a", "b"));
SignedConfig.PerSdkConfig config = SignedConfig.parsePerSdkConfig(json, setOf("a", "b"),
emptyMap());
assertThat(config.values).containsExactly("a", null);
}
@@ -173,7 +261,7 @@ public class SignedConfigTest {
public void testParsePerSdkConfigValuesInvalid() throws JSONException {
JSONObject json = new JSONObject("{\"minSdk\": 1, \"maxSdk\": 1, \"values\": \"foo\"}");
try {
SignedConfig.parsePerSdkConfig(json, emptySet());
SignedConfig.parsePerSdkConfig(json, emptySet(), emptyMap());
fail("Expected InvalidConfigException or JSONException");
} catch (JSONException | InvalidConfigException e) {
// expected
@@ -184,7 +272,7 @@ public class SignedConfigTest {
public void testParsePerSdkConfigConfigEntryInvalid() throws JSONException {
JSONObject json = new JSONObject("{\"minSdk\": 1, \"maxSdk\": 1, \"values\": [1, 2]}");
try {
SignedConfig.parsePerSdkConfig(json, emptySet());
SignedConfig.parsePerSdkConfig(json, emptySet(), emptyMap());
fail("Expected InvalidConfigException or JSONException");
} catch (JSONException | InvalidConfigException e) {
// expected
@@ -195,7 +283,7 @@ public class SignedConfigTest {
public void testParsePerSdkConfigConfigEntryNull() throws JSONException {
JSONObject json = new JSONObject("{\"minSdk\": 1, \"maxSdk\": 1, \"values\": [null]}");
try {
SignedConfig.parsePerSdkConfig(json, emptySet());
SignedConfig.parsePerSdkConfig(json, emptySet(), emptyMap());
fail("Expected InvalidConfigException or JSONException");
} catch (JSONException | InvalidConfigException e) {
// expected
@@ -205,14 +293,15 @@ public class SignedConfigTest {
@Test
public void testParseVersion() throws InvalidConfigException {
SignedConfig config = SignedConfig.parse(
"{\"version\": 1, \"config\": []}", emptySet());
"{\"version\": 1, \"config\": []}", emptySet(), emptyMap());
assertThat(config.version).isEqualTo(1);
}
@Test
public void testParseVersionInvalid() {
try {
SignedConfig.parse("{\"version\": \"notanint\", \"config\": []}", emptySet());
SignedConfig.parse("{\"version\": \"notanint\", \"config\": []}", emptySet(),
emptyMap());
fail("Expected InvalidConfigException");
} catch (InvalidConfigException e) {
//expected
@@ -222,7 +311,7 @@ public class SignedConfigTest {
@Test
public void testParseNoVersion() {
try {
SignedConfig.parse("{\"config\": []}", emptySet());
SignedConfig.parse("{\"config\": []}", emptySet(), emptyMap());
fail("Expected InvalidConfigException");
} catch (InvalidConfigException e) {
//expected
@@ -232,7 +321,7 @@ public class SignedConfigTest {
@Test
public void testParseNoConfig() {
try {
SignedConfig.parse("{\"version\": 1}", emptySet());
SignedConfig.parse("{\"version\": 1}", emptySet(), emptyMap());
fail("Expected InvalidConfigException");
} catch (InvalidConfigException e) {
//expected
@@ -242,7 +331,7 @@ public class SignedConfigTest {
@Test
public void testParseConfigNull() {
try {
SignedConfig.parse("{\"version\": 1, \"config\": null}", emptySet());
SignedConfig.parse("{\"version\": 1, \"config\": null}", emptySet(), emptyMap());
fail("Expected InvalidConfigException");
} catch (InvalidConfigException e) {
//expected
@@ -252,7 +341,7 @@ public class SignedConfigTest {
@Test
public void testParseVersionNull() {
try {
SignedConfig.parse("{\"version\": null, \"config\": []}", emptySet());
SignedConfig.parse("{\"version\": null, \"config\": []}", emptySet(), emptyMap());
fail("Expected InvalidConfigException");
} catch (InvalidConfigException e) {
//expected
@@ -262,7 +351,7 @@ public class SignedConfigTest {
@Test
public void testParseConfigInvalidEntry() {
try {
SignedConfig.parse("{\"version\": 1, \"config\": [{}]}", emptySet());
SignedConfig.parse("{\"version\": 1, \"config\": [{}]}", emptySet(), emptyMap());
fail("Expected InvalidConfigException");
} catch (InvalidConfigException e) {
//expected
@@ -273,7 +362,7 @@ public class SignedConfigTest {
public void testParseSdkConfigSingle() throws InvalidConfigException {
SignedConfig config = SignedConfig.parse(
"{\"version\": 1, \"config\":[{\"minSdk\": 1, \"maxSdk\": 1, \"values\": []}]}",
emptySet());
emptySet(), emptyMap());
assertThat(config.perSdkConfig).hasSize(1);
}
@@ -281,7 +370,8 @@ public class SignedConfigTest {
public void testParseSdkConfigMultiple() throws InvalidConfigException {
SignedConfig config = SignedConfig.parse(
"{\"version\": 1, \"config\":[{\"minSdk\": 1, \"maxSdk\": 1, \"values\": []}, "
+ "{\"minSdk\": 2, \"maxSdk\": 2, \"values\": []}]}", emptySet());
+ "{\"minSdk\": 2, \"maxSdk\": 2, \"values\": []}]}", emptySet(),
emptyMap());
assertThat(config.perSdkConfig).hasSize(2);
}
@@ -314,7 +404,6 @@ public class SignedConfigTest {
SignedConfig config = new SignedConfig(0, Arrays.asList(sdk13, sdk46));
assertThat(config.getMatchingConfig(2)).isEqualTo(sdk13);
}
@Test
public void testGetMatchingConfigNoMatch() {
SignedConfig.PerSdkConfig sdk1 = new SignedConfig.PerSdkConfig(