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