Merge "Parser for signed configuration."
diff --git a/services/core/java/com/android/server/signedconfig/InvalidConfigException.java b/services/core/java/com/android/server/signedconfig/InvalidConfigException.java
new file mode 100644
index 0000000..f01baa4
--- /dev/null
+++ b/services/core/java/com/android/server/signedconfig/InvalidConfigException.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2018 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.signedconfig;
+
+/**
+ * Thrown when there is a problem parsing the config embedded in an APK.
+ */
+public class InvalidConfigException extends Exception {
+
+ public InvalidConfigException(String message) {
+ super(message);
+ }
+
+ public InvalidConfigException(String message, Exception cause) {
+ super(message, cause);
+ }
+
+
+}
diff --git a/services/core/java/com/android/server/signedconfig/SignedConfig.java b/services/core/java/com/android/server/signedconfig/SignedConfig.java
new file mode 100644
index 0000000..a3f452c
--- /dev/null
+++ b/services/core/java/com/android/server/signedconfig/SignedConfig.java
@@ -0,0 +1,137 @@
+/*
+ * Copyright (C) 2018 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.signedconfig;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Represents signed configuration.
+ *
+ * <p>This configuration should only be used if the signature has already been verified.
+ */
+public class SignedConfig {
+
+ private static final String KEY_VERSION = "version";
+ private static final String KEY_CONFIG = "config";
+
+ private static final String CONFIG_KEY_MIN_SDK = "minSdk";
+ private static final String CONFIG_KEY_MAX_SDK = "maxSdk";
+ private static final String CONFIG_KEY_VALUES = "values";
+ // TODO it may be better to use regular key/value pairs in a JSON object, rather than an array
+ // of objects with the 2 keys below.
+ private static final String CONFIG_KEY_KEY = "key";
+ private static final String CONFIG_KEY_VALUE = "value";
+
+ /**
+ * Represents config values targetting to an SDK range.
+ */
+ public static class PerSdkConfig {
+ public final int minSdk;
+ public final int maxSdk;
+ public final Map<String, String> values;
+
+ public PerSdkConfig(int minSdk, int maxSdk, Map<String, String> values) {
+ this.minSdk = minSdk;
+ this.maxSdk = maxSdk;
+ this.values = Collections.unmodifiableMap(values);
+ }
+
+ }
+
+ public final int version;
+ public final List<PerSdkConfig> perSdkConfig;
+
+ public SignedConfig(int version, List<PerSdkConfig> perSdkConfig) {
+ this.version = version;
+ this.perSdkConfig = Collections.unmodifiableList(perSdkConfig);
+ }
+
+ /**
+ * Find matching sdk config for a given SDK level.
+ *
+ * @param sdkVersion SDK version of device.
+ * @return Matching config, of {@code null} if there is none.
+ */
+ public PerSdkConfig getMatchingConfig(int sdkVersion) {
+ for (PerSdkConfig config : perSdkConfig) {
+ if (config.minSdk <= sdkVersion && sdkVersion <= config.maxSdk) {
+ return config;
+ }
+ }
+ // nothing matching
+ return null;
+ }
+
+ /**
+ * Parse configuration from an APK.
+ *
+ * @param config config as read from the APK metadata.
+ * @return Parsed configuration.
+ * @throws InvalidConfigException If there's a problem parsing the config.
+ */
+ public static SignedConfig parse(String config, Set<String> allowedKeys)
+ throws InvalidConfigException {
+ try {
+ JSONObject json = new JSONObject(config);
+ int version = json.getInt(KEY_VERSION);
+
+ 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));
+ }
+
+ return new SignedConfig(version, parsedConfigs);
+ } catch (JSONException e) {
+ throw new InvalidConfigException("Could not parse JSON", e);
+ }
+
+ }
+
+ @VisibleForTesting
+ static PerSdkConfig parsePerSdkConfig(JSONObject json, Set<String> allowedKeys)
+ throws JSONException, InvalidConfigException {
+ int minSdk = json.getInt(CONFIG_KEY_MIN_SDK);
+ int maxSdk = json.getInt(CONFIG_KEY_MAX_SDK);
+ JSONArray valueArray = json.getJSONArray(CONFIG_KEY_VALUES);
+ Map<String, String> values = new HashMap<>();
+ 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)
+ : null;
+ if (!allowedKeys.contains(key)) {
+ throw new InvalidConfigException("Config key " + key + " is not allowed");
+ }
+ values.put(key, value);
+ }
+ return new PerSdkConfig(minSdk, maxSdk, values);
+ }
+
+}
diff --git a/services/tests/servicestests/src/com/android/server/signedconfig/SignedConfigTest.java b/services/tests/servicestests/src/com/android/server/signedconfig/SignedConfigTest.java
new file mode 100644
index 0000000..a9d4519
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/signedconfig/SignedConfigTest.java
@@ -0,0 +1,328 @@
+/*
+ * Copyright (C) 2018 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.signedconfig;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.fail;
+
+import static java.util.Collections.emptySet;
+
+import androidx.test.runner.AndroidJUnit4;
+
+import com.google.common.collect.Sets;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Set;
+
+
+/**
+ * Tests for {@link SignedConfig}
+ */
+@RunWith(AndroidJUnit4.class)
+public class SignedConfigTest {
+
+ private static Set<String> setOf(String... values) {
+ return Sets.newHashSet(values);
+ }
+
+ @Test
+ public void testParsePerSdkConfigSdkMinMax() throws JSONException, InvalidConfigException {
+ JSONObject json = new JSONObject("{\"minSdk\":2, \"maxSdk\": 3, \"values\": []}");
+ SignedConfig.PerSdkConfig config = SignedConfig.parsePerSdkConfig(json, emptySet());
+ assertThat(config.minSdk).isEqualTo(2);
+ assertThat(config.maxSdk).isEqualTo(3);
+ }
+
+ @Test
+ public void testParsePerSdkConfigNoMinSdk() throws JSONException {
+ JSONObject json = new JSONObject("{\"maxSdk\": 3, \"values\": []}");
+ try {
+ SignedConfig.parsePerSdkConfig(json, emptySet());
+ fail("Expected InvalidConfigException or JSONException");
+ } catch (JSONException | InvalidConfigException e) {
+ // expected
+ }
+ }
+
+ @Test
+ public void testParsePerSdkConfigNoMaxSdk() throws JSONException {
+ JSONObject json = new JSONObject("{\"minSdk\": 1, \"values\": []}");
+ try {
+ SignedConfig.parsePerSdkConfig(json, emptySet());
+ fail("Expected InvalidConfigException or JSONException");
+ } catch (JSONException | InvalidConfigException e) {
+ // expected
+ }
+ }
+
+ @Test
+ public void testParsePerSdkConfigNoValues() throws JSONException {
+ JSONObject json = new JSONObject("{\"minSdk\": 1, \"maxSdk\": 3}");
+ try {
+ SignedConfig.parsePerSdkConfig(json, emptySet());
+ fail("Expected InvalidConfigException or JSONException");
+ } catch (JSONException | InvalidConfigException e) {
+ // expected
+ }
+ }
+
+ @Test
+ public void testParsePerSdkConfigSdkNullMinSdk() throws JSONException, InvalidConfigException {
+ JSONObject json = new JSONObject("{\"minSdk\":null, \"maxSdk\": 3, \"values\": []}");
+ try {
+ SignedConfig.parsePerSdkConfig(json, emptySet());
+ fail("Expected InvalidConfigException or JSONException");
+ } catch (JSONException | InvalidConfigException e) {
+ // expected
+ }
+ }
+
+ @Test
+ public void testParsePerSdkConfigSdkNullMaxSdk() throws JSONException, InvalidConfigException {
+ JSONObject json = new JSONObject("{\"minSdk\":1, \"maxSdk\": null, \"values\": []}");
+ try {
+ SignedConfig.parsePerSdkConfig(json, emptySet());
+ fail("Expected InvalidConfigException or JSONException");
+ } catch (JSONException | InvalidConfigException e) {
+ // expected
+ }
+ }
+
+ @Test
+ public void testParsePerSdkConfigNullValues() throws JSONException {
+ JSONObject json = new JSONObject("{\"minSdk\": 1, \"maxSdk\": 3, \"values\": null}");
+ try {
+ SignedConfig.parsePerSdkConfig(json, emptySet());
+ fail("Expected InvalidConfigException or JSONException");
+ } catch (JSONException | InvalidConfigException e) {
+ // expected
+ }
+ }
+
+ @Test
+ public void testParsePerSdkConfigZeroValues()
+ throws JSONException, InvalidConfigException {
+ JSONObject json = new JSONObject("{\"minSdk\": 1, \"maxSdk\": 3, \"values\": []}");
+ SignedConfig.PerSdkConfig config = SignedConfig.parsePerSdkConfig(json, setOf("a", "b"));
+ assertThat(config.values).hasSize(0);
+ }
+
+ @Test
+ public void testParsePerSdkConfigSingleKey()
+ 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"));
+ assertThat(config.values).containsExactly("a", "1");
+ }
+
+ @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"));
+ assertThat(config.values).containsExactly("a", "1", "c", "2");
+ }
+
+ @Test
+ public void testParsePerSdkConfigSingleKeyNotAllowed() throws JSONException {
+ JSONObject json = new JSONObject(
+ "{\"minSdk\": 1, \"maxSdk\": 1, \"values\": [{\"key\":\"a\", \"value\": \"1\"}]}");
+ try {
+ SignedConfig.parsePerSdkConfig(json, setOf("b"));
+ fail("Expected InvalidConfigException or JSONException");
+ } catch (JSONException | InvalidConfigException e) {
+ // expected
+ }
+ }
+
+ @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"));
+ assertThat(config.values).containsExactly("a", null);
+ }
+
+ @Test
+ public void testParsePerSdkConfigValuesInvalid() throws JSONException {
+ JSONObject json = new JSONObject("{\"minSdk\": 1, \"maxSdk\": 1, \"values\": \"foo\"}");
+ try {
+ SignedConfig.parsePerSdkConfig(json, emptySet());
+ fail("Expected InvalidConfigException or JSONException");
+ } catch (JSONException | InvalidConfigException e) {
+ // expected
+ }
+ }
+
+ @Test
+ public void testParsePerSdkConfigConfigEntryInvalid() throws JSONException {
+ JSONObject json = new JSONObject("{\"minSdk\": 1, \"maxSdk\": 1, \"values\": [1, 2]}");
+ try {
+ SignedConfig.parsePerSdkConfig(json, emptySet());
+ fail("Expected InvalidConfigException or JSONException");
+ } catch (JSONException | InvalidConfigException e) {
+ // expected
+ }
+ }
+
+ @Test
+ public void testParsePerSdkConfigConfigEntryNull() throws JSONException {
+ JSONObject json = new JSONObject("{\"minSdk\": 1, \"maxSdk\": 1, \"values\": [null]}");
+ try {
+ SignedConfig.parsePerSdkConfig(json, emptySet());
+ fail("Expected InvalidConfigException or JSONException");
+ } catch (JSONException | InvalidConfigException e) {
+ // expected
+ }
+ }
+
+ @Test
+ public void testParseVersion() throws InvalidConfigException {
+ SignedConfig config = SignedConfig.parse(
+ "{\"version\": 1, \"config\": []}", emptySet());
+ assertThat(config.version).isEqualTo(1);
+ }
+
+ @Test
+ public void testParseVersionInvalid() {
+ try {
+ SignedConfig.parse("{\"version\": \"notanint\", \"config\": []}", emptySet());
+ fail("Expected InvalidConfigException");
+ } catch (InvalidConfigException e) {
+ //expected
+ }
+ }
+
+ @Test
+ public void testParseNoVersion() {
+ try {
+ SignedConfig.parse("{\"config\": []}", emptySet());
+ fail("Expected InvalidConfigException");
+ } catch (InvalidConfigException e) {
+ //expected
+ }
+ }
+
+ @Test
+ public void testParseNoConfig() {
+ try {
+ SignedConfig.parse("{\"version\": 1}", emptySet());
+ fail("Expected InvalidConfigException");
+ } catch (InvalidConfigException e) {
+ //expected
+ }
+ }
+
+ @Test
+ public void testParseConfigNull() {
+ try {
+ SignedConfig.parse("{\"version\": 1, \"config\": null}", emptySet());
+ fail("Expected InvalidConfigException");
+ } catch (InvalidConfigException e) {
+ //expected
+ }
+ }
+
+ @Test
+ public void testParseVersionNull() {
+ try {
+ SignedConfig.parse("{\"version\": null, \"config\": []}", emptySet());
+ fail("Expected InvalidConfigException");
+ } catch (InvalidConfigException e) {
+ //expected
+ }
+ }
+
+ @Test
+ public void testParseConfigInvalidEntry() {
+ try {
+ SignedConfig.parse("{\"version\": 1, \"config\": [{}]}", emptySet());
+ fail("Expected InvalidConfigException");
+ } catch (InvalidConfigException e) {
+ //expected
+ }
+ }
+
+ @Test
+ public void testParseSdkConfigSingle() throws InvalidConfigException {
+ SignedConfig config = SignedConfig.parse(
+ "{\"version\": 1, \"config\":[{\"minSdk\": 1, \"maxSdk\": 1, \"values\": []}]}",
+ emptySet());
+ assertThat(config.perSdkConfig).hasSize(1);
+ }
+
+ @Test
+ public void testParseSdkConfigMultiple() throws InvalidConfigException {
+ SignedConfig config = SignedConfig.parse(
+ "{\"version\": 1, \"config\":[{\"minSdk\": 1, \"maxSdk\": 1, \"values\": []}, "
+ + "{\"minSdk\": 2, \"maxSdk\": 2, \"values\": []}]}", emptySet());
+ assertThat(config.perSdkConfig).hasSize(2);
+ }
+
+ @Test
+ public void testGetMatchingConfigFirst() {
+ SignedConfig.PerSdkConfig sdk1 = new SignedConfig.PerSdkConfig(
+ 1, 1, Collections.emptyMap());
+ SignedConfig.PerSdkConfig sdk2 = new SignedConfig.PerSdkConfig(
+ 2, 2, Collections.emptyMap());
+ SignedConfig config = new SignedConfig(0, Arrays.asList(sdk1, sdk2));
+ assertThat(config.getMatchingConfig(1)).isEqualTo(sdk1);
+ }
+
+ @Test
+ public void testGetMatchingConfigSecond() {
+ SignedConfig.PerSdkConfig sdk1 = new SignedConfig.PerSdkConfig(
+ 1, 1, Collections.emptyMap());
+ SignedConfig.PerSdkConfig sdk2 = new SignedConfig.PerSdkConfig(
+ 2, 2, Collections.emptyMap());
+ SignedConfig config = new SignedConfig(0, Arrays.asList(sdk1, sdk2));
+ assertThat(config.getMatchingConfig(2)).isEqualTo(sdk2);
+ }
+
+ @Test
+ public void testGetMatchingConfigInRange() {
+ SignedConfig.PerSdkConfig sdk13 = new SignedConfig.PerSdkConfig(
+ 1, 3, Collections.emptyMap());
+ SignedConfig.PerSdkConfig sdk46 = new SignedConfig.PerSdkConfig(
+ 4, 6, Collections.emptyMap());
+ 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(
+ 1, 1, Collections.emptyMap());
+ SignedConfig.PerSdkConfig sdk2 = new SignedConfig.PerSdkConfig(
+ 2, 2, Collections.emptyMap());
+ SignedConfig config = new SignedConfig(0, Arrays.asList(sdk1, sdk2));
+ assertThat(config.getMatchingConfig(3)).isNull();
+ }
+
+}