1 /*
2  * Copyright 2016, 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.managedprovisioning.parser;
18 
19 import static android.app.admin.DevicePolicyManager.EXTRA_PROVISIONING_ADMIN_EXTRAS_BUNDLE;
20 import static android.app.admin.DevicePolicyManager.EXTRA_PROVISIONING_DEVICE_ADMIN_COMPONENT_NAME;
21 import static android.app.admin.DevicePolicyManager.EXTRA_PROVISIONING_DEVICE_ADMIN_MINIMUM_VERSION_CODE;
22 import static android.app.admin.DevicePolicyManager.EXTRA_PROVISIONING_DEVICE_ADMIN_PACKAGE_CHECKSUM;
23 import static android.app.admin.DevicePolicyManager.EXTRA_PROVISIONING_DEVICE_ADMIN_PACKAGE_DOWNLOAD_COOKIE_HEADER;
24 import static android.app.admin.DevicePolicyManager.EXTRA_PROVISIONING_DEVICE_ADMIN_PACKAGE_DOWNLOAD_LOCATION;
25 import static android.app.admin.DevicePolicyManager.EXTRA_PROVISIONING_DEVICE_ADMIN_PACKAGE_NAME;
26 import static android.app.admin.DevicePolicyManager.EXTRA_PROVISIONING_DEVICE_ADMIN_SIGNATURE_CHECKSUM;
27 import static android.app.admin.DevicePolicyManager.EXTRA_PROVISIONING_LEAVE_ALL_SYSTEM_APPS_ENABLED;
28 import static android.app.admin.DevicePolicyManager.EXTRA_PROVISIONING_LOCALE;
29 import static android.app.admin.DevicePolicyManager.EXTRA_PROVISIONING_LOCAL_TIME;
30 import static android.app.admin.DevicePolicyManager.EXTRA_PROVISIONING_SENSORS_PERMISSION_GRANT_OPT_OUT;
31 import static android.app.admin.DevicePolicyManager.EXTRA_PROVISIONING_SKIP_ENCRYPTION;
32 import static android.app.admin.DevicePolicyManager.EXTRA_PROVISIONING_TIME_ZONE;
33 import static android.app.admin.DevicePolicyManager.EXTRA_PROVISIONING_USE_MOBILE_DATA;
34 import static android.app.admin.DevicePolicyManager.EXTRA_PROVISIONING_WIFI_ANONYMOUS_IDENTITY;
35 import static android.app.admin.DevicePolicyManager.EXTRA_PROVISIONING_WIFI_CA_CERTIFICATE;
36 import static android.app.admin.DevicePolicyManager.EXTRA_PROVISIONING_WIFI_DOMAIN;
37 import static android.app.admin.DevicePolicyManager.EXTRA_PROVISIONING_WIFI_EAP_METHOD;
38 import static android.app.admin.DevicePolicyManager.EXTRA_PROVISIONING_WIFI_HIDDEN;
39 import static android.app.admin.DevicePolicyManager.EXTRA_PROVISIONING_WIFI_IDENTITY;
40 import static android.app.admin.DevicePolicyManager.EXTRA_PROVISIONING_WIFI_PAC_URL;
41 import static android.app.admin.DevicePolicyManager.EXTRA_PROVISIONING_WIFI_PASSWORD;
42 import static android.app.admin.DevicePolicyManager.EXTRA_PROVISIONING_WIFI_PHASE2_AUTH;
43 import static android.app.admin.DevicePolicyManager.EXTRA_PROVISIONING_WIFI_PROXY_BYPASS;
44 import static android.app.admin.DevicePolicyManager.EXTRA_PROVISIONING_WIFI_PROXY_HOST;
45 import static android.app.admin.DevicePolicyManager.EXTRA_PROVISIONING_WIFI_PROXY_PORT;
46 import static android.app.admin.DevicePolicyManager.EXTRA_PROVISIONING_WIFI_SECURITY_TYPE;
47 import static android.app.admin.DevicePolicyManager.EXTRA_PROVISIONING_WIFI_SSID;
48 import static android.app.admin.DevicePolicyManager.EXTRA_PROVISIONING_WIFI_USER_CERTIFICATE;
49 import static android.app.admin.DevicePolicyManager.MIME_TYPE_PROVISIONING_NFC;
50 import static android.nfc.NfcAdapter.ACTION_NDEF_DISCOVERED;
51 
52 import static com.android.internal.util.Preconditions.checkNotNull;
53 
54 import static java.nio.charset.StandardCharsets.UTF_8;
55 
56 import android.content.ComponentName;
57 import android.content.Context;
58 import android.content.Intent;
59 import android.nfc.NdefMessage;
60 import android.nfc.NdefRecord;
61 import android.nfc.NfcAdapter;
62 import android.os.Parcelable;
63 import android.os.PersistableBundle;
64 
65 import androidx.annotation.Nullable;
66 import androidx.annotation.VisibleForTesting;
67 
68 import com.android.managedprovisioning.common.IllegalProvisioningArgumentException;
69 import com.android.managedprovisioning.common.ManagedProvisioningSharedPreferences;
70 import com.android.managedprovisioning.common.ProvisionLogger;
71 import com.android.managedprovisioning.common.SettingsFacade;
72 import com.android.managedprovisioning.common.StoreUtils;
73 import com.android.managedprovisioning.model.PackageDownloadInfo;
74 import com.android.managedprovisioning.model.ProvisioningParams;
75 import com.android.managedprovisioning.model.WifiInfo;
76 
77 import java.io.IOException;
78 import java.io.StringReader;
79 import java.util.IllformedLocaleException;
80 import java.util.Properties;
81 
82 
83 /**
84  * A parser which parses provisioning data from intent which stores in {@link Properties}.
85  *
86  * <p>It is used to parse an intent contains the extra {@link NfcAdapter.EXTRA_NDEF_MESSAGES}, which
87  * indicates that provisioning was started via Nfc bump. This extra contains an NDEF message, which
88  * contains an NfcRecord with mime type {@link MIME_TYPE_PROVISIONING_NFC}. This record stores a
89  * serialized properties object, which contains the serialized extras described in the next option.
90  * A typical use case would be a programmer application that sends an Nfc bump to start Nfc
91  * provisioning from a programmer device.
92  */
93 @VisibleForTesting
94 public class PropertiesProvisioningDataParser implements ProvisioningDataParser {
95 
96     private final ParserUtils mParserUtils;
97     private final Context mContext;
98     private final ManagedProvisioningSharedPreferences mSharedPreferences;
99     private final SettingsFacade mSettingsFacade;
100 
PropertiesProvisioningDataParser(Context context, ParserUtils parserUtils, SettingsFacade settingsFacade)101     PropertiesProvisioningDataParser(Context context, ParserUtils parserUtils,
102             SettingsFacade settingsFacade) {
103         this(context, parserUtils, settingsFacade,
104                 new ManagedProvisioningSharedPreferences(context));
105     }
106 
107     @VisibleForTesting
PropertiesProvisioningDataParser(Context context, ParserUtils parserUtils, SettingsFacade settingsFacade, ManagedProvisioningSharedPreferences sharedPreferences)108     PropertiesProvisioningDataParser(Context context, ParserUtils parserUtils,
109             SettingsFacade settingsFacade, ManagedProvisioningSharedPreferences sharedPreferences) {
110         mContext = checkNotNull(context);
111         mParserUtils = checkNotNull(parserUtils);
112         mSettingsFacade = checkNotNull(settingsFacade);
113         mSharedPreferences = checkNotNull(sharedPreferences);
114     }
115 
116     @Nullable
getPropertyFromLongName(Properties properties, String longName)117     private String getPropertyFromLongName(Properties properties, String longName) {
118         if (properties.containsKey(longName)) {
119             return properties.getProperty(longName);
120         }
121         String shortName = ExtrasProvisioningDataParser.getShortExtraNames(longName);
122         if (properties.containsKey(shortName)) {
123             return properties.getProperty(shortName);
124         }
125         return null;
126     }
127 
parse(Intent nfcIntent)128     public ProvisioningParams parse(Intent nfcIntent)
129             throws IllegalProvisioningArgumentException {
130         if (!ACTION_NDEF_DISCOVERED.equals(nfcIntent.getAction())) {
131             throw new IllegalProvisioningArgumentException(
132                     "Only NFC action is supported in this parser.");
133         }
134 
135         ProvisionLogger.logi("Processing Nfc Payload.");
136         NdefRecord firstRecord = getFirstNdefRecord(nfcIntent);
137         if (firstRecord != null) {
138             try {
139                 Properties props = new Properties();
140                 props.load(new StringReader(new String(firstRecord.getPayload(), UTF_8)));
141 
142                 // For parsing non-string parameters.
143                 String s = null;
144 
145                 ProvisioningParams.Builder builder = ProvisioningParams.Builder.builder()
146                         .setProvisioningId(mSharedPreferences.incrementAndGetProvisioningId())
147                         .setStartedByTrustedSource(true)
148                         .setIsNfc(true)
149                         .setProvisioningAction(mParserUtils.extractProvisioningAction(
150                                 nfcIntent, mSettingsFacade, mContext))
151                         .setDeviceAdminPackageName(
152                                 getPropertyFromLongName(
153                                         props, EXTRA_PROVISIONING_DEVICE_ADMIN_PACKAGE_NAME));
154                 if ((s = getPropertyFromLongName(
155                         props, EXTRA_PROVISIONING_DEVICE_ADMIN_COMPONENT_NAME))
156                         != null) {
157                     builder.setDeviceAdminComponentName(ComponentName.unflattenFromString(s));
158                 }
159 
160                 // Parse time zone, locale and local time.
161                 builder.setTimeZone(getPropertyFromLongName(props, EXTRA_PROVISIONING_TIME_ZONE))
162                         .setLocale(StoreUtils.stringToLocale(
163                                 getPropertyFromLongName(props, EXTRA_PROVISIONING_LOCALE)));
164                 if ((s = getPropertyFromLongName(props, EXTRA_PROVISIONING_LOCAL_TIME)) != null) {
165                     builder.setLocalTime(Long.parseLong(s));
166                 }
167 
168                 // Parse WiFi configuration.
169                 builder.setWifiInfo(parseWifiInfoFromProperties(props))
170                         // Parse device admin package download info.
171                         .setDeviceAdminDownloadInfo(parsePackageDownloadInfoFromProperties(props))
172                         // Parse EMM customized key-value pairs.
173                         // Note: EXTRA_PROVISIONING_ADMIN_EXTRAS_BUNDLE property contains a
174                         // Properties object serialized into String. See Properties.store() and
175                         // Properties.load() for more details. The property value is optional.
176                         .setAdminExtrasBundle(deserializeExtrasBundle(props,
177                                 EXTRA_PROVISIONING_ADMIN_EXTRAS_BUNDLE));
178                 if ((s = getPropertyFromLongName(
179                         props, EXTRA_PROVISIONING_LEAVE_ALL_SYSTEM_APPS_ENABLED)) != null) {
180                     builder.setLeaveAllSystemAppsEnabled(Boolean.parseBoolean(s));
181                 }
182                 if ((s = getPropertyFromLongName(
183                         props, EXTRA_PROVISIONING_SKIP_ENCRYPTION)) != null) {
184                     builder.setSkipEncryption(Boolean.parseBoolean(s));
185                 }
186                 if ((s = getPropertyFromLongName(
187                         props, EXTRA_PROVISIONING_USE_MOBILE_DATA)) != null) {
188                     builder.setUseMobileData(Boolean.parseBoolean(s));
189                 }
190                 if ((s = getPropertyFromLongName(
191                         props, EXTRA_PROVISIONING_SENSORS_PERMISSION_GRANT_OPT_OUT)) != null) {
192                     builder.setDeviceOwnerPermissionGrantOptOut(Boolean.parseBoolean(s));
193                 }
194                 builder.setIsOrganizationOwnedProvisioning(true);
195                 // TODO(b/177849035): Remove NFC-specific logic
196                 builder.setReturnBeforePolicyCompliance(true);
197                 ProvisionLogger.logi("End processing Nfc Payload.");
198                 return builder.build();
199             } catch (IOException e) {
200                 throw new IllegalProvisioningArgumentException("Couldn't load payload", e);
201             } catch (NumberFormatException e) {
202                 throw new IllegalProvisioningArgumentException("Incorrect numberformat.", e);
203             } catch (IllformedLocaleException e) {
204                 throw new IllegalProvisioningArgumentException("Invalid locale.", e);
205             } catch (IllegalArgumentException e) {
206                 throw new IllegalProvisioningArgumentException("Invalid parameter found!", e);
207             } catch (NullPointerException e) {
208                 throw new IllegalProvisioningArgumentException(
209                         "Compulsory parameter not found!", e);
210             }
211         }
212         throw new IllegalProvisioningArgumentException(
213                 "Intent does not contain NfcRecord with the correct MIME type.");
214     }
215 
216     /**
217      * Parses Wifi configuration from an {@link Properties} and returns the result in
218      * {@link WifiInfo}.
219      */
220     @Nullable
parseWifiInfoFromProperties(Properties props)221     private WifiInfo parseWifiInfoFromProperties(Properties props) {
222         if (getPropertyFromLongName(props, EXTRA_PROVISIONING_WIFI_SSID) == null) {
223             return null;
224         }
225         WifiInfo.Builder builder = WifiInfo.Builder.builder()
226                 .setSsid(getPropertyFromLongName(props, EXTRA_PROVISIONING_WIFI_SSID))
227                 .setSecurityType(getPropertyFromLongName(
228                         props, EXTRA_PROVISIONING_WIFI_SECURITY_TYPE))
229                 .setPassword(getPropertyFromLongName(props, EXTRA_PROVISIONING_WIFI_PASSWORD))
230                 .setEapMethod(getPropertyFromLongName(props, EXTRA_PROVISIONING_WIFI_EAP_METHOD))
231                 .setPhase2Auth(getPropertyFromLongName(props, EXTRA_PROVISIONING_WIFI_PHASE2_AUTH))
232                 .setCaCertificate(getPropertyFromLongName(
233                         props, EXTRA_PROVISIONING_WIFI_CA_CERTIFICATE))
234                 .setUserCertificate(getPropertyFromLongName(
235                         props, EXTRA_PROVISIONING_WIFI_USER_CERTIFICATE))
236                 .setIdentity(getPropertyFromLongName(props, EXTRA_PROVISIONING_WIFI_IDENTITY))
237                 .setAnonymousIdentity(getPropertyFromLongName(
238                         props, EXTRA_PROVISIONING_WIFI_ANONYMOUS_IDENTITY))
239                 .setDomain(getPropertyFromLongName(props, EXTRA_PROVISIONING_WIFI_DOMAIN))
240                 .setProxyHost(getPropertyFromLongName(props, EXTRA_PROVISIONING_WIFI_PROXY_HOST))
241                 .setProxyBypassHosts(getPropertyFromLongName(
242                         props, EXTRA_PROVISIONING_WIFI_PROXY_BYPASS))
243                 .setPacUrl(getPropertyFromLongName(props, EXTRA_PROVISIONING_WIFI_PAC_URL));
244         // For parsing non-string parameters.
245         String s = null;
246         if ((s = getPropertyFromLongName(props, EXTRA_PROVISIONING_WIFI_PROXY_PORT)) != null) {
247             builder.setProxyPort(Integer.parseInt(s));
248         }
249         if ((s = getPropertyFromLongName(props, EXTRA_PROVISIONING_WIFI_HIDDEN)) != null) {
250             builder.setHidden(Boolean.parseBoolean(s));
251         }
252 
253         return builder.build();
254     }
255 
256     /**
257      * Parses device admin package download info from an {@link Properties} and returns the result
258      * in {@link PackageDownloadInfo}.
259      */
260     @Nullable
parsePackageDownloadInfoFromProperties(Properties props)261     private PackageDownloadInfo parsePackageDownloadInfoFromProperties(Properties props) {
262         if (getPropertyFromLongName(
263                 props, EXTRA_PROVISIONING_DEVICE_ADMIN_PACKAGE_DOWNLOAD_LOCATION) == null) {
264             return null;
265         }
266         PackageDownloadInfo.Builder builder = PackageDownloadInfo.Builder.builder()
267                 .setLocation(getPropertyFromLongName(props,
268                         EXTRA_PROVISIONING_DEVICE_ADMIN_PACKAGE_DOWNLOAD_LOCATION))
269                 .setCookieHeader(getPropertyFromLongName(props,
270                         EXTRA_PROVISIONING_DEVICE_ADMIN_PACKAGE_DOWNLOAD_COOKIE_HEADER));
271         // For parsing non-string parameters.
272         String s = null;
273         if ((s = getPropertyFromLongName(
274                 props, EXTRA_PROVISIONING_DEVICE_ADMIN_MINIMUM_VERSION_CODE)) != null) {
275             builder.setMinVersion(Integer.parseInt(s));
276         }
277         if ((s = getPropertyFromLongName(
278                 props, EXTRA_PROVISIONING_DEVICE_ADMIN_PACKAGE_CHECKSUM)) != null) {
279             builder.setPackageChecksum(StoreUtils.stringToByteArray(s));
280         }
281         if ((s = getPropertyFromLongName(props, EXTRA_PROVISIONING_DEVICE_ADMIN_SIGNATURE_CHECKSUM))
282                 != null) {
283             builder.setSignatureChecksum(StoreUtils.stringToByteArray(s));
284         }
285         return builder.build();
286     }
287 
288     /**
289      * Get a {@link PersistableBundle} from a String property in a Properties object.
290      * @param props the source of the extra
291      * @param extraName key into the Properties object
292      * @return the bundle or {@code null} if there was no property with the given name
293      * @throws IOException if there was an error parsing the propery
294      */
deserializeExtrasBundle(Properties props, String extraName)295     private PersistableBundle deserializeExtrasBundle(Properties props, String extraName)
296             throws IOException {
297         PersistableBundle extrasBundle = null;
298         String serializedExtras = getPropertyFromLongName(props, extraName);
299         if (serializedExtras != null) {
300             Properties extrasProp = new Properties();
301             extrasProp.load(new StringReader(serializedExtras));
302             extrasBundle = new PersistableBundle(extrasProp.size());
303             for (String propName : extrasProp.stringPropertyNames()) {
304                 extrasBundle.putString(propName, extrasProp.getProperty(propName));
305             }
306         }
307         return extrasBundle;
308     }
309 
310     /**
311      * @return the first {@link NdefRecord} found with a recognized MIME-type
312      */
getFirstNdefRecord(Intent nfcIntent)313     public static NdefRecord getFirstNdefRecord(Intent nfcIntent) {
314         // Only one first message with NFC_MIME_TYPE is used.
315         final Parcelable[] ndefMessages = nfcIntent.getParcelableArrayExtra(
316                 NfcAdapter.EXTRA_NDEF_MESSAGES);
317         if (ndefMessages != null) {
318             for (Parcelable rawMsg : ndefMessages) {
319                 NdefMessage msg = (NdefMessage) rawMsg;
320                 for (NdefRecord record : msg.getRecords()) {
321                     String mimeType = new String(record.getType(), UTF_8);
322 
323                     if (MIME_TYPE_PROVISIONING_NFC.equals(mimeType)) {
324                         return record;
325                     }
326 
327                     // Assume only first record of message is used.
328                     break;
329                 }
330             }
331         }
332         return null;
333     }
334 }
335