Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 9a7fdeb3 authored by Mathew Inwood's avatar Mathew Inwood Committed by Android (Google) Code Review
Browse files

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

parents d4c0617b 40ba4434
Loading
Loading
Loading
Loading
+41 −7
Original line number Diff line number Diff line
@@ -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);
+27 −1
Original line number Diff line number Diff line
@@ -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;
+116 −27
Original line number Diff line number Diff line
@@ -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(