1 /*
2  * Copyright (C) 2015 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 android.security.net.config;
18 
19 import android.util.Pair;
20 import java.util.HashSet;
21 import java.util.Locale;
22 import java.util.Set;
23 import javax.net.ssl.X509TrustManager;
24 
25 /**
26  * An application's network security configuration.
27  *
28  * <p>{@link #getConfigForHostname(String)} provides a means to obtain network security
29  * configuration to be used for communicating with a specific hostname.</p>
30  *
31  * @hide
32  */
33 public final class ApplicationConfig {
34     private static ApplicationConfig sInstance;
35     private static Object sLock = new Object();
36 
37     private Set<Pair<Domain, NetworkSecurityConfig>> mConfigs;
38     private NetworkSecurityConfig mDefaultConfig;
39     private X509TrustManager mTrustManager;
40 
41     private ConfigSource mConfigSource;
42     private boolean mInitialized;
43     private final Object mLock = new Object();
44 
ApplicationConfig(ConfigSource configSource)45     public ApplicationConfig(ConfigSource configSource) {
46         mConfigSource = configSource;
47         mInitialized = false;
48     }
49 
50     /**
51      * @hide
52      */
hasPerDomainConfigs()53     public boolean hasPerDomainConfigs() {
54         ensureInitialized();
55         return mConfigs != null && !mConfigs.isEmpty();
56     }
57 
58     /**
59      * Get the {@link NetworkSecurityConfig} corresponding to the provided hostname.
60      * When matching the most specific matching domain rule will be used, if no match exists
61      * then the default configuration will be returned.
62      *
63      * {@code NetworkSecurityConfig} objects returned by this method can be safely cached for
64      * {@code hostname}. Subsequent calls with the same hostname will always return the same
65      * {@code NetworkSecurityConfig}.
66      *
67      * @return {@link NetworkSecurityConfig} to be used to determine
68      * the network security configuration for connections to {@code hostname}.
69      */
getConfigForHostname(String hostname)70     public NetworkSecurityConfig getConfigForHostname(String hostname) {
71         ensureInitialized();
72         if (hostname == null || hostname.isEmpty() || mConfigs == null) {
73             return mDefaultConfig;
74         }
75         if (hostname.charAt(0) ==  '.') {
76             throw new IllegalArgumentException("hostname must not begin with a .");
77         }
78         // Domains are case insensitive.
79         hostname = hostname.toLowerCase(Locale.US);
80         // Normalize hostname by removing trailing . if present, all Domain hostnames are
81         // absolute.
82         if (hostname.charAt(hostname.length() - 1) == '.') {
83             hostname = hostname.substring(0, hostname.length() - 1);
84         }
85         // Find the Domain -> NetworkSecurityConfig entry with the most specific matching
86         // Domain entry for hostname.
87         // TODO: Use a smarter data structure for the lookup.
88         Pair<Domain, NetworkSecurityConfig> bestMatch = null;
89         for (Pair<Domain, NetworkSecurityConfig> entry : mConfigs) {
90             Domain domain = entry.first;
91             NetworkSecurityConfig config = entry.second;
92             // Check for an exact match.
93             if (domain.hostname.equals(hostname)) {
94                 return config;
95             }
96             // Otherwise check if the Domain includes sub-domains and that the hostname is a
97             // sub-domain of the Domain.
98             if (domain.subdomainsIncluded
99                     && hostname.endsWith(domain.hostname)
100                     && hostname.charAt(hostname.length() - domain.hostname.length() - 1) == '.') {
101                 if (bestMatch == null) {
102                     bestMatch = entry;
103                 } else if (domain.hostname.length() > bestMatch.first.hostname.length()) {
104                     bestMatch = entry;
105                 }
106             }
107         }
108         if (bestMatch != null) {
109             return bestMatch.second;
110         }
111         // If no match was found use the default configuration.
112         return mDefaultConfig;
113     }
114 
115     /**
116      * Returns the {@link X509TrustManager} that implements the checking of trust anchors and
117      * certificate pinning based on this configuration.
118      */
getTrustManager()119     public X509TrustManager getTrustManager() {
120         ensureInitialized();
121         return mTrustManager;
122     }
123 
124     /**
125      * Returns {@code true} if cleartext traffic is permitted for this application, which is the
126      * case only if all configurations permit cleartext traffic. For finer-grained policy use
127      * {@link #isCleartextTrafficPermitted(String)}.
128      */
isCleartextTrafficPermitted()129     public boolean isCleartextTrafficPermitted() {
130         ensureInitialized();
131         if (mConfigs != null) {
132             for (Pair<Domain, NetworkSecurityConfig> entry : mConfigs) {
133                 if (!entry.second.isCleartextTrafficPermitted()) {
134                     return false;
135                 }
136             }
137         }
138 
139         return mDefaultConfig.isCleartextTrafficPermitted();
140     }
141 
142     /**
143      * Returns {@code true} if cleartext traffic is permitted for this application when connecting
144      * to {@code hostname}.
145      */
isCleartextTrafficPermitted(String hostname)146     public boolean isCleartextTrafficPermitted(String hostname) {
147         return getConfigForHostname(hostname).isCleartextTrafficPermitted();
148     }
149 
handleTrustStorageUpdate()150     public void handleTrustStorageUpdate() {
151         synchronized(mLock) {
152             // If the config is uninitialized then there is no work to be done to handle an update,
153             // avoid needlessly parsing configs.
154             if (!mInitialized) {
155                 return;
156             }
157             mDefaultConfig.handleTrustStorageUpdate();
158             if (mConfigs != null) {
159                 Set<NetworkSecurityConfig> updatedConfigs =
160                         new HashSet<NetworkSecurityConfig>(mConfigs.size());
161                 for (Pair<Domain, NetworkSecurityConfig> entry : mConfigs) {
162                     if (updatedConfigs.add(entry.second)) {
163                         entry.second.handleTrustStorageUpdate();
164                     }
165                 }
166             }
167         }
168     }
169 
ensureInitialized()170     private void ensureInitialized() {
171         synchronized(mLock) {
172             if (mInitialized) {
173                 return;
174             }
175             mConfigs = mConfigSource.getPerDomainConfigs();
176             mDefaultConfig = mConfigSource.getDefaultConfig();
177             mConfigSource = null;
178             mTrustManager = new RootTrustManager(this);
179             mInitialized = true;
180         }
181     }
182 
setDefaultInstance(ApplicationConfig config)183     public static void setDefaultInstance(ApplicationConfig config) {
184         synchronized (sLock) {
185             sInstance = config;
186         }
187     }
188 
getDefaultInstance()189     public static ApplicationConfig getDefaultInstance() {
190         synchronized (sLock) {
191             return sInstance;
192         }
193     }
194 }
195