1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/mobile/android/base/sync/setup/SyncAuthenticatorService.java Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,257 @@ 1.4 +/* This Source Code Form is subject to the terms of the Mozilla Public 1.5 + * License, v. 2.0. If a copy of the MPL was not distributed with this 1.6 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 1.7 + 1.8 +package org.mozilla.gecko.sync.setup; 1.9 + 1.10 +import java.io.UnsupportedEncodingException; 1.11 +import java.security.NoSuchAlgorithmException; 1.12 + 1.13 +import org.mozilla.gecko.background.common.log.Logger; 1.14 +import org.mozilla.gecko.sync.SyncConstants; 1.15 +import org.mozilla.gecko.sync.Utils; 1.16 +import org.mozilla.gecko.sync.setup.activities.SetupSyncActivity; 1.17 + 1.18 +import android.accounts.AbstractAccountAuthenticator; 1.19 +import android.accounts.Account; 1.20 +import android.accounts.AccountAuthenticatorResponse; 1.21 +import android.accounts.AccountManager; 1.22 +import android.accounts.NetworkErrorException; 1.23 +import android.app.Service; 1.24 +import android.content.Context; 1.25 +import android.content.Intent; 1.26 +import android.os.Bundle; 1.27 +import android.os.IBinder; 1.28 + 1.29 +public class SyncAuthenticatorService extends Service { 1.30 + private static final String LOG_TAG = "SyncAuthService"; 1.31 + 1.32 + private SyncAccountAuthenticator sAccountAuthenticator = null; 1.33 + 1.34 + @Override 1.35 + public void onCreate() { 1.36 + Logger.debug(LOG_TAG, "onCreate"); 1.37 + sAccountAuthenticator = getAuthenticator(); 1.38 + } 1.39 + 1.40 + @Override 1.41 + public IBinder onBind(Intent intent) { 1.42 + if (intent.getAction().equals(android.accounts.AccountManager.ACTION_AUTHENTICATOR_INTENT)) { 1.43 + return getAuthenticator().getIBinder(); 1.44 + } 1.45 + return null; 1.46 + } 1.47 + 1.48 + private SyncAccountAuthenticator getAuthenticator() { 1.49 + if (sAccountAuthenticator == null) { 1.50 + sAccountAuthenticator = new SyncAccountAuthenticator(this); 1.51 + } 1.52 + return sAccountAuthenticator; 1.53 + } 1.54 + 1.55 + /** 1.56 + * Generate a "plain" auth token. 1.57 + * <p> 1.58 + * Android caches only the value of the key 1.59 + * <code>AccountManager.KEY_AUTHTOKEN</code>, so if a caller needs the other 1.60 + * keys in this bundle, it needs to invalidate the token (so that the bundle 1.61 + * is re-generated). 1.62 + * 1.63 + * @param context 1.64 + * Android context. 1.65 + * @param account 1.66 + * Android account. 1.67 + * @return a <code>Bundle</code> instance containing a subset of the following 1.68 + * keys: (caller's must check for missing keys) 1.69 + * <ul> 1.70 + * <li><code>AccountManager.KEY_ACCOUNT_TYPE</code>: the Android 1.71 + * Account's type</li> 1.72 + * 1.73 + * <li><code>AccountManager.KEY_ACCOUNT_NAME</code>: the Android 1.74 + * Account's name</li> 1.75 + * 1.76 + * <li><code>AccountManager.KEY_AUTHTOKEN</code>: the Sync account's 1.77 + * password </li> 1.78 + * 1.79 + * <li><code> Constants.OPTION_USERNAME</code>: the Sync account's 1.80 + * hashed username</li> 1.81 + * 1.82 + * <li><code>Constants.OPTION_SERVER</code>: the Sync account's 1.83 + * server</li> 1.84 + * 1.85 + * <li><code> Constants.OPTION_SYNCKEY</code>: the Sync account's 1.86 + * sync key</li> 1.87 + * 1.88 + * </ul> 1.89 + * @throws NetworkErrorException 1.90 + */ 1.91 + public static Bundle getPlainAuthToken(final Context context, final Account account) 1.92 + throws NetworkErrorException { 1.93 + // Extract the username and password from the Account Manager, and ask 1.94 + // the server for an appropriate AuthToken. 1.95 + final AccountManager am = AccountManager.get(context); 1.96 + final String password = am.getPassword(account); 1.97 + if (password == null) { 1.98 + Logger.warn(LOG_TAG, "Returning null bundle for getPlainAuthToken since Account password is null."); 1.99 + return null; 1.100 + } 1.101 + 1.102 + final Bundle result = new Bundle(); 1.103 + 1.104 + // This is a Sync account. 1.105 + result.putString(AccountManager.KEY_ACCOUNT_TYPE, SyncConstants.ACCOUNTTYPE_SYNC); 1.106 + 1.107 + // Server. 1.108 + String serverURL = am.getUserData(account, Constants.OPTION_SERVER); 1.109 + result.putString(Constants.OPTION_SERVER, serverURL); 1.110 + 1.111 + // Full username, before hashing. 1.112 + result.putString(AccountManager.KEY_ACCOUNT_NAME, account.name); 1.113 + 1.114 + // Username after hashing. 1.115 + try { 1.116 + String username = Utils.usernameFromAccount(account.name); 1.117 + Logger.pii(LOG_TAG, "Account " + account.name + " hashes to " + username + "."); 1.118 + Logger.debug(LOG_TAG, "Setting username. Null? " + (username == null)); 1.119 + result.putString(Constants.OPTION_USERNAME, username); 1.120 + } catch (NoSuchAlgorithmException e) { 1.121 + // Do nothing. Calling code must check for missing value. 1.122 + Logger.debug(LOG_TAG, "Exception in account lookup: " + e); 1.123 + } catch (UnsupportedEncodingException e) { 1.124 + // Do nothing. Calling code must check for missing value. 1.125 + Logger.debug(LOG_TAG, "Exception in account lookup: " + e); 1.126 + } 1.127 + 1.128 + // Sync key. 1.129 + final String syncKey = am.getUserData(account, Constants.OPTION_SYNCKEY); 1.130 + Logger.debug(LOG_TAG, "Setting sync key. Null? " + (syncKey == null)); 1.131 + result.putString(Constants.OPTION_SYNCKEY, syncKey); 1.132 + 1.133 + // Password. 1.134 + result.putString(AccountManager.KEY_AUTHTOKEN, password); 1.135 + return result; 1.136 + } 1.137 + 1.138 + private static class SyncAccountAuthenticator extends AbstractAccountAuthenticator { 1.139 + private Context mContext; 1.140 + public SyncAccountAuthenticator(Context context) { 1.141 + super(context); 1.142 + mContext = context; 1.143 + } 1.144 + 1.145 + @Override 1.146 + public Bundle addAccount(AccountAuthenticatorResponse response, 1.147 + String accountType, String authTokenType, String[] requiredFeatures, 1.148 + Bundle options) throws NetworkErrorException { 1.149 + Logger.debug(LOG_TAG, "addAccount()"); 1.150 + final Intent intent = new Intent(mContext, SetupSyncActivity.class); 1.151 + intent.putExtra(AccountManager.KEY_ACCOUNT_AUTHENTICATOR_RESPONSE, 1.152 + response); 1.153 + intent.putExtra("accountType", SyncConstants.ACCOUNTTYPE_SYNC); 1.154 + intent.putExtra(Constants.INTENT_EXTRA_IS_SETUP, true); 1.155 + 1.156 + final Bundle result = new Bundle(); 1.157 + result.putParcelable(AccountManager.KEY_INTENT, intent); 1.158 + 1.159 + return result; 1.160 + } 1.161 + 1.162 + @Override 1.163 + public Bundle confirmCredentials(AccountAuthenticatorResponse response, 1.164 + Account account, 1.165 + Bundle options) throws NetworkErrorException { 1.166 + Logger.debug(LOG_TAG, "confirmCredentials()"); 1.167 + return null; 1.168 + } 1.169 + 1.170 + @Override 1.171 + public Bundle editProperties(AccountAuthenticatorResponse response, 1.172 + String accountType) { 1.173 + Logger.debug(LOG_TAG, "editProperties"); 1.174 + return null; 1.175 + } 1.176 + 1.177 + @Override 1.178 + public Bundle getAuthToken(AccountAuthenticatorResponse response, 1.179 + Account account, String authTokenType, Bundle options) 1.180 + throws NetworkErrorException { 1.181 + Logger.debug(LOG_TAG, "getAuthToken()"); 1.182 + 1.183 + if (Constants.AUTHTOKEN_TYPE_PLAIN.equals(authTokenType)) { 1.184 + return getPlainAuthToken(mContext, account); 1.185 + } 1.186 + 1.187 + final Bundle result = new Bundle(); 1.188 + result.putString(AccountManager.KEY_ERROR_MESSAGE, "invalid authTokenType"); 1.189 + return result; 1.190 + } 1.191 + 1.192 + @Override 1.193 + public String getAuthTokenLabel(String authTokenType) { 1.194 + Logger.debug(LOG_TAG, "getAuthTokenLabel()"); 1.195 + return null; 1.196 + } 1.197 + 1.198 + @Override 1.199 + public Bundle hasFeatures(AccountAuthenticatorResponse response, 1.200 + Account account, String[] features) throws NetworkErrorException { 1.201 + Logger.debug(LOG_TAG, "hasFeatures()"); 1.202 + return null; 1.203 + } 1.204 + 1.205 + @Override 1.206 + public Bundle updateCredentials(AccountAuthenticatorResponse response, 1.207 + Account account, String authTokenType, Bundle options) 1.208 + throws NetworkErrorException { 1.209 + Logger.debug(LOG_TAG, "updateCredentials()"); 1.210 + return null; 1.211 + } 1.212 + 1.213 + /** 1.214 + * Bug 769745: persist pickled Sync account settings so that we can unpickle 1.215 + * after Fennec is moved to the SD card. 1.216 + * <p> 1.217 + * This is <b>not</b> called when an Android Account is blown away due to 1.218 + * the SD card being unmounted. 1.219 + * <p> 1.220 + * Broadcasting a Firefox intent to version sharing this Android Account is 1.221 + * a terrible hack, but it's better than the catching the generic 1.222 + * "accounts changed" broadcast intent and trying to figure out whether our 1.223 + * Account disappeared. 1.224 + */ 1.225 + @Override 1.226 + public Bundle getAccountRemovalAllowed(final AccountAuthenticatorResponse response, Account account) 1.227 + throws NetworkErrorException { 1.228 + Bundle result = super.getAccountRemovalAllowed(response, account); 1.229 + 1.230 + if (result == null || 1.231 + !result.containsKey(AccountManager.KEY_BOOLEAN_RESULT) || 1.232 + result.containsKey(AccountManager.KEY_INTENT)) { 1.233 + return result; 1.234 + } 1.235 + 1.236 + final boolean removalAllowed = result.getBoolean(AccountManager.KEY_BOOLEAN_RESULT); 1.237 + if (!removalAllowed) { 1.238 + return result; 1.239 + } 1.240 + 1.241 + // Bug 790931: Broadcast a message to all Firefox versions sharing this 1.242 + // Android Account type telling that this Sync Account has been deleted. 1.243 + // 1.244 + // We would really prefer to receive Android's 1.245 + // LOGIN_ACCOUNTS_CHANGED_ACTION broadcast, but that 1.246 + // doesn't include enough information about which Accounts changed to 1.247 + // correctly identify whether a Sync account has been removed (when some 1.248 + // Firefox versions are installed on the SD card). 1.249 + // 1.250 + // Broadcast intents protected with permissions are secure, so it's okay 1.251 + // to include password and sync key, etc. 1.252 + final Intent intent = SyncAccounts.makeSyncAccountDeletedIntent(mContext, AccountManager.get(mContext), account); 1.253 + Logger.info(LOG_TAG, "Account named " + account.name + " being removed; " + 1.254 + "broadcasting secure intent " + intent.getAction() + "."); 1.255 + mContext.sendBroadcast(intent, SyncConstants.PER_ACCOUNT_TYPE_PERMISSION); 1.256 + 1.257 + return result; 1.258 + } 1.259 + } 1.260 +}