// Copyright (c) 2009, Google Inc. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package net.tawacentral.roger.secrets; import java.security.SecureRandom; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.Map; import net.tawacentral.roger.secrets.Secret.LogEntry; import org.json.JSONException; import org.json.JSONObject; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.util.Log; /** * Provides support for Online Sync Agents. * * Sync process overview * * On each resume a roll call is broadcast. Agents that respond are recorded in * a list of available agents. * * Sync operations are always initiated from secrets. A sync request is * broadcast at the selected (or only) available agent, together with the * unencrypted secrets and a one-time validation key. The sync response is * validated against the key,and the returned secrets (updated or deleted) are * merged with the existing ones. * * Multiple concurrent sync requests are not supported. It is the caller's * responsibility to ensure there is no active request when calling * sendSecrets(). * * @author Chris Wood */ public class OnlineAgentManager extends BroadcastReceiver { private static final String LOG_TAG = "OnlineAgentManager"; private static final String SECRETS_PERMISSION = "net.tawacentral.roger.secrets.permission.SECRETS"; // broadcast ACTIONS private static final String ROLLCALL = "net.tawacentral.roger.secrets.OSA_ROLLCALL"; private static final String ROLLCALL_RESPONSE = "net.tawacentral.roger.secrets.OSA_ROLLCALL_RESPONSE"; private static final String SYNC = "net.tawacentral.roger.secrets.SYNC"; private static final String SYNC_RESPONSE = "net.tawacentral.roger.secrets.SYNC_RESPONSE"; private static final String SYNC_CANCEL = "net.tawacentral.roger.secrets.SYNC_CANCEL"; private static final String INTENT_CLASSID = "net.tawacentral.roger.secrets.ClassId"; private static final String INTENT_DISPLAYNAME = "net.tawacentral.roger.secrets.DisplayName"; private static final String INTENT_RESPONSEKEY = "net.tawacentral.roger.secrets.ResponseKey"; private static final String INTENT_SECRETS = "net.tawacentral.roger.secrets.Secrets"; // for the current request private static OnlineSyncAgent requestAgent; private static SecretsListActivity responseActivity; private static boolean active; /* * The response key is a randomly generated string that is provided to the * OSA as part of the sync request and must be returned in the response in * order for it to be accepted. The key is changed when the response is * received to ensure that any subsequent or unsolicited responses are * rejected. */ private static String responseKey; private static final int RESPONSEKEY_LENGTH = 8; private static Map<String, OnlineSyncAgent> AVAILABLE_AGENTS = new HashMap<String, OnlineSyncAgent>(); /* * Handle the received broadcast */ @Override public void onReceive(Context context, Intent intent) { Log.d(LOG_TAG, "Agent Manager received msg: " + intent.getAction()); // handle roll call response if (intent.getAction().equals(ROLLCALL_RESPONSE) && intent.getExtras() != null) { String classId = (String) intent.getExtras().get(INTENT_CLASSID); String displayName = (String) intent.getExtras().get(INTENT_DISPLAYNAME); if (classId == null || classId.length() == 0 || displayName == null || displayName.length() == 0) { // invalid info, so do not add it Log.e(LOG_TAG, "Received invalid OSA rollcall resp: classId=" + classId + ",displayName=" + displayName); } else { AVAILABLE_AGENTS .put(classId, new OnlineSyncAgent(displayName, classId)); Log.d(LOG_TAG, "Received OSA rollcall resp: " + classId + " " + displayName); } // handle sync response } else if (intent.getAction().equals(SYNC_RESPONSE) && validateResponse(intent)) { String secretsString = intent.getStringExtra(INTENT_SECRETS); ArrayList<Secret> secrets = null; if (secretsString != null) { try { secrets = FileUtils.fromJSONSecrets(new JSONObject(secretsString)); } catch (JSONException e) { Log.e(LOG_TAG, "Received invalid JSON secrets data", e); } } active = false; responseActivity.syncSecrets(secrets, requestAgent.getDisplayName()); requestAgent = null; responseKey = generateResponseKey(); // change response key } } /* * Validate the sync response from the agent. The key in the response must * match the one sent in the original sync request. * * @param intent * * @return true if response is OK, false otherwise */ private boolean validateResponse(Intent intent) { if (intent.getExtras() != null) { String classId = (String) intent.getExtras().get(INTENT_CLASSID); String responseKey = (String) intent.getExtras().get(INTENT_RESPONSEKEY); OnlineSyncAgent agent = AVAILABLE_AGENTS.get(classId); if (agent != null) { if (agent == requestAgent) { // is a response expected? if (OnlineAgentManager.responseKey != null // does the key match? && OnlineAgentManager.responseKey.equals(responseKey)) { if (active) return true; // if request not cancelled Log.w(LOG_TAG, "SYNC response received from agent " + classId + " after request was cancelled - discarded"); } else { Log.w(LOG_TAG, "SYNC response received from agent " + classId + " with invalid response key"); } } else { Log.w(LOG_TAG, "Unexpected SYNC response received from agent " + classId + " - no request outstanding"); } } else { Log.w(LOG_TAG, "SYNC response received from unknown app: " + classId); } } else { Log.w(LOG_TAG, "SYNC response received with no extras"); } return false; } /** * Generate a new response key * * @return String response key */ public static String generateResponseKey() { SecureRandom random = new SecureRandom(); byte[] keyBytes = new byte[RESPONSEKEY_LENGTH]; random.nextBytes(keyBytes); return new String(keyBytes); } /** * Get available agents * * @return collection of installed agents */ public static Collection<OnlineSyncAgent> getAvailableAgents() { return Collections.unmodifiableCollection(AVAILABLE_AGENTS.values()); } /** * Sends out the rollcall broadcast and will keep track of all OSAs that * respond. * * Forget previous agents - only ones that respond are considered available. * * @param context */ public static void sendRollCallBroadcast(Context context) { AVAILABLE_AGENTS.clear(); Intent broadcastIntent = new Intent(ROLLCALL); context.sendBroadcast(broadcastIntent, SECRETS_PERMISSION); Log.d(LOG_TAG, "sent broadcast"); } /** * Sends secrets to the specified OSA. * * Returns true if secrets are successfully sent, but makes no guarantees that * the secrets were received. * * A one-time key is sent to the OSA and must be returned in the reply for it * to be considered valid. * * @param agent * @param secrets * @param activity * @return true if secrets were sent */ public static boolean sendSecrets(OnlineSyncAgent agent, ArrayList<Secret> secrets, SecretsListActivity activity) { requestAgent = agent; responseActivity = activity; responseKey = generateResponseKey(); try { Intent secretsIntent = new Intent(SYNC); secretsIntent.setPackage(agent.getClassId()); secretsIntent.putExtra(INTENT_RESPONSEKEY, OnlineAgentManager.responseKey); String secretString = FileUtils.toJSONSecrets(secrets).toString(); secretsIntent.putExtra(INTENT_SECRETS, secretString); activity.sendBroadcast(secretsIntent, SECRETS_PERMISSION); Log.d(LOG_TAG, "Secrets sent to OSA " + agent.getClassId()); active = true; return true; } catch (Exception e) { Log.e(LOG_TAG, "Error sending secrets to OSA", e); // ignore the exception, false will be returned below } return false; } /** * Test for active request * @return true if active */ public static boolean isActive() { return active; } /** * Cancel the active request */ public static void cancel() { OnlineAgentManager.active = false; Intent secretsIntent = new Intent(SYNC_CANCEL); secretsIntent.setPackage(requestAgent.getClassId()); responseActivity.sendBroadcast(secretsIntent, SECRETS_PERMISSION); } /* Helper functions */ /** * Add, update or delete the current secrets in the given collection. * * Assumes that the collection sort sequences are the same. * * @param secrets * - target secrets collection * @param changedSecrets * - added, changed or deleted secrets */ public static void syncSecrets(ArrayList<Secret> secrets, ArrayList<Secret> changedSecrets) { for (Secret changedSecret : changedSecrets) { boolean done = false; for (int i = 0; i < secrets.size(); i++) { Secret existingSecret = secrets.get(i); int compare = changedSecret.compareTo(existingSecret); if (compare < 0 && !changedSecret.isDeleted()) { secrets.add(i, changedSecret); done = true; Log.d(LOG_TAG, "syncSecrets: added '" + changedSecret.getDescription() + "'"); break; } else if (compare == 0) { if (changedSecret.isDeleted()) { secrets.remove(existingSecret); Log.d(LOG_TAG, "syncSecrets: removed '" + changedSecret.getDescription() + "'"); } else { existingSecret.update(changedSecret, LogEntry.SYNCED); Log.d(LOG_TAG, "syncSecrets: updated '" + changedSecret.getDescription() + "'"); } done = true; break; } } if (!done && !changedSecret.isDeleted()) secrets.add(changedSecret); Log.d(LOG_TAG, "syncSecrets: added '" + changedSecret.getDescription() + "'"); } } }