1/*
2 * Copyright 2014, 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
17package com.android.managedprovisioning;
18
19import static android.app.admin.DevicePolicyManager.EXTRA_PROVISIONING_TIME_ZONE;
20import static android.app.admin.DevicePolicyManager.EXTRA_PROVISIONING_LOCAL_TIME;
21import static android.app.admin.DevicePolicyManager.EXTRA_PROVISIONING_LOCALE;
22import static android.app.admin.DevicePolicyManager.EXTRA_PROVISIONING_WIFI_SSID;
23import static android.app.admin.DevicePolicyManager.EXTRA_PROVISIONING_WIFI_HIDDEN;
24import static android.app.admin.DevicePolicyManager.EXTRA_PROVISIONING_WIFI_SECURITY_TYPE;
25import static android.app.admin.DevicePolicyManager.EXTRA_PROVISIONING_WIFI_PASSWORD;
26import static android.app.admin.DevicePolicyManager.EXTRA_PROVISIONING_WIFI_PROXY_HOST;
27import static android.app.admin.DevicePolicyManager.EXTRA_PROVISIONING_WIFI_PROXY_PORT;
28import static android.app.admin.DevicePolicyManager.EXTRA_PROVISIONING_WIFI_PROXY_BYPASS;
29import static android.app.admin.DevicePolicyManager.EXTRA_PROVISIONING_WIFI_PAC_URL;
30import static android.app.admin.DevicePolicyManager.EXTRA_PROVISIONING_ADMIN_EXTRAS_BUNDLE;
31import static android.app.admin.DevicePolicyManager.EXTRA_PROVISIONING_DEVICE_ADMIN_PACKAGE_NAME;
32import static android.app.admin.DevicePolicyManager.EXTRA_PROVISIONING_DEVICE_ADMIN_PACKAGE_DOWNLOAD_LOCATION;
33import static android.app.admin.DevicePolicyManager.EXTRA_PROVISIONING_DEVICE_ADMIN_PACKAGE_DOWNLOAD_COOKIE_HEADER;
34import static android.app.admin.DevicePolicyManager.EXTRA_PROVISIONING_DEVICE_ADMIN_PACKAGE_CHECKSUM;
35import static android.app.admin.DevicePolicyManager.MIME_TYPE_PROVISIONING_NFC;
36import static java.nio.charset.StandardCharsets.UTF_8;
37
38import android.content.Intent;
39import android.nfc.NdefMessage;
40import android.nfc.NdefRecord;
41import android.nfc.NfcAdapter;
42import android.os.Bundle;
43import android.os.Parcelable;
44import android.os.PersistableBundle;
45import android.text.TextUtils;
46import android.util.Base64;
47
48import java.io.IOException;
49import java.io.StringReader;
50import java.util.Enumeration;
51import java.util.HashMap;
52import java.util.IllformedLocaleException;
53import java.util.Locale;
54import java.util.Properties;
55
56/**
57 * This class can initialize a {@link ProvisioningParams} object from an intent.
58 * A {@link ProvisioningParams} object stores various parameters for the device owner provisioning.
59 * There are two kinds of intents that can be parsed it into {@link ProvisioningParams}:
60 *
61 * <p>
62 * Intent was received via Nfc.
63 * The intent contains the extra {@link NfcAdapter.EXTRA_NDEF_MESSAGES}, which indicates that
64 * provisioning was started via Nfc bump. This extra contains an NDEF message, which contains an
65 * NfcRecord with mime type {@link MIME_TYPE_PROVISIONING_NFC}. This record stores a serialized
66 * properties object, which contains the serialized extra's described in the next option.
67 * A typical use case would be a programmer application that sends an Nfc bump to start Nfc
68 * provisioning from a programmer device.
69 *
70 * <p>
71 * Intent was received directly.
72 * The intent contains the extra {@link #EXTRA_PROVISIONING_DEVICE_ADMIN_PACKAGE_NAME},
73 * and may contain {@link #EXTRA_PROVISIONING_TIME_ZONE},
74 * {@link #EXTRA_PROVISIONING_LOCAL_TIME}, and {@link #EXTRA_PROVISIONING_LOCALE}. A download
75 * location may be specified in {@link #EXTRA_PROVISIONING_DEVICE_ADMIN_PACKAGE_DOWNLOAD_LOCATION}
76 * together with an optional http cookie header
77 * {@link #EXTRA_PROVISIONING_DEVICE_ADMIN_PACKAGE_DOWNLOAD_COOKIE_HEADER} accompanied by the SHA-1
78 * sum of the target file {@link #EXTRA_PROVISIONING_DEVICE_ADMIN_PACKAGE_CHECKSUM}.
79 * Additional information to send through to the device admin may be specified in
80 * {@link #EXTRA_PROVISIONING_ADMIN_EXTRAS_BUNDLE}.
81 * Furthermore a wifi network may be specified in {@link #EXTRA_PROVISIONING_WIFI_SSID}, and if
82 * applicable {@link #EXTRA_PROVISIONING_WIFI_HIDDEN},
83 * {@link #EXTRA_PROVISIONING_WIFI_SECURITY_TYPE}, {@link #EXTRA_PROVISIONING_WIFI_PASSWORD},
84 * {@link #EXTRA_PROVISIONING_WIFI_PROXY_HOST}, {@link #EXTRA_PROVISIONING_WIFI_PROXY_PORT},
85 * {@link #EXTRA_PROVISIONING_WIFI_PROXY_BYPASS}.
86 * A typical use case would be the {@link BootReminder} sending the intent after device encryption
87 * and reboot.
88 *
89 * <p>
90 * Furthermore this class can construct the bundle of extras for the second kind of intent given a
91 * {@link ProvisioningParams}, and it keeps track of the types of the extras in the
92 * DEVICE_OWNER_x_EXTRAS, with x the appropriate type.
93 */
94public class MessageParser {
95    private static final String EXTRA_PROVISIONING_STARTED_BY_NFC  =
96            "com.android.managedprovisioning.extra.started_by_nfc";
97
98    protected static final String[] DEVICE_OWNER_STRING_EXTRAS = {
99        EXTRA_PROVISIONING_TIME_ZONE,
100        EXTRA_PROVISIONING_LOCALE,
101        EXTRA_PROVISIONING_WIFI_SSID,
102        EXTRA_PROVISIONING_WIFI_SECURITY_TYPE,
103        EXTRA_PROVISIONING_WIFI_PASSWORD,
104        EXTRA_PROVISIONING_WIFI_PROXY_HOST,
105        EXTRA_PROVISIONING_WIFI_PROXY_BYPASS,
106        EXTRA_PROVISIONING_WIFI_PAC_URL,
107        EXTRA_PROVISIONING_DEVICE_ADMIN_PACKAGE_NAME,
108        EXTRA_PROVISIONING_DEVICE_ADMIN_PACKAGE_DOWNLOAD_LOCATION,
109        EXTRA_PROVISIONING_DEVICE_ADMIN_PACKAGE_DOWNLOAD_COOKIE_HEADER,
110        EXTRA_PROVISIONING_DEVICE_ADMIN_PACKAGE_CHECKSUM
111    };
112
113    protected static final String[] DEVICE_OWNER_LONG_EXTRAS = {
114        EXTRA_PROVISIONING_LOCAL_TIME
115    };
116
117    protected static final String[] DEVICE_OWNER_INT_EXTRAS = {
118        EXTRA_PROVISIONING_WIFI_PROXY_PORT
119    };
120
121    protected static final String[] DEVICE_OWNER_BOOLEAN_EXTRAS = {
122        EXTRA_PROVISIONING_WIFI_HIDDEN,
123        EXTRA_PROVISIONING_STARTED_BY_NFC
124    };
125
126    protected static final String[] DEVICE_OWNER_PERSISTABLE_BUNDLE_EXTRAS = {
127        EXTRA_PROVISIONING_ADMIN_EXTRAS_BUNDLE
128    };
129
130    public void addProvisioningParamsToBundle(Bundle bundle, ProvisioningParams params) {
131        bundle.putString(EXTRA_PROVISIONING_TIME_ZONE, params.mTimeZone);
132        bundle.putString(EXTRA_PROVISIONING_LOCALE, params.getLocaleAsString());
133        bundle.putString(EXTRA_PROVISIONING_WIFI_SSID, params.mWifiSsid);
134        bundle.putString(EXTRA_PROVISIONING_WIFI_SECURITY_TYPE, params.mWifiSecurityType);
135        bundle.putString(EXTRA_PROVISIONING_WIFI_PASSWORD, params.mWifiPassword);
136        bundle.putString(EXTRA_PROVISIONING_WIFI_PROXY_HOST, params.mWifiProxyHost);
137        bundle.putString(EXTRA_PROVISIONING_WIFI_PROXY_BYPASS, params.mWifiProxyBypassHosts);
138        bundle.putString(EXTRA_PROVISIONING_WIFI_PAC_URL, params.mWifiPacUrl);
139        bundle.putString(EXTRA_PROVISIONING_DEVICE_ADMIN_PACKAGE_NAME,
140                params.mDeviceAdminPackageName);
141        bundle.putString(EXTRA_PROVISIONING_DEVICE_ADMIN_PACKAGE_DOWNLOAD_LOCATION,
142                params.mDeviceAdminPackageDownloadLocation);
143        bundle.putString(EXTRA_PROVISIONING_DEVICE_ADMIN_PACKAGE_DOWNLOAD_COOKIE_HEADER,
144                params.mDeviceAdminPackageDownloadCookieHeader);
145        bundle.putString(EXTRA_PROVISIONING_DEVICE_ADMIN_PACKAGE_CHECKSUM,
146                params.getDeviceAdminPackageChecksumAsString());
147
148        bundle.putLong(EXTRA_PROVISIONING_LOCAL_TIME, params.mLocalTime);
149
150        bundle.putInt(EXTRA_PROVISIONING_WIFI_PROXY_PORT, params.mWifiProxyPort);
151
152        bundle.putBoolean(EXTRA_PROVISIONING_WIFI_HIDDEN, params.mWifiHidden);
153        bundle.putBoolean(EXTRA_PROVISIONING_STARTED_BY_NFC, params.mStartedByNfc);
154
155        bundle.putParcelable(EXTRA_PROVISIONING_ADMIN_EXTRAS_BUNDLE, params.mAdminExtrasBundle);
156    }
157
158    public ProvisioningParams parseIntent(Intent intent) throws ParseException {
159        ProvisionLogger.logi("Processing intent.");
160        if (intent.hasExtra(NfcAdapter.EXTRA_NDEF_MESSAGES)) {
161            return parseNfcIntent(intent);
162        } else {
163            return parseNonNfcIntent(intent);
164        }
165    }
166
167    public ProvisioningParams parseNfcIntent(Intent nfcIntent)
168        throws ParseException {
169        ProvisionLogger.logi("Processing Nfc Payload.");
170        // Only one first message with NFC_MIME_TYPE is used.
171        for (Parcelable rawMsg : nfcIntent
172                     .getParcelableArrayExtra(NfcAdapter.EXTRA_NDEF_MESSAGES)) {
173            NdefMessage msg = (NdefMessage) rawMsg;
174
175            // Assume only first record of message is used.
176            NdefRecord firstRecord = msg.getRecords()[0];
177            String mimeType = new String(firstRecord.getType(), UTF_8);
178
179            if (MIME_TYPE_PROVISIONING_NFC.equals(mimeType)) {
180                ProvisioningParams params = parseProperties(new String(firstRecord.getPayload()
181                                , UTF_8));
182                params.mStartedByNfc = true;
183                return params;
184            }
185        }
186        throw new ParseException(
187                "Intent does not contain NfcRecord with the correct MIME type.",
188                R.string.device_owner_error_general);
189    }
190
191    // Note: You can't include the EXTRA_PROVISIONING_ADMIN_EXTRAS_BUNDLE in the properties that is
192    // send over Nfc, because there is no publicly documented conversion from PersistableBundle to
193    // String.
194    private ProvisioningParams parseProperties(String data)
195            throws ParseException {
196        ProvisioningParams params = new ProvisioningParams();
197        try {
198            Properties props = new Properties();
199            props.load(new StringReader(data));
200
201            String s; // Used for parsing non-Strings.
202            params.mTimeZone
203                    = props.getProperty(EXTRA_PROVISIONING_TIME_ZONE);
204            if ((s = props.getProperty(EXTRA_PROVISIONING_LOCALE)) != null) {
205                params.mLocale = stringToLocale(s);
206            }
207            params.mWifiSsid = props.getProperty(EXTRA_PROVISIONING_WIFI_SSID);
208            params.mWifiSecurityType = props.getProperty(EXTRA_PROVISIONING_WIFI_SECURITY_TYPE);
209            params.mWifiPassword = props.getProperty(EXTRA_PROVISIONING_WIFI_PASSWORD);
210            params.mWifiProxyHost = props.getProperty(EXTRA_PROVISIONING_WIFI_PROXY_HOST);
211            params.mWifiProxyBypassHosts = props.getProperty(EXTRA_PROVISIONING_WIFI_PROXY_BYPASS);
212            params.mWifiPacUrl = props.getProperty(EXTRA_PROVISIONING_WIFI_PAC_URL);
213            params.mDeviceAdminPackageName
214                    = props.getProperty(EXTRA_PROVISIONING_DEVICE_ADMIN_PACKAGE_NAME);
215            params.mDeviceAdminPackageDownloadLocation
216                    = props.getProperty(EXTRA_PROVISIONING_DEVICE_ADMIN_PACKAGE_DOWNLOAD_LOCATION);
217            params.mDeviceAdminPackageDownloadCookieHeader = props.getProperty(
218                    EXTRA_PROVISIONING_DEVICE_ADMIN_PACKAGE_DOWNLOAD_COOKIE_HEADER);
219            if ((s = props.getProperty(EXTRA_PROVISIONING_DEVICE_ADMIN_PACKAGE_CHECKSUM)) != null) {
220                params.mDeviceAdminPackageChecksum = stringToByteArray(s);
221            }
222
223            if ((s = props.getProperty(EXTRA_PROVISIONING_LOCAL_TIME)) != null) {
224                params.mLocalTime = Long.parseLong(s);
225            }
226
227            if ((s = props.getProperty(EXTRA_PROVISIONING_WIFI_PROXY_PORT)) != null) {
228                params.mWifiProxyPort = Integer.parseInt(s);
229            }
230
231            if ((s = props.getProperty(EXTRA_PROVISIONING_WIFI_HIDDEN)) != null) {
232                params.mWifiHidden = Boolean.parseBoolean(s);
233            }
234
235            checkValidityOfProvisioningParams(params);
236            return params;
237        } catch (IOException e) {
238            throw new ParseException("Couldn't load payload",
239                    R.string.device_owner_error_general, e);
240        } catch (NumberFormatException e) {
241            throw new ParseException("Incorrect numberformat.",
242                    R.string.device_owner_error_general, e);
243        } catch (IllformedLocaleException e) {
244            throw new ParseException("Invalid locale.",
245                    R.string.device_owner_error_general, e);
246        }
247    }
248
249    public ProvisioningParams parseNonNfcIntent(Intent intent)
250        throws ParseException {
251        ProvisionLogger.logi("Processing intent.");
252        ProvisioningParams params = new ProvisioningParams();
253
254        params.mTimeZone = intent.getStringExtra(EXTRA_PROVISIONING_TIME_ZONE);
255        String localeString = intent.getStringExtra(EXTRA_PROVISIONING_LOCALE);
256        if (localeString != null) {
257            params.mLocale = stringToLocale(localeString);
258        }
259        params.mWifiSsid = intent.getStringExtra(EXTRA_PROVISIONING_WIFI_SSID);
260        params.mWifiSecurityType = intent.getStringExtra(EXTRA_PROVISIONING_WIFI_SECURITY_TYPE);
261        params.mWifiPassword = intent.getStringExtra(EXTRA_PROVISIONING_WIFI_PASSWORD);
262        params.mWifiProxyHost = intent.getStringExtra(EXTRA_PROVISIONING_WIFI_PROXY_HOST);
263        params.mWifiProxyBypassHosts = intent.getStringExtra(EXTRA_PROVISIONING_WIFI_PROXY_BYPASS);
264        params.mWifiPacUrl = intent.getStringExtra(EXTRA_PROVISIONING_WIFI_PAC_URL);
265        params.mDeviceAdminPackageName
266                = intent.getStringExtra(EXTRA_PROVISIONING_DEVICE_ADMIN_PACKAGE_NAME);
267        params.mDeviceAdminPackageDownloadLocation
268                = intent.getStringExtra(EXTRA_PROVISIONING_DEVICE_ADMIN_PACKAGE_DOWNLOAD_LOCATION);
269        params.mDeviceAdminPackageDownloadCookieHeader = intent.getStringExtra(
270                EXTRA_PROVISIONING_DEVICE_ADMIN_PACKAGE_DOWNLOAD_COOKIE_HEADER);
271        String hashString = intent.getStringExtra(EXTRA_PROVISIONING_DEVICE_ADMIN_PACKAGE_CHECKSUM);
272        if (hashString != null) {
273            params.mDeviceAdminPackageChecksum = stringToByteArray(hashString);
274        }
275
276        params.mLocalTime = intent.getLongExtra(EXTRA_PROVISIONING_LOCAL_TIME,
277                ProvisioningParams.DEFAULT_LOCAL_TIME);
278
279        params.mWifiProxyPort = intent.getIntExtra(EXTRA_PROVISIONING_WIFI_PROXY_PORT,
280                ProvisioningParams.DEFAULT_WIFI_PROXY_PORT);
281
282        params.mWifiHidden = intent.getBooleanExtra(EXTRA_PROVISIONING_WIFI_HIDDEN,
283                ProvisioningParams.DEFAULT_WIFI_HIDDEN);
284        params.mStartedByNfc = intent.getBooleanExtra(EXTRA_PROVISIONING_STARTED_BY_NFC,
285                false);
286
287        try {
288            params.mAdminExtrasBundle = (PersistableBundle) intent.getParcelableExtra(
289                    EXTRA_PROVISIONING_ADMIN_EXTRAS_BUNDLE);
290        } catch (ClassCastException e) {
291            throw new ParseException("Extra " + EXTRA_PROVISIONING_ADMIN_EXTRAS_BUNDLE
292                    + " must be of type PersistableBundle.",
293                    R.string.device_owner_error_general, e);
294        }
295
296        checkValidityOfProvisioningParams(params);
297        return params;
298    }
299
300    /**
301     * Check whether necessary fields are set.
302     */
303    private void checkValidityOfProvisioningParams(ProvisioningParams params)
304        throws ParseException  {
305        if (TextUtils.isEmpty(params.mDeviceAdminPackageName)) {
306            throw new ParseException("Must provide the name of the device admin package.",
307                    R.string.device_owner_error_general);
308        }
309        if (!TextUtils.isEmpty(params.mDeviceAdminPackageDownloadLocation)) {
310            if (params.mDeviceAdminPackageChecksum == null ||
311                    params.mDeviceAdminPackageChecksum.length == 0) {
312                throw new ParseException("Checksum of installer file is required for downloading " +
313                        "device admin file, but not provided.",
314                        R.string.device_owner_error_general);
315            }
316        }
317    }
318
319    /**
320     * Exception thrown when the ProvisioningParams initialization failed completely.
321     *
322     * Note: We're using a custom exception to avoid catching subsequent exceptions that might be
323     * significant.
324     */
325    public static class ParseException extends Exception {
326        private int mErrorMessageId;
327
328        public ParseException(String message, int errorMessageId) {
329            super(message);
330            mErrorMessageId = errorMessageId;
331        }
332
333        public ParseException(String message, int errorMessageId, Throwable t) {
334            super(message, t);
335            mErrorMessageId = errorMessageId;
336        }
337
338        public int getErrorMessageId() {
339            return mErrorMessageId;
340        }
341    }
342
343    public static byte[] stringToByteArray(String s)
344        throws NumberFormatException {
345        try {
346            return Base64.decode(s, Base64.URL_SAFE);
347        } catch (IllegalArgumentException e) {
348            throw new NumberFormatException("Incorrect checksum format.");
349        }
350    }
351
352    public static Locale stringToLocale(String s)
353        throws IllformedLocaleException {
354        return new Locale.Builder().setLanguageTag(s.replace("_", "-")).build();
355    }
356}
357