1 /* 2 * Copyright (C) 2018 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.server.signedconfig; 18 19 import com.android.internal.annotations.VisibleForTesting; 20 21 import org.json.JSONArray; 22 import org.json.JSONException; 23 import org.json.JSONObject; 24 25 import java.util.ArrayList; 26 import java.util.Collections; 27 import java.util.HashMap; 28 import java.util.List; 29 import java.util.Map; 30 import java.util.Set; 31 32 /** 33 * Represents signed configuration. 34 * 35 * <p>This configuration should only be used if the signature has already been verified. 36 * 37 * This class also parses signed config from JSON. The format expected is: 38 * <pre> 39 * { 40 * "version": 1 41 * "config": [ 42 * { 43 * "min_sdk": 28, 44 * "max_sdk": 29, 45 * "values": { 46 * "key": "value", 47 * "key2": "value2" 48 * ... 49 * } 50 * }, 51 * ... 52 * ], 53 * } 54 * </pre> 55 */ 56 public class SignedConfig { 57 58 private static final String KEY_VERSION = "version"; 59 private static final String KEY_CONFIG = "config"; 60 61 private static final String CONFIG_KEY_MIN_SDK = "min_sdk"; 62 private static final String CONFIG_KEY_MAX_SDK = "max_sdk"; 63 private static final String CONFIG_KEY_VALUES = "values"; 64 65 /** 66 * Represents config values targeting an SDK range. 67 */ 68 public static class PerSdkConfig { 69 public final int minSdk; 70 public final int maxSdk; 71 public final Map<String, String> values; 72 PerSdkConfig(int minSdk, int maxSdk, Map<String, String> values)73 public PerSdkConfig(int minSdk, int maxSdk, Map<String, String> values) { 74 this.minSdk = minSdk; 75 this.maxSdk = maxSdk; 76 this.values = Collections.unmodifiableMap(values); 77 } 78 79 } 80 81 public final int version; 82 public final List<PerSdkConfig> perSdkConfig; 83 SignedConfig(int version, List<PerSdkConfig> perSdkConfig)84 public SignedConfig(int version, List<PerSdkConfig> perSdkConfig) { 85 this.version = version; 86 this.perSdkConfig = Collections.unmodifiableList(perSdkConfig); 87 } 88 89 /** 90 * Find matching sdk config for a given SDK level. 91 * 92 * @param sdkVersion SDK version of device. 93 * @return Matching config, of {@code null} if there is none. 94 */ getMatchingConfig(int sdkVersion)95 public PerSdkConfig getMatchingConfig(int sdkVersion) { 96 for (PerSdkConfig config : perSdkConfig) { 97 if (config.minSdk <= sdkVersion && sdkVersion <= config.maxSdk) { 98 return config; 99 } 100 } 101 // nothing matching 102 return null; 103 } 104 105 /** 106 * Parse configuration from an APK. 107 * 108 * @param config Config string as read from the APK metadata. 109 * @param allowedKeys Set of allowed keys in the config. Any key/value mapping for a key not in 110 * this set will result in an {@link InvalidConfigException} being thrown. 111 * @param keyValueMappers Mappings for values per key. The keys in the top level map should be 112 * a subset of {@code allowedKeys}. The keys in the inner map indicate 113 * the set of allowed values for that keys value. This map will be 114 * applied to the value in the configuration. This is intended to allow 115 * enum-like values to be encoded as strings in the configuration, and 116 * mapped back to integers when the configuration is parsed. 117 * 118 * <p>Any config key with a value that does not appear in the 119 * corresponding map will result in an {@link InvalidConfigException} 120 * being thrown. 121 * @return Parsed configuration. 122 * @throws InvalidConfigException If there's a problem parsing the config. 123 */ parse(String config, Set<String> allowedKeys, Map<String, Map<String, String>> keyValueMappers)124 public static SignedConfig parse(String config, Set<String> allowedKeys, 125 Map<String, Map<String, String>> keyValueMappers) 126 throws InvalidConfigException { 127 try { 128 JSONObject json = new JSONObject(config); 129 int version = json.getInt(KEY_VERSION); 130 131 JSONArray perSdkConfig = json.getJSONArray(KEY_CONFIG); 132 List<PerSdkConfig> parsedConfigs = new ArrayList<>(); 133 for (int i = 0; i < perSdkConfig.length(); ++i) { 134 parsedConfigs.add(parsePerSdkConfig(perSdkConfig.getJSONObject(i), allowedKeys, 135 keyValueMappers)); 136 } 137 138 return new SignedConfig(version, parsedConfigs); 139 } catch (JSONException e) { 140 throw new InvalidConfigException("Could not parse JSON", e); 141 } 142 143 } 144 quoted(Object s)145 private static CharSequence quoted(Object s) { 146 if (s == null) { 147 return "null"; 148 } else { 149 return "\"" + s + "\""; 150 } 151 } 152 153 @VisibleForTesting parsePerSdkConfig(JSONObject json, Set<String> allowedKeys, Map<String, Map<String, String>> keyValueMappers)154 static PerSdkConfig parsePerSdkConfig(JSONObject json, Set<String> allowedKeys, 155 Map<String, Map<String, String>> keyValueMappers) 156 throws JSONException, InvalidConfigException { 157 int minSdk = json.getInt(CONFIG_KEY_MIN_SDK); 158 int maxSdk = json.getInt(CONFIG_KEY_MAX_SDK); 159 JSONObject valuesJson = json.getJSONObject(CONFIG_KEY_VALUES); 160 Map<String, String> values = new HashMap<>(); 161 for (String key : valuesJson.keySet()) { 162 Object valueObject = valuesJson.get(key); 163 String value = valueObject == JSONObject.NULL || valueObject == null 164 ? null 165 : valueObject.toString(); 166 if (!allowedKeys.contains(key)) { 167 throw new InvalidConfigException("Config key " + key + " is not allowed"); 168 } 169 if (keyValueMappers.containsKey(key)) { 170 Map<String, String> mapper = keyValueMappers.get(key); 171 if (!mapper.containsKey(value)) { 172 throw new InvalidConfigException( 173 "Config key " + key + " contains unsupported value " + quoted(value)); 174 } 175 value = mapper.get(value); 176 } 177 values.put(key, value); 178 } 179 return new PerSdkConfig(minSdk, maxSdk, values); 180 } 181 182 } 183