/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ package org.mozilla.gecko.sync.stage; import java.io.IOException; import java.net.URISyntaxException; import java.util.HashSet; import java.util.Set; import org.json.simple.parser.ParseException; import org.mozilla.gecko.sync.CollectionKeys; import org.mozilla.gecko.sync.CryptoRecord; import org.mozilla.gecko.sync.ExtendedJSONObject; import org.mozilla.gecko.sync.GlobalSession; import org.mozilla.gecko.sync.InfoCollections; import org.mozilla.gecko.sync.Logger; import org.mozilla.gecko.sync.NoCollectionKeysSetException; import org.mozilla.gecko.sync.NonObjectJSONException; import org.mozilla.gecko.sync.crypto.CryptoException; import org.mozilla.gecko.sync.crypto.KeyBundle; import org.mozilla.gecko.sync.crypto.PersistedCrypto5Keys; import org.mozilla.gecko.sync.net.SyncStorageRecordRequest; import org.mozilla.gecko.sync.net.SyncStorageRequestDelegate; import org.mozilla.gecko.sync.net.SyncStorageResponse; public class EnsureCrypto5KeysStage extends AbstractNonRepositorySyncStage implements SyncStorageRequestDelegate { public EnsureCrypto5KeysStage(GlobalSession session) { super(session); } private static final String LOG_TAG = "EnsureC5KeysStage"; private static final String CRYPTO_COLLECTION = "crypto"; protected boolean retrying = false; @Override public void execute() throws NoSuchStageException { InfoCollections infoCollections = session.config.infoCollections; if (infoCollections == null) { session.abort(null, "No info/collections set in EnsureCrypto5KeysStage."); return; } PersistedCrypto5Keys pck = session.config.persistedCryptoKeys(); long lastModified = pck.lastModified(); if (retrying || !infoCollections.updateNeeded(CRYPTO_COLLECTION, lastModified)) { // Try to use our local collection keys for this session. Logger.debug(LOG_TAG, "Trying to use persisted collection keys for this session."); CollectionKeys keys = pck.keys(); if (keys != null) { Logger.trace(LOG_TAG, "Using persisted collection keys for this session."); session.config.setCollectionKeys(keys); session.advance(); return; } Logger.trace(LOG_TAG, "Failed to use persisted collection keys for this session."); } // We need an update: fetch fresh keys. Logger.debug(LOG_TAG, "Fetching fresh collection keys for this session."); try { SyncStorageRecordRequest request = new SyncStorageRecordRequest(session.wboURI(CRYPTO_COLLECTION, "keys")); request.delegate = this; request.get(); } catch (URISyntaxException e) { session.abort(e, "Invalid URI."); } } @Override public String credentials() { return session.credentials(); } @Override public String ifUnmodifiedSince() { // TODO: last key time! return null; } protected void setAndPersist(PersistedCrypto5Keys pck, CollectionKeys keys, long timestamp) { session.config.setCollectionKeys(keys); pck.persistKeys(keys); pck.persistLastModified(timestamp); } /** * Return collections where either the individual key has changed, or if the * new default key is not the same as the old default key, where the * collection is using the default key. */ protected Set<String> collectionsToUpdate(CollectionKeys oldKeys, CollectionKeys newKeys) { // These keys have explicitly changed; they definitely need updating. Set<String> changedKeys = new HashSet<String>(CollectionKeys.differences(oldKeys, newKeys)); boolean defaultKeyChanged = true; // Most pessimistic is to assume default key has changed. KeyBundle newDefaultKeyBundle = null; try { KeyBundle oldDefaultKeyBundle = oldKeys.defaultKeyBundle(); newDefaultKeyBundle = newKeys.defaultKeyBundle(); defaultKeyChanged = !oldDefaultKeyBundle.equals(newDefaultKeyBundle); } catch (NoCollectionKeysSetException e) { Logger.warn(LOG_TAG, "NoCollectionKeysSetException in EnsureCrypto5KeysStage.", e); } if (newDefaultKeyBundle == null) { Logger.trace(LOG_TAG, "New default key not provided; returning changed individual keys."); return changedKeys; } if (!defaultKeyChanged) { Logger.trace(LOG_TAG, "New default key is the same as old default key; returning changed individual keys."); return changedKeys; } // New keys have a different default/sync key; check known collections against the default key. Logger.debug(LOG_TAG, "New default key is not the same as old default key."); for (Stage stage : Stage.getNamedStages()) { String name = stage.getRepositoryName(); if (!newKeys.keyBundleForCollectionIsNotDefault(name)) { // Default key has changed, so this collection has changed. changedKeys.add(name); } } return changedKeys; } @Override public void handleRequestSuccess(SyncStorageResponse response) { // Take the timestamp from the response since it is later than the timestamp from info/collections. long responseTimestamp = response.normalizedWeaveTimestamp(); CollectionKeys keys = new CollectionKeys(); try { ExtendedJSONObject body = response.jsonObjectBody(); if (Logger.LOG_PERSONAL_INFORMATION) { Logger.pii(LOG_TAG, "Fetched keys: " + body.toJSONString()); } keys.setKeyPairsFromWBO(CryptoRecord.fromJSONRecord(body), session.config.syncKeyBundle); } catch (IllegalStateException e) { session.abort(e, "Invalid keys WBO."); return; } catch (ParseException e) { session.abort(e, "Invalid keys WBO."); return; } catch (NonObjectJSONException e) { session.abort(e, "Invalid keys WBO."); return; } catch (IOException e) { // Some kind of lower-level error. session.abort(e, "IOException fetching keys."); return; } catch (CryptoException e) { session.abort(e, "CryptoException handling keys WBO."); return; } PersistedCrypto5Keys pck = session.config.persistedCryptoKeys(); if (!pck.persistedKeysExist()) { // New keys, and no old keys! Persist keys and server timestamp. Logger.trace(LOG_TAG, "Setting fetched keys for this session; persisting fetched keys and last modified."); setAndPersist(pck, keys, responseTimestamp); session.advance(); return; } // New keys, but we had old keys. Check for differences. CollectionKeys oldKeys = pck.keys(); Set<String> changedCollections = collectionsToUpdate(oldKeys, keys); if (!changedCollections.isEmpty()) { // New keys, different from old keys. Logger.trace(LOG_TAG, "Fetched keys are not the same as persisted keys; " + "setting fetched keys for this session before resetting changed engines."); setAndPersist(pck, keys, responseTimestamp); session.resetStagesByName(changedCollections); session.abort(null, "crypto/keys changed on server."); return; } // New keys don't differ from old keys; persist timestamp and move on. Logger.trace(LOG_TAG, "Fetched keys are the same as persisted keys; persisting only last modified."); session.config.setCollectionKeys(oldKeys); pck.persistLastModified(response.normalizedWeaveTimestamp()); session.advance(); } @Override public void handleRequestFailure(SyncStorageResponse response) { if (retrying) { // Should happen very rarely -- this means we uploaded our crypto/keys // successfully, but failed to re-download. session.handleHTTPError(response, "Failure while re-downloading already uploaded keys."); return; } int statusCode = response.getStatusCode(); if (statusCode == 404) { Logger.info(LOG_TAG, "Got 404 fetching keys. Fresh starting since keys are missing on server."); session.freshStart(); return; } session.handleHTTPError(response, "Failure fetching keys: got response status code " + statusCode); } @Override public void handleRequestError(Exception ex) { session.abort(ex, "Failure fetching keys."); } }