• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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