forked from mirrors/gecko-dev
MozReview-Commit-ID: EjyAssqiQk8 --HG-- extra : rebase_source : d783829bc7fced3044d0d076c4786a6957d29bb6
273 lines
8.8 KiB
JavaScript
273 lines
8.8 KiB
JavaScript
/* 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/. */
|
|
|
|
"use strict";
|
|
|
|
this.EXPORTED_SYMBOLS = ['ExtensionStorageEngine', 'EncryptionRemoteTransformer',
|
|
'KeyRingEncryptionRemoteTransformer'];
|
|
|
|
const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
|
|
|
|
Cu.import("resource://services-crypto/utils.js");
|
|
Cu.import("resource://services-sync/constants.js");
|
|
Cu.import("resource://services-sync/engines.js");
|
|
Cu.import("resource://services-sync/keys.js");
|
|
Cu.import("resource://services-sync/util.js");
|
|
Cu.import("resource://services-common/async.js");
|
|
XPCOMUtils.defineLazyModuleGetter(this, "ExtensionStorageSync",
|
|
"resource://gre/modules/ExtensionStorageSync.jsm");
|
|
XPCOMUtils.defineLazyModuleGetter(this, "fxAccounts",
|
|
"resource://gre/modules/FxAccounts.jsm");
|
|
XPCOMUtils.defineLazyModuleGetter(this, "Task",
|
|
"resource://gre/modules/Task.jsm");
|
|
|
|
/**
|
|
* The Engine that manages syncing for the web extension "storage"
|
|
* API, and in particular ext.storage.sync.
|
|
*
|
|
* ext.storage.sync is implemented using Kinto, so it has mechanisms
|
|
* for syncing that we do not need to integrate in the Firefox Sync
|
|
* framework, so this is something of a stub.
|
|
*/
|
|
this.ExtensionStorageEngine = function ExtensionStorageEngine(service) {
|
|
SyncEngine.call(this, "Extension-Storage", service);
|
|
};
|
|
ExtensionStorageEngine.prototype = {
|
|
__proto__: SyncEngine.prototype,
|
|
_trackerObj: ExtensionStorageTracker,
|
|
// we don't need these since we implement our own sync logic
|
|
_storeObj: undefined,
|
|
_recordObj: undefined,
|
|
|
|
syncPriority: 10,
|
|
allowSkippedRecord: false,
|
|
|
|
_sync() {
|
|
return Async.promiseSpinningly(ExtensionStorageSync.syncAll());
|
|
},
|
|
|
|
get enabled() {
|
|
// By default, we sync extension storage if we sync addons. This
|
|
// lets us simplify the UX since users probably don't consider
|
|
// "extension preferences" a separate category of syncing.
|
|
// However, we also respect engine.extension-storage.force, which
|
|
// can be set to true or false, if a power user wants to customize
|
|
// the behavior despite the lack of UI.
|
|
const forced = Svc.Prefs.get("engine." + this.prefName + ".force", undefined);
|
|
if (forced !== undefined) {
|
|
return forced;
|
|
}
|
|
return Svc.Prefs.get("engine.addons", false);
|
|
},
|
|
};
|
|
|
|
function ExtensionStorageTracker(name, engine) {
|
|
Tracker.call(this, name, engine);
|
|
}
|
|
ExtensionStorageTracker.prototype = {
|
|
__proto__: Tracker.prototype,
|
|
|
|
startTracking() {
|
|
Svc.Obs.add("ext.storage.sync-changed", this);
|
|
},
|
|
|
|
stopTracking() {
|
|
Svc.Obs.remove("ext.storage.sync-changed", this);
|
|
},
|
|
|
|
observe(subject, topic, data) {
|
|
Tracker.prototype.observe.call(this, subject, topic, data);
|
|
|
|
if (this.ignoreAll) {
|
|
return;
|
|
}
|
|
|
|
if (topic !== "ext.storage.sync-changed") {
|
|
return;
|
|
}
|
|
|
|
// Single adds, removes and changes are not so important on their
|
|
// own, so let's just increment score a bit.
|
|
this.score += SCORE_INCREMENT_MEDIUM;
|
|
},
|
|
|
|
// Override a bunch of methods which don't do anything for us.
|
|
// This is a performance hack.
|
|
ignoreID() {
|
|
},
|
|
unignoreID() {
|
|
},
|
|
addChangedID() {
|
|
},
|
|
removeChangedID() {
|
|
},
|
|
clearChangedIDs() {
|
|
},
|
|
};
|
|
|
|
/**
|
|
* Utility function to enforce an order of fields when computing an HMAC.
|
|
*/
|
|
function ciphertextHMAC(keyBundle, id, IV, ciphertext) {
|
|
const hasher = keyBundle.sha256HMACHasher;
|
|
return Utils.bytesAsHex(Utils.digestUTF8(id + IV + ciphertext, hasher));
|
|
}
|
|
|
|
/**
|
|
* A "remote transformer" that the Kinto library will use to
|
|
* encrypt/decrypt records when syncing.
|
|
*
|
|
* This is an "abstract base class". Subclass this and override
|
|
* getKeys() to use it.
|
|
*/
|
|
class EncryptionRemoteTransformer {
|
|
encode(record) {
|
|
const self = this;
|
|
return Task.spawn(function* () {
|
|
const keyBundle = yield self.getKeys();
|
|
if (record.ciphertext) {
|
|
throw new Error("Attempt to reencrypt??");
|
|
}
|
|
let id = record.id;
|
|
if (!record.id) {
|
|
throw new Error("Record ID is missing or invalid");
|
|
}
|
|
|
|
let IV = Svc.Crypto.generateRandomIV();
|
|
let ciphertext = Svc.Crypto.encrypt(JSON.stringify(record),
|
|
keyBundle.encryptionKeyB64, IV);
|
|
let hmac = ciphertextHMAC(keyBundle, id, IV, ciphertext);
|
|
const encryptedResult = {ciphertext, IV, hmac, id};
|
|
if (record.hasOwnProperty("last_modified")) {
|
|
encryptedResult.last_modified = record.last_modified;
|
|
}
|
|
return encryptedResult;
|
|
});
|
|
}
|
|
|
|
decode(record) {
|
|
const self = this;
|
|
return Task.spawn(function* () {
|
|
if (!record.ciphertext) {
|
|
// This can happen for tombstones if a record is deleted.
|
|
if (record.deleted) {
|
|
return record;
|
|
}
|
|
throw new Error("No ciphertext: nothing to decrypt?");
|
|
}
|
|
const keyBundle = yield self.getKeys();
|
|
// Authenticate the encrypted blob with the expected HMAC
|
|
let computedHMAC = ciphertextHMAC(keyBundle, record.id, record.IV, record.ciphertext);
|
|
|
|
if (computedHMAC != record.hmac) {
|
|
Utils.throwHMACMismatch(record.hmac, computedHMAC);
|
|
}
|
|
|
|
// Handle invalid data here. Elsewhere we assume that cleartext is an object.
|
|
let cleartext = Svc.Crypto.decrypt(record.ciphertext,
|
|
keyBundle.encryptionKeyB64, record.IV);
|
|
let jsonResult = JSON.parse(cleartext);
|
|
if (!jsonResult || typeof jsonResult !== "object") {
|
|
throw new Error("Decryption failed: result is <" + jsonResult + ">, not an object.");
|
|
}
|
|
|
|
// Verify that the encrypted id matches the requested record's id.
|
|
// This should always be true, because we compute the HMAC over
|
|
// the original record's ID, and that was verified already (above).
|
|
if (jsonResult.id != record.id) {
|
|
throw new Error("Record id mismatch: " + jsonResult.id + " != " + record.id);
|
|
}
|
|
|
|
if (record.hasOwnProperty("last_modified")) {
|
|
jsonResult.last_modified = record.last_modified;
|
|
}
|
|
|
|
return jsonResult;
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Retrieve keys to use during encryption.
|
|
*
|
|
* Returns a Promise<KeyBundle>.
|
|
*/
|
|
getKeys() {
|
|
throw new Error("override getKeys in a subclass");
|
|
}
|
|
}
|
|
// You can inject this
|
|
EncryptionRemoteTransformer.prototype._fxaService = fxAccounts;
|
|
|
|
/**
|
|
* An EncryptionRemoteTransformer that provides a keybundle derived
|
|
* from the user's kB, suitable for encrypting a keyring.
|
|
*/
|
|
class KeyRingEncryptionRemoteTransformer extends EncryptionRemoteTransformer {
|
|
getKeys() {
|
|
const self = this;
|
|
return Task.spawn(function* () {
|
|
const user = yield self._fxaService.getSignedInUser();
|
|
// FIXME: we should permit this if the user is self-hosting
|
|
// their storage
|
|
if (!user) {
|
|
throw new Error("user isn't signed in to FxA; can't sync");
|
|
}
|
|
|
|
if (!user.kB) {
|
|
throw new Error("user doesn't have kB");
|
|
}
|
|
|
|
let kB = Utils.hexToBytes(user.kB);
|
|
|
|
let keyMaterial = CryptoUtils.hkdf(kB, undefined,
|
|
"identity.mozilla.com/picl/v1/chrome.storage.sync", 2 * 32);
|
|
let bundle = new BulkKeyBundle();
|
|
// [encryptionKey, hmacKey]
|
|
bundle.keyPair = [keyMaterial.slice(0, 32), keyMaterial.slice(32, 64)];
|
|
return bundle;
|
|
});
|
|
}
|
|
// Pass through the kbHash field from the unencrypted record. If
|
|
// encryption fails, we can use this to try to detect whether we are
|
|
// being compromised or if the record here was encoded with a
|
|
// different kB.
|
|
encode(record) {
|
|
const encodePromise = super.encode(record);
|
|
return Task.spawn(function* () {
|
|
const encoded = yield encodePromise;
|
|
encoded.kbHash = record.kbHash;
|
|
return encoded;
|
|
});
|
|
}
|
|
|
|
decode(record) {
|
|
const decodePromise = super.decode(record);
|
|
return Task.spawn(function* () {
|
|
try {
|
|
return yield decodePromise;
|
|
} catch (e) {
|
|
if (Utils.isHMACMismatch(e)) {
|
|
const currentKBHash = yield ExtensionStorageSync.getKBHash();
|
|
if (record.kbHash != currentKBHash) {
|
|
// Some other client encoded this with a kB that we don't
|
|
// have access to.
|
|
KeyRingEncryptionRemoteTransformer.throwOutdatedKB(currentKBHash, record.kbHash);
|
|
}
|
|
}
|
|
throw e;
|
|
}
|
|
});
|
|
}
|
|
|
|
// Generator and discriminator for KB-is-outdated exceptions.
|
|
static throwOutdatedKB(shouldBe, is) {
|
|
throw new Error(`kB hash on record is outdated: should be ${shouldBe}, is ${is}`);
|
|
}
|
|
|
|
static isOutdatedKB(exc) {
|
|
const kbMessage = "kB hash on record is outdated: ";
|
|
return exc && exc.message && exc.message.indexOf &&
|
|
(exc.message.indexOf(kbMessage) == 0);
|
|
}
|
|
}
|