| /* |
| * 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. |
| * |
| * This class also parses signed config from JSON. The format expected is: |
| * <pre> |
| * { |
| * "version": 1 |
| * "config": [ |
| * { |
| * "min_sdk": 28, |
| * "max_sdk": 29, |
| * "values": { |
| * "key": "value", |
| * "key2": "value2" |
| * ... |
| * } |
| * }, |
| * ... |
| * ], |
| * } |
| * </pre> |
| */ |
| 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 = "min_sdk"; |
| private static final String CONFIG_KEY_MAX_SDK = "max_sdk"; |
| private static final String CONFIG_KEY_VALUES = "values"; |
| |
| /** |
| * Represents config values targeting 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 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, |
| Map<String, Map<String, String>> keyValueMappers) |
| 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, |
| keyValueMappers)); |
| } |
| |
| return new SignedConfig(version, parsedConfigs); |
| } catch (JSONException e) { |
| throw new InvalidConfigException("Could not parse JSON", e); |
| } |
| |
| } |
| |
| private static CharSequence quoted(Object s) { |
| if (s == null) { |
| return "null"; |
| } else { |
| return "\"" + s + "\""; |
| } |
| } |
| |
| @VisibleForTesting |
| 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); |
| JSONObject valuesJson = json.getJSONObject(CONFIG_KEY_VALUES); |
| Map<String, String> values = new HashMap<>(); |
| for (String key : valuesJson.keySet()) { |
| Object valueObject = valuesJson.get(key); |
| 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); |
| } |
| |
| } |