diff options
| author | 2018-11-28 11:27:27 +0000 | |
|---|---|---|
| committer | 2018-12-06 17:27:32 +0000 | |
| commit | b0b51c326e06db55931b57a23afcf1b73cdf8b80 (patch) | |
| tree | 63080e690d1783a841f9a5a6b3dcc3d00c9b3aa3 | |
| parent | 9f7fcda6c45c78d01c11eeacfb8f67a23c97442e (diff) | |
Parser for signed configuration.
Signed configuration is stored an JSON inside APK meta-data. The format
of the JSON is as follows:
{
"version": n,
"config": [
{
"minSdk": n,
"maxSdk": n,
"values": [
{
"key": "global settings key",
"value": "value for key"
},
...
],
...
}
]
}
Test: atest SignedConfigTest
Bug: 110509075
Change-Id: I4d345b1f03e5c5f5af37de093eb6e5cb2429c42c
3 files changed, 498 insertions, 0 deletions
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 000000000000..f01baa4b0c5d --- /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 000000000000..a3f452c27d13 --- /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 000000000000..a9d4519104ba --- /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(); + } + +} |