diff -r 000000000000 -r 6474c204b198 services/sync/modules/util.js --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/services/sync/modules/util.js Wed Dec 31 06:09:35 2014 +0100 @@ -0,0 +1,679 @@ +/* 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/. */ + +this.EXPORTED_SYMBOLS = ["XPCOMUtils", "Services", "Utils", "Async", "Svc", "Str"]; + +const {classes: Cc, interfaces: Ci, results: Cr, utils: Cu} = Components; + +Cu.import("resource://gre/modules/Log.jsm"); +Cu.import("resource://services-common/observers.js"); +Cu.import("resource://services-common/stringbundle.js"); +Cu.import("resource://services-common/utils.js"); +Cu.import("resource://services-common/async.js", this); +Cu.import("resource://services-crypto/utils.js"); +Cu.import("resource://services-sync/constants.js"); +Cu.import("resource://gre/modules/Preferences.jsm"); +Cu.import("resource://gre/modules/Services.jsm", this); +Cu.import("resource://gre/modules/XPCOMUtils.jsm", this); +Cu.import("resource://gre/modules/osfile.jsm", this); +Cu.import("resource://gre/modules/Task.jsm", this); + +/* + * Utility functions + */ + +this.Utils = { + // Alias in functions from CommonUtils. These previously were defined here. + // In the ideal world, references to these would be removed. + nextTick: CommonUtils.nextTick, + namedTimer: CommonUtils.namedTimer, + exceptionStr: CommonUtils.exceptionStr, + stackTrace: CommonUtils.stackTrace, + makeURI: CommonUtils.makeURI, + encodeUTF8: CommonUtils.encodeUTF8, + decodeUTF8: CommonUtils.decodeUTF8, + safeAtoB: CommonUtils.safeAtoB, + byteArrayToString: CommonUtils.byteArrayToString, + bytesAsHex: CommonUtils.bytesAsHex, + hexToBytes: CommonUtils.hexToBytes, + encodeBase32: CommonUtils.encodeBase32, + decodeBase32: CommonUtils.decodeBase32, + + // Aliases from CryptoUtils. + generateRandomBytes: CryptoUtils.generateRandomBytes, + computeHTTPMACSHA1: CryptoUtils.computeHTTPMACSHA1, + digestUTF8: CryptoUtils.digestUTF8, + digestBytes: CryptoUtils.digestBytes, + sha1: CryptoUtils.sha1, + sha1Base32: CryptoUtils.sha1Base32, + makeHMACKey: CryptoUtils.makeHMACKey, + makeHMACHasher: CryptoUtils.makeHMACHasher, + hkdfExpand: CryptoUtils.hkdfExpand, + pbkdf2Generate: CryptoUtils.pbkdf2Generate, + deriveKeyFromPassphrase: CryptoUtils.deriveKeyFromPassphrase, + getHTTPMACSHA1Header: CryptoUtils.getHTTPMACSHA1Header, + + /** + * Wrap a function to catch all exceptions and log them + * + * @usage MyObj._catch = Utils.catch; + * MyObj.foo = function() { this._catch(func)(); } + * + * Optionally pass a function which will be called if an + * exception occurs. + */ + catch: function Utils_catch(func, exceptionCallback) { + let thisArg = this; + return function WrappedCatch() { + try { + return func.call(thisArg); + } + catch(ex) { + thisArg._log.debug("Exception: " + Utils.exceptionStr(ex)); + if (exceptionCallback) { + return exceptionCallback.call(thisArg, ex); + } + return null; + } + }; + }, + + /** + * Wrap a function to call lock before calling the function then unlock. + * + * @usage MyObj._lock = Utils.lock; + * MyObj.foo = function() { this._lock(func)(); } + */ + lock: function lock(label, func) { + let thisArg = this; + return function WrappedLock() { + if (!thisArg.lock()) { + throw "Could not acquire lock. Label: \"" + label + "\"."; + } + + try { + return func.call(thisArg); + } + finally { + thisArg.unlock(); + } + }; + }, + + isLockException: function isLockException(ex) { + return ex && ex.indexOf && ex.indexOf("Could not acquire lock.") == 0; + }, + + /** + * Wrap functions to notify when it starts and finishes executing or if it + * threw an error. + * + * The message is a combination of a provided prefix, the local name, and + * the event. Possible events are: "start", "finish", "error". The subject + * is the function's return value on "finish" or the caught exception on + * "error". The data argument is the predefined data value. + * + * Example: + * + * @usage function MyObj(name) { + * this.name = name; + * this._notify = Utils.notify("obj:"); + * } + * MyObj.prototype = { + * foo: function() this._notify("func", "data-arg", function () { + * //... + * }(), + * }; + */ + notify: function Utils_notify(prefix) { + return function NotifyMaker(name, data, func) { + let thisArg = this; + let notify = function(state, subject) { + let mesg = prefix + name + ":" + state; + thisArg._log.trace("Event: " + mesg); + Observers.notify(mesg, subject, data); + }; + + return function WrappedNotify() { + try { + notify("start", null); + let ret = func.call(thisArg); + notify("finish", ret); + return ret; + } + catch(ex) { + notify("error", ex); + throw ex; + } + }; + }; + }, + + runInTransaction: function(db, callback, thisObj) { + let hasTransaction = false; + try { + db.beginTransaction(); + hasTransaction = true; + } catch(e) { /* om nom nom exceptions */ } + + try { + return callback.call(thisObj); + } finally { + if (hasTransaction) { + db.commitTransaction(); + } + } + }, + + /** + * GUIDs are 9 random bytes encoded with base64url (RFC 4648). + * That makes them 12 characters long with 72 bits of entropy. + */ + makeGUID: function makeGUID() { + return CommonUtils.encodeBase64URL(Utils.generateRandomBytes(9)); + }, + + _base64url_regex: /^[-abcdefghijklmnopqrstuvwxyz0123456789_]{12}$/i, + checkGUID: function checkGUID(guid) { + return !!guid && this._base64url_regex.test(guid); + }, + + /** + * Add a simple getter/setter to an object that defers access of a property + * to an inner property. + * + * @param obj + * Object to add properties to defer in its prototype + * @param defer + * Property of obj to defer to + * @param prop + * Property name to defer (or an array of property names) + */ + deferGetSet: function Utils_deferGetSet(obj, defer, prop) { + if (Array.isArray(prop)) + return prop.map(function(prop) Utils.deferGetSet(obj, defer, prop)); + + let prot = obj.prototype; + + // Create a getter if it doesn't exist yet + if (!prot.__lookupGetter__(prop)) { + prot.__defineGetter__(prop, function () { + return this[defer][prop]; + }); + } + + // Create a setter if it doesn't exist yet + if (!prot.__lookupSetter__(prop)) { + prot.__defineSetter__(prop, function (val) { + this[defer][prop] = val; + }); + } + }, + + lazyStrings: function Weave_lazyStrings(name) { + let bundle = "chrome://weave/locale/services/" + name + ".properties"; + return function() new StringBundle(bundle); + }, + + deepEquals: function eq(a, b) { + // If they're triple equals, then it must be equals! + if (a === b) + return true; + + // If they weren't equal, they must be objects to be different + if (typeof a != "object" || typeof b != "object") + return false; + + // But null objects won't have properties to compare + if (a === null || b === null) + return false; + + // Make sure all of a's keys have a matching value in b + for (let k in a) + if (!eq(a[k], b[k])) + return false; + + // Do the same for b's keys but skip those that we already checked + for (let k in b) + if (!(k in a) && !eq(a[k], b[k])) + return false; + + return true; + }, + + // Generator and discriminator for HMAC exceptions. + // Split these out in case we want to make them richer in future, and to + // avoid inevitable confusion if the message changes. + throwHMACMismatch: function throwHMACMismatch(shouldBe, is) { + throw "Record SHA256 HMAC mismatch: should be " + shouldBe + ", is " + is; + }, + + isHMACMismatch: function isHMACMismatch(ex) { + const hmacFail = "Record SHA256 HMAC mismatch: "; + return ex && ex.indexOf && (ex.indexOf(hmacFail) == 0); + }, + + /** + * Turn RFC 4648 base32 into our own user-friendly version. + * ABCDEFGHIJKLMNOPQRSTUVWXYZ234567 + * becomes + * abcdefghijk8mn9pqrstuvwxyz234567 + */ + base32ToFriendly: function base32ToFriendly(input) { + return input.toLowerCase() + .replace("l", '8', "g") + .replace("o", '9', "g"); + }, + + base32FromFriendly: function base32FromFriendly(input) { + return input.toUpperCase() + .replace("8", 'L', "g") + .replace("9", 'O', "g"); + }, + + /** + * Key manipulation. + */ + + // Return an octet string in friendly base32 *with no trailing =*. + encodeKeyBase32: function encodeKeyBase32(keyData) { + return Utils.base32ToFriendly( + Utils.encodeBase32(keyData)) + .slice(0, SYNC_KEY_ENCODED_LENGTH); + }, + + decodeKeyBase32: function decodeKeyBase32(encoded) { + return Utils.decodeBase32( + Utils.base32FromFriendly( + Utils.normalizePassphrase(encoded))) + .slice(0, SYNC_KEY_DECODED_LENGTH); + }, + + base64Key: function base64Key(keyData) { + return btoa(keyData); + }, + + /** + * N.B., salt should be base64 encoded, even though we have to decode + * it later! + */ + derivePresentableKeyFromPassphrase : function derivePresentableKeyFromPassphrase(passphrase, salt, keyLength, forceJS) { + let k = CryptoUtils.deriveKeyFromPassphrase(passphrase, salt, keyLength, + forceJS); + return Utils.encodeKeyBase32(k); + }, + + /** + * N.B., salt should be base64 encoded, even though we have to decode + * it later! + */ + deriveEncodedKeyFromPassphrase : function deriveEncodedKeyFromPassphrase(passphrase, salt, keyLength, forceJS) { + let k = CryptoUtils.deriveKeyFromPassphrase(passphrase, salt, keyLength, + forceJS); + return Utils.base64Key(k); + }, + + /** + * Take a base64-encoded 128-bit AES key, returning it as five groups of five + * uppercase alphanumeric characters, separated by hyphens. + * A.K.A. base64-to-base32 encoding. + */ + presentEncodedKeyAsSyncKey : function presentEncodedKeyAsSyncKey(encodedKey) { + return Utils.encodeKeyBase32(atob(encodedKey)); + }, + + /** + * Load a JSON file from disk in the profile directory. + * + * @param filePath + * JSON file path load from profile. Loaded file will be + * /.json. i.e. Do not specify the ".json" + * extension. + * @param that + * Object to use for logging and "this" for callback. + * @param callback + * Function to process json object as its first argument. If the file + * could not be loaded, the first argument will be undefined. + */ + jsonLoad: Task.async(function*(filePath, that, callback) { + let path = OS.Path.join(OS.Constants.Path.profileDir, "weave", filePath + ".json"); + + if (that._log) { + that._log.trace("Loading json from disk: " + filePath); + } + + let json; + + try { + json = yield CommonUtils.readJSON(path); + } catch (e if e instanceof OS.File.Error && e.becauseNoSuchFile) { + // Ignore non-existent files. + } catch (e) { + if (that._log) { + that._log.debug("Failed to load json: " + + CommonUtils.exceptionStr(e)); + } + } + + callback.call(that, json); + }), + + /** + * Save a json-able object to disk in the profile directory. + * + * @param filePath + * JSON file path save to .json + * @param that + * Object to use for logging and "this" for callback + * @param obj + * Function to provide json-able object to save. If this isn't a + * function, it'll be used as the object to make a json string. + * @param callback + * Function called when the write has been performed. Optional. + * The first argument will be a Components.results error + * constant on error or null if no error was encountered (and + * the file saved successfully). + */ + jsonSave: Task.async(function*(filePath, that, obj, callback) { + let path = OS.Path.join(OS.Constants.Path.profileDir, "weave", + ...(filePath + ".json").split("/")); + let dir = OS.Path.dirname(path); + let error = null; + + try { + yield OS.File.makeDir(dir, { from: OS.Constants.Path.profileDir }); + + if (that._log) { + that._log.trace("Saving json to disk: " + path); + } + + let json = typeof obj == "function" ? obj.call(that) : obj; + + yield CommonUtils.writeJSON(json, path); + } catch (e) { + error = e + } + + if (typeof callback == "function") { + callback.call(that, error); + } + }), + + getErrorString: function Utils_getErrorString(error, args) { + try { + return Str.errors.get(error, args || null); + } catch (e) {} + + // basically returns "Unknown Error" + return Str.errors.get("error.reason.unknown"); + }, + + /** + * Generate 26 characters. + */ + generatePassphrase: function generatePassphrase() { + // Note that this is a different base32 alphabet to the one we use for + // other tasks. It's lowercase, uses different letters, and needs to be + // decoded with decodeKeyBase32, not just decodeBase32. + return Utils.encodeKeyBase32(CryptoUtils.generateRandomBytes(16)); + }, + + /** + * The following are the methods supported for UI use: + * + * * isPassphrase: + * determines whether a string is either a normalized or presentable + * passphrase. + * * hyphenatePassphrase: + * present a normalized passphrase for display. This might actually + * perform work beyond just hyphenation; sorry. + * * hyphenatePartialPassphrase: + * present a fragment of a normalized passphrase for display. + * * normalizePassphrase: + * take a presentable passphrase and reduce it to a normalized + * representation for storage. normalizePassphrase can safely be called + * on normalized input. + * * normalizeAccount: + * take user input for account/username, cleaning up appropriately. + */ + + isPassphrase: function(s) { + if (s) { + return /^[abcdefghijkmnpqrstuvwxyz23456789]{26}$/.test(Utils.normalizePassphrase(s)); + } + return false; + }, + + /** + * Hyphenate a passphrase (26 characters) into groups. + * abbbbccccddddeeeeffffggggh + * => + * a-bbbbc-cccdd-ddeee-effff-ggggh + */ + hyphenatePassphrase: function hyphenatePassphrase(passphrase) { + // For now, these are the same. + return Utils.hyphenatePartialPassphrase(passphrase, true); + }, + + hyphenatePartialPassphrase: function hyphenatePartialPassphrase(passphrase, omitTrailingDash) { + if (!passphrase) + return null; + + // Get the raw data input. Just base32. + let data = passphrase.toLowerCase().replace(/[^abcdefghijkmnpqrstuvwxyz23456789]/g, ""); + + // This is the neatest way to do this. + if ((data.length == 1) && !omitTrailingDash) + return data + "-"; + + // Hyphenate it. + let y = data.substr(0,1); + let z = data.substr(1).replace(/(.{1,5})/g, "-$1"); + + // Correct length? We're done. + if ((z.length == 30) || omitTrailingDash) + return y + z; + + // Add a trailing dash if appropriate. + return (y + z.replace(/([^-]{5})$/, "$1-")).substr(0, SYNC_KEY_HYPHENATED_LENGTH); + }, + + normalizePassphrase: function normalizePassphrase(pp) { + // Short var name... have you seen the lines below?! + // Allow leading and trailing whitespace. + pp = pp.trim().toLowerCase(); + + // 20-char sync key. + if (pp.length == 23 && + [5, 11, 17].every(function(i) pp[i] == '-')) { + + return pp.slice(0, 5) + pp.slice(6, 11) + + pp.slice(12, 17) + pp.slice(18, 23); + } + + // "Modern" 26-char key. + if (pp.length == 31 && + [1, 7, 13, 19, 25].every(function(i) pp[i] == '-')) { + + return pp.slice(0, 1) + pp.slice(2, 7) + + pp.slice(8, 13) + pp.slice(14, 19) + + pp.slice(20, 25) + pp.slice(26, 31); + } + + // Something else -- just return. + return pp; + }, + + normalizeAccount: function normalizeAccount(acc) { + return acc.trim(); + }, + + /** + * Create an array like the first but without elements of the second. Reuse + * arrays if possible. + */ + arraySub: function arraySub(minuend, subtrahend) { + if (!minuend.length || !subtrahend.length) + return minuend; + return minuend.filter(function(i) subtrahend.indexOf(i) == -1); + }, + + /** + * Build the union of two arrays. Reuse arrays if possible. + */ + arrayUnion: function arrayUnion(foo, bar) { + if (!foo.length) + return bar; + if (!bar.length) + return foo; + return foo.concat(Utils.arraySub(bar, foo)); + }, + + bind2: function Async_bind2(object, method) { + return function innerBind() { return method.apply(object, arguments); }; + }, + + /** + * Is there a master password configured, regardless of current lock state? + */ + mpEnabled: function mpEnabled() { + let modules = Cc["@mozilla.org/security/pkcs11moduledb;1"] + .getService(Ci.nsIPKCS11ModuleDB); + let sdrSlot = modules.findSlotByName(""); + let status = sdrSlot.status; + let slots = Ci.nsIPKCS11Slot; + + return status != slots.SLOT_UNINITIALIZED && status != slots.SLOT_READY; + }, + + /** + * Is there a master password configured and currently locked? + */ + mpLocked: function mpLocked() { + let modules = Cc["@mozilla.org/security/pkcs11moduledb;1"] + .getService(Ci.nsIPKCS11ModuleDB); + let sdrSlot = modules.findSlotByName(""); + let status = sdrSlot.status; + let slots = Ci.nsIPKCS11Slot; + + if (status == slots.SLOT_READY || status == slots.SLOT_LOGGED_IN + || status == slots.SLOT_UNINITIALIZED) + return false; + + if (status == slots.SLOT_NOT_LOGGED_IN) + return true; + + // something wacky happened, pretend MP is locked + return true; + }, + + // If Master Password is enabled and locked, present a dialog to unlock it. + // Return whether the system is unlocked. + ensureMPUnlocked: function ensureMPUnlocked() { + if (!Utils.mpLocked()) { + return true; + } + let sdr = Cc["@mozilla.org/security/sdr;1"] + .getService(Ci.nsISecretDecoderRing); + try { + sdr.encryptString("bacon"); + return true; + } catch(e) {} + return false; + }, + + /** + * Return a value for a backoff interval. Maximum is eight hours, unless + * Status.backoffInterval is higher. + * + */ + calculateBackoff: function calculateBackoff(attempts, baseInterval, + statusInterval) { + let backoffInterval = attempts * + (Math.floor(Math.random() * baseInterval) + + baseInterval); + return Math.max(Math.min(backoffInterval, MAXIMUM_BACKOFF_INTERVAL), + statusInterval); + }, + + /** + * Return a set of hostnames (including the protocol) which may have + * credentials for sync itself stored in the login manager. + * + * In general, these hosts will not have their passwords synced, will be + * reset when we drop sync credentials, etc. + */ + getSyncCredentialsHosts: function() { + // This is somewhat expensive and the result static, so we cache the result. + if (this._syncCredentialsHosts) { + return this._syncCredentialsHosts; + } + let result = new Set(); + // the legacy sync host. + result.add(PWDMGR_HOST); + // The FxA hosts - these almost certainly all have the same hostname, but + // better safe than sorry... + for (let prefName of ["identity.fxaccounts.remote.force_auth.uri", + "identity.fxaccounts.remote.signup.uri", + "identity.fxaccounts.remote.signin.uri", + "identity.fxaccounts.settings.uri"]) { + let prefVal; + try { + prefVal = Services.prefs.getCharPref(prefName); + } catch (_) { + continue; + } + let uri = Services.io.newURI(prefVal, null, null); + result.add(uri.prePath); + } + return this._syncCredentialsHosts = result; + }, +}; + +XPCOMUtils.defineLazyGetter(Utils, "_utf8Converter", function() { + let converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"] + .createInstance(Ci.nsIScriptableUnicodeConverter); + converter.charset = "UTF-8"; + return converter; +}); + +/* + * Commonly-used services + */ +this.Svc = {}; +Svc.Prefs = new Preferences(PREFS_BRANCH); +Svc.DefaultPrefs = new Preferences({branch: PREFS_BRANCH, defaultBranch: true}); +Svc.Obs = Observers; + +let _sessionCID = Services.appinfo.ID == SEAMONKEY_ID ? + "@mozilla.org/suite/sessionstore;1" : + "@mozilla.org/browser/sessionstore;1"; + +[ + ["Idle", "@mozilla.org/widget/idleservice;1", "nsIIdleService"], + ["Session", _sessionCID, "nsISessionStore"] +].forEach(function([name, contract, iface]) { + XPCOMUtils.defineLazyServiceGetter(Svc, name, contract, iface); +}); + +XPCOMUtils.defineLazyModuleGetter(Svc, "FormHistory", "resource://gre/modules/FormHistory.jsm"); + +Svc.__defineGetter__("Crypto", function() { + let cryptoSvc; + let ns = {}; + Cu.import("resource://services-crypto/WeaveCrypto.js", ns); + cryptoSvc = new ns.WeaveCrypto(); + delete Svc.Crypto; + return Svc.Crypto = cryptoSvc; +}); + +this.Str = {}; +["errors", "sync"].forEach(function(lazy) { + XPCOMUtils.defineLazyGetter(Str, lazy, Utils.lazyStrings(lazy)); +}); + +Svc.Obs.add("xpcom-shutdown", function () { + for (let name in Svc) + delete Svc[name]; +});