1package android.security.net.config;
2
3import android.content.Context;
4import android.content.res.Resources;
5import android.content.res.XmlResourceParser;
6import android.os.Build;
7import android.util.ArraySet;
8import android.util.Base64;
9import android.util.Pair;
10import com.android.internal.annotations.VisibleForTesting;
11import com.android.internal.util.XmlUtils;
12
13import org.xmlpull.v1.XmlPullParser;
14import org.xmlpull.v1.XmlPullParserException;
15
16import java.io.IOException;
17import java.text.ParseException;
18import java.text.SimpleDateFormat;
19import java.util.ArrayList;
20import java.util.Collection;
21import java.util.Date;
22import java.util.List;
23import java.util.Locale;
24import java.util.Set;
25
26/**
27 * {@link ConfigSource} based on an XML configuration file.
28 *
29 * @hide
30 */
31public class XmlConfigSource implements ConfigSource {
32    private static final int CONFIG_BASE = 0;
33    private static final int CONFIG_DOMAIN = 1;
34    private static final int CONFIG_DEBUG = 2;
35
36    private final Object mLock = new Object();
37    private final int mResourceId;
38    private final boolean mDebugBuild;
39    private final int mTargetSdkVersion;
40    private final int mTargetSandboxVesrsion;
41
42    private boolean mInitialized;
43    private NetworkSecurityConfig mDefaultConfig;
44    private Set<Pair<Domain, NetworkSecurityConfig>> mDomainMap;
45    private Context mContext;
46
47    @VisibleForTesting
48    public XmlConfigSource(Context context, int resourceId) {
49        this(context, resourceId, false);
50    }
51
52    @VisibleForTesting
53    public XmlConfigSource(Context context, int resourceId, boolean debugBuild) {
54        this(context, resourceId, debugBuild, Build.VERSION_CODES.CUR_DEVELOPMENT);
55    }
56
57    @VisibleForTesting
58    public XmlConfigSource(Context context, int resourceId, boolean debugBuild,
59            int targetSdkVersion) {
60        this(context, resourceId, debugBuild, targetSdkVersion, 1 /*targetSandboxVersion*/);
61    }
62
63    public XmlConfigSource(Context context, int resourceId, boolean debugBuild,
64            int targetSdkVersion, int targetSandboxVesrsion) {
65        mResourceId = resourceId;
66        mContext = context;
67        mDebugBuild = debugBuild;
68        mTargetSdkVersion = targetSdkVersion;
69        mTargetSandboxVesrsion = targetSandboxVesrsion;
70    }
71
72    public Set<Pair<Domain, NetworkSecurityConfig>> getPerDomainConfigs() {
73        ensureInitialized();
74        return mDomainMap;
75    }
76
77    public NetworkSecurityConfig getDefaultConfig() {
78        ensureInitialized();
79        return mDefaultConfig;
80    }
81
82    private static final String getConfigString(int configType) {
83        switch (configType) {
84            case CONFIG_BASE:
85                return "base-config";
86            case CONFIG_DOMAIN:
87                return "domain-config";
88            case CONFIG_DEBUG:
89                return "debug-overrides";
90            default:
91                throw new IllegalArgumentException("Unknown config type: " + configType);
92        }
93    }
94
95    private void ensureInitialized() {
96        synchronized (mLock) {
97            if (mInitialized) {
98                return;
99            }
100            try (XmlResourceParser parser = mContext.getResources().getXml(mResourceId)) {
101                parseNetworkSecurityConfig(parser);
102                mContext = null;
103                mInitialized = true;
104            } catch (Resources.NotFoundException | XmlPullParserException | IOException
105                    | ParserException e) {
106                throw new RuntimeException("Failed to parse XML configuration from "
107                        + mContext.getResources().getResourceEntryName(mResourceId), e);
108            }
109        }
110    }
111
112    private Pin parsePin(XmlResourceParser parser)
113            throws IOException, XmlPullParserException, ParserException {
114        String digestAlgorithm = parser.getAttributeValue(null, "digest");
115        if (!Pin.isSupportedDigestAlgorithm(digestAlgorithm)) {
116            throw new ParserException(parser, "Unsupported pin digest algorithm: "
117                    + digestAlgorithm);
118        }
119        if (parser.next() != XmlPullParser.TEXT) {
120            throw new ParserException(parser, "Missing pin digest");
121        }
122        String digest = parser.getText().trim();
123        byte[] decodedDigest = null;
124        try {
125            decodedDigest = Base64.decode(digest, 0);
126        } catch (IllegalArgumentException e) {
127            throw new ParserException(parser, "Invalid pin digest", e);
128        }
129        int expectedLength = Pin.getDigestLength(digestAlgorithm);
130        if (decodedDigest.length != expectedLength) {
131            throw new ParserException(parser, "digest length " + decodedDigest.length
132                    + " does not match expected length for " + digestAlgorithm + " of "
133                    + expectedLength);
134        }
135        if (parser.next() != XmlPullParser.END_TAG) {
136            throw new ParserException(parser, "pin contains additional elements");
137        }
138        return new Pin(digestAlgorithm, decodedDigest);
139    }
140
141    private PinSet parsePinSet(XmlResourceParser parser)
142            throws IOException, XmlPullParserException, ParserException {
143        String expirationDate = parser.getAttributeValue(null, "expiration");
144        long expirationTimestampMilis = Long.MAX_VALUE;
145        if (expirationDate != null) {
146            try {
147                SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
148                sdf.setLenient(false);
149                Date date = sdf.parse(expirationDate);
150                if (date == null) {
151                    throw new ParserException(parser, "Invalid expiration date in pin-set");
152                }
153                expirationTimestampMilis = date.getTime();
154            } catch (ParseException e) {
155                throw new ParserException(parser, "Invalid expiration date in pin-set", e);
156            }
157        }
158
159        int outerDepth = parser.getDepth();
160        Set<Pin> pins = new ArraySet<>();
161        while (XmlUtils.nextElementWithin(parser, outerDepth)) {
162            String tagName = parser.getName();
163            if (tagName.equals("pin")) {
164                pins.add(parsePin(parser));
165            } else {
166                XmlUtils.skipCurrentTag(parser);
167            }
168        }
169        return new PinSet(pins, expirationTimestampMilis);
170    }
171
172    private Domain parseDomain(XmlResourceParser parser, Set<String> seenDomains)
173            throws IOException, XmlPullParserException, ParserException {
174        boolean includeSubdomains =
175                parser.getAttributeBooleanValue(null, "includeSubdomains", false);
176        if (parser.next() != XmlPullParser.TEXT) {
177            throw new ParserException(parser, "Domain name missing");
178        }
179        String domain = parser.getText().trim().toLowerCase(Locale.US);
180        if (parser.next() != XmlPullParser.END_TAG) {
181            throw new ParserException(parser, "domain contains additional elements");
182        }
183        // Domains are matched using a most specific match, so don't allow duplicates.
184        // includeSubdomains isn't relevant here, both android.com + subdomains and android.com
185        // match for android.com equally. Do not allow any duplicates period.
186        if (!seenDomains.add(domain)) {
187            throw new ParserException(parser, domain + " has already been specified");
188        }
189        return new Domain(domain, includeSubdomains);
190    }
191
192    private CertificatesEntryRef parseCertificatesEntry(XmlResourceParser parser,
193            boolean defaultOverridePins)
194            throws IOException, XmlPullParserException, ParserException {
195        boolean overridePins =
196                parser.getAttributeBooleanValue(null, "overridePins", defaultOverridePins);
197        int sourceId = parser.getAttributeResourceValue(null, "src", -1);
198        String sourceString = parser.getAttributeValue(null, "src");
199        CertificateSource source = null;
200        if (sourceString == null) {
201            throw new ParserException(parser, "certificates element missing src attribute");
202        }
203        if (sourceId != -1) {
204            // TODO: Cache ResourceCertificateSources by sourceId
205            source = new ResourceCertificateSource(sourceId, mContext);
206        } else if ("system".equals(sourceString)) {
207            source = SystemCertificateSource.getInstance();
208        } else if ("user".equals(sourceString)) {
209            source = UserCertificateSource.getInstance();
210        } else {
211            throw new ParserException(parser, "Unknown certificates src. "
212                    + "Should be one of system|user|@resourceVal");
213        }
214        XmlUtils.skipCurrentTag(parser);
215        return new CertificatesEntryRef(source, overridePins);
216    }
217
218    private Collection<CertificatesEntryRef> parseTrustAnchors(XmlResourceParser parser,
219            boolean defaultOverridePins)
220            throws IOException, XmlPullParserException, ParserException {
221        int outerDepth = parser.getDepth();
222        List<CertificatesEntryRef> anchors = new ArrayList<>();
223        while (XmlUtils.nextElementWithin(parser, outerDepth)) {
224            String tagName = parser.getName();
225            if (tagName.equals("certificates")) {
226                anchors.add(parseCertificatesEntry(parser, defaultOverridePins));
227            } else {
228                XmlUtils.skipCurrentTag(parser);
229            }
230        }
231        return anchors;
232    }
233
234    private List<Pair<NetworkSecurityConfig.Builder, Set<Domain>>> parseConfigEntry(
235            XmlResourceParser parser, Set<String> seenDomains,
236            NetworkSecurityConfig.Builder parentBuilder, int configType)
237            throws IOException, XmlPullParserException, ParserException {
238        List<Pair<NetworkSecurityConfig.Builder, Set<Domain>>> builders = new ArrayList<>();
239        NetworkSecurityConfig.Builder builder = new NetworkSecurityConfig.Builder();
240        builder.setParent(parentBuilder);
241        Set<Domain> domains = new ArraySet<>();
242        boolean seenPinSet = false;
243        boolean seenTrustAnchors = false;
244        boolean defaultOverridePins = configType == CONFIG_DEBUG;
245        String configName = parser.getName();
246        int outerDepth = parser.getDepth();
247        // Add this builder now so that this builder occurs before any of its children. This
248        // makes the final build pass easier.
249        builders.add(new Pair<>(builder, domains));
250        // Parse config attributes. Only set values that are present, config inheritence will
251        // handle the rest.
252        for (int i = 0; i < parser.getAttributeCount(); i++) {
253            String name = parser.getAttributeName(i);
254            if ("hstsEnforced".equals(name)) {
255                builder.setHstsEnforced(
256                        parser.getAttributeBooleanValue(i,
257                                NetworkSecurityConfig.DEFAULT_HSTS_ENFORCED));
258            } else if ("cleartextTrafficPermitted".equals(name)) {
259                builder.setCleartextTrafficPermitted(
260                        parser.getAttributeBooleanValue(i,
261                                NetworkSecurityConfig.DEFAULT_CLEARTEXT_TRAFFIC_PERMITTED));
262            }
263        }
264        // Parse the config elements.
265        while (XmlUtils.nextElementWithin(parser, outerDepth)) {
266            String tagName = parser.getName();
267            if ("domain".equals(tagName)) {
268                if (configType != CONFIG_DOMAIN) {
269                    throw new ParserException(parser,
270                            "domain element not allowed in " + getConfigString(configType));
271                }
272                Domain domain = parseDomain(parser, seenDomains);
273                domains.add(domain);
274            } else if ("trust-anchors".equals(tagName)) {
275                if (seenTrustAnchors) {
276                    throw new ParserException(parser,
277                            "Multiple trust-anchor elements not allowed");
278                }
279                builder.addCertificatesEntryRefs(
280                        parseTrustAnchors(parser, defaultOverridePins));
281                seenTrustAnchors = true;
282            } else if ("pin-set".equals(tagName)) {
283                if (configType != CONFIG_DOMAIN) {
284                    throw new ParserException(parser,
285                            "pin-set element not allowed in " + getConfigString(configType));
286                }
287                if (seenPinSet) {
288                    throw new ParserException(parser, "Multiple pin-set elements not allowed");
289                }
290                builder.setPinSet(parsePinSet(parser));
291                seenPinSet = true;
292            } else if ("domain-config".equals(tagName)) {
293                if (configType != CONFIG_DOMAIN) {
294                    throw new ParserException(parser,
295                            "Nested domain-config not allowed in " + getConfigString(configType));
296                }
297                builders.addAll(parseConfigEntry(parser, seenDomains, builder, configType));
298            } else {
299                XmlUtils.skipCurrentTag(parser);
300            }
301        }
302        if (configType == CONFIG_DOMAIN && domains.isEmpty()) {
303            throw new ParserException(parser, "No domain elements in domain-config");
304        }
305        return builders;
306    }
307
308    private void addDebugAnchorsIfNeeded(NetworkSecurityConfig.Builder debugConfigBuilder,
309            NetworkSecurityConfig.Builder builder) {
310        if (debugConfigBuilder == null || !debugConfigBuilder.hasCertificatesEntryRefs()) {
311            return;
312        }
313        // Don't add trust anchors if not already present, the builder will inherit the anchors
314        // from its parent, and that's where the trust anchors should be added.
315        if (!builder.hasCertificatesEntryRefs()) {
316            return;
317        }
318
319        builder.addCertificatesEntryRefs(debugConfigBuilder.getCertificatesEntryRefs());
320    }
321
322    private void parseNetworkSecurityConfig(XmlResourceParser parser)
323            throws IOException, XmlPullParserException, ParserException {
324        Set<String> seenDomains = new ArraySet<>();
325        List<Pair<NetworkSecurityConfig.Builder, Set<Domain>>> builders = new ArrayList<>();
326        NetworkSecurityConfig.Builder baseConfigBuilder = null;
327        NetworkSecurityConfig.Builder debugConfigBuilder = null;
328        boolean seenDebugOverrides = false;
329        boolean seenBaseConfig = false;
330
331        XmlUtils.beginDocument(parser, "network-security-config");
332        int outerDepth = parser.getDepth();
333        while (XmlUtils.nextElementWithin(parser, outerDepth)) {
334            if ("base-config".equals(parser.getName())) {
335                if (seenBaseConfig) {
336                    throw new ParserException(parser, "Only one base-config allowed");
337                }
338                seenBaseConfig = true;
339                baseConfigBuilder =
340                        parseConfigEntry(parser, seenDomains, null, CONFIG_BASE).get(0).first;
341            } else if ("domain-config".equals(parser.getName())) {
342                builders.addAll(
343                        parseConfigEntry(parser, seenDomains, baseConfigBuilder, CONFIG_DOMAIN));
344            } else if ("debug-overrides".equals(parser.getName())) {
345                if (seenDebugOverrides) {
346                    throw new ParserException(parser, "Only one debug-overrides allowed");
347                }
348                if (mDebugBuild) {
349                    debugConfigBuilder =
350                            parseConfigEntry(parser, null, null, CONFIG_DEBUG).get(0).first;
351                } else {
352                    XmlUtils.skipCurrentTag(parser);
353                }
354                seenDebugOverrides = true;
355            } else {
356                XmlUtils.skipCurrentTag(parser);
357            }
358        }
359        // If debug is true and there was no debug-overrides in the file check for an extra
360        // _debug resource.
361        if (mDebugBuild && debugConfigBuilder == null) {
362            debugConfigBuilder = parseDebugOverridesResource();
363        }
364
365        // Use the platform default as the parent of the base config for any values not provided
366        // there. If there is no base config use the platform default.
367        NetworkSecurityConfig.Builder platformDefaultBuilder =
368                NetworkSecurityConfig.getDefaultBuilder(mTargetSdkVersion, mTargetSandboxVesrsion);
369        addDebugAnchorsIfNeeded(debugConfigBuilder, platformDefaultBuilder);
370        if (baseConfigBuilder != null) {
371            baseConfigBuilder.setParent(platformDefaultBuilder);
372            addDebugAnchorsIfNeeded(debugConfigBuilder, baseConfigBuilder);
373        } else {
374            baseConfigBuilder = platformDefaultBuilder;
375        }
376        // Build the per-domain config mapping.
377        Set<Pair<Domain, NetworkSecurityConfig>> configs = new ArraySet<>();
378
379        for (Pair<NetworkSecurityConfig.Builder, Set<Domain>> entry : builders) {
380            NetworkSecurityConfig.Builder builder = entry.first;
381            Set<Domain> domains = entry.second;
382            // Set the parent of configs that do not have a parent to the base-config. This can
383            // happen if the base-config comes after a domain-config in the file.
384            // Note that this is safe with regards to children because of the order that
385            // parseConfigEntry returns builders, the parent is always before the children. The
386            // children builders will not have build called until _after_ their parents have their
387            // parent set so everything is consistent.
388            if (builder.getParent() == null) {
389                builder.setParent(baseConfigBuilder);
390            }
391            addDebugAnchorsIfNeeded(debugConfigBuilder, builder);
392            NetworkSecurityConfig config = builder.build();
393            for (Domain domain : domains) {
394                configs.add(new Pair<>(domain, config));
395            }
396        }
397        mDefaultConfig = baseConfigBuilder.build();
398        mDomainMap = configs;
399    }
400
401    private NetworkSecurityConfig.Builder parseDebugOverridesResource()
402            throws IOException, XmlPullParserException, ParserException {
403        Resources resources = mContext.getResources();
404        String packageName = resources.getResourcePackageName(mResourceId);
405        String entryName = resources.getResourceEntryName(mResourceId);
406        int resId = resources.getIdentifier(entryName + "_debug", "xml", packageName);
407        // No debug-overrides resource was found, nothing to parse.
408        if (resId == 0) {
409            return null;
410        }
411        NetworkSecurityConfig.Builder debugConfigBuilder = null;
412        // Parse debug-overrides out of the _debug resource.
413        try (XmlResourceParser parser = resources.getXml(resId)) {
414            XmlUtils.beginDocument(parser, "network-security-config");
415            int outerDepth = parser.getDepth();
416            boolean seenDebugOverrides = false;
417            while (XmlUtils.nextElementWithin(parser, outerDepth)) {
418                if ("debug-overrides".equals(parser.getName())) {
419                    if (seenDebugOverrides) {
420                        throw new ParserException(parser, "Only one debug-overrides allowed");
421                    }
422                    if (mDebugBuild) {
423                        debugConfigBuilder =
424                                parseConfigEntry(parser, null, null, CONFIG_DEBUG).get(0).first;
425                    } else {
426                        XmlUtils.skipCurrentTag(parser);
427                    }
428                    seenDebugOverrides = true;
429                } else {
430                    XmlUtils.skipCurrentTag(parser);
431                }
432            }
433        }
434
435        return debugConfigBuilder;
436    }
437
438    public static class ParserException extends Exception {
439
440        public ParserException(XmlPullParser parser, String message, Throwable cause) {
441            super(message + " at: " + parser.getPositionDescription(), cause);
442        }
443
444        public ParserException(XmlPullParser parser, String message) {
445            this(parser, message, null);
446        }
447    }
448}
449