/* * Copyright (C) 2021 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package android.app.admin; import static android.app.admin.DevicePolicyManager.ACTION_PROVISION_MANAGED_DEVICE_FROM_TRUSTED_SOURCE; import static android.app.admin.DevicePolicyManager.EXTRA_PROVISIONING_ADMIN_EXTRAS_BUNDLE; import static android.app.admin.DevicePolicyManager.EXTRA_PROVISIONING_ALLOW_OFFLINE; import static android.app.admin.DevicePolicyManager.EXTRA_PROVISIONING_DEVICE_ADMIN_COMPONENT_NAME; import static android.app.admin.DevicePolicyManager.EXTRA_PROVISIONING_DEVICE_ADMIN_MINIMUM_VERSION_CODE; import static android.app.admin.DevicePolicyManager.EXTRA_PROVISIONING_DEVICE_ADMIN_PACKAGE_NAME; import static android.app.admin.DevicePolicyManager.EXTRA_PROVISIONING_KEEP_ACCOUNT_ON_MIGRATION; import static android.app.admin.DevicePolicyManager.EXTRA_PROVISIONING_KEEP_SCREEN_ON; import static android.app.admin.DevicePolicyManager.EXTRA_PROVISIONING_LEAVE_ALL_SYSTEM_APPS_ENABLED; import static android.app.admin.DevicePolicyManager.EXTRA_PROVISIONING_LOCAL_TIME; import static android.app.admin.DevicePolicyManager.EXTRA_PROVISIONING_RETURN_BEFORE_POLICY_COMPLIANCE; import static android.app.admin.DevicePolicyManager.EXTRA_PROVISIONING_ROLE_HOLDER_EXTRAS_BUNDLE; import static android.app.admin.DevicePolicyManager.EXTRA_PROVISIONING_SENSORS_PERMISSION_GRANT_OPT_OUT; import static android.app.admin.DevicePolicyManager.EXTRA_PROVISIONING_SHOULD_LAUNCH_RESULT_INTENT; import static android.app.admin.DevicePolicyManager.EXTRA_PROVISIONING_SKIP_EDUCATION_SCREENS; import static android.app.admin.DevicePolicyManager.EXTRA_PROVISIONING_SKIP_ENCRYPTION; import static android.app.admin.DevicePolicyManager.EXTRA_PROVISIONING_SKIP_OWNERSHIP_DISCLAIMER; import static android.app.admin.DevicePolicyManager.EXTRA_PROVISIONING_SUPPORTED_MODES; import static android.app.admin.DevicePolicyManager.EXTRA_PROVISIONING_TRIGGER; import static android.app.admin.DevicePolicyManager.EXTRA_PROVISIONING_USE_MOBILE_DATA; import static android.app.admin.DevicePolicyManager.EXTRA_PROVISIONING_WIFI_HIDDEN; import static android.app.admin.DevicePolicyManager.EXTRA_PROVISIONING_WIFI_PROXY_PORT; import static android.app.admin.DevicePolicyManager.MIME_TYPE_PROVISIONING_NFC; import static android.app.admin.DevicePolicyManager.PROVISIONING_TRIGGER_NFC; import static android.nfc.NfcAdapter.EXTRA_NDEF_MESSAGES; import static java.nio.charset.StandardCharsets.UTF_8; import static java.util.Objects.requireNonNull; import android.annotation.NonNull; import android.annotation.Nullable; import android.content.ComponentName; import android.content.Intent; import android.nfc.NdefMessage; import android.nfc.NdefRecord; import android.nfc.NfcAdapter; import android.os.Bundle; import android.os.Parcelable; import android.os.PersistableBundle; import android.util.Log; import java.io.IOException; import java.io.StringReader; import java.util.Enumeration; import java.util.HashMap; import java.util.Map; import java.util.Properties; import java.util.Set; /** * Utility class that provides functionality to create provisioning intents from nfc intents. */ final class ProvisioningIntentHelper { private static final Map EXTRAS_TO_CLASS_MAP = createExtrasToClassMap(); private static final String TAG = "ProvisioningIntentHelper"; /** * This class is never instantiated */ private ProvisioningIntentHelper() { } @Nullable public static Intent createProvisioningIntentFromNfcIntent(@NonNull Intent nfcIntent) { requireNonNull(nfcIntent); if (!NfcAdapter.ACTION_NDEF_DISCOVERED.equals(nfcIntent.getAction())) { Log.e(TAG, "Wrong Nfc action: " + nfcIntent.getAction()); return null; } NdefRecord firstRecord = getFirstNdefRecord(nfcIntent); if (firstRecord != null) { return createProvisioningIntentFromNdefRecord(firstRecord); } return null; } private static Intent createProvisioningIntentFromNdefRecord(NdefRecord firstRecord) { requireNonNull(firstRecord); Properties properties = loadPropertiesFromPayload(firstRecord.getPayload()); if (properties == null) { Log.e(TAG, "Failed to load NdefRecord properties."); return null; } Bundle bundle = createBundleFromProperties(properties); if (!containsRequiredProvisioningExtras(bundle)) { Log.e(TAG, "Bundle does not contain the required provisioning extras."); return null; } return createProvisioningIntentFromBundle(bundle); } private static Properties loadPropertiesFromPayload(byte[] payload) { Properties properties = new Properties(); try { properties.load(new StringReader(new String(payload, UTF_8))); } catch (IOException e) { Log.e(TAG, "NFC Intent properties loading failed."); return null; } return properties; } private static Bundle createBundleFromProperties(Properties properties) { Enumeration propertyNames = properties.propertyNames(); Bundle bundle = new Bundle(); while (propertyNames.hasMoreElements()) { String propertyName = (String) propertyNames.nextElement(); addPropertyToBundle(propertyName, properties, bundle); } return bundle; } private static void addPropertyToBundle( String propertyName, Properties properties, Bundle bundle) { if (EXTRAS_TO_CLASS_MAP.get(propertyName) == ComponentName.class) { ComponentName componentName = ComponentName.unflattenFromString( properties.getProperty(propertyName)); bundle.putParcelable(propertyName, componentName); } else if (EXTRAS_TO_CLASS_MAP.get(propertyName) == PersistableBundle.class) { try { bundle.putParcelable(propertyName, deserializeExtrasBundle(properties, propertyName)); } catch (IOException e) { Log.e(TAG, "Failed to parse " + propertyName + ".", e); } } else if (EXTRAS_TO_CLASS_MAP.get(propertyName) == Boolean.class) { bundle.putBoolean(propertyName, Boolean.parseBoolean(properties.getProperty(propertyName))); } else if (EXTRAS_TO_CLASS_MAP.get(propertyName) == Long.class) { bundle.putLong(propertyName, Long.parseLong(properties.getProperty(propertyName))); } else if (EXTRAS_TO_CLASS_MAP.get(propertyName) == Integer.class) { bundle.putInt(propertyName, Integer.parseInt(properties.getProperty(propertyName))); } else { bundle.putString(propertyName, properties.getProperty(propertyName)); } } /** * Get a {@link PersistableBundle} from a {@code String} property in a {@link Properties} * object. * @param properties the source of the extra * @param extraName key into the {@link Properties} object * @return the {@link PersistableBundle} or {@code null} if there was no property with the * given name * @throws IOException if there was an error parsing the property */ private static PersistableBundle deserializeExtrasBundle( Properties properties, String extraName) throws IOException { String serializedExtras = properties.getProperty(extraName); if (serializedExtras == null) { return null; } Properties bundleProperties = new Properties(); bundleProperties.load(new StringReader(serializedExtras)); PersistableBundle extrasBundle = new PersistableBundle(bundleProperties.size()); Set propertyNames = bundleProperties.stringPropertyNames(); for (String propertyName : propertyNames) { extrasBundle.putString(propertyName, bundleProperties.getProperty(propertyName)); } return extrasBundle; } private static Intent createProvisioningIntentFromBundle(Bundle bundle) { requireNonNull(bundle); Intent provisioningIntent = new Intent(ACTION_PROVISION_MANAGED_DEVICE_FROM_TRUSTED_SOURCE); provisioningIntent.putExtras(bundle); provisioningIntent.putExtra(EXTRA_PROVISIONING_TRIGGER, PROVISIONING_TRIGGER_NFC); return provisioningIntent; } private static boolean containsRequiredProvisioningExtras(Bundle bundle) { return bundle.containsKey(EXTRA_PROVISIONING_DEVICE_ADMIN_PACKAGE_NAME) || bundle.containsKey(EXTRA_PROVISIONING_DEVICE_ADMIN_COMPONENT_NAME); } /** * Returns the first {@link NdefRecord} found with a recognized MIME-type */ private static NdefRecord getFirstNdefRecord(Intent nfcIntent) { Parcelable[] ndefMessages = nfcIntent.getParcelableArrayExtra(EXTRA_NDEF_MESSAGES); if (ndefMessages == null) { Log.i(TAG, "No EXTRA_NDEF_MESSAGES from nfcIntent"); return null; } for (Parcelable rawMsg : ndefMessages) { NdefMessage msg = (NdefMessage) rawMsg; for (NdefRecord record : msg.getRecords()) { String mimeType = new String(record.getType(), UTF_8); // Only one first message with NFC_MIME_TYPE is used. if (MIME_TYPE_PROVISIONING_NFC.equals(mimeType)) { return record; } // Assume only first record of message is used. break; } } Log.i(TAG, "No compatible records found on nfcIntent"); return null; } private static Map createExtrasToClassMap() { Map map = new HashMap<>(); for (String extra : getBooleanExtras()) { map.put(extra, Boolean.class); } for (String extra : getLongExtras()) { map.put(extra, Long.class); } for (String extra : getIntExtras()) { map.put(extra, Integer.class); } for (String extra : getComponentNameExtras()) { map.put(extra, ComponentName.class); } for (String extra : getPersistableBundleExtras()) { map.put(extra, PersistableBundle.class); } return map; } private static Set getPersistableBundleExtras() { return Set.of( EXTRA_PROVISIONING_ADMIN_EXTRAS_BUNDLE, EXTRA_PROVISIONING_ROLE_HOLDER_EXTRAS_BUNDLE); } private static Set getComponentNameExtras() { return Set.of(EXTRA_PROVISIONING_DEVICE_ADMIN_COMPONENT_NAME); } private static Set getIntExtras() { return Set.of( EXTRA_PROVISIONING_WIFI_PROXY_PORT, EXTRA_PROVISIONING_DEVICE_ADMIN_MINIMUM_VERSION_CODE, EXTRA_PROVISIONING_SUPPORTED_MODES); } private static Set getLongExtras() { return Set.of(EXTRA_PROVISIONING_LOCAL_TIME); } private static Set getBooleanExtras() { return Set.of( EXTRA_PROVISIONING_ALLOW_OFFLINE, EXTRA_PROVISIONING_SHOULD_LAUNCH_RESULT_INTENT, EXTRA_PROVISIONING_KEEP_ACCOUNT_ON_MIGRATION, EXTRA_PROVISIONING_LEAVE_ALL_SYSTEM_APPS_ENABLED, EXTRA_PROVISIONING_WIFI_HIDDEN, EXTRA_PROVISIONING_SENSORS_PERMISSION_GRANT_OPT_OUT, EXTRA_PROVISIONING_SKIP_ENCRYPTION, EXTRA_PROVISIONING_SKIP_EDUCATION_SCREENS, EXTRA_PROVISIONING_USE_MOBILE_DATA, EXTRA_PROVISIONING_SKIP_OWNERSHIP_DISCLAIMER, EXTRA_PROVISIONING_RETURN_BEFORE_POLICY_COMPLIANCE, EXTRA_PROVISIONING_KEEP_SCREEN_ON); } }