Bug 709324, Bug 730643: persist crypto/keys and meta/global. r=rnewman

This commit is contained in:
Nick Alexander 2012-04-12 20:15:53 -07:00
parent 173372bc56
commit d285b0ba7c
15 changed files with 498 additions and 443 deletions

View File

@ -1,39 +1,6 @@
/* ***** BEGIN LICENSE BLOCK *****
* Version: MPL 1.1/GPL 2.0/LGPL 2.1
*
* The contents of this file are subject to the Mozilla Public License Version
* 1.1 (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.mozilla.org/MPL/
*
* Software distributed under the License is distributed on an "AS IS" basis,
* WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
* for the specific language governing rights and limitations under the
* License.
*
* The Original Code is Android Sync Client.
*
* The Initial Developer of the Original Code is
* the Mozilla Foundation.
* Portions created by the Initial Developer are Copyright (C) 2011
* the Initial Developer. All Rights Reserved.
*
* Contributor(s):
* Richard Newman <rnewman@mozilla.com>
*
* Alternatively, the contents of this file may be used under the terms of
* either the GNU General Public License Version 2 or later (the "GPL"), or
* the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
* in which case the provisions of the GPL or the LGPL are applicable instead
* of those above. If you wish to allow use of your version of this file only
* under the terms of either the GPL or the LGPL, and not to allow others to
* use your version of this file under the terms of the MPL, indicate your
* decision by deleting the provisions above and replace them with the notice
* and other provisions required by the GPL or the LGPL. If you do not delete
* the provisions above, a recipient may use your version of this file under
* the terms of any one of the MPL, the GPL or the LGPL.
*
* ***** END LICENSE BLOCK ***** */
/* 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;
@ -48,8 +15,6 @@ import org.mozilla.apache.commons.codec.binary.Base64;
import org.mozilla.gecko.sync.crypto.CryptoException;
import org.mozilla.gecko.sync.crypto.KeyBundle;
import android.util.Log;
public class CollectionKeys {
private static final String LOG_TAG = "CollectionKeys";
private KeyBundle defaultKeyBundle = null;
@ -61,7 +26,7 @@ public class CollectionKeys {
return ck.asCryptoRecord();
} catch (NoCollectionKeysSetException e) {
// Cannot occur.
Log.e(LOG_TAG, "generateCollectionKeys returned a value with no default key. Unpossible.", e);
Logger.error(LOG_TAG, "generateCollectionKeys returned a value with no default key.", e);
throw new IllegalStateException("CollectionKeys should not have null default key.");
}
}
@ -138,7 +103,18 @@ public class CollectionKeys {
return record;
}
public static CollectionKeys fromCryptoRecord(CryptoRecord keys, KeyBundle syncKeyBundle) throws CryptoException, IOException, ParseException, NonObjectJSONException {
/**
* Set my key bundle and collection keys with the given key bundle and data
* (possibly decrypted) from the given record.
*
* @param keys
* A "crypto/keys" <code>CryptoRecord</code>, encrypted with
* <code>syncKeyBundle</code> if <code>syncKeyBundle</code> is non-null.
* @param syncKeyBundle
* If non-null, the sync key bundle to decrypt <code>keys</code> with.
*/
public void setKeyPairsFromWBO(CryptoRecord keys, KeyBundle syncKeyBundle)
throws CryptoException, IOException, ParseException, NonObjectJSONException {
if (syncKeyBundle != null) {
keys.keyBundle = syncKeyBundle;
keys.decrypt();
@ -153,33 +129,6 @@ public class CollectionKeys {
collectionKeys.put(pair.getKey(), bundle);
}
CollectionKeys ck = new CollectionKeys();
ck.collectionKeyBundles = collectionKeys;
ck.defaultKeyBundle = defaultKey;
return ck;
}
/**
* Take a downloaded record, and the Sync Key, decrypting the record and
* setting our own keys accordingly.
*/
public void setKeyPairsFromWBO(CryptoRecord keys, KeyBundle syncKeyBundle)
throws CryptoException,
IOException,
ParseException,
NonObjectJSONException {
keys.keyBundle = syncKeyBundle;
keys.decrypt();
ExtendedJSONObject cleartext = keys.payload;
KeyBundle defaultKey = arrayToKeyBundle((JSONArray) cleartext.get("default"));
ExtendedJSONObject collections = cleartext.getObject("collections");
HashMap<String, KeyBundle> collectionKeys = new HashMap<String, KeyBundle>();
for (Entry<String, Object> pair : collections.entryIterable()) {
KeyBundle bundle = arrayToKeyBundle((JSONArray) pair.getValue());
collectionKeys.put(pair.getKey(), bundle);
}
this.collectionKeyBundles = collectionKeys;
this.defaultKeyBundle = defaultKey;
}

View File

@ -1,47 +1,9 @@
/* ***** BEGIN LICENSE BLOCK *****
* Version: MPL 1.1/GPL 2.0/LGPL 2.1
*
* The contents of this file are subject to the Mozilla Public License Version
* 1.1 (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.mozilla.org/MPL/
*
* Software distributed under the License is distributed on an "AS IS" basis,
* WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
* for the specific language governing rights and limitations under the
* License.
*
* The Original Code is Android Sync Client.
*
* The Initial Developer of the Original Code is
* the Mozilla Foundation.
* Portions created by the Initial Developer are Copyright (C) 2011
* the Initial Developer. All Rights Reserved.
*
* Contributor(s):
* Richard Newman <rnewman@mozilla.com>
*
* Alternatively, the contents of this file may be used under the terms of
* either the GNU General Public License Version 2 or later (the "GPL"), or
* the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
* in which case the provisions of the GPL or the LGPL are applicable instead
* of those above. If you wish to allow use of your version of this file only
* under the terms of either the GPL or the LGPL, and not to allow others to
* use your version of this file under the terms of the MPL, indicate your
* decision by deleting the provisions above and replace them with the notice
* and other provisions required by the GPL or the LGPL. If you do not delete
* the provisions above, a recipient may use your version of this file under
* the terms of any one of the MPL, the GPL or the LGPL.
*
* ***** END LICENSE BLOCK ***** */
/* 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;
import org.mozilla.gecko.sync.crypto.KeyBundle;
public interface CredentialsSource {
public abstract String credentials();
public abstract CollectionKeys getCollectionKeys();
public abstract KeyBundle keyForCollection(String collection) throws NoCollectionKeysSetException;
}

View File

@ -37,7 +37,7 @@ import org.mozilla.gecko.sync.stage.PasswordsServerSyncStage;
import org.mozilla.gecko.sync.stage.CheckPreconditionsStage;
import org.mozilla.gecko.sync.stage.CompletedStage;
import org.mozilla.gecko.sync.stage.EnsureClusterURLStage;
import org.mozilla.gecko.sync.stage.EnsureKeysStage;
import org.mozilla.gecko.sync.stage.EnsureCrypto5KeysStage;
import org.mozilla.gecko.sync.stage.FennecTabsServerSyncStage;
import org.mozilla.gecko.sync.stage.FetchInfoCollectionsStage;
import org.mozilla.gecko.sync.stage.FetchMetaGlobalStage;
@ -70,16 +70,8 @@ public class GlobalSession implements CredentialsSource, PrefsSource, HttpRespon
/*
* Key accessors.
*/
public void setCollectionKeys(CollectionKeys k) {
config.setCollectionKeys(k);
}
@Override
public CollectionKeys getCollectionKeys() {
return config.collectionKeys;
}
@Override
public KeyBundle keyForCollection(String collection) throws NoCollectionKeysSetException {
return config.keyForCollection(collection);
public KeyBundle keyBundleForCollection(String collection) throws NoCollectionKeysSetException {
return config.getCollectionKeys().keyBundleForCollection(collection);
}
/*
@ -193,7 +185,7 @@ public class GlobalSession implements CredentialsSource, PrefsSource, HttpRespon
stages.put(Stage.ensureClusterURL, new EnsureClusterURLStage());
stages.put(Stage.fetchInfoCollections, new FetchInfoCollectionsStage());
stages.put(Stage.fetchMetaGlobal, new FetchMetaGlobalStage());
stages.put(Stage.ensureKeysStage, new EnsureKeysStage());
stages.put(Stage.ensureKeysStage, new EnsureCrypto5KeysStage());
stages.put(Stage.syncClientsEngine, new SyncClientsEngineStage());
// TODO: more stages.
@ -273,7 +265,6 @@ public class GlobalSession implements CredentialsSource, PrefsSource, HttpRespon
return this.getContext().getSharedPreferences(name, mode);
}
@Override
public Context getContext() {
return this.context;
}
@ -355,13 +346,6 @@ public class GlobalSession implements CredentialsSource, PrefsSource, HttpRespon
}
}
public void fetchMetaGlobal(MetaGlobalDelegate callback) throws URISyntaxException {
if (this.config.metaGlobal == null) {
this.config.metaGlobal = new MetaGlobal(config.metaURL(), credentials());
}
this.config.metaGlobal.fetch(callback);
}
public void fetchInfoCollections(InfoCollectionsDelegate callback) throws URISyntaxException {
if (this.config.infoCollections == null) {
this.config.infoCollections = new InfoCollections(config.infoURL(), credentials());
@ -429,6 +413,8 @@ public class GlobalSession implements CredentialsSource, PrefsSource, HttpRespon
* meta/global callbacks.
*/
public void processMetaGlobal(MetaGlobal global) {
config.metaGlobal = global;
Long storageVersion = global.getStorageVersion();
if (storageVersion < STORAGE_VERSION) {
// Outdated server.
@ -450,9 +436,7 @@ public class GlobalSession implements CredentialsSource, PrefsSource, HttpRespon
if (!remoteSyncID.equals(localSyncID)) {
// Sync ID has changed. Reset timestamps and fetch new keys.
resetClient(null);
if (config.collectionKeys != null) {
config.collectionKeys.clear();
}
config.purgeCryptoKeys();
config.syncID = remoteSyncID;
// TODO TODO TODO
}
@ -503,7 +487,7 @@ public class GlobalSession implements CredentialsSource, PrefsSource, HttpRespon
@Override
public void onWiped(long timestamp) {
session.resetClient(null);
session.config.collectionKeys.clear(); // TODO: make sure we clear our keys timestamp.
session.config.purgeCryptoKeys();
session.config.persistToPrefs();
MetaGlobal mg = new MetaGlobal(metaURL, credentials);
@ -565,54 +549,6 @@ public class GlobalSession implements CredentialsSource, PrefsSource, HttpRespon
Logger.warn(LOG_TAG, "Got error uploading new meta/global.", e);
freshStartDelegate.onFreshStartFailed(e);
}
@Override
public MetaGlobalDelegate deferred() {
final MetaGlobalDelegate self = this;
return new MetaGlobalDelegate() {
@Override
public void handleSuccess(final MetaGlobal global, final SyncStorageResponse response) {
ThreadPool.run(new Runnable() {
@Override
public void run() {
self.handleSuccess(global, response);
}});
}
@Override
public void handleMissing(final MetaGlobal global, final SyncStorageResponse response) {
ThreadPool.run(new Runnable() {
@Override
public void run() {
self.handleMissing(global, response);
}});
}
@Override
public void handleFailure(final SyncStorageResponse response) {
ThreadPool.run(new Runnable() {
@Override
public void run() {
self.handleFailure(response);
}});
}
@Override
public void handleError(final Exception e) {
ThreadPool.run(new Runnable() {
@Override
public void run() {
self.handleError(e);
}});
}
@Override
public MetaGlobalDelegate deferred() {
return this;
}
};
}
});
}

View File

@ -1,39 +1,6 @@
/* ***** BEGIN LICENSE BLOCK *****
* Version: MPL 1.1/GPL 2.0/LGPL 2.1
*
* The contents of this file are subject to the Mozilla Public License Version
* 1.1 (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.mozilla.org/MPL/
*
* Software distributed under the License is distributed on an "AS IS" basis,
* WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
* for the specific language governing rights and limitations under the
* License.
*
* The Original Code is Android Sync Client.
*
* The Initial Developer of the Original Code is
* the Mozilla Foundation.
* Portions created by the Initial Developer are Copyright (C) 2011
* the Initial Developer. All Rights Reserved.
*
* Contributor(s):
* Richard Newman <rnewman@mozilla.com>
*
* Alternatively, the contents of this file may be used under the terms of
* either the GNU General Public License Version 2 or later (the "GPL"), or
* the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
* in which case the provisions of the GPL or the LGPL are applicable instead
* of those above. If you wish to allow use of your version of this file only
* under the terms of either the GPL or the LGPL, and not to allow others to
* use your version of this file under the terms of the MPL, indicate your
* decision by deleting the provisions above and replace them with the notice
* and other provisions required by the GPL or the LGPL. If you do not delete
* the provisions above, a recipient may use your version of this file under
* the terms of any one of the MPL, the GPL or the LGPL.
*
* ***** END LICENSE BLOCK ***** */
/* 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;
@ -56,17 +23,13 @@ public class InfoCollections implements SyncStorageRequestDelegate {
protected String infoURL;
protected String credentials;
// Fetched objects.
protected SyncStorageResponse response;
private ExtendedJSONObject record;
// Fields.
// Rather than storing decimal/double timestamps, as provided by the
// server, we convert immediately to milliseconds since epoch.
private HashMap<String, Long> timestamps;
public HashMap<String, Long> getTimestamps() {
if (!this.wasSuccessful()) {
if (this.timestamps == null) {
throw new IllegalStateException("No record fetched.");
}
return this.timestamps;
@ -76,9 +39,31 @@ public class InfoCollections implements SyncStorageRequestDelegate {
return this.getTimestamps().get(collection);
}
public boolean wasSuccessful() {
return this.response.wasSuccessful() &&
this.timestamps != null;
/**
* Test if a given collection needs to be updated.
*
* @param collection
* The collection to test.
* @param lastModified
* Timestamp when local record was last modified.
*/
public boolean updateNeeded(String collection, long lastModified) {
Logger.trace(LOG_TAG, "Testing " + collection + " for updateNeeded. Local last modified is " + lastModified + ".");
// No local record of modification time? Need an update.
if (lastModified <= 0) {
return true;
}
// No meta/global on the server? We need an update. The server fetch will fail and
// then we will upload a fresh meta/global.
Long serverLastModified = getTimestamp(collection);
if (serverLastModified == null) {
return true;
}
// Otherwise, we need an update if our modification time is stale.
return (serverLastModified.longValue() > lastModified);
}
// Temporary location to store our callback.
@ -90,7 +75,7 @@ public class InfoCollections implements SyncStorageRequestDelegate {
}
public void fetch(InfoCollectionsDelegate callback) {
if (this.response == null) {
if (this.timestamps == null) {
this.callback = callback;
this.doFetch();
return;
@ -118,29 +103,12 @@ public class InfoCollections implements SyncStorageRequestDelegate {
}
}
public SyncStorageResponse getResponse() {
return this.response;
}
protected ExtendedJSONObject ensureRecord() {
if (record == null) {
record = new ExtendedJSONObject();
}
return record;
}
protected void setRecord(ExtendedJSONObject record) {
this.record = record;
}
@SuppressWarnings("unchecked")
private void unpack(SyncStorageResponse response) throws IllegalStateException, IOException, ParseException, NonObjectJSONException {
this.response = response;
this.setRecord(response.jsonObjectBody());
Log.i(LOG_TAG, "info/collections is " + this.record.toJSONString());
public void setFromRecord(ExtendedJSONObject record) throws IllegalStateException, IOException, ParseException, NonObjectJSONException {
Log.i(LOG_TAG, "info/collections is " + record.toJSONString());
HashMap<String, Long> map = new HashMap<String, Long>();
Set<Entry<String, Object>> entrySet = this.record.object.entrySet();
Set<Entry<String, Object>> entrySet = record.object.entrySet();
for (Entry<String, Object> entry : entrySet) {
// These objects are most likely going to be Doubles. Regardless, we
// want to get them in a more sane time format.
@ -175,7 +143,7 @@ public class InfoCollections implements SyncStorageRequestDelegate {
public void handleRequestSuccess(SyncStorageResponse response) {
if (response.wasSuccessful()) {
try {
this.unpack(response);
this.setFromRecord(response.jsonObjectBody());
this.callback.handleSuccess(this);
this.callback = null;
} catch (Exception e) {

View File

@ -1,39 +1,6 @@
/* ***** BEGIN LICENSE BLOCK *****
* Version: MPL 1.1/GPL 2.0/LGPL 2.1
*
* The contents of this file are subject to the Mozilla Public License Version
* 1.1 (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.mozilla.org/MPL/
*
* Software distributed under the License is distributed on an "AS IS" basis,
* WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
* for the specific language governing rights and limitations under the
* License.
*
* The Original Code is Android Sync Client.
*
* The Initial Developer of the Original Code is
* the Mozilla Foundation.
* Portions created by the Initial Developer are Copyright (C) 2011
* the Initial Developer. All Rights Reserved.
*
* Contributor(s):
* Richard Newman <rnewman@mozilla.com>
*
* Alternatively, the contents of this file may be used under the terms of
* either the GNU General Public License Version 2 or later (the "GPL"), or
* the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
* in which case the provisions of the GPL or the LGPL are applicable instead
* of those above. If you wish to allow use of your version of this file only
* under the terms of either the GPL or the LGPL, and not to allow others to
* use your version of this file under the terms of the MPL, indicate your
* decision by deleting the provisions above and replace them with the notice
* and other provisions required by the GPL or the LGPL. If you do not delete
* the provisions above, a recipient may use your version of this file under
* the terms of any one of the MPL, the GPL or the LGPL.
*
* ***** END LICENSE BLOCK ***** */
/* 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;
@ -53,12 +20,6 @@ public class MetaGlobal implements SyncStorageRequestDelegate {
protected String metaURL;
protected String credentials;
public boolean isModified;
protected boolean isNew;
// Fetched object.
private CryptoRecord record;
// Fields.
protected ExtendedJSONObject engines;
protected Long storageVersion;
@ -104,51 +65,52 @@ public class MetaGlobal implements SyncStorageRequestDelegate {
}
}
private CryptoRecord ensureRecord() {
if (this.record == null) {
this.record = new CryptoRecord(new ExtendedJSONObject());
}
return this.record;
protected ExtendedJSONObject asRecordContents() {
ExtendedJSONObject json = new ExtendedJSONObject();
json.put("storageVersion", storageVersion);
json.put("engines", engines);
json.put("syncID", syncID);
return json;
}
protected void setRecord(ExtendedJSONObject obj) throws IOException, ParseException, NonObjectJSONException {
this.record = CryptoRecord.fromJSONRecord(obj);
public CryptoRecord asCryptoRecord() {
ExtendedJSONObject payload = this.asRecordContents();
CryptoRecord record = new CryptoRecord(payload);
record.collection = "meta";
record.guid = "global";
record.deleted = false;
return record;
}
private void unpack(SyncStorageResponse response) throws IllegalStateException, IOException, ParseException, NonObjectJSONException {
this.setRecord(response.jsonObjectBody());
public void setFromRecord(CryptoRecord record) throws IllegalStateException, IOException, ParseException, NonObjectJSONException {
Log.i(LOG_TAG, "meta/global is " + record.payload.toJSONString());
this.isModified = false;
this.storageVersion = (Long) record.payload.get("storageVersion");
this.engines = record.payload.getObject("engines");
this.engines = record.payload.getObject("engines");
this.syncID = (String) record.payload.get("syncID");
}
public Long getStorageVersion() {
return this.storageVersion;
}
public void setStorageVersion(Long version) {
this.storageVersion = version;
this.ensureRecord().payload.put("storageVersion", version);
this.isModified = true;
}
public ExtendedJSONObject getEngines() {
return engines;
}
public void setEngines(ExtendedJSONObject engines) {
this.engines = engines;
this.ensureRecord().payload.put("engines", engines);
this.isModified = true;
}
public String getSyncID() {
return syncID;
}
public void setSyncID(String syncID) {
this.syncID = syncID;
this.ensureRecord().payload.put("syncID", syncID);
this.isModified = true;
}
// SyncStorageRequestDelegate methods for fetching.
@ -169,7 +131,6 @@ public class MetaGlobal implements SyncStorageRequestDelegate {
}
private void handleUploadSuccess(SyncStorageResponse response) {
this.isModified = false;
this.callback.handleSuccess(this, response);
this.callback = null;
}
@ -177,7 +138,8 @@ public class MetaGlobal implements SyncStorageRequestDelegate {
private void handleDownloadSuccess(SyncStorageResponse response) {
if (response.wasSuccessful()) {
try {
this.unpack(response);
CryptoRecord record = CryptoRecord.fromJSONRecord(response.jsonObjectBody());
this.setFromRecord(record);
this.callback.handleSuccess(this, response);
this.callback = null;
} catch (Exception e) {

View File

@ -0,0 +1,75 @@
/* 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;
import org.mozilla.gecko.sync.CryptoRecord;
import org.mozilla.gecko.sync.Logger;
import android.content.SharedPreferences;
public class PersistedMetaGlobal {
public static final String LOG_TAG = "PersistedMetaGlobal";
public static final String META_GLOBAL_SERVER_RESPONSE_BODY = "metaGlobalServerResponseBody";
public static final String META_GLOBAL_LAST_MODIFIED = "metaGlobalLastModified";
protected SharedPreferences prefs;
public PersistedMetaGlobal(SharedPreferences prefs) {
this.prefs = prefs;
}
public MetaGlobal metaGlobal() {
String json = prefs.getString(META_GLOBAL_SERVER_RESPONSE_BODY, null);
if (json == null) {
return null;
}
MetaGlobal metaGlobal = null;
try {
CryptoRecord cryptoRecord = CryptoRecord.fromJSONRecord(json);
MetaGlobal mg = new MetaGlobal(null, null);
mg.setFromRecord(cryptoRecord);
metaGlobal = mg;
} catch (Exception e) {
Logger.warn(LOG_TAG, "Got exception decrypting persisted meta/global.", e);
}
return metaGlobal;
}
public void persistMetaGlobal(MetaGlobal metaGlobal) {
if (metaGlobal == null) {
Logger.debug(LOG_TAG, "Clearing persisted meta/global.");
prefs.edit().remove(META_GLOBAL_SERVER_RESPONSE_BODY).commit();
return;
}
try {
CryptoRecord cryptoRecord = metaGlobal.asCryptoRecord();
String json = cryptoRecord.toJSONString();
Logger.debug(LOG_TAG, "Persisting meta/global.");
prefs.edit().putString(META_GLOBAL_SERVER_RESPONSE_BODY, json).commit();
} catch (Exception e) {
Logger.warn(LOG_TAG, "Got exception encrypting while persisting meta/global.", e);
}
}
public long lastModified() {
return prefs.getLong(META_GLOBAL_LAST_MODIFIED, -1);
}
public void persistLastModified(long lastModified) {
if (lastModified <= 0) {
Logger.debug(LOG_TAG, "Clearing persisted meta/global last modified timestamp.");
prefs.edit().remove(META_GLOBAL_LAST_MODIFIED).commit();
return;
}
Logger.debug(LOG_TAG, "Persisting meta/global last modified timestamp " + lastModified + ".");
prefs.edit().putLong(META_GLOBAL_LAST_MODIFIED, lastModified).commit();
}
public void purge() {
persistLastModified(-1);
persistMetaGlobal(null);
}
}

View File

@ -1,43 +1,9 @@
/* ***** BEGIN LICENSE BLOCK *****
* Version: MPL 1.1/GPL 2.0/LGPL 2.1
*
* The contents of this file are subject to the Mozilla Public License Version
* 1.1 (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.mozilla.org/MPL/
*
* Software distributed under the License is distributed on an "AS IS" basis,
* WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
* for the specific language governing rights and limitations under the
* License.
*
* The Original Code is Android Sync Client.
*
* The Initial Developer of the Original Code is
* the Mozilla Foundation.
* Portions created by the Initial Developer are Copyright (C) 2011
* the Initial Developer. All Rights Reserved.
*
* Contributor(s):
* Richard Newman <rnewman@mozilla.com>
*
* Alternatively, the contents of this file may be used under the terms of
* either the GNU General Public License Version 2 or later (the "GPL"), or
* the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
* in which case the provisions of the GPL or the LGPL are applicable instead
* of those above. If you wish to allow use of your version of this file only
* under the terms of either the GPL or the LGPL, and not to allow others to
* use your version of this file under the terms of the MPL, indicate your
* decision by deleting the provisions above and replace them with the notice
* and other provisions required by the GPL or the LGPL. If you do not delete
* the provisions above, a recipient may use your version of this file under
* the terms of any one of the MPL, the GPL or the LGPL.
*
* ***** END LICENSE BLOCK ***** */
/* 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;
import android.content.Context;
import android.content.SharedPreferences;
/**
@ -51,8 +17,6 @@ import android.content.SharedPreferences;
*
*/
public interface PrefsSource {
public Context getContext();
/**
* Return a SharedPreferences instance.
* @param name

View File

@ -10,6 +10,7 @@ import java.util.Map;
import java.util.Set;
import org.mozilla.gecko.sync.crypto.KeyBundle;
import org.mozilla.gecko.sync.crypto.PersistedCrypto5Keys;
import android.content.SharedPreferences;
import android.content.SharedPreferences.Editor;
@ -232,7 +233,9 @@ public class SyncConfiguration implements CredentialsSource {
syncID = prefs.getString("syncID", null);
Logger.info(LOG_TAG, "Set syncID from bundle: " + syncID);
}
// TODO: MetaGlobal, password, infoCollections, collectionKeys.
// We don't set crypto/keys here because we need the syncKeyBundle to decrypt the JSON
// and we won't have it on construction.
// TODO: MetaGlobal, password, infoCollections.
}
public void persistToPrefs() {
@ -258,16 +261,10 @@ public class SyncConfiguration implements CredentialsSource {
return username + ":" + password;
}
@Override
public CollectionKeys getCollectionKeys() {
return collectionKeys;
}
@Override
public KeyBundle keyForCollection(String collection) throws NoCollectionKeysSetException {
return getCollectionKeys().keyBundleForCollection(collection);
}
public void setCollectionKeys(CollectionKeys k) {
collectionKeys = k;
}
@ -379,4 +376,19 @@ public class SyncConfiguration implements CredentialsSource {
public long getPersistedServerClientRecordTimestamp() {
return getPrefs().getLong(SyncConfiguration.CLIENT_RECORD_TIMESTAMP, 0);
}
public void purgeCryptoKeys() {
if (collectionKeys != null) {
collectionKeys.clear();
}
persistedCryptoKeys().purge();
}
public PersistedCrypto5Keys persistedCryptoKeys() {
return new PersistedCrypto5Keys(getPrefs(), syncKeyBundle);
}
public PersistedMetaGlobal persistedMetaGlobal() {
return new PersistedMetaGlobal(getPrefs());
}
}

View File

@ -0,0 +1,99 @@
/* 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.crypto;
import org.mozilla.gecko.sync.CollectionKeys;
import org.mozilla.gecko.sync.CryptoRecord;
import org.mozilla.gecko.sync.Logger;
import android.content.SharedPreferences;
public class PersistedCrypto5Keys {
public static final String LOG_TAG = "PersistedC5Keys";
public static final String CRYPTO5_KEYS_SERVER_RESPONSE_BODY = "crypto5KeysServerResponseBody";
public static final String CRYPTO5_KEYS_LAST_MODIFIED = "crypto5KeysLastModified";
protected SharedPreferences prefs;
protected KeyBundle syncKeyBundle;
public PersistedCrypto5Keys(SharedPreferences prefs, KeyBundle syncKeyBundle) {
if (syncKeyBundle == null) {
throw new IllegalArgumentException("Null syncKeyBundle passed in to PersistedCrypto5Keys constructor.");
}
this.prefs = prefs;
this.syncKeyBundle = syncKeyBundle;
}
/**
* Get persisted crypto/keys.
* <p>
* crypto/keys is fetched from an encrypted JSON-encoded <code>CryptoRecord</code>.
*
* @return A <code>CollectionKeys</code> instance or <code>null</code> if none
* is currently persisted.
*/
public CollectionKeys keys() {
String keysJSON = prefs.getString(CRYPTO5_KEYS_SERVER_RESPONSE_BODY, null);
if (keysJSON == null) {
return null;
}
try {
CryptoRecord cryptoRecord = CryptoRecord.fromJSONRecord(keysJSON);
CollectionKeys keys = new CollectionKeys();
keys.setKeyPairsFromWBO(cryptoRecord, syncKeyBundle);
return keys;
} catch (Exception e) {
Logger.warn(LOG_TAG, "Got exception decrypting persisted crypto/keys.", e);
return null;
}
}
/**
* Persist crypto/keys.
* <p>
* crypto/keys is stored as an encrypted JSON-encoded <code>CryptoRecord</code>.
*
* @param keys
* The <code>CollectionKeys</code> object to persist, which should
* have the same default key bundle as the sync key bundle.
*/
public void persistKeys(CollectionKeys keys) {
if (keys == null) {
Logger.debug(LOG_TAG, "Clearing persisted crypto/keys.");
prefs.edit().remove(CRYPTO5_KEYS_SERVER_RESPONSE_BODY).commit();
return;
}
try {
CryptoRecord cryptoRecord = keys.asCryptoRecord();
cryptoRecord.keyBundle = syncKeyBundle;
cryptoRecord.encrypt();
String keysJSON = cryptoRecord.toJSONString();
Logger.debug(LOG_TAG, "Persisting crypto/keys.");
prefs.edit().putString(CRYPTO5_KEYS_SERVER_RESPONSE_BODY, keysJSON).commit();
} catch (Exception e) {
Logger.warn(LOG_TAG, "Got exception encrypting while persisting crypto/keys.", e);
}
}
public long lastModified() {
return prefs.getLong(CRYPTO5_KEYS_LAST_MODIFIED, -1);
}
public void persistLastModified(long lastModified) {
if (lastModified <= 0) {
Logger.debug(LOG_TAG, "Clearing persisted crypto/keys last modified timestamp.");
prefs.edit().remove(CRYPTO5_KEYS_LAST_MODIFIED).commit();
return;
}
Logger.debug(LOG_TAG, "Persisting crypto/keys last modified timestamp " + lastModified + ".");
prefs.edit().putLong(CRYPTO5_KEYS_LAST_MODIFIED, lastModified).commit();
}
public void purge() {
persistLastModified(-1);
persistKeys(null);
}
}

View File

@ -1,39 +1,6 @@
/* ***** BEGIN LICENSE BLOCK *****
* Version: MPL 1.1/GPL 2.0/LGPL 2.1
*
* The contents of this file are subject to the Mozilla Public License Version
* 1.1 (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.mozilla.org/MPL/
*
* Software distributed under the License is distributed on an "AS IS" basis,
* WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
* for the specific language governing rights and limitations under the
* License.
*
* The Original Code is Android Sync Client.
*
* The Initial Developer of the Original Code is
* the Mozilla Foundation.
* Portions created by the Initial Developer are Copyright (C) 2011
* the Initial Developer. All Rights Reserved.
*
* Contributor(s):
* Richard Newman <rnewman@mozilla.com>
*
* Alternatively, the contents of this file may be used under the terms of
* either the GNU General Public License Version 2 or later (the "GPL"), or
* the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
* in which case the provisions of the GPL or the LGPL are applicable instead
* of those above. If you wish to allow use of your version of this file only
* under the terms of either the GPL or the LGPL, and not to allow others to
* use your version of this file under the terms of the MPL, indicate your
* decision by deleting the provisions above and replace them with the notice
* and other provisions required by the GPL or the LGPL. If you do not delete
* the provisions above, a recipient may use your version of this file under
* the terms of any one of the MPL, the GPL or the LGPL.
*
* ***** END LICENSE BLOCK ***** */
/* 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.delegates;
@ -45,5 +12,4 @@ public interface MetaGlobalDelegate {
public void handleMissing(MetaGlobal global, SyncStorageResponse response);
public void handleFailure(SyncStorageResponse response);
public void handleError(Exception e);
public MetaGlobalDelegate deferred();
}

View File

@ -0,0 +1,175 @@
/* 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.io.UnsupportedEncodingException;
import java.net.URISyntaxException;
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.NonObjectJSONException;
import org.mozilla.gecko.sync.crypto.CryptoException;
import org.mozilla.gecko.sync.crypto.PersistedCrypto5Keys;
import org.mozilla.gecko.sync.delegates.KeyUploadDelegate;
import org.mozilla.gecko.sync.net.SyncStorageRecordRequest;
import org.mozilla.gecko.sync.net.SyncStorageRequestDelegate;
import org.mozilla.gecko.sync.net.SyncStorageResponse;
public class EnsureCrypto5KeysStage implements GlobalSyncStage, SyncStorageRequestDelegate, KeyUploadDelegate {
private static final String LOG_TAG = "EnsureC5KeysStage";
private static final String CRYPTO_COLLECTION = "crypto";
protected GlobalSession session;
protected boolean retrying = false;
@Override
public void execute(GlobalSession session) throws NoSuchStageException {
this.session = session;
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 (!infoCollections.updateNeeded(CRYPTO_COLLECTION, lastModified)) {
// Try to use our local collection keys for this session.
Logger.info(LOG_TAG, "Trying to use persisted collection keys for this session.");
CollectionKeys keys = pck.keys();
if (keys != null) {
Logger.info(LOG_TAG, "Using persisted collection keys for this session.");
session.config.setCollectionKeys(keys);
session.advance();
return;
}
Logger.info(LOG_TAG, "Failed to use persisted collection keys for this session.");
}
// We need an update: fetch or upload keys as necessary.
Logger.info(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;
}
@Override
public void handleRequestSuccess(SyncStorageResponse response) {
CollectionKeys k = new CollectionKeys();
try {
ExtendedJSONObject body = response.jsonObjectBody();
if (Logger.LOG_PERSONAL_INFORMATION) {
Logger.pii(LOG_TAG, "Fetched keys: " + body.toJSONString());
}
k.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;
}
// New keys! Persist keys and server timestamp.
Logger.info(LOG_TAG, "Setting fetched keys for this session.");
session.config.setCollectionKeys(k);
Logger.trace(LOG_TAG, "Persisting fetched keys and last modified.");
PersistedCrypto5Keys pck = session.config.persistedCryptoKeys();
pck.persistKeys(k);
// Take the timestamp from the response since it is later than the timestamp from info/collections.
pck.persistLastModified(response.normalizedWeaveTimestamp());
session.advance();
}
@Override
public void handleRequestFailure(SyncStorageResponse response) {
if (retrying) {
session.handleHTTPError(response, "Failure in refetching uploaded keys.");
return;
}
int statusCode = response.getStatusCode();
Logger.debug(LOG_TAG, "Got " + statusCode + " fetching keys.");
if (statusCode == 404) {
// No keys. Generate and upload, then refetch.
CryptoRecord keysWBO;
try {
keysWBO = CollectionKeys.generateCollectionKeysRecord();
} catch (CryptoException e) {
session.abort(e, "Couldn't generate new key bundle.");
return;
}
keysWBO.keyBundle = session.config.syncKeyBundle;
try {
keysWBO.encrypt();
} catch (UnsupportedEncodingException e) {
// Shouldn't occur, so let's not waste too much time on niceties. TODO
session.abort(e, "Couldn't encrypt new key bundle: unsupported encoding.");
return;
} catch (CryptoException e) {
session.abort(e, "Couldn't encrypt new key bundle.");
return;
}
session.uploadKeys(keysWBO, this);
return;
}
session.handleHTTPError(response, "Failure fetching keys.");
}
@Override
public void handleRequestError(Exception ex) {
session.abort(ex, "Failure fetching keys.");
}
@Override
public void onKeysUploaded() {
Logger.debug(LOG_TAG, "New keys uploaded. Starting stage again to fetch them.");
try {
retrying = true;
this.execute(this.session);
} catch (NoSuchStageException e) {
session.abort(e, "No such stage.");
}
}
@Override
public void onKeyUploadFailed(Exception e) {
Logger.warn(LOG_TAG, "Key upload failed. Aborting sync.");
session.abort(e, "Key upload failed.");
}
}

View File

@ -1,50 +1,20 @@
/* ***** BEGIN LICENSE BLOCK *****
* Version: MPL 1.1/GPL 2.0/LGPL 2.1
*
* The contents of this file are subject to the Mozilla Public License Version
* 1.1 (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.mozilla.org/MPL/
*
* Software distributed under the License is distributed on an "AS IS" basis,
* WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
* for the specific language governing rights and limitations under the
* License.
*
* The Original Code is Android Sync Client.
*
* The Initial Developer of the Original Code is
* the Mozilla Foundation.
* Portions created by the Initial Developer are Copyright (C) 2011
* the Initial Developer. All Rights Reserved.
*
* Contributor(s):
* Richard Newman <rnewman@mozilla.com>
*
* Alternatively, the contents of this file may be used under the terms of
* either the GNU General Public License Version 2 or later (the "GPL"), or
* the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
* in which case the provisions of the GPL or the LGPL are applicable instead
* of those above. If you wish to allow use of your version of this file only
* under the terms of either the GPL or the LGPL, and not to allow others to
* use your version of this file under the terms of the MPL, indicate your
* decision by deleting the provisions above and replace them with the notice
* and other provisions required by the GPL or the LGPL. If you do not delete
* the provisions above, a recipient may use your version of this file under
* the terms of any one of the MPL, the GPL or the LGPL.
*
* ***** END LICENSE BLOCK ***** */
/* 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.net.URISyntaxException;
import org.mozilla.gecko.sync.GlobalSession;
import org.mozilla.gecko.sync.InfoCollections;
import org.mozilla.gecko.sync.Logger;
import org.mozilla.gecko.sync.MetaGlobal;
import org.mozilla.gecko.sync.PersistedMetaGlobal;
import org.mozilla.gecko.sync.delegates.MetaGlobalDelegate;
import org.mozilla.gecko.sync.net.SyncStorageResponse;
public class FetchMetaGlobalStage implements GlobalSyncStage {
private static final String LOG_TAG = "FetchMetaGlobalStage";
private static final String META_COLLECTION = "meta";
public class StageMetaGlobalDelegate implements MetaGlobalDelegate {
@ -55,6 +25,12 @@ public class FetchMetaGlobalStage implements GlobalSyncStage {
@Override
public void handleSuccess(MetaGlobal global, SyncStorageResponse response) {
Logger.trace(LOG_TAG, "Persisting fetched meta/global and last modified.");
PersistedMetaGlobal pmg = session.config.persistedMetaGlobal();
pmg.persistMetaGlobal(global);
// Take the timestamp from the response since it is later than the timestamp from info/collections.
pmg.persistLastModified(response.normalizedWeaveTimestamp());
session.processMetaGlobal(global);
}
@ -72,21 +48,32 @@ public class FetchMetaGlobalStage implements GlobalSyncStage {
public void handleMissing(MetaGlobal global, SyncStorageResponse response) {
session.processMissingMetaGlobal(global);
}
@Override
public MetaGlobalDelegate deferred() {
// TODO: defer!
return this;
}
}
@Override
public void execute(GlobalSession session) throws NoSuchStageException {
try {
session.fetchMetaGlobal(new StageMetaGlobalDelegate(session));
} catch (URISyntaxException e) {
session.abort(e, "Invalid URI.");
InfoCollections infoCollections = session.config.infoCollections;
if (infoCollections == null) {
session.abort(null, "No info/collections set in FetchMetaGlobalStage.");
return;
}
}
long lastModified = session.config.persistedMetaGlobal().lastModified();
if (!infoCollections.updateNeeded(META_COLLECTION, lastModified)) {
// Try to use our local collection keys for this session.
Logger.info(LOG_TAG, "Trying to use persisted meta/global for this session.");
MetaGlobal global = session.config.persistedMetaGlobal().metaGlobal();
if (global != null) {
Logger.info(LOG_TAG, "Using persisted meta/global for this session.");
session.processMetaGlobal(global); // Calls session.advance().
return;
}
Logger.info(LOG_TAG, "Failed to use persisted meta/global for this session.");
}
// We need an update: fetch or upload meta/global as necessary.
Logger.info(LOG_TAG, "Fetching fresh meta/global for this session.");
MetaGlobal global = new MetaGlobal(session.config.metaURL(), session.credentials());
global.fetch(new StageMetaGlobalDelegate(session));
}
}

View File

@ -67,7 +67,7 @@ public abstract class ServerSyncStage implements
*/
protected Repository wrappedServerRepo() throws NoCollectionKeysSetException, URISyntaxException {
String collection = this.getCollection();
KeyBundle collectionKey = session.keyForCollection(collection);
KeyBundle collectionKey = session.keyBundleForCollection(collection);
Crypto5MiddlewareRepository cryptoRepo = new Crypto5MiddlewareRepository(getRemoteRepository(), collectionKey);
cryptoRepo.recordFactory = getRecordFactory();
return cryptoRepo;

View File

@ -162,7 +162,7 @@ public class SyncClientsEngineStage implements GlobalSyncStage {
@Override
public KeyBundle keyBundle() {
try {
return session.keyForCollection(COLLECTION_NAME);
return session.keyBundleForCollection(COLLECTION_NAME);
} catch (NoCollectionKeysSetException e) {
session.abort(e, "No collection keys set.");
return null;
@ -241,7 +241,7 @@ public class SyncClientsEngineStage implements GlobalSyncStage {
@Override
public KeyBundle keyBundle() {
try {
return session.keyForCollection(COLLECTION_NAME);
return session.keyBundleForCollection(COLLECTION_NAME);
} catch (NoCollectionKeysSetException e) {
session.abort(e, "No collection keys set.");
return null;

File diff suppressed because one or more lines are too long