/* Copyright (C) 2010 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 com.android.exchange.adapter; import android.app.admin.DevicePolicyManager; import android.content.Context; import android.content.res.Resources; import android.os.storage.StorageManager; import android.os.storage.StorageVolume; import com.android.emailcommon.provider.Policy; import com.android.exchange.EasSyncService; import com.android.exchange.ExchangeService; import com.android.exchange.R; import com.android.exchange.SecurityPolicyDelegate; import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserException; import org.xmlpull.v1.XmlPullParserFactory; import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; import java.util.ArrayList; /** * Parse the result of the Provision command * * Assuming a successful parse, we store the PolicySet and the policy key */ public class ProvisionParser extends Parser { private final EasSyncService mService; Policy mPolicy = null; String mSecuritySyncKey = null; boolean mRemoteWipe = false; boolean mIsSupportable = true; // An array of string resource id's describing policies that are unsupported by the device/app String[] mUnsupportedPolicies; boolean smimeRequired = false; public ProvisionParser(InputStream in, EasSyncService service) throws IOException { super(in); mService = service; } public Policy getPolicy() { return mPolicy; } public String getSecuritySyncKey() { return mSecuritySyncKey; } public void setSecuritySyncKey(String securitySyncKey) { mSecuritySyncKey = securitySyncKey; } public boolean getRemoteWipe() { return mRemoteWipe; } public boolean hasSupportablePolicySet() { return (mPolicy != null) && mIsSupportable; } public void clearUnsupportedPolicies() { mPolicy = SecurityPolicyDelegate.clearUnsupportedPolicies(mService.mContext, mPolicy); mIsSupportable = true; mUnsupportedPolicies = null; } public String[] getUnsupportedPolicies() { return mUnsupportedPolicies; } private void setPolicy(Policy policy) { policy.normalize(); mPolicy = policy; } private boolean deviceSupportsEncryption() { DevicePolicyManager dpm = (DevicePolicyManager) mService.mContext.getSystemService(Context.DEVICE_POLICY_SERVICE); int status = dpm.getStorageEncryptionStatus(); return status != DevicePolicyManager.ENCRYPTION_STATUS_UNSUPPORTED; } private void parseProvisionDocWbxml() throws IOException { Policy policy = new Policy(); ArrayList<Integer> unsupportedList = new ArrayList<Integer>(); boolean passwordEnabled = false; while (nextTag(Tags.PROVISION_EAS_PROVISION_DOC) != END) { boolean tagIsSupported = true; int res = 0; switch (tag) { case Tags.PROVISION_DEVICE_PASSWORD_ENABLED: if (getValueInt() == 1) { passwordEnabled = true; if (policy.mPasswordMode == Policy.PASSWORD_MODE_NONE) { policy.mPasswordMode = Policy.PASSWORD_MODE_SIMPLE; } } break; case Tags.PROVISION_MIN_DEVICE_PASSWORD_LENGTH: policy.mPasswordMinLength = getValueInt(); break; case Tags.PROVISION_ALPHA_DEVICE_PASSWORD_ENABLED: if (getValueInt() == 1) { policy.mPasswordMode = Policy.PASSWORD_MODE_STRONG; } break; case Tags.PROVISION_MAX_INACTIVITY_TIME_DEVICE_LOCK: // EAS gives us seconds, which is, happily, what the PolicySet requires policy.mMaxScreenLockTime = getValueInt(); break; case Tags.PROVISION_MAX_DEVICE_PASSWORD_FAILED_ATTEMPTS: policy.mPasswordMaxFails = getValueInt(); break; case Tags.PROVISION_DEVICE_PASSWORD_EXPIRATION: policy.mPasswordExpirationDays = getValueInt(); break; case Tags.PROVISION_DEVICE_PASSWORD_HISTORY: policy.mPasswordHistory = getValueInt(); break; case Tags.PROVISION_ALLOW_CAMERA: policy.mDontAllowCamera = (getValueInt() == 0); break; case Tags.PROVISION_ALLOW_SIMPLE_DEVICE_PASSWORD: // Ignore this unless there's any MSFT documentation for what this means // Hint: I haven't seen any that's more specific than "simple" getValue(); break; // The following policies, if false, can't be supported at the moment case Tags.PROVISION_ALLOW_STORAGE_CARD: case Tags.PROVISION_ALLOW_UNSIGNED_APPLICATIONS: case Tags.PROVISION_ALLOW_UNSIGNED_INSTALLATION_PACKAGES: case Tags.PROVISION_ALLOW_WIFI: case Tags.PROVISION_ALLOW_TEXT_MESSAGING: case Tags.PROVISION_ALLOW_POP_IMAP_EMAIL: case Tags.PROVISION_ALLOW_IRDA: case Tags.PROVISION_ALLOW_HTML_EMAIL: case Tags.PROVISION_ALLOW_BROWSER: case Tags.PROVISION_ALLOW_CONSUMER_EMAIL: case Tags.PROVISION_ALLOW_INTERNET_SHARING: if (getValueInt() == 0) { tagIsSupported = false; switch(tag) { case Tags.PROVISION_ALLOW_STORAGE_CARD: res = R.string.policy_dont_allow_storage_cards; break; case Tags.PROVISION_ALLOW_UNSIGNED_APPLICATIONS: res = R.string.policy_dont_allow_unsigned_apps; break; case Tags.PROVISION_ALLOW_UNSIGNED_INSTALLATION_PACKAGES: res = R.string.policy_dont_allow_unsigned_installers; break; case Tags.PROVISION_ALLOW_WIFI: res = R.string.policy_dont_allow_wifi; break; case Tags.PROVISION_ALLOW_TEXT_MESSAGING: res = R.string.policy_dont_allow_text_messaging; break; case Tags.PROVISION_ALLOW_POP_IMAP_EMAIL: res = R.string.policy_dont_allow_pop_imap; break; case Tags.PROVISION_ALLOW_IRDA: res = R.string.policy_dont_allow_irda; break; case Tags.PROVISION_ALLOW_HTML_EMAIL: res = R.string.policy_dont_allow_html; policy.mDontAllowHtml = true; break; case Tags.PROVISION_ALLOW_BROWSER: res = R.string.policy_dont_allow_browser; break; case Tags.PROVISION_ALLOW_CONSUMER_EMAIL: res = R.string.policy_dont_allow_consumer_email; break; case Tags.PROVISION_ALLOW_INTERNET_SHARING: res = R.string.policy_dont_allow_internet_sharing; break; } if (res > 0) { unsupportedList.add(res); } } break; case Tags.PROVISION_ATTACHMENTS_ENABLED: policy.mDontAllowAttachments = getValueInt() != 1; break; // Bluetooth: 0 = no bluetooth; 1 = only hands-free; 2 = allowed case Tags.PROVISION_ALLOW_BLUETOOTH: if (getValueInt() != 2) { tagIsSupported = false; unsupportedList.add(R.string.policy_bluetooth_restricted); } break; // We may now support device (internal) encryption; we'll check this capability // below with the call to SecurityPolicy.isSupported() case Tags.PROVISION_REQUIRE_DEVICE_ENCRYPTION: if (getValueInt() == 1) { if (!deviceSupportsEncryption()) { tagIsSupported = false; unsupportedList.add(R.string.policy_require_encryption); } else { policy.mRequireEncryption = true; } } break; // Note that DEVICE_ENCRYPTION_ENABLED refers to SD card encryption, which the OS // does not yet support. case Tags.PROVISION_DEVICE_ENCRYPTION_ENABLED: if (getValueInt() == 1) { log("Policy requires SD card encryption"); // Let's see if this can be supported on our device... if (deviceSupportsEncryption()) { StorageManager sm = (StorageManager)mService.mContext.getSystemService( Context.STORAGE_SERVICE); // NOTE: Private API! // Go through volumes; if ANY are removable, we can't support this // policy. StorageVolume[] volumeList = sm.getVolumeList(); for (StorageVolume volume: volumeList) { if (volume.isRemovable()) { tagIsSupported = false; log("Removable: " + volume.getDescription()); break; // Break only from the storage volume loop } else { log("Not Removable: " + volume.getDescription()); } } if (tagIsSupported) { // If this policy is requested, we MUST also require encryption log("Device supports SD card encryption"); policy.mRequireEncryption = true; break; } } else { log("Device doesn't support encryption; failing"); tagIsSupported = false; } // If we fall through, we can't support the policy unsupportedList.add(R.string.policy_require_sd_encryption); } break; // Note this policy; we enforce it in ExchangeService case Tags.PROVISION_REQUIRE_MANUAL_SYNC_WHEN_ROAMING: policy.mRequireManualSyncWhenRoaming = getValueInt() == 1; break; // We are allowed to accept policies, regardless of value of this tag // TODO: When we DO support a recovery password, we need to store the value in // the account (so we know to utilize it) case Tags.PROVISION_PASSWORD_RECOVERY_ENABLED: // Read, but ignore, value policy.mPasswordRecoveryEnabled = getValueInt() == 1; break; // The following policies, if true, can't be supported at the moment case Tags.PROVISION_REQUIRE_SIGNED_SMIME_MESSAGES: case Tags.PROVISION_REQUIRE_ENCRYPTED_SMIME_MESSAGES: case Tags.PROVISION_REQUIRE_SIGNED_SMIME_ALGORITHM: case Tags.PROVISION_REQUIRE_ENCRYPTION_SMIME_ALGORITHM: if (getValueInt() == 1) { tagIsSupported = false; if (!smimeRequired) { unsupportedList.add(R.string.policy_require_smime); smimeRequired = true; } } break; case Tags.PROVISION_MAX_ATTACHMENT_SIZE: int max = getValueInt(); if (max > 0) { policy.mMaxAttachmentSize = max; } break; // Complex characters are supported case Tags.PROVISION_MIN_DEVICE_PASSWORD_COMPLEX_CHARS: policy.mPasswordComplexChars = getValueInt(); break; // The following policies are moot; they allow functionality that we don't support case Tags.PROVISION_ALLOW_DESKTOP_SYNC: case Tags.PROVISION_ALLOW_SMIME_ENCRYPTION_NEGOTIATION: case Tags.PROVISION_ALLOW_SMIME_SOFT_CERTS: case Tags.PROVISION_ALLOW_REMOTE_DESKTOP: skipTag(); break; // We don't handle approved/unapproved application lists case Tags.PROVISION_UNAPPROVED_IN_ROM_APPLICATION_LIST: case Tags.PROVISION_APPROVED_APPLICATION_LIST: // Parse and throw away the content if (specifiesApplications(tag)) { tagIsSupported = false; if (tag == Tags.PROVISION_UNAPPROVED_IN_ROM_APPLICATION_LIST) { unsupportedList.add(R.string.policy_app_blacklist); } else { unsupportedList.add(R.string.policy_app_whitelist); } } break; // We accept calendar age, since we never ask for more than two weeks, and that's // the most restrictive policy case Tags.PROVISION_MAX_CALENDAR_AGE_FILTER: policy.mMaxCalendarLookback = getValueInt(); break; // We handle max email lookback case Tags.PROVISION_MAX_EMAIL_AGE_FILTER: policy.mMaxEmailLookback = getValueInt(); break; // We currently reject these next two policies case Tags.PROVISION_MAX_EMAIL_BODY_TRUNCATION_SIZE: case Tags.PROVISION_MAX_EMAIL_HTML_BODY_TRUNCATION_SIZE: String value = getValue(); // -1 indicates no required truncation if (!value.equals("-1")) { max = Integer.parseInt(value); if (tag == Tags.PROVISION_MAX_EMAIL_BODY_TRUNCATION_SIZE) { policy.mMaxTextTruncationSize = max; unsupportedList.add(R.string.policy_text_truncation); } else { policy.mMaxHtmlTruncationSize = max; unsupportedList.add(R.string.policy_html_truncation); } tagIsSupported = false; } break; default: skipTag(); } if (!tagIsSupported) { log("Policy not supported: " + tag); mIsSupportable = false; } } // Make sure policy settings are valid; password not enabled trumps other password settings if (!passwordEnabled) { policy.mPasswordMode = Policy.PASSWORD_MODE_NONE; } setPolicy(policy); // We can only determine whether encryption is supported on device by using isSupported here if (!SecurityPolicyDelegate.isSupported(mService.mContext, policy)) { log("SecurityPolicy reports PolicySet not supported."); mIsSupportable = false; unsupportedList.add(R.string.policy_require_encryption); } if (!unsupportedList.isEmpty()) { mUnsupportedPolicies = new String[unsupportedList.size()]; int i = 0; Context context = ExchangeService.getContext(); if (context != null) { Resources resources = context.getResources(); for (int res: unsupportedList) { mUnsupportedPolicies[i++] = resources.getString(res); } } } } /** * Return whether or not either of the application list tags specifies any applications * @param endTag the tag whose children we're walking through * @return whether any applications were specified (by name or by hash) * @throws IOException */ private boolean specifiesApplications(int endTag) throws IOException { boolean specifiesApplications = false; while (nextTag(endTag) != END) { switch (tag) { case Tags.PROVISION_APPLICATION_NAME: case Tags.PROVISION_HASH: specifiesApplications = true; break; default: skipTag(); } } return specifiesApplications; } /*package*/ void parseProvisionDocXml(String doc) throws IOException { Policy policy = new Policy(); try { XmlPullParserFactory factory = XmlPullParserFactory.newInstance(); XmlPullParser parser = factory.newPullParser(); parser.setInput(new ByteArrayInputStream(doc.getBytes()), "UTF-8"); int type = parser.getEventType(); if (type == XmlPullParser.START_DOCUMENT) { type = parser.next(); if (type == XmlPullParser.START_TAG) { String tagName = parser.getName(); if (tagName.equals("wap-provisioningdoc")) { parseWapProvisioningDoc(parser, policy); } } } } catch (XmlPullParserException e) { throw new IOException(); } setPolicy(policy); } /** * Return true if password is required; otherwise false. */ private boolean parseSecurityPolicy(XmlPullParser parser, Policy policy) throws XmlPullParserException, IOException { boolean passwordRequired = true; while (true) { int type = parser.nextTag(); if (type == XmlPullParser.END_TAG && parser.getName().equals("characteristic")) { break; } else if (type == XmlPullParser.START_TAG) { String tagName = parser.getName(); if (tagName.equals("parm")) { String name = parser.getAttributeValue(null, "name"); if (name.equals("4131")) { String value = parser.getAttributeValue(null, "value"); if (value.equals("1")) { passwordRequired = false; } } } } } return passwordRequired; } private void parseCharacteristic(XmlPullParser parser, Policy policy) throws XmlPullParserException, IOException { boolean enforceInactivityTimer = true; while (true) { int type = parser.nextTag(); if (type == XmlPullParser.END_TAG && parser.getName().equals("characteristic")) { break; } else if (type == XmlPullParser.START_TAG) { if (parser.getName().equals("parm")) { String name = parser.getAttributeValue(null, "name"); String value = parser.getAttributeValue(null, "value"); if (name.equals("AEFrequencyValue")) { if (enforceInactivityTimer) { if (value.equals("0")) { policy.mMaxScreenLockTime = 1; } else { policy.mMaxScreenLockTime = 60*Integer.parseInt(value); } } } else if (name.equals("AEFrequencyType")) { // "0" here means we don't enforce an inactivity timeout if (value.equals("0")) { enforceInactivityTimer = false; } } else if (name.equals("DeviceWipeThreshold")) { policy.mPasswordMaxFails = Integer.parseInt(value); } else if (name.equals("CodewordFrequency")) { // Ignore; has no meaning for us } else if (name.equals("MinimumPasswordLength")) { policy.mPasswordMinLength = Integer.parseInt(value); } else if (name.equals("PasswordComplexity")) { if (value.equals("0")) { policy.mPasswordMode = Policy.PASSWORD_MODE_STRONG; } else { policy.mPasswordMode = Policy.PASSWORD_MODE_SIMPLE; } } } } } } private void parseRegistry(XmlPullParser parser, Policy policy) throws XmlPullParserException, IOException { while (true) { int type = parser.nextTag(); if (type == XmlPullParser.END_TAG && parser.getName().equals("characteristic")) { break; } else if (type == XmlPullParser.START_TAG) { String name = parser.getName(); if (name.equals("characteristic")) { parseCharacteristic(parser, policy); } } } } private void parseWapProvisioningDoc(XmlPullParser parser, Policy policy) throws XmlPullParserException, IOException { while (true) { int type = parser.nextTag(); if (type == XmlPullParser.END_TAG && parser.getName().equals("wap-provisioningdoc")) { break; } else if (type == XmlPullParser.START_TAG) { String name = parser.getName(); if (name.equals("characteristic")) { String atype = parser.getAttributeValue(null, "type"); if (atype.equals("SecurityPolicy")) { // If a password isn't required, stop here if (!parseSecurityPolicy(parser, policy)) { return; } } else if (atype.equals("Registry")) { parseRegistry(parser, policy); return; } } } } } private void parseProvisionData() throws IOException { while (nextTag(Tags.PROVISION_DATA) != END) { if (tag == Tags.PROVISION_EAS_PROVISION_DOC) { parseProvisionDocWbxml(); } else { skipTag(); } } } private void parsePolicy() throws IOException { String policyType = null; while (nextTag(Tags.PROVISION_POLICY) != END) { switch (tag) { case Tags.PROVISION_POLICY_TYPE: policyType = getValue(); mService.userLog("Policy type: ", policyType); break; case Tags.PROVISION_POLICY_KEY: mSecuritySyncKey = getValue(); break; case Tags.PROVISION_STATUS: mService.userLog("Policy status: ", getValue()); break; case Tags.PROVISION_DATA: if (policyType.equalsIgnoreCase(EasSyncService.EAS_2_POLICY_TYPE)) { // Parse the old style XML document parseProvisionDocXml(getValue()); } else { // Parse the newer WBXML data parseProvisionData(); } break; default: skipTag(); } } } private void parsePolicies() throws IOException { while (nextTag(Tags.PROVISION_POLICIES) != END) { if (tag == Tags.PROVISION_POLICY) { parsePolicy(); } else { skipTag(); } } } private void parseDeviceInformation() throws IOException { while (nextTag(Tags.SETTINGS_DEVICE_INFORMATION) != END) { if (tag == Tags.SETTINGS_STATUS) { mService.userLog("DeviceInformation status: " + getValue()); } else { skipTag(); } } } @Override public boolean parse() throws IOException { boolean res = false; if (nextTag(START_DOCUMENT) != Tags.PROVISION_PROVISION) { throw new IOException(); } while (nextTag(START_DOCUMENT) != END_DOCUMENT) { switch (tag) { case Tags.PROVISION_STATUS: int status = getValueInt(); mService.userLog("Provision status: ", status); res = (status == 1); break; case Tags.SETTINGS_DEVICE_INFORMATION: parseDeviceInformation(); break; case Tags.PROVISION_POLICIES: parsePolicies(); break; case Tags.PROVISION_REMOTE_WIPE: // Indicate remote wipe command received mRemoteWipe = true; break; default: skipTag(); } } return res; } }