package com.zegoggles.smssync.preferences;
import android.content.Context;
import android.content.SharedPreferences;
import android.preference.PreferenceManager;
import android.text.TextUtils;
import android.util.Log;
import com.fsck.k9.mail.AuthType;
import com.zegoggles.smssync.R;
import com.zegoggles.smssync.auth.OAuth2Client;
import com.zegoggles.smssync.auth.TokenRefresher;
import com.zegoggles.smssync.auth.XOAuthConsumer;
import org.apache.commons.codec.binary.Base64;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.util.Locale;
import static com.zegoggles.smssync.App.TAG;
import static com.zegoggles.smssync.preferences.ServerPreferences.Defaults.SERVER_PROTOCOL;
public class AuthPreferences {
private static final String UTF_8 = "UTF-8";
private final Context context;
private final SharedPreferences preferences;
private final ServerPreferences serverPreferences;
/**
* Preference key containing the Google account username.
*/
public static final String LOGIN_USER = "login_user";
/**
* Preference key containing the Google account password.
*/
public static final String LOGIN_PASSWORD = "login_password";
public static final String SERVER_AUTHENTICATION = "server_authentication";
@Deprecated private static final String OAUTH_TOKEN = "oauth_token";
@Deprecated private static final String OAUTH_TOKEN_SECRET = "oauth_token_secret";
@Deprecated private static final String OAUTH_USER = "oauth_user";
private static final String OAUTH2_USER = "oauth2_user";
private static final String OAUTH2_TOKEN = "oauth2_token";
private static final String OAUTH2_REFRESH_TOKEN = "oauth2_refresh_token";
/**
* IMAP URI.
*
* This should be in the form of:
* <ol>
* <li><code>imap+ssl+://XOAUTH2:ENCODED_USERNAME:ENCODED_TOKEN@imap.gmail.com:993</code></li>
* <li><code>imap+ssl+://XOAUTH:ENCODED_USERNAME:ENCODED_TOKEN@imap.gmail.com:993</code></li>
* <li><code>imap+ssl+://PLAIN:ENCODED_USERNAME:ENCODED_PASSWOR@imap.gmail.com:993</code></li>
* <li><code>imap://PLAIN:ENCODED_USERNAME:ENCODED_PASSWOR@imap.gmail.com:993</code></li>
* <li><code>imap://PLAIN:ENCODED_USERNAME:ENCODED_PASSWOR@imap.gmail.com</code></li>
* </ol>
*/
private static final String IMAP_URI = "imap%s://%s:%s:%s@%s";
public AuthPreferences(Context context) {
this(context, new ServerPreferences(context));
}
/* package */ AuthPreferences(Context context, ServerPreferences serverPreferences) {
this.context = context.getApplicationContext();
this.serverPreferences = serverPreferences;
this.preferences = PreferenceManager.getDefaultSharedPreferences(this.context);
}
@Deprecated
public XOAuthConsumer getOAuthConsumer() {
return new XOAuthConsumer(
getOauthUsername(),
getOauthToken(),
getOauthTokenSecret());
}
public String getOauth2Token() {
return getCredentials().getString(OAUTH2_TOKEN, null);
}
public String getOauth2RefreshToken() {
return getCredentials().getString(OAUTH2_REFRESH_TOKEN, null);
}
@Deprecated
public boolean hasOauthTokens() {
return getOauthUsername() != null &&
getOauthToken() != null &&
getOauthTokenSecret() != null;
}
public boolean hasOAuth2Tokens() {
return getOauth2Username() != null &&
getOauth2Token() != null;
}
public String getUsername() {
return preferences.getString(OAUTH_USER, getOauth2Username());
}
public boolean needsMigration() {
return hasOauthTokens() && !hasOAuth2Tokens();
}
public void setOauth2Token(String username, String accessToken, String refreshToken) {
preferences.edit()
.putString(OAUTH2_USER, username)
.commit();
getCredentials().edit()
.putString(OAUTH2_TOKEN, accessToken)
.commit();
getCredentials().edit()
.putString(OAUTH2_REFRESH_TOKEN, refreshToken)
.commit();
}
@Deprecated
public void clearOAuth1Data() {
preferences.edit().remove(OAUTH_USER).commit();
getCredentials().edit()
.remove(OAUTH_TOKEN)
.remove(OAUTH_TOKEN_SECRET)
.commit();
}
public void clearOauth2Data() {
final String oauth2token = getOauth2Token();
preferences.edit()
.remove(OAUTH2_USER)
.commit();
getCredentials().edit()
.remove(OAUTH2_TOKEN)
.remove(OAUTH2_REFRESH_TOKEN)
.commit();
if (!TextUtils.isEmpty(oauth2token)) {
new TokenRefresher(context, new OAuth2Client(getOAuth2ClientId()), this).invalidateToken(oauth2token);
}
}
public String getOAuth2ClientId() {
return context.getString(R.string.oauth2_client_id);
}
public void setImapPassword(String s) {
getCredentials().edit().putString(LOGIN_PASSWORD, s).commit();
}
public void setImapUser(String s) {
preferences.edit().putString(LOGIN_USER, s).commit();
}
public boolean useXOAuth() {
return getAuthMode() == AuthMode.XOAUTH && serverPreferences.isGmail();
}
public String getUserEmail() {
switch (getAuthMode()) {
case XOAUTH:
return getUsername();
default:
return getImapUsername();
}
}
public boolean isLoginInformationSet() {
switch (getAuthMode()) {
case PLAIN:
return !TextUtils.isEmpty(getImapPassword()) &&
!TextUtils.isEmpty(getImapUsername());
case XOAUTH:
return hasOauthTokens() || hasOAuth2Tokens();
default:
return false;
}
}
public String getStoreUri() {
if (useXOAuth()) {
if (hasOAuth2Tokens()) {
return formatUri(AuthType.XOAUTH2, SERVER_PROTOCOL, getOauth2Username(), generateXOAuth2Token());
} else {
Log.w(TAG, "No valid xoauth1/2 tokens");
return null;
}
} else {
return formatUri(AuthType.PLAIN,
serverPreferences.getServerProtocol(),
getImapUsername(),
getImapPassword());
}
}
private String formatUri(AuthType authType, String serverProtocol, String username, String password) {
return String.format(IMAP_URI,
serverProtocol,
authType.name().toUpperCase(Locale.US),
// NB: there's a bug in K9mail-library which requires double-encoding of uris
// https://github.com/k9mail/k-9/commit/b0d401c3b73c6b57402dc81d3cfd6488a71a1b98
encode(encode(username)),
encode(encode(password)),
serverPreferences.getServerAddress());
}
@Deprecated
private String getOauthTokenSecret() {
return getCredentials().getString(OAUTH_TOKEN_SECRET, null);
}
@Deprecated
private String getOauthToken() {
return getCredentials().getString(OAUTH_TOKEN, null);
}
@Deprecated
private String getOauthUsername() {
return preferences.getString(OAUTH_USER, null);
}
private String getOauth2Username() {
return preferences.getString(OAUTH2_USER, null);
}
private AuthMode getAuthMode() {
return new Preferences(context).getDefaultType(SERVER_AUTHENTICATION, AuthMode.class, AuthMode.XOAUTH);
}
/* package */ void setServerAuthMode(AuthType authType) {
preferences.edit().putString(AuthPreferences.SERVER_AUTHENTICATION, authType.name()).commit();
}
// All sensitive information is stored in a separate prefs file so we can
// backup the rest without exposing sensitive data
private SharedPreferences getCredentials() {
return context.getSharedPreferences("credentials", Context.MODE_PRIVATE);
}
private String getImapUsername() {
return preferences.getString(LOGIN_USER, null);
}
private String getImapPassword() {
return getCredentials().getString(LOGIN_PASSWORD, null);
}
/**
* TODO: this should probably be handled in K9
*
* <p>
* The SASL XOAUTH2 initial client response has the following format:
* </p>
* <code>base64("user="{User}"^Aauth=Bearer "{Access Token}"^A^A")</code>
* <p>
* For example, before base64-encoding, the initial client response might look like this:
* </p>
* <code>user=someuser@example.com^Aauth=Bearer vF9dft4qmTc2Nvb3RlckBhdHRhdmlzdGEuY29tCg==^A^A</code>
* <p/>
* <em>Note:</em> ^A represents a Control+A (\001).
*
* @see <a href="https://developers.google.com/google-apps/gmail/xoauth2_protocol#the_sasl_xoauth2_mechanism">
* The SASL XOAUTH2 Mechanism</a>
*/
private String generateXOAuth2Token() {
final String username = getOauth2Username();
final String token = getOauth2Token();
final String formatted = "user=" + username + "\001auth=Bearer " + token + "\001\001";
try {
return new String(Base64.encodeBase64(formatted.getBytes(UTF_8)), UTF_8);
} catch (UnsupportedEncodingException e) {
throw new RuntimeException(e);
}
}
private static String encode(String s) {
try {
return s == null ? "" : URLEncoder.encode(s, UTF_8);
} catch (UnsupportedEncodingException e) {
throw new RuntimeException(e);
}
}
}