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
18package com.android.emailcommon.provider;
19
20import android.content.ContentValues;
21import android.content.Context;
22import android.database.Cursor;
23import android.net.Uri;
24import android.os.Parcel;
25import android.os.Parcelable;
26import android.text.TextUtils;
27
28import com.android.emailcommon.utility.SSLUtils;
29import com.android.mail.utils.LogUtils;
30import com.google.common.annotations.VisibleForTesting;
31
32import org.json.JSONException;
33import org.json.JSONObject;
34
35import java.net.URI;
36import java.net.URISyntaxException;
37
38public 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