Home | History | Annotate | Download | only in provider
      1 /*
      2  * Copyright (C) 2011 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 
     18 package com.android.emailcommon.provider;
     19 
     20 import android.content.ContentValues;
     21 import android.content.Context;
     22 import android.database.Cursor;
     23 import android.net.Uri;
     24 import android.os.Parcel;
     25 import android.os.Parcelable;
     26 import android.text.TextUtils;
     27 
     28 import com.android.emailcommon.utility.SSLUtils;
     29 import com.android.mail.utils.LogUtils;
     30 import com.google.common.annotations.VisibleForTesting;
     31 
     32 import org.json.JSONException;
     33 import org.json.JSONObject;
     34 
     35 import java.net.URI;
     36 import java.net.URISyntaxException;
     37 
     38 public class HostAuth extends EmailContent implements Parcelable {
     39     public static final String TABLE_NAME = "HostAuth";
     40     public static Uri CONTENT_URI;
     41 
     42     public static void initHostAuth() {
     43         CONTENT_URI = Uri.parse(EmailContent.CONTENT_URI + "/hostauth");
     44     }
     45 
     46     // These legacy constants should be used in code created prior to Email2
     47     public static final String LEGACY_SCHEME_SMTP = "smtp";
     48 
     49     public static final String SCHEME_TRUST_ALL_CERTS = "trustallcerts";
     50 
     51     public static final int PORT_UNKNOWN = -1;
     52 
     53     public static final int FLAG_NONE         = 0x00;    // No flags
     54     public static final int FLAG_SSL          = 0x01;    // Use SSL
     55     public static final int FLAG_TLS          = 0x02;    // Use TLS
     56     public static final int FLAG_AUTHENTICATE = 0x04;    // Use name/password for authentication
     57     public static final int FLAG_TRUST_ALL    = 0x08;    // Trust all certificates
     58     public static final int FLAG_OAUTH        = 0x10;    // Use OAuth for authentication
     59     // Mask of settings directly configurable by the user
     60     public static final int USER_CONFIG_MASK  = 0x1b;
     61     public static final int FLAG_TRANSPORTSECURITY_MASK = FLAG_SSL | FLAG_TLS | FLAG_TRUST_ALL;
     62 
     63     public String mProtocol;
     64     public String mAddress;
     65     public int mPort;
     66     public int mFlags;
     67     public String mLogin;
     68     public String mPassword;
     69     public String mDomain;
     70     public String mClientCertAlias = null;
     71     // NOTE: The server certificate is NEVER automatically retrieved from EmailProvider
     72     public byte[] mServerCert = null;
     73     public long mCredentialKey;
     74 
     75     @VisibleForTesting
     76     static final String JSON_TAG_CREDENTIAL = "credential";
     77     public transient Credential mCredential;
     78 
     79     public static final int CONTENT_ID_COLUMN = 0;
     80     public static final int CONTENT_PROTOCOL_COLUMN = 1;
     81     public static final int CONTENT_ADDRESS_COLUMN = 2;
     82     public static final int CONTENT_PORT_COLUMN = 3;
     83     public static final int CONTENT_FLAGS_COLUMN = 4;
     84     public static final int CONTENT_LOGIN_COLUMN = 5;
     85     public static final int CONTENT_PASSWORD_COLUMN = 6;
     86     public static final int CONTENT_DOMAIN_COLUMN = 7;
     87     public static final int CONTENT_CLIENT_CERT_ALIAS_COLUMN = 8;
     88     public static final int CONTENT_CREDENTIAL_KEY_COLUMN = 9;
     89 
     90     public static final String[] CONTENT_PROJECTION = new String[] {
     91             HostAuthColumns._ID, HostAuthColumns.PROTOCOL, HostAuthColumns.ADDRESS,
     92             HostAuthColumns.PORT, HostAuthColumns.FLAGS, HostAuthColumns.LOGIN,
     93             HostAuthColumns.PASSWORD, HostAuthColumns.DOMAIN, HostAuthColumns.CLIENT_CERT_ALIAS,
     94             HostAuthColumns.CREDENTIAL_KEY
     95     };
     96 
     97     public HostAuth() {
     98         mBaseUri = CONTENT_URI;
     99         mPort = PORT_UNKNOWN;
    100         mCredentialKey = -1;
    101     }
    102 
    103      /**
    104      * Restore a HostAuth from the database, given its unique id
    105      * @param context for provider loads
    106      * @param id corresponds to rowid
    107      * @return the instantiated HostAuth
    108      */
    109     public static HostAuth restoreHostAuthWithId(Context context, long id) {
    110         return EmailContent.restoreContentWithId(context, HostAuth.class,
    111                 HostAuth.CONTENT_URI, HostAuth.CONTENT_PROJECTION, id);
    112     }
    113 
    114     /**
    115      * Returns the credential object for this HostAuth. This will load from the
    116      * database if the HosAuth has a valid credential key, or return null if not.
    117      */
    118     public Credential getCredential(Context context) {
    119         if (mCredential == null) {
    120             if (mCredentialKey >= 0) {
    121                 mCredential = Credential.restoreCredentialsWithId(context, mCredentialKey);
    122             }
    123         }
    124         return mCredential;
    125     }
    126 
    127     /**
    128      * getOrCreateCredential Return the credential object for this HostAuth,
    129      * creating it if it does not yet exist. This should not be called on the
    130      * main thread.
    131      *
    132      * As a side-effect, it also ensures FLAG_OAUTH is set. Use {@link #removeCredential()} to clear
    133      *
    134      * @param context for provider loads
    135      * @return the credential object for this HostAuth
    136      */
    137     public Credential getOrCreateCredential(Context context) {
    138         mFlags |= FLAG_OAUTH;
    139         if (mCredential == null) {
    140             if (mCredentialKey >= 0) {
    141                 mCredential = Credential.restoreCredentialsWithId(context, mCredentialKey);
    142             } else {
    143                 mCredential = new Credential();
    144             }
    145         }
    146         return mCredential;
    147     }
    148 
    149     /**
    150      * Clear the credential object.
    151      */
    152     public void removeCredential() {
    153         mCredential = null;
    154         mCredentialKey = -1;
    155         mFlags &= ~FLAG_OAUTH;
    156     }
    157 
    158     /**
    159      * Builds a URI scheme name given the parameters for a {@code HostAuth}. If
    160      * a {@code clientAlias} is provided, this indicates that a secure
    161      * connection must be used.
    162      *
    163      * This is not used in live code, but is kept here for reference when creating providers.xml
    164      * entries
    165      */
    166     @SuppressWarnings("unused")
    167     public static String getSchemeString(String protocol, int flags, String clientAlias) {
    168         String security = "";
    169         switch (flags & USER_CONFIG_MASK) {
    170             case FLAG_SSL:
    171                 security = "+ssl+";
    172                 break;
    173             case FLAG_SSL | FLAG_TRUST_ALL:
    174                 security = "+ssl+trustallcerts";
    175                 break;
    176             case FLAG_TLS:
    177                 security = "+tls+";
    178                 break;
    179             case FLAG_TLS | FLAG_TRUST_ALL:
    180                 security = "+tls+trustallcerts";
    181                 break;
    182         }
    183 
    184         if (!TextUtils.isEmpty(clientAlias)) {
    185             if (TextUtils.isEmpty(security)) {
    186                 throw new IllegalArgumentException(
    187                         "Can't specify a certificate alias for a non-secure connection");
    188             }
    189             if (!security.endsWith("+")) {
    190                 security += "+";
    191             }
    192             security += SSLUtils.escapeForSchemeName(clientAlias);
    193         }
    194 
    195         return protocol + security;
    196     }
    197 
    198     /**
    199      * Returns the flags for the specified scheme.
    200      */
    201     public static int getSchemeFlags(String scheme) {
    202         String[] schemeParts = scheme.split("\\+");
    203         int flags = HostAuth.FLAG_NONE;
    204         if (schemeParts.length >= 2) {
    205             String part1 = schemeParts[1];
    206             if ("ssl".equals(part1)) {
    207                 flags |= HostAuth.FLAG_SSL;
    208             } else if ("tls".equals(part1)) {
    209                 flags |= HostAuth.FLAG_TLS;
    210             }
    211             if (schemeParts.length >= 3) {
    212                 String part2 = schemeParts[2];
    213                 if (SCHEME_TRUST_ALL_CERTS.equals(part2)) {
    214                     flags |= HostAuth.FLAG_TRUST_ALL;
    215                 }
    216             }
    217         }
    218         return flags;
    219     }
    220 
    221     @Override
    222     public void restore(Cursor cursor) {
    223         mBaseUri = CONTENT_URI;
    224         mId = cursor.getLong(CONTENT_ID_COLUMN);
    225         mProtocol = cursor.getString(CONTENT_PROTOCOL_COLUMN);
    226         mAddress = cursor.getString(CONTENT_ADDRESS_COLUMN);
    227         mPort = cursor.getInt(CONTENT_PORT_COLUMN);
    228         mFlags = cursor.getInt(CONTENT_FLAGS_COLUMN);
    229         mLogin = cursor.getString(CONTENT_LOGIN_COLUMN);
    230         mPassword = cursor.getString(CONTENT_PASSWORD_COLUMN);
    231         mDomain = cursor.getString(CONTENT_DOMAIN_COLUMN);
    232         mClientCertAlias = cursor.getString(CONTENT_CLIENT_CERT_ALIAS_COLUMN);
    233         mCredentialKey = cursor.getLong(CONTENT_CREDENTIAL_KEY_COLUMN);
    234         if (mCredentialKey != -1) {
    235             mFlags |= FLAG_OAUTH;
    236         }
    237     }
    238 
    239     @Override
    240     public ContentValues toContentValues() {
    241         ContentValues values = new ContentValues();
    242         values.put(HostAuthColumns.PROTOCOL, mProtocol);
    243         values.put(HostAuthColumns.ADDRESS, mAddress);
    244         values.put(HostAuthColumns.PORT, mPort);
    245         values.put(HostAuthColumns.FLAGS, mFlags);
    246         values.put(HostAuthColumns.LOGIN, mLogin);
    247         values.put(HostAuthColumns.PASSWORD, mPassword);
    248         values.put(HostAuthColumns.DOMAIN, mDomain);
    249         values.put(HostAuthColumns.CLIENT_CERT_ALIAS, mClientCertAlias);
    250         values.put(HostAuthColumns.CREDENTIAL_KEY, mCredentialKey);
    251         values.put(HostAuthColumns.ACCOUNT_KEY, 0); // Need something to satisfy the DB
    252 
    253         return values;
    254     }
    255 
    256     protected JSONObject toJson() {
    257         try {
    258             final JSONObject json = new JSONObject();
    259             json.put(HostAuthColumns.PROTOCOL, mProtocol);
    260             json.put(HostAuthColumns.ADDRESS, mAddress);
    261             json.put(HostAuthColumns.PORT, mPort);
    262             json.put(HostAuthColumns.FLAGS, mFlags);
    263             json.put(HostAuthColumns.LOGIN, mLogin);
    264             json.putOpt(HostAuthColumns.PASSWORD, mPassword);
    265             json.putOpt(HostAuthColumns.DOMAIN, mDomain);
    266             json.putOpt(HostAuthColumns.CLIENT_CERT_ALIAS, mClientCertAlias);
    267             if (mCredential != null) {
    268                 json.putOpt(JSON_TAG_CREDENTIAL, mCredential.toJson());
    269             }
    270             return json;
    271         } catch (final JSONException e) {
    272             LogUtils.d(LogUtils.TAG, e, "Exception while serializing HostAuth");
    273         }
    274         return null;
    275     }
    276 
    277     protected static HostAuth fromJson(final JSONObject json) {
    278         try {
    279             final HostAuth h = new HostAuth();
    280             h.mProtocol = json.getString(HostAuthColumns.PROTOCOL);
    281             h.mAddress = json.getString(HostAuthColumns.ADDRESS);
    282             h.mPort = json.getInt(HostAuthColumns.PORT);
    283             h.mFlags = json.getInt(HostAuthColumns.FLAGS);
    284             h.mLogin = json.getString(HostAuthColumns.LOGIN);
    285             h.mPassword = json.optString(HostAuthColumns.PASSWORD);
    286             h.mDomain = json.optString(HostAuthColumns.DOMAIN);
    287             h.mClientCertAlias = json.optString(HostAuthColumns.CLIENT_CERT_ALIAS);
    288             final JSONObject credJson = json.optJSONObject(JSON_TAG_CREDENTIAL);
    289             if (credJson != null) {
    290                 h.mCredential = Credential.fromJson(credJson);
    291             }
    292             return h;
    293         } catch (final JSONException e) {
    294             LogUtils.d(LogUtils.TAG, e, "Exception while deserializing HostAuth");
    295         }
    296         return null;
    297     }
    298 
    299     /**
    300      * Ensure that all optionally-loaded fields are populated from the provider.
    301      * @param context for provider loads
    302      */
    303     public void ensureLoaded(final Context context) {
    304         getCredential(context);
    305     }
    306 
    307     /**
    308      * Sets the user name and password from URI user info string
    309      */
    310     public void setLogin(String userInfo) {
    311         String userName = null;
    312         String userPassword = null;
    313         if (!TextUtils.isEmpty(userInfo)) {
    314             String[] userInfoParts = userInfo.split(":", 2);
    315             userName = userInfoParts[0];
    316             if (userInfoParts.length > 1) {
    317                 userPassword = userInfoParts[1];
    318             }
    319         }
    320         setLogin(userName, userPassword);
    321     }
    322 
    323     public void setUserName(final String userName) {
    324         mLogin = userName;
    325         if (TextUtils.isEmpty(mLogin)) {
    326             mFlags &= ~FLAG_AUTHENTICATE;
    327         } else {
    328             mFlags |= FLAG_AUTHENTICATE;
    329         }
    330     }
    331 
    332     /**
    333      * Sets the user name and password
    334      */
    335     public void setLogin(String userName, String userPassword) {
    336         mLogin = userName;
    337         mPassword = userPassword;
    338 
    339         if (TextUtils.isEmpty(mLogin)) {
    340             mFlags &= ~FLAG_AUTHENTICATE;
    341         } else {
    342             mFlags |= FLAG_AUTHENTICATE;
    343         }
    344     }
    345 
    346     /**
    347      * Returns the login information. [0] is the username and [1] is the password.
    348      */
    349     public String[] getLogin() {
    350         String trimUser = (mLogin != null) ? mLogin.trim() : null;
    351         return new String[] { trimUser, mPassword };
    352     }
    353 
    354     public void setConnection(String protocol, String address, int port, int flags) {
    355         setConnection(protocol, address, port, flags, null);
    356     }
    357 
    358     /**
    359      * Sets the internal connection parameters based on the specified parameter values.
    360      * @param protocol the mail protocol to use (e.g. "eas", "imap").
    361      * @param address the address of the server
    362      * @param port the port for the connection
    363      * @param flags flags indicating the security and type of the connection
    364      * @param clientCertAlias an optional alias to use if a client user certificate is to be
    365      *     presented during connection establishment. If this is non-empty, it must be the case
    366      *     that flags indicates use of a secure connection
    367      */
    368     public void setConnection(String protocol, String address,
    369             int port, int flags, String clientCertAlias) {
    370         // Set protocol, security, and additional flags based on uri scheme
    371         mProtocol = protocol;
    372 
    373         mFlags &= ~(FLAG_SSL | FLAG_TLS | FLAG_TRUST_ALL);
    374         mFlags |= (flags & USER_CONFIG_MASK);
    375 
    376         boolean useSecureConnection = (flags & (FLAG_SSL | FLAG_TLS)) != 0;
    377         if (!useSecureConnection && !TextUtils.isEmpty(clientCertAlias)) {
    378             throw new IllegalArgumentException("Can't use client alias on non-secure connections");
    379         }
    380 
    381         mAddress = address;
    382         mPort = port;
    383         if (mPort == PORT_UNKNOWN) {
    384             boolean useSSL = ((mFlags & FLAG_SSL) != 0);
    385             if (LEGACY_SCHEME_SMTP.equals(mProtocol)) {
    386                 mPort = useSSL ? 465 : 587;
    387             }
    388         }
    389 
    390         mClientCertAlias = clientCertAlias;
    391     }
    392 
    393 
    394     /** Convenience method to determine if SSL is used. */
    395     public boolean shouldUseSsl() {
    396         return (mFlags & FLAG_SSL) != 0;
    397     }
    398 
    399     /** Convenience method to determine if all server certs should be used. */
    400     public boolean shouldTrustAllServerCerts() {
    401         return (mFlags & FLAG_TRUST_ALL) != 0;
    402     }
    403 
    404     /**
    405      * Supports Parcelable
    406      */
    407     @Override
    408     public int describeContents() {
    409         return 0;
    410     }
    411 
    412     /**
    413      * Supports Parcelable
    414      */
    415     public static final Parcelable.Creator<HostAuth> CREATOR
    416             = new Parcelable.Creator<HostAuth>() {
    417         @Override
    418         public HostAuth createFromParcel(Parcel in) {
    419             return new HostAuth(in);
    420         }
    421 
    422         @Override
    423         public HostAuth[] newArray(int size) {
    424             return new HostAuth[size];
    425         }
    426     };
    427 
    428     /**
    429      * Supports Parcelable
    430      */
    431     @Override
    432     public void writeToParcel(Parcel dest, int flags) {
    433         // mBaseUri is not parceled
    434         dest.writeLong(mId);
    435         dest.writeString(mProtocol);
    436         dest.writeString(mAddress);
    437         dest.writeInt(mPort);
    438         dest.writeInt(mFlags);
    439         dest.writeString(mLogin);
    440         dest.writeString(mPassword);
    441         dest.writeString(mDomain);
    442         dest.writeString(mClientCertAlias);
    443         if ((mFlags & FLAG_OAUTH) != 0) {
    444             // TODO: This is nasty, but to be compatible with backward Exchange, we can't make any
    445             // change to the parcelable format. But we need Credential objects to be here.
    446             // So... only parcel or unparcel Credentials if the OAUTH flag is set. This will never
    447             // be set on HostAuth going to or coming from Exchange.
    448             dest.writeLong(mCredentialKey);
    449             if (mCredential == null) {
    450                 Credential.EMPTY.writeToParcel(dest, flags);
    451             } else {
    452                 mCredential.writeToParcel(dest, flags);
    453             }
    454         }
    455     }
    456 
    457     /**
    458      * Supports Parcelable
    459      */
    460     public HostAuth(Parcel in) {
    461         mBaseUri = CONTENT_URI;
    462         mId = in.readLong();
    463         mProtocol = in.readString();
    464         mAddress = in.readString();
    465         mPort = in.readInt();
    466         mFlags = in.readInt();
    467         mLogin = in.readString();
    468         mPassword = in.readString();
    469         mDomain = in.readString();
    470         mClientCertAlias = in.readString();
    471         if ((mFlags & FLAG_OAUTH) != 0) {
    472             // TODO: This is nasty, but to be compatible with backward Exchange, we can't make any
    473             // change to the parcelable format. But we need Credential objects to be here.
    474             // So... only parcel or unparcel Credentials if the OAUTH flag is set. This will never
    475             // be set on HostAuth going to or coming from Exchange.
    476             mCredentialKey = in.readLong();
    477             mCredential = new Credential(in);
    478             if (mCredential.equals(Credential.EMPTY)) {
    479                 mCredential = null;
    480             }
    481         } else {
    482             mCredentialKey = -1;
    483         }
    484     }
    485 
    486     @Override
    487     public boolean equals(Object o) {
    488         if (!(o instanceof HostAuth)) {
    489             return false;
    490         }
    491         HostAuth that = (HostAuth)o;
    492         return mPort == that.mPort
    493                 && mId == that.mId
    494                 && mFlags == that.mFlags
    495                 && TextUtils.equals(mProtocol, that.mProtocol)
    496                 && TextUtils.equals(mAddress, that.mAddress)
    497                 && TextUtils.equals(mLogin, that.mLogin)
    498                 && TextUtils.equals(mPassword, that.mPassword)
    499                 && TextUtils.equals(mDomain, that.mDomain)
    500                 && TextUtils.equals(mClientCertAlias, that.mClientCertAlias);
    501                 // We don't care about the server certificate for equals
    502     }
    503 
    504     /**
    505      * The flag, password, and client cert alias are the only items likely to change after a
    506      * HostAuth is created
    507      */
    508     @Override
    509     public int hashCode() {
    510         int hashCode = 29;
    511         if (mPassword != null) {
    512             hashCode += mPassword.hashCode();
    513         }
    514         if (mClientCertAlias != null) {
    515             hashCode += (mClientCertAlias.hashCode() << 8);
    516         }
    517         return (hashCode << 8) + mFlags;
    518     }
    519 
    520     /**
    521      * Legacy URI parser. Used in parsing template from provider.xml
    522      * Example string:
    523      *   "eas+ssl+trustallcerts://user:password@server/domain:123"
    524      *
    525      * Note that the use of client certificate is specified in the URI, a secure connection type
    526      * must be used.
    527      */
    528     public void setHostAuthFromString(String uriString)
    529             throws URISyntaxException {
    530         URI uri = new URI(uriString);
    531         String path = uri.getPath();
    532         String domain = null;
    533         if (!TextUtils.isEmpty(path)) {
    534             // Strip off the leading slash that begins the path.
    535             domain = path.substring(1);
    536         }
    537         mDomain = domain;
    538         setLogin(uri.getUserInfo());
    539 
    540         String scheme = uri.getScheme();
    541         setConnection(scheme, uri.getHost(), uri.getPort());
    542     }
    543 
    544     /**
    545      * Legacy code for setting connection values from a "scheme" (see above)
    546      */
    547     public void setConnection(String scheme, String host, int port) {
    548         String[] schemeParts = scheme.split("\\+");
    549         String protocol = schemeParts[0];
    550         String clientCertAlias = null;
    551         int flags = getSchemeFlags(scheme);
    552 
    553         // Example scheme: "eas+ssl+trustallcerts" or "eas+tls+trustallcerts+client-cert-alias"
    554         if (schemeParts.length > 3) {
    555             clientCertAlias = schemeParts[3];
    556         } else if (schemeParts.length > 2) {
    557             if (!SCHEME_TRUST_ALL_CERTS.equals(schemeParts[2])) {
    558                 mClientCertAlias = schemeParts[2];
    559             }
    560         }
    561 
    562         setConnection(protocol, host, port, flags, clientCertAlias);
    563     }
    564 
    565     public static String getProtocolFromString(String uriString) {
    566         final Uri uri = Uri.parse(uriString);
    567         final String scheme = uri.getScheme();
    568         final String[] schemeParts = scheme.split("\\+");
    569         return schemeParts[0];
    570     }
    571 
    572     @Override
    573     public String toString() {
    574         return "[protocol " + mProtocol + "]";
    575     }
    576 }
    577