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

Commit b0b51c32 authored by Mathew Inwood's avatar Mathew Inwood
Browse files

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
parent 9f7fcda6
Loading
Loading
Loading
Loading
+33 −0
Original line number Diff line number Diff line
/*
 * 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);
    }


}
+137 −0
Original line number Diff line number Diff line
/*
 * 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);
    }

}
+328 −0
Original line number Diff line number Diff line
/*
 * 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();
    }

}