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