Thu, 22 Jan 2015 13:21:57 +0100
Incorporate requested changes from Mozilla in review:
https://bugzilla.mozilla.org/show_bug.cgi?id=1123480#c6
1 /* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
5 this.EXPORTED_SYMBOLS = ["XPCOMUtils", "Services", "Utils", "Async", "Svc", "Str"];
7 const {classes: Cc, interfaces: Ci, results: Cr, utils: Cu} = Components;
9 Cu.import("resource://gre/modules/Log.jsm");
10 Cu.import("resource://services-common/observers.js");
11 Cu.import("resource://services-common/stringbundle.js");
12 Cu.import("resource://services-common/utils.js");
13 Cu.import("resource://services-common/async.js", this);
14 Cu.import("resource://services-crypto/utils.js");
15 Cu.import("resource://services-sync/constants.js");
16 Cu.import("resource://gre/modules/Preferences.jsm");
17 Cu.import("resource://gre/modules/Services.jsm", this);
18 Cu.import("resource://gre/modules/XPCOMUtils.jsm", this);
19 Cu.import("resource://gre/modules/osfile.jsm", this);
20 Cu.import("resource://gre/modules/Task.jsm", this);
22 /*
23 * Utility functions
24 */
26 this.Utils = {
27 // Alias in functions from CommonUtils. These previously were defined here.
28 // In the ideal world, references to these would be removed.
29 nextTick: CommonUtils.nextTick,
30 namedTimer: CommonUtils.namedTimer,
31 exceptionStr: CommonUtils.exceptionStr,
32 stackTrace: CommonUtils.stackTrace,
33 makeURI: CommonUtils.makeURI,
34 encodeUTF8: CommonUtils.encodeUTF8,
35 decodeUTF8: CommonUtils.decodeUTF8,
36 safeAtoB: CommonUtils.safeAtoB,
37 byteArrayToString: CommonUtils.byteArrayToString,
38 bytesAsHex: CommonUtils.bytesAsHex,
39 hexToBytes: CommonUtils.hexToBytes,
40 encodeBase32: CommonUtils.encodeBase32,
41 decodeBase32: CommonUtils.decodeBase32,
43 // Aliases from CryptoUtils.
44 generateRandomBytes: CryptoUtils.generateRandomBytes,
45 computeHTTPMACSHA1: CryptoUtils.computeHTTPMACSHA1,
46 digestUTF8: CryptoUtils.digestUTF8,
47 digestBytes: CryptoUtils.digestBytes,
48 sha1: CryptoUtils.sha1,
49 sha1Base32: CryptoUtils.sha1Base32,
50 makeHMACKey: CryptoUtils.makeHMACKey,
51 makeHMACHasher: CryptoUtils.makeHMACHasher,
52 hkdfExpand: CryptoUtils.hkdfExpand,
53 pbkdf2Generate: CryptoUtils.pbkdf2Generate,
54 deriveKeyFromPassphrase: CryptoUtils.deriveKeyFromPassphrase,
55 getHTTPMACSHA1Header: CryptoUtils.getHTTPMACSHA1Header,
57 /**
58 * Wrap a function to catch all exceptions and log them
59 *
60 * @usage MyObj._catch = Utils.catch;
61 * MyObj.foo = function() { this._catch(func)(); }
62 *
63 * Optionally pass a function which will be called if an
64 * exception occurs.
65 */
66 catch: function Utils_catch(func, exceptionCallback) {
67 let thisArg = this;
68 return function WrappedCatch() {
69 try {
70 return func.call(thisArg);
71 }
72 catch(ex) {
73 thisArg._log.debug("Exception: " + Utils.exceptionStr(ex));
74 if (exceptionCallback) {
75 return exceptionCallback.call(thisArg, ex);
76 }
77 return null;
78 }
79 };
80 },
82 /**
83 * Wrap a function to call lock before calling the function then unlock.
84 *
85 * @usage MyObj._lock = Utils.lock;
86 * MyObj.foo = function() { this._lock(func)(); }
87 */
88 lock: function lock(label, func) {
89 let thisArg = this;
90 return function WrappedLock() {
91 if (!thisArg.lock()) {
92 throw "Could not acquire lock. Label: \"" + label + "\".";
93 }
95 try {
96 return func.call(thisArg);
97 }
98 finally {
99 thisArg.unlock();
100 }
101 };
102 },
104 isLockException: function isLockException(ex) {
105 return ex && ex.indexOf && ex.indexOf("Could not acquire lock.") == 0;
106 },
108 /**
109 * Wrap functions to notify when it starts and finishes executing or if it
110 * threw an error.
111 *
112 * The message is a combination of a provided prefix, the local name, and
113 * the event. Possible events are: "start", "finish", "error". The subject
114 * is the function's return value on "finish" or the caught exception on
115 * "error". The data argument is the predefined data value.
116 *
117 * Example:
118 *
119 * @usage function MyObj(name) {
120 * this.name = name;
121 * this._notify = Utils.notify("obj:");
122 * }
123 * MyObj.prototype = {
124 * foo: function() this._notify("func", "data-arg", function () {
125 * //...
126 * }(),
127 * };
128 */
129 notify: function Utils_notify(prefix) {
130 return function NotifyMaker(name, data, func) {
131 let thisArg = this;
132 let notify = function(state, subject) {
133 let mesg = prefix + name + ":" + state;
134 thisArg._log.trace("Event: " + mesg);
135 Observers.notify(mesg, subject, data);
136 };
138 return function WrappedNotify() {
139 try {
140 notify("start", null);
141 let ret = func.call(thisArg);
142 notify("finish", ret);
143 return ret;
144 }
145 catch(ex) {
146 notify("error", ex);
147 throw ex;
148 }
149 };
150 };
151 },
153 runInTransaction: function(db, callback, thisObj) {
154 let hasTransaction = false;
155 try {
156 db.beginTransaction();
157 hasTransaction = true;
158 } catch(e) { /* om nom nom exceptions */ }
160 try {
161 return callback.call(thisObj);
162 } finally {
163 if (hasTransaction) {
164 db.commitTransaction();
165 }
166 }
167 },
169 /**
170 * GUIDs are 9 random bytes encoded with base64url (RFC 4648).
171 * That makes them 12 characters long with 72 bits of entropy.
172 */
173 makeGUID: function makeGUID() {
174 return CommonUtils.encodeBase64URL(Utils.generateRandomBytes(9));
175 },
177 _base64url_regex: /^[-abcdefghijklmnopqrstuvwxyz0123456789_]{12}$/i,
178 checkGUID: function checkGUID(guid) {
179 return !!guid && this._base64url_regex.test(guid);
180 },
182 /**
183 * Add a simple getter/setter to an object that defers access of a property
184 * to an inner property.
185 *
186 * @param obj
187 * Object to add properties to defer in its prototype
188 * @param defer
189 * Property of obj to defer to
190 * @param prop
191 * Property name to defer (or an array of property names)
192 */
193 deferGetSet: function Utils_deferGetSet(obj, defer, prop) {
194 if (Array.isArray(prop))
195 return prop.map(function(prop) Utils.deferGetSet(obj, defer, prop));
197 let prot = obj.prototype;
199 // Create a getter if it doesn't exist yet
200 if (!prot.__lookupGetter__(prop)) {
201 prot.__defineGetter__(prop, function () {
202 return this[defer][prop];
203 });
204 }
206 // Create a setter if it doesn't exist yet
207 if (!prot.__lookupSetter__(prop)) {
208 prot.__defineSetter__(prop, function (val) {
209 this[defer][prop] = val;
210 });
211 }
212 },
214 lazyStrings: function Weave_lazyStrings(name) {
215 let bundle = "chrome://weave/locale/services/" + name + ".properties";
216 return function() new StringBundle(bundle);
217 },
219 deepEquals: function eq(a, b) {
220 // If they're triple equals, then it must be equals!
221 if (a === b)
222 return true;
224 // If they weren't equal, they must be objects to be different
225 if (typeof a != "object" || typeof b != "object")
226 return false;
228 // But null objects won't have properties to compare
229 if (a === null || b === null)
230 return false;
232 // Make sure all of a's keys have a matching value in b
233 for (let k in a)
234 if (!eq(a[k], b[k]))
235 return false;
237 // Do the same for b's keys but skip those that we already checked
238 for (let k in b)
239 if (!(k in a) && !eq(a[k], b[k]))
240 return false;
242 return true;
243 },
245 // Generator and discriminator for HMAC exceptions.
246 // Split these out in case we want to make them richer in future, and to
247 // avoid inevitable confusion if the message changes.
248 throwHMACMismatch: function throwHMACMismatch(shouldBe, is) {
249 throw "Record SHA256 HMAC mismatch: should be " + shouldBe + ", is " + is;
250 },
252 isHMACMismatch: function isHMACMismatch(ex) {
253 const hmacFail = "Record SHA256 HMAC mismatch: ";
254 return ex && ex.indexOf && (ex.indexOf(hmacFail) == 0);
255 },
257 /**
258 * Turn RFC 4648 base32 into our own user-friendly version.
259 * ABCDEFGHIJKLMNOPQRSTUVWXYZ234567
260 * becomes
261 * abcdefghijk8mn9pqrstuvwxyz234567
262 */
263 base32ToFriendly: function base32ToFriendly(input) {
264 return input.toLowerCase()
265 .replace("l", '8', "g")
266 .replace("o", '9', "g");
267 },
269 base32FromFriendly: function base32FromFriendly(input) {
270 return input.toUpperCase()
271 .replace("8", 'L', "g")
272 .replace("9", 'O', "g");
273 },
275 /**
276 * Key manipulation.
277 */
279 // Return an octet string in friendly base32 *with no trailing =*.
280 encodeKeyBase32: function encodeKeyBase32(keyData) {
281 return Utils.base32ToFriendly(
282 Utils.encodeBase32(keyData))
283 .slice(0, SYNC_KEY_ENCODED_LENGTH);
284 },
286 decodeKeyBase32: function decodeKeyBase32(encoded) {
287 return Utils.decodeBase32(
288 Utils.base32FromFriendly(
289 Utils.normalizePassphrase(encoded)))
290 .slice(0, SYNC_KEY_DECODED_LENGTH);
291 },
293 base64Key: function base64Key(keyData) {
294 return btoa(keyData);
295 },
297 /**
298 * N.B., salt should be base64 encoded, even though we have to decode
299 * it later!
300 */
301 derivePresentableKeyFromPassphrase : function derivePresentableKeyFromPassphrase(passphrase, salt, keyLength, forceJS) {
302 let k = CryptoUtils.deriveKeyFromPassphrase(passphrase, salt, keyLength,
303 forceJS);
304 return Utils.encodeKeyBase32(k);
305 },
307 /**
308 * N.B., salt should be base64 encoded, even though we have to decode
309 * it later!
310 */
311 deriveEncodedKeyFromPassphrase : function deriveEncodedKeyFromPassphrase(passphrase, salt, keyLength, forceJS) {
312 let k = CryptoUtils.deriveKeyFromPassphrase(passphrase, salt, keyLength,
313 forceJS);
314 return Utils.base64Key(k);
315 },
317 /**
318 * Take a base64-encoded 128-bit AES key, returning it as five groups of five
319 * uppercase alphanumeric characters, separated by hyphens.
320 * A.K.A. base64-to-base32 encoding.
321 */
322 presentEncodedKeyAsSyncKey : function presentEncodedKeyAsSyncKey(encodedKey) {
323 return Utils.encodeKeyBase32(atob(encodedKey));
324 },
326 /**
327 * Load a JSON file from disk in the profile directory.
328 *
329 * @param filePath
330 * JSON file path load from profile. Loaded file will be
331 * <profile>/<filePath>.json. i.e. Do not specify the ".json"
332 * extension.
333 * @param that
334 * Object to use for logging and "this" for callback.
335 * @param callback
336 * Function to process json object as its first argument. If the file
337 * could not be loaded, the first argument will be undefined.
338 */
339 jsonLoad: Task.async(function*(filePath, that, callback) {
340 let path = OS.Path.join(OS.Constants.Path.profileDir, "weave", filePath + ".json");
342 if (that._log) {
343 that._log.trace("Loading json from disk: " + filePath);
344 }
346 let json;
348 try {
349 json = yield CommonUtils.readJSON(path);
350 } catch (e if e instanceof OS.File.Error && e.becauseNoSuchFile) {
351 // Ignore non-existent files.
352 } catch (e) {
353 if (that._log) {
354 that._log.debug("Failed to load json: " +
355 CommonUtils.exceptionStr(e));
356 }
357 }
359 callback.call(that, json);
360 }),
362 /**
363 * Save a json-able object to disk in the profile directory.
364 *
365 * @param filePath
366 * JSON file path save to <filePath>.json
367 * @param that
368 * Object to use for logging and "this" for callback
369 * @param obj
370 * Function to provide json-able object to save. If this isn't a
371 * function, it'll be used as the object to make a json string.
372 * @param callback
373 * Function called when the write has been performed. Optional.
374 * The first argument will be a Components.results error
375 * constant on error or null if no error was encountered (and
376 * the file saved successfully).
377 */
378 jsonSave: Task.async(function*(filePath, that, obj, callback) {
379 let path = OS.Path.join(OS.Constants.Path.profileDir, "weave",
380 ...(filePath + ".json").split("/"));
381 let dir = OS.Path.dirname(path);
382 let error = null;
384 try {
385 yield OS.File.makeDir(dir, { from: OS.Constants.Path.profileDir });
387 if (that._log) {
388 that._log.trace("Saving json to disk: " + path);
389 }
391 let json = typeof obj == "function" ? obj.call(that) : obj;
393 yield CommonUtils.writeJSON(json, path);
394 } catch (e) {
395 error = e
396 }
398 if (typeof callback == "function") {
399 callback.call(that, error);
400 }
401 }),
403 getErrorString: function Utils_getErrorString(error, args) {
404 try {
405 return Str.errors.get(error, args || null);
406 } catch (e) {}
408 // basically returns "Unknown Error"
409 return Str.errors.get("error.reason.unknown");
410 },
412 /**
413 * Generate 26 characters.
414 */
415 generatePassphrase: function generatePassphrase() {
416 // Note that this is a different base32 alphabet to the one we use for
417 // other tasks. It's lowercase, uses different letters, and needs to be
418 // decoded with decodeKeyBase32, not just decodeBase32.
419 return Utils.encodeKeyBase32(CryptoUtils.generateRandomBytes(16));
420 },
422 /**
423 * The following are the methods supported for UI use:
424 *
425 * * isPassphrase:
426 * determines whether a string is either a normalized or presentable
427 * passphrase.
428 * * hyphenatePassphrase:
429 * present a normalized passphrase for display. This might actually
430 * perform work beyond just hyphenation; sorry.
431 * * hyphenatePartialPassphrase:
432 * present a fragment of a normalized passphrase for display.
433 * * normalizePassphrase:
434 * take a presentable passphrase and reduce it to a normalized
435 * representation for storage. normalizePassphrase can safely be called
436 * on normalized input.
437 * * normalizeAccount:
438 * take user input for account/username, cleaning up appropriately.
439 */
441 isPassphrase: function(s) {
442 if (s) {
443 return /^[abcdefghijkmnpqrstuvwxyz23456789]{26}$/.test(Utils.normalizePassphrase(s));
444 }
445 return false;
446 },
448 /**
449 * Hyphenate a passphrase (26 characters) into groups.
450 * abbbbccccddddeeeeffffggggh
451 * =>
452 * a-bbbbc-cccdd-ddeee-effff-ggggh
453 */
454 hyphenatePassphrase: function hyphenatePassphrase(passphrase) {
455 // For now, these are the same.
456 return Utils.hyphenatePartialPassphrase(passphrase, true);
457 },
459 hyphenatePartialPassphrase: function hyphenatePartialPassphrase(passphrase, omitTrailingDash) {
460 if (!passphrase)
461 return null;
463 // Get the raw data input. Just base32.
464 let data = passphrase.toLowerCase().replace(/[^abcdefghijkmnpqrstuvwxyz23456789]/g, "");
466 // This is the neatest way to do this.
467 if ((data.length == 1) && !omitTrailingDash)
468 return data + "-";
470 // Hyphenate it.
471 let y = data.substr(0,1);
472 let z = data.substr(1).replace(/(.{1,5})/g, "-$1");
474 // Correct length? We're done.
475 if ((z.length == 30) || omitTrailingDash)
476 return y + z;
478 // Add a trailing dash if appropriate.
479 return (y + z.replace(/([^-]{5})$/, "$1-")).substr(0, SYNC_KEY_HYPHENATED_LENGTH);
480 },
482 normalizePassphrase: function normalizePassphrase(pp) {
483 // Short var name... have you seen the lines below?!
484 // Allow leading and trailing whitespace.
485 pp = pp.trim().toLowerCase();
487 // 20-char sync key.
488 if (pp.length == 23 &&
489 [5, 11, 17].every(function(i) pp[i] == '-')) {
491 return pp.slice(0, 5) + pp.slice(6, 11)
492 + pp.slice(12, 17) + pp.slice(18, 23);
493 }
495 // "Modern" 26-char key.
496 if (pp.length == 31 &&
497 [1, 7, 13, 19, 25].every(function(i) pp[i] == '-')) {
499 return pp.slice(0, 1) + pp.slice(2, 7)
500 + pp.slice(8, 13) + pp.slice(14, 19)
501 + pp.slice(20, 25) + pp.slice(26, 31);
502 }
504 // Something else -- just return.
505 return pp;
506 },
508 normalizeAccount: function normalizeAccount(acc) {
509 return acc.trim();
510 },
512 /**
513 * Create an array like the first but without elements of the second. Reuse
514 * arrays if possible.
515 */
516 arraySub: function arraySub(minuend, subtrahend) {
517 if (!minuend.length || !subtrahend.length)
518 return minuend;
519 return minuend.filter(function(i) subtrahend.indexOf(i) == -1);
520 },
522 /**
523 * Build the union of two arrays. Reuse arrays if possible.
524 */
525 arrayUnion: function arrayUnion(foo, bar) {
526 if (!foo.length)
527 return bar;
528 if (!bar.length)
529 return foo;
530 return foo.concat(Utils.arraySub(bar, foo));
531 },
533 bind2: function Async_bind2(object, method) {
534 return function innerBind() { return method.apply(object, arguments); };
535 },
537 /**
538 * Is there a master password configured, regardless of current lock state?
539 */
540 mpEnabled: function mpEnabled() {
541 let modules = Cc["@mozilla.org/security/pkcs11moduledb;1"]
542 .getService(Ci.nsIPKCS11ModuleDB);
543 let sdrSlot = modules.findSlotByName("");
544 let status = sdrSlot.status;
545 let slots = Ci.nsIPKCS11Slot;
547 return status != slots.SLOT_UNINITIALIZED && status != slots.SLOT_READY;
548 },
550 /**
551 * Is there a master password configured and currently locked?
552 */
553 mpLocked: function mpLocked() {
554 let modules = Cc["@mozilla.org/security/pkcs11moduledb;1"]
555 .getService(Ci.nsIPKCS11ModuleDB);
556 let sdrSlot = modules.findSlotByName("");
557 let status = sdrSlot.status;
558 let slots = Ci.nsIPKCS11Slot;
560 if (status == slots.SLOT_READY || status == slots.SLOT_LOGGED_IN
561 || status == slots.SLOT_UNINITIALIZED)
562 return false;
564 if (status == slots.SLOT_NOT_LOGGED_IN)
565 return true;
567 // something wacky happened, pretend MP is locked
568 return true;
569 },
571 // If Master Password is enabled and locked, present a dialog to unlock it.
572 // Return whether the system is unlocked.
573 ensureMPUnlocked: function ensureMPUnlocked() {
574 if (!Utils.mpLocked()) {
575 return true;
576 }
577 let sdr = Cc["@mozilla.org/security/sdr;1"]
578 .getService(Ci.nsISecretDecoderRing);
579 try {
580 sdr.encryptString("bacon");
581 return true;
582 } catch(e) {}
583 return false;
584 },
586 /**
587 * Return a value for a backoff interval. Maximum is eight hours, unless
588 * Status.backoffInterval is higher.
589 *
590 */
591 calculateBackoff: function calculateBackoff(attempts, baseInterval,
592 statusInterval) {
593 let backoffInterval = attempts *
594 (Math.floor(Math.random() * baseInterval) +
595 baseInterval);
596 return Math.max(Math.min(backoffInterval, MAXIMUM_BACKOFF_INTERVAL),
597 statusInterval);
598 },
600 /**
601 * Return a set of hostnames (including the protocol) which may have
602 * credentials for sync itself stored in the login manager.
603 *
604 * In general, these hosts will not have their passwords synced, will be
605 * reset when we drop sync credentials, etc.
606 */
607 getSyncCredentialsHosts: function() {
608 // This is somewhat expensive and the result static, so we cache the result.
609 if (this._syncCredentialsHosts) {
610 return this._syncCredentialsHosts;
611 }
612 let result = new Set();
613 // the legacy sync host.
614 result.add(PWDMGR_HOST);
615 // The FxA hosts - these almost certainly all have the same hostname, but
616 // better safe than sorry...
617 for (let prefName of ["identity.fxaccounts.remote.force_auth.uri",
618 "identity.fxaccounts.remote.signup.uri",
619 "identity.fxaccounts.remote.signin.uri",
620 "identity.fxaccounts.settings.uri"]) {
621 let prefVal;
622 try {
623 prefVal = Services.prefs.getCharPref(prefName);
624 } catch (_) {
625 continue;
626 }
627 let uri = Services.io.newURI(prefVal, null, null);
628 result.add(uri.prePath);
629 }
630 return this._syncCredentialsHosts = result;
631 },
632 };
634 XPCOMUtils.defineLazyGetter(Utils, "_utf8Converter", function() {
635 let converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"]
636 .createInstance(Ci.nsIScriptableUnicodeConverter);
637 converter.charset = "UTF-8";
638 return converter;
639 });
641 /*
642 * Commonly-used services
643 */
644 this.Svc = {};
645 Svc.Prefs = new Preferences(PREFS_BRANCH);
646 Svc.DefaultPrefs = new Preferences({branch: PREFS_BRANCH, defaultBranch: true});
647 Svc.Obs = Observers;
649 let _sessionCID = Services.appinfo.ID == SEAMONKEY_ID ?
650 "@mozilla.org/suite/sessionstore;1" :
651 "@mozilla.org/browser/sessionstore;1";
653 [
654 ["Idle", "@mozilla.org/widget/idleservice;1", "nsIIdleService"],
655 ["Session", _sessionCID, "nsISessionStore"]
656 ].forEach(function([name, contract, iface]) {
657 XPCOMUtils.defineLazyServiceGetter(Svc, name, contract, iface);
658 });
660 XPCOMUtils.defineLazyModuleGetter(Svc, "FormHistory", "resource://gre/modules/FormHistory.jsm");
662 Svc.__defineGetter__("Crypto", function() {
663 let cryptoSvc;
664 let ns = {};
665 Cu.import("resource://services-crypto/WeaveCrypto.js", ns);
666 cryptoSvc = new ns.WeaveCrypto();
667 delete Svc.Crypto;
668 return Svc.Crypto = cryptoSvc;
669 });
671 this.Str = {};
672 ["errors", "sync"].forEach(function(lazy) {
673 XPCOMUtils.defineLazyGetter(Str, lazy, Utils.lazyStrings(lazy));
674 });
676 Svc.Obs.add("xpcom-shutdown", function () {
677 for (let name in Svc)
678 delete Svc[name];
679 });