Merge "Add key-value mappers for improved safety."
diff --git a/services/core/java/com/android/server/signedconfig/SignedConfig.java b/services/core/java/com/android/server/signedconfig/SignedConfig.java
index a3f452c..e6bb800 100644
--- a/services/core/java/com/android/server/signedconfig/SignedConfig.java
+++ b/services/core/java/com/android/server/signedconfig/SignedConfig.java
@@ -48,7 +48,7 @@
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 @@
/**
* 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 @@
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 @@
}
+ 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 @@
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);
diff --git a/services/core/java/com/android/server/signedconfig/SignedConfigApplicator.java b/services/core/java/com/android/server/signedconfig/SignedConfigApplicator.java
index 2312f5f..e4d799a 100644
--- a/services/core/java/com/android/server/signedconfig/SignedConfigApplicator.java
+++ b/services/core/java/com/android/server/signedconfig/SignedConfigApplicator.java
@@ -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 @@
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 @@
}
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;
diff --git a/services/tests/servicestests/src/com/android/server/signedconfig/SignedConfigTest.java b/services/tests/servicestests/src/com/android/server/signedconfig/SignedConfigTest.java
index a9d4519..70fadd1 100644
--- a/services/tests/servicestests/src/com/android/server/signedconfig/SignedConfigTest.java
+++ b/services/tests/servicestests/src/com/android/server/signedconfig/SignedConfigTest.java
@@ -20,8 +20,11 @@
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 java.util.Arrays;
import java.util.Collections;
+import java.util.Map;
import java.util.Set;
@@ -46,10 +50,25 @@
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 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 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 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 @@
}
@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 @@
}
@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 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 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,18 +153,30 @@
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 {
JSONObject json = new JSONObject(
"{\"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,7 +185,7 @@
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
@@ -161,11 +193,67 @@
}
@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 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 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 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 @@
@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 @@
@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 @@
@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 @@
@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 @@
@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 @@
@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 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 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 @@
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(