blob: ca71cf930cb4f71fd4241892d960a2623a41f2e5 [file] [log] [blame]
/* 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;
}
}