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 
initHostAuth()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 
HostAuth()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      */
restoreHostAuthWithId(Context context, long id)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      */
getCredential(Context context)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      */
getOrCreateCredential(Context context)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      */
removeCredential()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")
getSchemeString(String protocol, int flags, String clientAlias)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      */
getSchemeFlags(String scheme)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
restore(Cursor cursor)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
toContentValues()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 
toJson()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 
fromJson(final JSONObject json)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      */
ensureLoaded(final Context context)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      */
setLogin(String userInfo)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 
setUserName(final String userName)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      */
setLogin(String userName, String userPassword)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      */
getLogin()349     public String[] getLogin() {
350         String trimUser = (mLogin != null) ? mLogin.trim() : null;
351         return new String[] { trimUser, mPassword };
352     }
353 
setConnection(String protocol, String address, int port, int flags)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      */
setConnection(String protocol, String address, int port, int flags, String clientCertAlias)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. */
shouldUseSsl()395     public boolean shouldUseSsl() {
396         return (mFlags & FLAG_SSL) != 0;
397     }
398 
399     /** Convenience method to determine if all server certs should be used. */
shouldTrustAllServerCerts()400     public boolean shouldTrustAllServerCerts() {
401         return (mFlags & FLAG_TRUST_ALL) != 0;
402     }
403 
404     /**
405      * Supports Parcelable
406      */
407     @Override
describeContents()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
writeToParcel(Parcel dest, int flags)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      */
HostAuth(Parcel in)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
equals(Object o)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
hashCode()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      */
setHostAuthFromString(String uriString)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      */
setConnection(String scheme, String host, int port)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 
getProtocolFromString(String uriString)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
toString()573     public String toString() {
574         return "[protocol " + mProtocol + "]";
575     }
576 }
577