/* * Copyright (C) 2011 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.emailcommon.provider; import android.content.ContentValues; import android.content.Context; import android.database.Cursor; import android.net.Uri; import android.os.Parcel; import android.os.Parcelable; import android.text.TextUtils; import com.android.emailcommon.utility.SSLUtils; import com.android.mail.utils.LogUtils; import com.google.common.annotations.VisibleForTesting; import org.json.JSONException; import org.json.JSONObject; import java.net.URI; import java.net.URISyntaxException; public class HostAuth extends EmailContent implements Parcelable { public static final String TABLE_NAME = "HostAuth"; public static Uri CONTENT_URI; public static void initHostAuth() { CONTENT_URI = Uri.parse(EmailContent.CONTENT_URI + "/hostauth"); } // These legacy constants should be used in code created prior to Email2 public static final String LEGACY_SCHEME_SMTP = "smtp"; public static final String SCHEME_TRUST_ALL_CERTS = "trustallcerts"; public static final int PORT_UNKNOWN = -1; public static final int FLAG_NONE = 0x00; // No flags public static final int FLAG_SSL = 0x01; // Use SSL public static final int FLAG_TLS = 0x02; // Use TLS public static final int FLAG_AUTHENTICATE = 0x04; // Use name/password for authentication public static final int FLAG_TRUST_ALL = 0x08; // Trust all certificates public static final int FLAG_OAUTH = 0x10; // Use OAuth for authentication // Mask of settings directly configurable by the user public static final int USER_CONFIG_MASK = 0x1b; public static final int FLAG_TRANSPORTSECURITY_MASK = FLAG_SSL | FLAG_TLS | FLAG_TRUST_ALL; public String mProtocol; public String mAddress; public int mPort; public int mFlags; public String mLogin; public String mPassword; public String mDomain; public String mClientCertAlias = null; // NOTE: The server certificate is NEVER automatically retrieved from EmailProvider public byte[] mServerCert = null; public long mCredentialKey; @VisibleForTesting static final String JSON_TAG_CREDENTIAL = "credential"; public transient Credential mCredential; public static final int CONTENT_ID_COLUMN = 0; public static final int CONTENT_PROTOCOL_COLUMN = 1; public static final int CONTENT_ADDRESS_COLUMN = 2; public static final int CONTENT_PORT_COLUMN = 3; public static final int CONTENT_FLAGS_COLUMN = 4; public static final int CONTENT_LOGIN_COLUMN = 5; public static final int CONTENT_PASSWORD_COLUMN = 6; public static final int CONTENT_DOMAIN_COLUMN = 7; public static final int CONTENT_CLIENT_CERT_ALIAS_COLUMN = 8; public static final int CONTENT_CREDENTIAL_KEY_COLUMN = 9; public static final String[] CONTENT_PROJECTION = new String[] { HostAuthColumns._ID, HostAuthColumns.PROTOCOL, HostAuthColumns.ADDRESS, HostAuthColumns.PORT, HostAuthColumns.FLAGS, HostAuthColumns.LOGIN, HostAuthColumns.PASSWORD, HostAuthColumns.DOMAIN, HostAuthColumns.CLIENT_CERT_ALIAS, HostAuthColumns.CREDENTIAL_KEY }; public HostAuth() { mBaseUri = CONTENT_URI; mPort = PORT_UNKNOWN; mCredentialKey = -1; } /** * Restore a HostAuth from the database, given its unique id * @param context for provider loads * @param id corresponds to rowid * @return the instantiated HostAuth */ public static HostAuth restoreHostAuthWithId(Context context, long id) { return EmailContent.restoreContentWithId(context, HostAuth.class, HostAuth.CONTENT_URI, HostAuth.CONTENT_PROJECTION, id); } /** * Returns the credential object for this HostAuth. This will load from the * database if the HosAuth has a valid credential key, or return null if not. */ public Credential getCredential(Context context) { if (mCredential == null) { if (mCredentialKey >= 0) { mCredential = Credential.restoreCredentialsWithId(context, mCredentialKey); } } return mCredential; } /** * getOrCreateCredential Return the credential object for this HostAuth, * creating it if it does not yet exist. This should not be called on the * main thread. * * As a side-effect, it also ensures FLAG_OAUTH is set. Use {@link #removeCredential()} to clear * * @param context for provider loads * @return the credential object for this HostAuth */ public Credential getOrCreateCredential(Context context) { mFlags |= FLAG_OAUTH; if (mCredential == null) { if (mCredentialKey >= 0) { mCredential = Credential.restoreCredentialsWithId(context, mCredentialKey); } else { mCredential = new Credential(); } } return mCredential; } /** * Clear the credential object. */ public void removeCredential() { mCredential = null; mCredentialKey = -1; mFlags &= ~FLAG_OAUTH; } /** * Builds a URI scheme name given the parameters for a {@code HostAuth}. If * a {@code clientAlias} is provided, this indicates that a secure * connection must be used. * * This is not used in live code, but is kept here for reference when creating providers.xml * entries */ @SuppressWarnings("unused") public static String getSchemeString(String protocol, int flags, String clientAlias) { String security = ""; switch (flags & USER_CONFIG_MASK) { case FLAG_SSL: security = "+ssl+"; break; case FLAG_SSL | FLAG_TRUST_ALL: security = "+ssl+trustallcerts"; break; case FLAG_TLS: security = "+tls+"; break; case FLAG_TLS | FLAG_TRUST_ALL: security = "+tls+trustallcerts"; break; } if (!TextUtils.isEmpty(clientAlias)) { if (TextUtils.isEmpty(security)) { throw new IllegalArgumentException( "Can't specify a certificate alias for a non-secure connection"); } if (!security.endsWith("+")) { security += "+"; } security += SSLUtils.escapeForSchemeName(clientAlias); } return protocol + security; } /** * Returns the flags for the specified scheme. */ public static int getSchemeFlags(String scheme) { String[] schemeParts = scheme.split("\\+"); int flags = HostAuth.FLAG_NONE; if (schemeParts.length >= 2) { String part1 = schemeParts[1]; if ("ssl".equals(part1)) { flags |= HostAuth.FLAG_SSL; } else if ("tls".equals(part1)) { flags |= HostAuth.FLAG_TLS; } if (schemeParts.length >= 3) { String part2 = schemeParts[2]; if (SCHEME_TRUST_ALL_CERTS.equals(part2)) { flags |= HostAuth.FLAG_TRUST_ALL; } } } return flags; } @Override public void restore(Cursor cursor) { mBaseUri = CONTENT_URI; mId = cursor.getLong(CONTENT_ID_COLUMN); mProtocol = cursor.getString(CONTENT_PROTOCOL_COLUMN); mAddress = cursor.getString(CONTENT_ADDRESS_COLUMN); mPort = cursor.getInt(CONTENT_PORT_COLUMN); mFlags = cursor.getInt(CONTENT_FLAGS_COLUMN); mLogin = cursor.getString(CONTENT_LOGIN_COLUMN); mPassword = cursor.getString(CONTENT_PASSWORD_COLUMN); mDomain = cursor.getString(CONTENT_DOMAIN_COLUMN); mClientCertAlias = cursor.getString(CONTENT_CLIENT_CERT_ALIAS_COLUMN); mCredentialKey = cursor.getLong(CONTENT_CREDENTIAL_KEY_COLUMN); if (mCredentialKey != -1) { mFlags |= FLAG_OAUTH; } } @Override public ContentValues toContentValues() { ContentValues values = new ContentValues(); values.put(HostAuthColumns.PROTOCOL, mProtocol); values.put(HostAuthColumns.ADDRESS, mAddress); values.put(HostAuthColumns.PORT, mPort); values.put(HostAuthColumns.FLAGS, mFlags); values.put(HostAuthColumns.LOGIN, mLogin); values.put(HostAuthColumns.PASSWORD, mPassword); values.put(HostAuthColumns.DOMAIN, mDomain); values.put(HostAuthColumns.CLIENT_CERT_ALIAS, mClientCertAlias); values.put(HostAuthColumns.CREDENTIAL_KEY, mCredentialKey); values.put(HostAuthColumns.ACCOUNT_KEY, 0); // Need something to satisfy the DB return values; } protected JSONObject toJson() { try { final JSONObject json = new JSONObject(); json.put(HostAuthColumns.PROTOCOL, mProtocol); json.put(HostAuthColumns.ADDRESS, mAddress); json.put(HostAuthColumns.PORT, mPort); json.put(HostAuthColumns.FLAGS, mFlags); json.put(HostAuthColumns.LOGIN, mLogin); json.putOpt(HostAuthColumns.PASSWORD, mPassword); json.putOpt(HostAuthColumns.DOMAIN, mDomain); json.putOpt(HostAuthColumns.CLIENT_CERT_ALIAS, mClientCertAlias); if (mCredential != null) { json.putOpt(JSON_TAG_CREDENTIAL, mCredential.toJson()); } return json; } catch (final JSONException e) { LogUtils.d(LogUtils.TAG, e, "Exception while serializing HostAuth"); } return null; } protected static HostAuth fromJson(final JSONObject json) { try { final HostAuth h = new HostAuth(); h.mProtocol = json.getString(HostAuthColumns.PROTOCOL); h.mAddress = json.getString(HostAuthColumns.ADDRESS); h.mPort = json.getInt(HostAuthColumns.PORT); h.mFlags = json.getInt(HostAuthColumns.FLAGS); h.mLogin = json.getString(HostAuthColumns.LOGIN); h.mPassword = json.optString(HostAuthColumns.PASSWORD); h.mDomain = json.optString(HostAuthColumns.DOMAIN); h.mClientCertAlias = json.optString(HostAuthColumns.CLIENT_CERT_ALIAS); final JSONObject credJson = json.optJSONObject(JSON_TAG_CREDENTIAL); if (credJson != null) { h.mCredential = Credential.fromJson(credJson); } return h; } catch (final JSONException e) { LogUtils.d(LogUtils.TAG, e, "Exception while deserializing HostAuth"); } return null; } /** * Ensure that all optionally-loaded fields are populated from the provider. * @param context for provider loads */ public void ensureLoaded(final Context context) { getCredential(context); } /** * Sets the user name and password from URI user info string */ public void setLogin(String userInfo) { String userName = null; String userPassword = null; if (!TextUtils.isEmpty(userInfo)) { String[] userInfoParts = userInfo.split(":", 2); userName = userInfoParts[0]; if (userInfoParts.length > 1) { userPassword = userInfoParts[1]; } } setLogin(userName, userPassword); } public void setUserName(final String userName) { mLogin = userName; if (TextUtils.isEmpty(mLogin)) { mFlags &= ~FLAG_AUTHENTICATE; } else { mFlags |= FLAG_AUTHENTICATE; } } /** * Sets the user name and password */ public void setLogin(String userName, String userPassword) { mLogin = userName; mPassword = userPassword; if (TextUtils.isEmpty(mLogin)) { mFlags &= ~FLAG_AUTHENTICATE; } else { mFlags |= FLAG_AUTHENTICATE; } } /** * Returns the login information. [0] is the username and [1] is the password. */ public String[] getLogin() { String trimUser = (mLogin != null) ? mLogin.trim() : null; return new String[] { trimUser, mPassword }; } public void setConnection(String protocol, String address, int port, int flags) { setConnection(protocol, address, port, flags, null); } /** * Sets the internal connection parameters based on the specified parameter values. * @param protocol the mail protocol to use (e.g. "eas", "imap"). * @param address the address of the server * @param port the port for the connection * @param flags flags indicating the security and type of the connection * @param clientCertAlias an optional alias to use if a client user certificate is to be * presented during connection establishment. If this is non-empty, it must be the case * that flags indicates use of a secure connection */ public void setConnection(String protocol, String address, int port, int flags, String clientCertAlias) { // Set protocol, security, and additional flags based on uri scheme mProtocol = protocol; mFlags &= ~(FLAG_SSL | FLAG_TLS | FLAG_TRUST_ALL); mFlags |= (flags & USER_CONFIG_MASK); boolean useSecureConnection = (flags & (FLAG_SSL | FLAG_TLS)) != 0; if (!useSecureConnection && !TextUtils.isEmpty(clientCertAlias)) { throw new IllegalArgumentException("Can't use client alias on non-secure connections"); } mAddress = address; mPort = port; if (mPort == PORT_UNKNOWN) { boolean useSSL = ((mFlags & FLAG_SSL) != 0); if (LEGACY_SCHEME_SMTP.equals(mProtocol)) { mPort = useSSL ? 465 : 587; } } mClientCertAlias = clientCertAlias; } /** Convenience method to determine if SSL is used. */ public boolean shouldUseSsl() { return (mFlags & FLAG_SSL) != 0; } /** Convenience method to determine if all server certs should be used. */ public boolean shouldTrustAllServerCerts() { return (mFlags & FLAG_TRUST_ALL) != 0; } /** * Supports Parcelable */ @Override public int describeContents() { return 0; } /** * Supports Parcelable */ public static final Parcelable.Creator<HostAuth> CREATOR = new Parcelable.Creator<HostAuth>() { @Override public HostAuth createFromParcel(Parcel in) { return new HostAuth(in); } @Override public HostAuth[] newArray(int size) { return new HostAuth[size]; } }; /** * Supports Parcelable */ @Override public void writeToParcel(Parcel dest, int flags) { // mBaseUri is not parceled dest.writeLong(mId); dest.writeString(mProtocol); dest.writeString(mAddress); dest.writeInt(mPort); dest.writeInt(mFlags); dest.writeString(mLogin); dest.writeString(mPassword); dest.writeString(mDomain); dest.writeString(mClientCertAlias); if ((mFlags & FLAG_OAUTH) != 0) { // TODO: This is nasty, but to be compatible with backward Exchange, we can't make any // change to the parcelable format. But we need Credential objects to be here. // So... only parcel or unparcel Credentials if the OAUTH flag is set. This will never // be set on HostAuth going to or coming from Exchange. dest.writeLong(mCredentialKey); if (mCredential == null) { Credential.EMPTY.writeToParcel(dest, flags); } else { mCredential.writeToParcel(dest, flags); } } } /** * Supports Parcelable */ public HostAuth(Parcel in) { mBaseUri = CONTENT_URI; mId = in.readLong(); mProtocol = in.readString(); mAddress = in.readString(); mPort = in.readInt(); mFlags = in.readInt(); mLogin = in.readString(); mPassword = in.readString(); mDomain = in.readString(); mClientCertAlias = in.readString(); if ((mFlags & FLAG_OAUTH) != 0) { // TODO: This is nasty, but to be compatible with backward Exchange, we can't make any // change to the parcelable format. But we need Credential objects to be here. // So... only parcel or unparcel Credentials if the OAUTH flag is set. This will never // be set on HostAuth going to or coming from Exchange. mCredentialKey = in.readLong(); mCredential = new Credential(in); if (mCredential.equals(Credential.EMPTY)) { mCredential = null; } } else { mCredentialKey = -1; } } @Override public boolean equals(Object o) { if (!(o instanceof HostAuth)) { return false; } HostAuth that = (HostAuth)o; return mPort == that.mPort && mId == that.mId && mFlags == that.mFlags && TextUtils.equals(mProtocol, that.mProtocol) && TextUtils.equals(mAddress, that.mAddress) && TextUtils.equals(mLogin, that.mLogin) && TextUtils.equals(mPassword, that.mPassword) && TextUtils.equals(mDomain, that.mDomain) && TextUtils.equals(mClientCertAlias, that.mClientCertAlias); // We don't care about the server certificate for equals } /** * The flag, password, and client cert alias are the only items likely to change after a * HostAuth is created */ @Override public int hashCode() { int hashCode = 29; if (mPassword != null) { hashCode += mPassword.hashCode(); } if (mClientCertAlias != null) { hashCode += (mClientCertAlias.hashCode() << 8); } return (hashCode << 8) + mFlags; } /** * Legacy URI parser. Used in parsing template from provider.xml * Example string: * "eas+ssl+trustallcerts://user:password@server/domain:123" * * Note that the use of client certificate is specified in the URI, a secure connection type * must be used. */ public void setHostAuthFromString(String uriString) throws URISyntaxException { URI uri = new URI(uriString); String path = uri.getPath(); String domain = null; if (!TextUtils.isEmpty(path)) { // Strip off the leading slash that begins the path. domain = path.substring(1); } mDomain = domain; setLogin(uri.getUserInfo()); String scheme = uri.getScheme(); setConnection(scheme, uri.getHost(), uri.getPort()); } /** * Legacy code for setting connection values from a "scheme" (see above) */ public void setConnection(String scheme, String host, int port) { String[] schemeParts = scheme.split("\\+"); String protocol = schemeParts[0]; String clientCertAlias = null; int flags = getSchemeFlags(scheme); // Example scheme: "eas+ssl+trustallcerts" or "eas+tls+trustallcerts+client-cert-alias" if (schemeParts.length > 3) { clientCertAlias = schemeParts[3]; } else if (schemeParts.length > 2) { if (!SCHEME_TRUST_ALL_CERTS.equals(schemeParts[2])) { mClientCertAlias = schemeParts[2]; } } setConnection(protocol, host, port, flags, clientCertAlias); } public static String getProtocolFromString(String uriString) { final Uri uri = Uri.parse(uriString); final String scheme = uri.getScheme(); final String[] schemeParts = scheme.split("\\+"); return schemeParts[0]; } @Override public String toString() { return "[protocol " + mProtocol + "]"; } }