1 /* 2 * Copyright (C) 2021 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.app.admin; 18 19 import static android.app.admin.DevicePolicyManager.ACTION_PROVISION_MANAGED_DEVICE_FROM_TRUSTED_SOURCE; 20 import static android.app.admin.DevicePolicyManager.EXTRA_PROVISIONING_ADMIN_EXTRAS_BUNDLE; 21 import static android.app.admin.DevicePolicyManager.EXTRA_PROVISIONING_ALLOW_OFFLINE; 22 import static android.app.admin.DevicePolicyManager.EXTRA_PROVISIONING_DEVICE_ADMIN_COMPONENT_NAME; 23 import static android.app.admin.DevicePolicyManager.EXTRA_PROVISIONING_DEVICE_ADMIN_MINIMUM_VERSION_CODE; 24 import static android.app.admin.DevicePolicyManager.EXTRA_PROVISIONING_DEVICE_ADMIN_PACKAGE_NAME; 25 import static android.app.admin.DevicePolicyManager.EXTRA_PROVISIONING_KEEP_ACCOUNT_ON_MIGRATION; 26 import static android.app.admin.DevicePolicyManager.EXTRA_PROVISIONING_KEEP_SCREEN_ON; 27 import static android.app.admin.DevicePolicyManager.EXTRA_PROVISIONING_LEAVE_ALL_SYSTEM_APPS_ENABLED; 28 import static android.app.admin.DevicePolicyManager.EXTRA_PROVISIONING_LOCAL_TIME; 29 import static android.app.admin.DevicePolicyManager.EXTRA_PROVISIONING_RETURN_BEFORE_POLICY_COMPLIANCE; 30 import static android.app.admin.DevicePolicyManager.EXTRA_PROVISIONING_ROLE_HOLDER_EXTRAS_BUNDLE; 31 import static android.app.admin.DevicePolicyManager.EXTRA_PROVISIONING_SENSORS_PERMISSION_GRANT_OPT_OUT; 32 import static android.app.admin.DevicePolicyManager.EXTRA_PROVISIONING_SHOULD_LAUNCH_RESULT_INTENT; 33 import static android.app.admin.DevicePolicyManager.EXTRA_PROVISIONING_SKIP_EDUCATION_SCREENS; 34 import static android.app.admin.DevicePolicyManager.EXTRA_PROVISIONING_SKIP_ENCRYPTION; 35 import static android.app.admin.DevicePolicyManager.EXTRA_PROVISIONING_SKIP_OWNERSHIP_DISCLAIMER; 36 import static android.app.admin.DevicePolicyManager.EXTRA_PROVISIONING_SUPPORTED_MODES; 37 import static android.app.admin.DevicePolicyManager.EXTRA_PROVISIONING_TRIGGER; 38 import static android.app.admin.DevicePolicyManager.EXTRA_PROVISIONING_USE_MOBILE_DATA; 39 import static android.app.admin.DevicePolicyManager.EXTRA_PROVISIONING_WIFI_HIDDEN; 40 import static android.app.admin.DevicePolicyManager.EXTRA_PROVISIONING_WIFI_PROXY_PORT; 41 import static android.app.admin.DevicePolicyManager.MIME_TYPE_PROVISIONING_NFC; 42 import static android.app.admin.DevicePolicyManager.PROVISIONING_TRIGGER_NFC; 43 import static android.nfc.NfcAdapter.EXTRA_NDEF_MESSAGES; 44 45 import static java.nio.charset.StandardCharsets.UTF_8; 46 import static java.util.Objects.requireNonNull; 47 48 import android.annotation.NonNull; 49 import android.annotation.Nullable; 50 import android.content.ComponentName; 51 import android.content.Intent; 52 import android.nfc.NdefMessage; 53 import android.nfc.NdefRecord; 54 import android.nfc.NfcAdapter; 55 import android.os.Bundle; 56 import android.os.Parcelable; 57 import android.os.PersistableBundle; 58 import android.util.Log; 59 60 import java.io.IOException; 61 import java.io.StringReader; 62 import java.util.Enumeration; 63 import java.util.HashMap; 64 import java.util.Map; 65 import java.util.Properties; 66 import java.util.Set; 67 68 /** 69 * Utility class that provides functionality to create provisioning intents from nfc intents. 70 */ 71 final class ProvisioningIntentHelper { 72 73 private static final Map<String, Class> EXTRAS_TO_CLASS_MAP = createExtrasToClassMap(); 74 75 private static final String TAG = "ProvisioningIntentHelper"; 76 77 /** 78 * This class is never instantiated 79 */ ProvisioningIntentHelper()80 private ProvisioningIntentHelper() { } 81 82 @Nullable createProvisioningIntentFromNfcIntent(@onNull Intent nfcIntent)83 public static Intent createProvisioningIntentFromNfcIntent(@NonNull Intent nfcIntent) { 84 requireNonNull(nfcIntent); 85 86 if (!NfcAdapter.ACTION_NDEF_DISCOVERED.equals(nfcIntent.getAction())) { 87 Log.e(TAG, "Wrong Nfc action: " + nfcIntent.getAction()); 88 return null; 89 } 90 91 NdefRecord firstRecord = getFirstNdefRecord(nfcIntent); 92 93 if (firstRecord != null) { 94 return createProvisioningIntentFromNdefRecord(firstRecord); 95 } 96 97 return null; 98 } 99 100 createProvisioningIntentFromNdefRecord(NdefRecord firstRecord)101 private static Intent createProvisioningIntentFromNdefRecord(NdefRecord firstRecord) { 102 requireNonNull(firstRecord); 103 104 Properties properties = loadPropertiesFromPayload(firstRecord.getPayload()); 105 106 if (properties == null) { 107 Log.e(TAG, "Failed to load NdefRecord properties."); 108 return null; 109 } 110 111 Bundle bundle = createBundleFromProperties(properties); 112 113 if (!containsRequiredProvisioningExtras(bundle)) { 114 Log.e(TAG, "Bundle does not contain the required provisioning extras."); 115 return null; 116 } 117 118 return createProvisioningIntentFromBundle(bundle); 119 } 120 loadPropertiesFromPayload(byte[] payload)121 private static Properties loadPropertiesFromPayload(byte[] payload) { 122 Properties properties = new Properties(); 123 124 try { 125 properties.load(new StringReader(new String(payload, UTF_8))); 126 } catch (IOException e) { 127 Log.e(TAG, "NFC Intent properties loading failed."); 128 return null; 129 } 130 131 return properties; 132 } 133 createBundleFromProperties(Properties properties)134 private static Bundle createBundleFromProperties(Properties properties) { 135 Enumeration propertyNames = properties.propertyNames(); 136 Bundle bundle = new Bundle(); 137 138 while (propertyNames.hasMoreElements()) { 139 String propertyName = (String) propertyNames.nextElement(); 140 addPropertyToBundle(propertyName, properties, bundle); 141 } 142 return bundle; 143 } 144 addPropertyToBundle( String propertyName, Properties properties, Bundle bundle)145 private static void addPropertyToBundle( 146 String propertyName, Properties properties, Bundle bundle) { 147 if (EXTRAS_TO_CLASS_MAP.get(propertyName) == ComponentName.class) { 148 ComponentName componentName = ComponentName.unflattenFromString( 149 properties.getProperty(propertyName)); 150 bundle.putParcelable(propertyName, componentName); 151 } else if (EXTRAS_TO_CLASS_MAP.get(propertyName) == PersistableBundle.class) { 152 try { 153 bundle.putParcelable(propertyName, 154 deserializeExtrasBundle(properties, propertyName)); 155 } catch (IOException e) { 156 Log.e(TAG, 157 "Failed to parse " + propertyName + ".", e); 158 } 159 } else if (EXTRAS_TO_CLASS_MAP.get(propertyName) == Boolean.class) { 160 bundle.putBoolean(propertyName, 161 Boolean.parseBoolean(properties.getProperty(propertyName))); 162 } else if (EXTRAS_TO_CLASS_MAP.get(propertyName) == Long.class) { 163 bundle.putLong(propertyName, Long.parseLong(properties.getProperty(propertyName))); 164 } else if (EXTRAS_TO_CLASS_MAP.get(propertyName) == Integer.class) { 165 bundle.putInt(propertyName, Integer.parseInt(properties.getProperty(propertyName))); 166 } 167 else { 168 bundle.putString(propertyName, properties.getProperty(propertyName)); 169 } 170 } 171 172 /** 173 * Get a {@link PersistableBundle} from a {@code String} property in a {@link Properties} 174 * object. 175 * @param properties the source of the extra 176 * @param extraName key into the {@link Properties} object 177 * @return the {@link PersistableBundle} or {@code null} if there was no property with the 178 * given name 179 * @throws IOException if there was an error parsing the property 180 */ deserializeExtrasBundle( Properties properties, String extraName)181 private static PersistableBundle deserializeExtrasBundle( 182 Properties properties, String extraName) throws IOException { 183 String serializedExtras = properties.getProperty(extraName); 184 if (serializedExtras == null) { 185 return null; 186 } 187 Properties bundleProperties = new Properties(); 188 bundleProperties.load(new StringReader(serializedExtras)); 189 PersistableBundle extrasBundle = new PersistableBundle(bundleProperties.size()); 190 Set<String> propertyNames = bundleProperties.stringPropertyNames(); 191 for (String propertyName : propertyNames) { 192 extrasBundle.putString(propertyName, bundleProperties.getProperty(propertyName)); 193 } 194 return extrasBundle; 195 } 196 createProvisioningIntentFromBundle(Bundle bundle)197 private static Intent createProvisioningIntentFromBundle(Bundle bundle) { 198 requireNonNull(bundle); 199 200 Intent provisioningIntent = new Intent(ACTION_PROVISION_MANAGED_DEVICE_FROM_TRUSTED_SOURCE); 201 202 provisioningIntent.putExtras(bundle); 203 204 provisioningIntent.putExtra(EXTRA_PROVISIONING_TRIGGER, PROVISIONING_TRIGGER_NFC); 205 206 return provisioningIntent; 207 } 208 containsRequiredProvisioningExtras(Bundle bundle)209 private static boolean containsRequiredProvisioningExtras(Bundle bundle) { 210 return bundle.containsKey(EXTRA_PROVISIONING_DEVICE_ADMIN_PACKAGE_NAME) || 211 bundle.containsKey(EXTRA_PROVISIONING_DEVICE_ADMIN_COMPONENT_NAME); 212 } 213 214 /** 215 * Returns the first {@link NdefRecord} found with a recognized MIME-type 216 */ getFirstNdefRecord(Intent nfcIntent)217 private static NdefRecord getFirstNdefRecord(Intent nfcIntent) { 218 Parcelable[] ndefMessages = nfcIntent.getParcelableArrayExtra(EXTRA_NDEF_MESSAGES); 219 if (ndefMessages == null) { 220 Log.i(TAG, "No EXTRA_NDEF_MESSAGES from nfcIntent"); 221 return null; 222 } 223 224 for (Parcelable rawMsg : ndefMessages) { 225 NdefMessage msg = (NdefMessage) rawMsg; 226 for (NdefRecord record : msg.getRecords()) { 227 String mimeType = new String(record.getType(), UTF_8); 228 229 // Only one first message with NFC_MIME_TYPE is used. 230 if (MIME_TYPE_PROVISIONING_NFC.equals(mimeType)) { 231 return record; 232 } 233 234 // Assume only first record of message is used. 235 break; 236 } 237 } 238 239 Log.i(TAG, "No compatible records found on nfcIntent"); 240 return null; 241 } 242 createExtrasToClassMap()243 private static Map<String, Class> createExtrasToClassMap() { 244 Map<String, Class> map = new HashMap<>(); 245 for (String extra : getBooleanExtras()) { 246 map.put(extra, Boolean.class); 247 } 248 for (String extra : getLongExtras()) { 249 map.put(extra, Long.class); 250 } 251 for (String extra : getIntExtras()) { 252 map.put(extra, Integer.class); 253 } 254 for (String extra : getComponentNameExtras()) { 255 map.put(extra, ComponentName.class); 256 } 257 for (String extra : getPersistableBundleExtras()) { 258 map.put(extra, PersistableBundle.class); 259 } 260 return map; 261 } 262 getPersistableBundleExtras()263 private static Set<String> getPersistableBundleExtras() { 264 return Set.of( 265 EXTRA_PROVISIONING_ADMIN_EXTRAS_BUNDLE, 266 EXTRA_PROVISIONING_ROLE_HOLDER_EXTRAS_BUNDLE); 267 } 268 getComponentNameExtras()269 private static Set<String> getComponentNameExtras() { 270 return Set.of(EXTRA_PROVISIONING_DEVICE_ADMIN_COMPONENT_NAME); 271 } 272 getIntExtras()273 private static Set<String> getIntExtras() { 274 return Set.of( 275 EXTRA_PROVISIONING_WIFI_PROXY_PORT, 276 EXTRA_PROVISIONING_DEVICE_ADMIN_MINIMUM_VERSION_CODE, 277 EXTRA_PROVISIONING_SUPPORTED_MODES); 278 } 279 getLongExtras()280 private static Set<String> getLongExtras() { 281 return Set.of(EXTRA_PROVISIONING_LOCAL_TIME); 282 } 283 getBooleanExtras()284 private static Set<String> getBooleanExtras() { 285 return Set.of( 286 EXTRA_PROVISIONING_ALLOW_OFFLINE, 287 EXTRA_PROVISIONING_SHOULD_LAUNCH_RESULT_INTENT, 288 EXTRA_PROVISIONING_KEEP_ACCOUNT_ON_MIGRATION, 289 EXTRA_PROVISIONING_LEAVE_ALL_SYSTEM_APPS_ENABLED, 290 EXTRA_PROVISIONING_WIFI_HIDDEN, 291 EXTRA_PROVISIONING_SENSORS_PERMISSION_GRANT_OPT_OUT, 292 EXTRA_PROVISIONING_SKIP_ENCRYPTION, 293 EXTRA_PROVISIONING_SKIP_EDUCATION_SCREENS, 294 EXTRA_PROVISIONING_USE_MOBILE_DATA, 295 EXTRA_PROVISIONING_SKIP_OWNERSHIP_DISCLAIMER, 296 EXTRA_PROVISIONING_RETURN_BEFORE_POLICY_COMPLIANCE, 297 EXTRA_PROVISIONING_KEEP_SCREEN_ON); 298 } 299 } 300