Bug 1046245 - enumerateDevices origin-unique ids persisted to disk. r=jesup
☠☠ backed out by 99c3baae746d ☠ ☠
authorJan-Ivar Bruaroey <jib@mozilla.com>
Tue, 03 Mar 2015 09:51:05 -0500
changeset 266724 01606cf19a777a84436b5edea1232d0fef37b3f2
parent 266723 2ed2b15fe94055f08517d4c8fff63ce97fe30988
child 266725 87dc145f4da8b721dfcc04b4de8508d58e354e40
push id830
push userraliiev@mozilla.com
push dateFri, 19 Jun 2015 19:24:37 +0000
treeherdermozilla-release@932614382a68 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersjesup
bugs1046245
milestone39.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1046245 - enumerateDevices origin-unique ids persisted to disk. r=jesup
dom/media/MediaManager.cpp
dom/media/MediaManager.h
--- a/dom/media/MediaManager.cpp
+++ b/dom/media/MediaManager.cpp
@@ -21,16 +21,19 @@
 #include "nsISupportsArray.h"
 #include "nsIDocShell.h"
 #include "nsIDocument.h"
 #include "nsISupportsPrimitives.h"
 #include "nsIInterfaceRequestorUtils.h"
 #include "nsIIDNService.h"
 #include "nsNetUtil.h"
 #include "nsPrincipal.h"
+#include "nsAppDirectoryServiceDefs.h"
+#include "nsIInputStream.h"
+#include "nsILineInputStream.h"
 #include "mozilla/Types.h"
 #include "mozilla/PeerIdentity.h"
 #include "mozilla/dom/ContentChild.h"
 #include "mozilla/dom/File.h"
 #include "mozilla/dom/MediaStreamBinding.h"
 #include "mozilla/dom/MediaStreamTrackBinding.h"
 #include "mozilla/dom/GetUserMediaRequestBinding.h"
 #include "mozilla/Preferences.h"
@@ -1354,148 +1357,337 @@ public:
     return NS_OK;
   }
 
 private:
   nsAutoPtr<GetUserMediaTask> mTask;
 };
 #endif
 
+// A file in the profile dir is used to persist mOriginUuids used to anonymize
+// deviceIds to be unique per origin, to avoid them being supercookies.
+
+#define ORIGINUUIDS_FILE "enumerate_devices.txt"
+#define ORIGINUUIDS_VERSION "1"
+
+#define HMAC_LENGTH 20
+
 /**
  * Similar to GetUserMediaTask, but used for the chrome-only
  * GetUserMediaDevices function. Enumerates a list of audio & video devices,
  * wraps them up in nsIMediaDevice objects and returns it to the success
  * callback.
+ *
+ * All code in this class runs on the MediaManager thread.
  */
 class GetUserMediaDevicesTask : public Task
 {
-  static unsigned char* unconst_uchar_cast(const char *s) {
-    return reinterpret_cast<unsigned char*>(const_cast<char*>(s));
-  }
-
-  // Cribbed from nricectx.cpp
-  static nsresult
-  hmac_sha1(const char *key, int keyl, const char *buf, int bufl,
-            unsigned char *result) {
-    const CK_MECHANISM_TYPE mech = CKM_SHA_1_HMAC;
-    PK11SlotInfo *slot = 0;
-    MOZ_ASSERT(keyl > 0);
-    SECItem keyi = { siBuffer, unconst_uchar_cast(key),
-                     static_cast<unsigned int>(keyl) };
-    PK11SymKey *skey = 0;
-    PK11Context *hmac_ctx = 0;
-    SECStatus status;
-    unsigned int hmac_len;
-    SECItem param = { siBuffer, nullptr, 0 };
-    nsresult rv = NS_ERROR_UNEXPECTED;
-
-    slot = PK11_GetInternalKeySlot();
-    if (!slot) {
-      goto abort;
-    }
-    skey = PK11_ImportSymKey(slot, mech, PK11_OriginUnwrap, CKA_SIGN, &keyi,
-                             nullptr);
-    if (!skey) {
-      goto abort;
+  class OriginUuidsLoader
+  {
+    static unsigned char*
+    unconst_uchar_cast(const char *s) {
+      return reinterpret_cast<unsigned char*>(const_cast<char*>(s));
     }
 
-    hmac_ctx = PK11_CreateContextBySymKey(mech, CKA_SIGN, skey, &param);
-    if (!hmac_ctx) {
-      goto abort;
-    }
-    status = PK11_DigestBegin(hmac_ctx);
-    if (status != SECSuccess) {
-      goto abort;
-    }
-    status = PK11_DigestOp(hmac_ctx, unconst_uchar_cast(buf), bufl);
-    if (status != SECSuccess) {
-      goto abort;
-    }
-    status = PK11_DigestFinal(hmac_ctx, result, &hmac_len, 20);
-    if (status != SECSuccess) {
-      goto abort;
-    }
-    MOZ_ASSERT(hmac_len == 20);
-    rv = NS_OK;
+    // Cribbed from nricectx.cpp
+    static nsresult
+    hmac_sha1(const char *key, int keyl, const char *buf, int bufl,
+              unsigned char *result)
+    {
+      const CK_MECHANISM_TYPE mech = CKM_SHA_1_HMAC;
+      PK11SlotInfo *slot = 0;
+      MOZ_ASSERT(keyl > 0);
+      SECItem keyi = { siBuffer, unconst_uchar_cast(key),
+                       static_cast<unsigned int>(keyl) };
+      PK11SymKey *skey = 0;
+      PK11Context *hmac_ctx = 0;
+      SECStatus status;
+      unsigned int hmac_len;
+      SECItem param = { siBuffer, nullptr, 0 };
+      nsresult rv = NS_ERROR_UNEXPECTED;
+
+      slot = PK11_GetInternalKeySlot();
+      if (!slot) {
+        goto abort;
+      }
+      skey = PK11_ImportSymKey(slot, mech, PK11_OriginUnwrap, CKA_SIGN, &keyi,
+                               nullptr);
+      if (!skey) {
+        goto abort;
+      }
 
-  abort:
-    if (hmac_ctx) {
-      PK11_DestroyContext(hmac_ctx, PR_TRUE);
-    }
-    if (skey) {
-      PK11_FreeSymKey(skey);
-    }
-    if (slot) {
-      PK11_FreeSlot(slot);
-    }
-    return rv;
-  }
-
-  nsresult AnonymizeId(nsAString& aId, const nsACString& origin) {
-    // deviceId would be a supercookie if we returned it. Anonymize it:
-    // 1. Get (or create) a persistent uuid for this origin.
-    // 2. Return hmac_sha1(uuid, id) - an anonymized id unique to origin.
+      hmac_ctx = PK11_CreateContextBySymKey(mech, CKA_SIGN, skey, &param);
+      if (!hmac_ctx) {
+        goto abort;
+      }
+      status = PK11_DigestBegin(hmac_ctx);
+      if (status != SECSuccess) {
+        goto abort;
+      }
+      status = PK11_DigestOp(hmac_ctx, unconst_uchar_cast(buf), bufl);
+      if (status != SECSuccess) {
+        goto abort;
+      }
+      status = PK11_DigestFinal(hmac_ctx, result, &hmac_len, HMAC_LENGTH);
+      if (status != SECSuccess) {
+        goto abort;
+      }
+      MOZ_ASSERT(hmac_len == HMAC_LENGTH);
+      rv = NS_OK;
 
-    static bool loaded = false;
-    if (!loaded) {
-      // load OriginUuids from disk.
-    }
-    OriginUuid* originUuid;
-    if (!mManager->mOriginUuids.Get(origin, &originUuid)) {
-      char uuid[NSID_LENGTH];
-      {
-        nsresult rv;
-        nsID id;
-        {
-          nsCOMPtr<nsIUUIDGenerator> uuidgen =
-              do_GetService("@mozilla.org/uuid-generator;1", &rv);
-          NS_ENSURE_SUCCESS(rv, rv);
-          rv = uuidgen->GenerateUUIDInPlace(&id);
-          NS_ENSURE_SUCCESS(rv, rv);
-        }
-        id.ToProvidedString(uuid);
+    abort:
+      if (hmac_ctx) {
+        PK11_DestroyContext(hmac_ctx, PR_TRUE);
       }
-      originUuid = new OriginUuid(uuid, false);
-      mManager->mOriginUuids.Put(origin, originUuid);
+      if (skey) {
+        PK11_FreeSymKey(skey);
+      }
+      if (slot) {
+        PK11_FreeSlot(slot);
+      }
+      return rv;
     }
 
-    unsigned char mac[20];
+  public:
+    OriginUuidsLoader()
+    : mUnsaved(false), mManager(MediaManager::GetInstance()) {}
+
+    nsresult
+    AnonymizeId(nsAString& aId, const nsACString& origin, bool aInPrivateBrowsing)
     {
+      // The persistent deviceId would be a supercookie if we returned it. See
+      // http://w3c.github.io/mediacapture-main/getusermedia.html#attributes-8
+      //
+      // 1. Get (or create) a persistent uuid for this origin.
+      // 2. Return hmac_sha1(uuid, id) - an anonymized id unique to origin.
+
+      static bool loaded = false;
+      if (!loaded) {
+        Load();
+        loaded = true;
+      }
+      OriginUuid* originUuid;
+      if (!mManager->mOriginUuids.Get(origin, &originUuid)) {
+        nsresult rv;
+        nsCOMPtr<nsIUUIDGenerator> uuidgen =
+            do_GetService("@mozilla.org/uuid-generator;1", &rv);
+        NS_ENSURE_SUCCESS(rv, rv);
+
+        nsID nsid;
+        rv = uuidgen->GenerateUUIDInPlace(&nsid);
+        NS_ENSURE_SUCCESS(rv, rv);
+        char uuid[NSID_LENGTH];
+        nsid.ToProvidedString(uuid);
+
+        originUuid = new OriginUuid(uuid, aInPrivateBrowsing);
+        mManager->mOriginUuids.Put(origin, originUuid);
+        mUnsaved = true;
+      }
+
+      unsigned char mac[HMAC_LENGTH];
       NS_ConvertUTF16toUTF8 id(aId);
       hmac_sha1(originUuid->mUuid.get(), originUuid->mUuid.Length(),
                 id.get(), id.Length(), mac);
+
+      char hex[sizeof(mac) * 2 + 1];
+      auto& m = mac;
+      PR_snprintf(hex, sizeof(hex), // Use first 16 bytes of hmac as id
+                  "%.2x%.2x%.2x%.2x%.2x%.2x%.2x%.2x"
+                  "%.2x%.2x%.2x%.2x%.2x%.2x%.2x%.2x",
+                  m[0], m[1], m[2], m[3], m[4], m[5], m[6], m[7],
+                  m[8], m[9], m[10],m[11], m[12], m[13], m[14], m[15]);
+      aId = NS_ConvertUTF8toUTF16(hex);
+      return NS_OK;
     }
-    char hex[sizeof(mac) * 2 + 1];
-    auto& m = mac;
-    PR_snprintf(hex, sizeof(hex), // Use first 16 bytes of hmac as id
-                "%.2x%.2x%.2x%.2x%.2x%.2x%.2x%.2x"
-                "%.2x%.2x%.2x%.2x%.2x%.2x%.2x%.2x",
-                m[0], m[1], m[2], m[3], m[4], m[5], m[6], m[7],
-                m[8], m[9], m[10],m[11], m[12], m[13], m[14], m[15]);
-    aId = NS_ConvertUTF8toUTF16(hex);
-    return NS_OK;
-  }
+
+    already_AddRefed<nsIFile>
+    GetFile()
+    {
+      MOZ_ASSERT(mManager->mProfileDir);
+      nsCOMPtr<nsIFile> file;
+      nsresult rv = mManager->mProfileDir->Clone(getter_AddRefs(file));
+      if (NS_WARN_IF(NS_FAILED(rv))) {
+        return nullptr;
+      }
+      file->Append(NS_LITERAL_STRING(ORIGINUUIDS_FILE));
+      return file.forget();
+    }
+
+    // Format of file is (first line is version #):
+    //
+    // 1
+    // {54b1d3ad-18ad-0546-a551-80c1bf425057} http://fiddle.jshell.net
+    // {f9c2a045-53b4-0546-bdd0-b96f41850f8a} http://mozilla.github.io
+    // etc.
+
+    nsresult Read()
+    {
+      nsCOMPtr<nsIFile> file = GetFile();
+      if (NS_WARN_IF(!file)) {
+        return NS_ERROR_UNEXPECTED;
+      }
+      bool exists;
+      nsresult rv = file->Exists(&exists);
+      if (NS_WARN_IF(NS_FAILED(rv))) {
+        return rv;
+      }
+      if (!exists) {
+        return NS_OK;
+      }
+
+      nsCOMPtr<nsIInputStream> stream;
+      rv = NS_NewLocalFileInputStream(getter_AddRefs(stream), file);
+      if (NS_WARN_IF(NS_FAILED(rv))) {
+        return rv;
+      }
+      nsCOMPtr<nsILineInputStream> i = do_QueryInterface(stream);
+      MOZ_ASSERT(i);
+
+      nsCString line;
+      bool hasMoreLines;
+      rv = i->ReadLine(line, &hasMoreLines);
+      if (NS_WARN_IF(NS_FAILED(rv))) {
+        return rv;
+      }
+      if (!line.EqualsLiteral(ORIGINUUIDS_VERSION)) {
+        return NS_ERROR_FAILURE;
+      }
+
+      while (hasMoreLines) {
+        rv = i->ReadLine(line, &hasMoreLines); \
+        if (NS_WARN_IF(NS_FAILED(rv))) {
+          return rv;
+        }
+        int32_t f = line.FindChar(' ');
+        // Ignore any line that has no space, per format in the comment above.
+        if (f >= 0) {
+          const nsACString& uuid = Substring(line, 0, f);
+          const nsACString& origin = Substring(line, f+1);
+          mManager->mOriginUuids.Put(origin, new OriginUuid(uuid, false));
+        }
+      }
+      return NS_OK;
+    }
+
+    static PLDHashOperator
+    HashWriter(const nsACString& aOrigin, OriginUuid* aOriginUuid, void *aUserArg)
+    {
+      auto* stream = static_cast<nsIOutputStream *>(aUserArg);
+
+      if (!aOriginUuid->mPrivateBrowsing) {
+        nsCString buffer;
+        buffer.Append(aOriginUuid->mUuid);
+        buffer.Append(' ');
+        buffer.Append(aOrigin);
+        buffer.Append('\n');
+
+        uint32_t count;
+        nsresult rv = stream->Write(buffer.Data(), buffer.Length(), &count);
+        if (NS_WARN_IF(NS_FAILED(rv)) || count != buffer.Length()) {
+          return PL_DHASH_STOP;
+        }
+      }
+      return PL_DHASH_NEXT;
+    }
+
+    nsresult
+    Write()
+    {
+      nsCOMPtr<nsIFile> file = GetFile();
+      if (NS_WARN_IF(!file)) {
+        return NS_ERROR_UNEXPECTED;
+      }
+
+      nsCOMPtr<nsIOutputStream> stream;
+      nsresult rv = NS_NewSafeLocalFileOutputStream(getter_AddRefs(stream), file);
+      if (NS_WARN_IF(NS_FAILED(rv))) {
+        return rv;
+      }
+      nsAutoCString buffer;
+      buffer.AppendLiteral(ORIGINUUIDS_VERSION);
+      buffer.Append('\n');
+
+      uint32_t count;
+      rv = stream->Write(buffer.Data(), buffer.Length(), &count);
+      if (NS_WARN_IF(NS_FAILED(rv))) {
+        return rv;
+      }
+      if (count != buffer.Length()) {
+        return NS_ERROR_UNEXPECTED;
+      }
+      mManager->mOriginUuids.EnumerateRead(HashWriter, stream.get());
+
+      nsCOMPtr<nsISafeOutputStream> safeStream = do_QueryInterface(stream);
+      MOZ_ASSERT(safeStream);
+
+      rv = safeStream->Finish();
+      if (NS_WARN_IF(NS_FAILED(rv))) {
+        return rv;
+      }
+      return NS_OK;
+    }
+
+    nsresult Load()
+    {
+      nsresult rv = Read();
+      if (NS_WARN_IF(NS_FAILED(rv))) {
+        Delete();
+      }
+      return rv;
+    }
+
+    nsresult Save()
+    {
+      nsresult rv = Write();
+      if (NS_FAILED(rv)) {
+        NS_WARNING("Failed to write data for EnumerateDevices id-persistence.");
+        Delete();
+      }
+      return rv;
+    }
+
+    nsresult Delete()
+    {
+      nsCOMPtr<nsIFile> file = GetFile();
+      if (NS_WARN_IF(!file)) {
+        return NS_ERROR_UNEXPECTED;
+      }
+      nsresult rv = file->Remove(false);
+      if (rv == NS_ERROR_FILE_NOT_FOUND) {
+        return NS_OK;
+      }
+      if (NS_WARN_IF(NS_FAILED(rv))) {
+        return rv;
+      }
+      return NS_OK;
+    }
+
+    bool mUnsaved;
+  private:
+    nsRefPtr<MediaManager> mManager;
+  };
 
 public:
   GetUserMediaDevicesTask(
     const MediaStreamConstraints& aConstraints,
     already_AddRefed<nsIGetUserMediaDevicesSuccessCallback> aOnSuccess,
     already_AddRefed<nsIDOMGetUserMediaErrorCallback> aOnFailure,
     uint64_t aWindowId, nsACString& aAudioLoopbackDev,
     nsACString& aVideoLoopbackDev, bool aPrivileged, const nsACString& aOrigin,
-    bool aUseFakeDevices)
+    bool aInPrivateBrowsing, bool aUseFakeDevices)
     : mConstraints(aConstraints)
     , mOnSuccess(aOnSuccess)
     , mOnFailure(aOnFailure)
     , mManager(MediaManager::GetInstance())
     , mWindowId(aWindowId)
     , mLoopbackAudioDevice(aAudioLoopbackDev)
     , mLoopbackVideoDevice(aVideoLoopbackDev)
     , mPrivileged(aPrivileged)
     , mOrigin(aOrigin)
+    , mInPrivateBrowsing(aInPrivateBrowsing)
     , mUseFakeDevices(aUseFakeDevices) {}
 
   void // NS_IMETHOD
   Run()
   {
     NS_ASSERTION(!NS_IsMainThread(), "Don't call on main thread");
 
     nsRefPtr<MediaEngine> backend;
@@ -1523,25 +1715,29 @@ public:
                  mLoopbackAudioDevice.get());
       for (auto& source : sources) {
         result->AppendElement(source);
       }
     }
 
     nsresult rv = NS_OK;
     if (!mPrivileged) {
+      OriginUuidsLoader loader;
       for (auto& source : *result) {
         nsString id;
         source->GetId(id);
-        rv = AnonymizeId(id, mOrigin);
+        rv = loader.AnonymizeId(id, mOrigin, mInPrivateBrowsing);
         if (NS_FAILED(rv)) {
           break;
         }
         source->SetId(id);
       }
+      if (loader.mUnsaved) {
+        loader.Save();
+      }
     }
     if (NS_SUCCEEDED(rv)) {
       NS_DispatchToMainThread(new DeviceSuccessCallbackRunnable(mWindowId,
                                                                 mOnSuccess,
                                                                 mOnFailure,
                                                                 result.forget()));
     } else {
       nsRefPtr<MediaMgrError> error = new
@@ -1566,16 +1762,17 @@ private:
   const nsString mCallId;
   // Audio & Video loopback devices to be used based on
   // the preference settings. This is currently used for
   // automated media tests only.
   nsCString mLoopbackAudioDevice;
   nsCString mLoopbackVideoDevice;
   bool mPrivileged;
   nsCString mOrigin;
+  bool mInPrivateBrowsing;
   bool mUseFakeDevices;
 };
 
 MediaManager::MediaManager()
   : mMediaThread(nullptr)
   , mMutex("mozilla::MediaManager")
   , mBackend(nullptr) {
   mPrefs.mWidth  = 0; // adaptive default
@@ -1998,34 +2195,45 @@ MediaManager::GetUserMediaDevices(nsPIDO
   nsAdoptingCString loopbackVideoDevice =
     Preferences::GetCString("media.video_loopback_dev");
   bool useFakeStreams =
     Preferences::GetBool("media.navigator.streams.fake", false);
 
   nsCString origin;
   nsPrincipal::GetOriginForURI(aWindow->GetDocumentURI(),
                                getter_Copies(origin));
-
+  bool inPrivateBrowsing;
+  {
+    nsCOMPtr<nsIDocument> doc = aWindow->GetDoc();
+    nsCOMPtr<nsILoadContext> loadContext = doc->GetLoadContext();
+    inPrivateBrowsing = loadContext && loadContext->UsePrivateBrowsing();
+  }
   MediaManager::GetMessageLoop()->PostTask(FROM_HERE,
     new GetUserMediaDevicesTask(
       aConstraints, onSuccess.forget(), onFailure.forget(),
       (aInnerWindowID ? aInnerWindowID : aWindow->WindowID()),
       loopbackAudioDevice, loopbackVideoDevice, aPrivileged, origin,
-      useFakeStreams));
+      inPrivateBrowsing, useFakeStreams));
 
   return NS_OK;
 }
 
 nsresult
 MediaManager::EnumerateDevices(nsPIDOMWindow* aWindow,
                                nsIGetUserMediaDevicesSuccessCallback* aOnSuccess,
                                nsIDOMGetUserMediaErrorCallback* aOnFailure)
 {
   NS_ASSERTION(NS_IsMainThread(), "Only call on main thread");
 
+  if (!mProfileDir) {
+    nsresult rv = NS_GetSpecialDirectory(NS_APP_USER_PROFILE_50_DIR,
+                                         getter_AddRefs(mProfileDir));
+    NS_ENSURE_SUCCESS(rv, rv);
+  }
+
   MediaStreamConstraints c;
   c.mVideo.SetAsBoolean() = true;
   c.mAudio.SetAsBoolean() = true;
 
   AddWindowID(aWindow->WindowID());
   return GetUserMediaDevices(aWindow, c, aOnSuccess, aOnFailure, 0, false);
 }
 
--- a/dom/media/MediaManager.h
+++ b/dom/media/MediaManager.h
@@ -460,16 +460,19 @@ private:
 
 class OriginUuid
 {
 public:
   OriginUuid(char *aUuid, bool aPrivateBrowsing)
   : mPrivateBrowsing(aPrivateBrowsing) {
     mUuid.Append(aUuid);
   }
+  OriginUuid(const nsACString& aUuid, bool aPrivateBrowsing)
+  : mUuid(aUuid), mPrivateBrowsing(aPrivateBrowsing) {}
+
   nsCString mUuid;
   bool mPrivateBrowsing;
 };
 
 typedef nsTArray<nsRefPtr<GetUserMediaCallbackMediaStreamListener> > StreamListeners;
 typedef nsClassHashtable<nsUint64HashKey, StreamListeners> WindowTable;
 
 class MediaDevice : public nsIMediaDevice
@@ -610,16 +613,17 @@ private:
   void IterateWindowListeners(nsPIDOMWindow *aWindow,
                               WindowListenerCallback aCallback,
                               void *aData);
 
   void StopMediaStreams();
 
   // ONLY access from MediaManagerThread so we don't need to lock
   nsClassHashtable<nsCStringHashKey, OriginUuid> mOriginUuids;
+  nsCOMPtr<nsIFile> mProfileDir;
 
   // ONLY access from MainThread so we don't need to lock
   WindowTable mActiveWindows;
   nsClassHashtable<nsStringHashKey, GetUserMediaTask> mActiveCallbacks;
   nsClassHashtable<nsUint64HashKey, nsTArray<nsString>> mCallIds;
 
   // Always exists
   nsAutoPtr<base::Thread> mMediaThread;