Bug 1299784 - Include a hashed version of the device ID with the sync ping r?markh,bsmedberg draft
authorThom Chiovoloni <tchiovoloni@mozilla.com>
Wed, 07 Sep 2016 16:49:21 -0400
changeset 411701 90972ec26384d64d57f9c960767067a55b1de314
parent 411700 093a7b8b72859f2d56297c07c800c1e5cc0ef216
child 530787 5c29b08e295613e55897f8b284dbc308a6e9fc52
push id28955
push userbmo:tchiovoloni@mozilla.com
push dateThu, 08 Sep 2016 13:20:40 +0000
reviewersmarkh, bsmedberg
bugs1299784
milestone51.0a1
Bug 1299784 - Include a hashed version of the device ID with the sync ping r?markh,bsmedberg MozReview-Commit-ID: 3sPSeBNrF8z
services/crypto/modules/utils.js
services/sync/modules/browserid_identity.js
services/sync/modules/telemetry.js
services/sync/modules/util.js
services/sync/tests/unit/sync_ping_schema.json
toolkit/components/telemetry/docs/data/sync-ping.rst
--- a/services/crypto/modules/utils.js
+++ b/services/crypto/modules/utils.js
@@ -101,16 +101,23 @@ this.CryptoUtils = {
   sha1: function sha1(message) {
     return CommonUtils.bytesAsHex(CryptoUtils.UTF8AndSHA1(message));
   },
 
   sha1Base32: function sha1Base32(message) {
     return CommonUtils.encodeBase32(CryptoUtils.UTF8AndSHA1(message));
   },
 
+  sha256(message) {
+    let hasher = Cc["@mozilla.org/security/hash;1"]
+                 .createInstance(Ci.nsICryptoHash);
+    hasher.init(hasher.SHA256);
+    return CommonUtils.bytesAsHex(CryptoUtils.digestUTF8(message, hasher));
+  },
+
   /**
    * Produce an HMAC key object from a key string.
    */
   makeHMACKey: function makeHMACKey(str) {
     return Svc.KeyFactory.keyFromString(Ci.nsIKeyObject.HMAC, str);
   },
 
   /**
--- a/services/sync/modules/browserid_identity.js
+++ b/services/sync/modules/browserid_identity.js
@@ -116,16 +116,20 @@ this.BrowserIDManager.prototype = {
 
   hashedUID() {
     if (!this._token) {
       throw new Error("hashedUID: Don't have token");
     }
     return this._token.hashed_fxa_uid
   },
 
+  deviceID() {
+    return this._signedInUser && this._signedInUser.deviceId;
+  },
+
   initialize: function() {
     for (let topic of OBSERVER_TOPICS) {
       Services.obs.addObserver(this, topic, false);
     }
     // and a background fetch of account data just so we can set this.account,
     // so we have a username available before we've actually done a login.
     // XXX - this is actually a hack just for tests and really shouldn't be
     // necessary. Also, you'd think it would be safe to allow this.account to
--- a/services/sync/modules/telemetry.js
+++ b/services/sync/modules/telemetry.js
@@ -199,16 +199,17 @@ class TelemetryRecord {
 
   toJSON() {
     let result = {
       when: this.when,
       uid: this.uid,
       took: this.took,
       failureReason: this.failureReason,
       status: this.status,
+      deviceID: this.deviceID,
     };
     let engines = [];
     for (let engine of this.engines) {
       engines.push(engine.toJSON());
     }
     if (engines.length > 0) {
       result.engines = engines;
     }
@@ -223,18 +224,26 @@ class TelemetryRecord {
       this.onEngineStop(this.currentEngine.name);
     }
     if (error) {
       this.failureReason = transformError(error);
     }
 
     try {
       this.uid = Weave.Service.identity.hashedUID();
+      let deviceID = Weave.Service.identity.deviceID();
+      if (deviceID) {
+        // Combine the raw device id with the metrics uid to create a stable
+        // unique identifier that can't be mapped back to the user's FxA
+        // identity without knowing the metrics HMAC key.
+        this.deviceID = Utils.sha256(deviceID + this.uid);
+      }
     } catch (e) {
       this.uid = "0".repeat(32);
+      this.deviceID = undefined;
     }
 
     // Check for engine statuses. -- We do this now, and not in engine.finished
     // to make sure any statuses that get set "late" are recorded
     for (let engine of this.engines) {
       let status = Status.engines[engine.name];
       if (status && status !== constants.ENGINE_SUCCEEDED) {
         engine.status = status;
--- a/services/sync/modules/util.js
+++ b/services/sync/modules/util.js
@@ -47,16 +47,17 @@ this.Utils = {
 
   // Aliases from CryptoUtils.
   generateRandomBytes: CryptoUtils.generateRandomBytes,
   computeHTTPMACSHA1: CryptoUtils.computeHTTPMACSHA1,
   digestUTF8: CryptoUtils.digestUTF8,
   digestBytes: CryptoUtils.digestBytes,
   sha1: CryptoUtils.sha1,
   sha1Base32: CryptoUtils.sha1Base32,
+  sha256: CryptoUtils.sha256,
   makeHMACKey: CryptoUtils.makeHMACKey,
   makeHMACHasher: CryptoUtils.makeHMACHasher,
   hkdfExpand: CryptoUtils.hkdfExpand,
   pbkdf2Generate: CryptoUtils.pbkdf2Generate,
   deriveKeyFromPassphrase: CryptoUtils.deriveKeyFromPassphrase,
   getHTTPMACSHA1Header: CryptoUtils.getHTTPMACSHA1Header,
 
   /**
--- a/services/sync/tests/unit/sync_ping_schema.json
+++ b/services/sync/tests/unit/sync_ping_schema.json
@@ -21,16 +21,20 @@
       "required": ["when", "uid", "took"],
       "properties": {
         "didLogin": { "type": "boolean" },
         "when": { "type": "integer" },
         "uid": {
           "type": "string",
           "pattern": "^[0-9a-f]{32}$"
         },
+        "deviceID": {
+          "type": "string",
+          "pattern": "^[0-9a-f]{64}$"
+        },
         "status": {
           "type": "object",
           "anyOf": [
             { "required": ["sync"] },
             { "required": ["service"] }
           ],
           "additionalProperties": false,
           "properties": {
--- a/toolkit/components/telemetry/docs/data/sync-ping.rst
+++ b/toolkit/components/telemetry/docs/data/sync-ping.rst
@@ -20,16 +20,17 @@ Structure:
         version: 1,
         discarded: <integer count> // Number of syncs discarded -- left out if zero.
         why: <string>, // Why did we submit the ping? Either "shutdown" or "schedule".
         // Array of recorded syncs. The ping is not submitted if this would be empty
         syncs: [{
           when: <integer milliseconds since epoch>,
           took: <integer duration in milliseconds>,
           uid: <string>, // Hashed FxA unique ID, or string of 32 zeros.
+          deviceID: <string>, // Hashed FxA Device ID, hex string of 64 characters, not included if the user is not logged in.
           didLogin: <bool>, // Optional, is this the first sync after login? Excluded if we don't know.
           why: <string>, // Optional, why the sync occured, excluded if we don't know.
 
           // Optional, excluded if there was no error.
           failureReason: {
             name: <string>, // "httperror", "networkerror", "shutdownerror", etc.
             code: <integer>, // Only present for "httperror" and "networkerror".
             error: <string>, // Only present for "othererror" and "unexpectederror".