Bug 1336208 - Part 5: Implementing the font downloading for fingerprinting resistance. r?jfkthame,arthuredelstein,gijs draft
authorTim Huang <tihuang@mozilla.com>
Tue, 28 Nov 2017 14:47:22 +0800
changeset 710228 fb14402dfad0d86b7529c6452793a0bfe7860212
parent 707552 9dd463e88bc34a71bff0a2d931e4b60e41b42070
child 710229 14ab0e063a87ae6f6980b926bf6ed9df9b288916
push id92788
push userbmo:tihuang@mozilla.com
push dateFri, 08 Dec 2017 23:11:53 +0000
reviewersjfkthame, arthuredelstein, gijs
bugs1336208
milestone59.0a1
Bug 1336208 - Part 5: Implementing the font downloading for fingerprinting resistance. r?jfkthame,arthuredelstein,gijs This patch enables Firefox to download fonts according to the fonts list, which is downloaded through Kinto Server, when fingerprinting resistance is enabled. This patch adds FontsDownloader.js. which is responsible for downloading fonts from the server. The nsRFPService will issue FontsDownloader to try to download fonts when 'privacy.resistFingerprinting' is flipped to true. The FontsDownloader will check whether local fonts are up-to-date and download fonts if they are outdated. The downloaded fonts will be first put in a staging directory and be moved to the actual font directory at the next start-up of Firefox. The downloaded fonts will be moved into the GRE directory if it is writable, otherwise, they will be moved to the font directory in the profile directory. From then on, we will maintain fonts on that. In addition, this adds a new IPC method in PContent for allowing content process to issue parent process to download fonts. This is necessary since the sync of Kinto data could happen out of parent process. MozReview-Commit-ID: KrMGdiq8UdF
browser/components/nsBrowserGlue.js
dom/ipc/ContentParent.cpp
dom/ipc/ContentParent.h
dom/ipc/PContent.ipdl
modules/libpref/init/all.js
toolkit/components/resistfingerprinting/FontsDownloader.jsm
toolkit/components/resistfingerprinting/moz.build
toolkit/components/resistfingerprinting/nsRFPService.cpp
toolkit/components/resistfingerprinting/nsRFPService.h
--- a/browser/components/nsBrowserGlue.js
+++ b/browser/components/nsBrowserGlue.js
@@ -64,16 +64,17 @@ XPCOMUtils.defineLazyModuleGetters(this,
   SessionStore: "resource:///modules/sessionstore/SessionStore.jsm",
   ShellService: "resource:///modules/ShellService.jsm",
   SimpleServiceDiscovery: "resource://gre/modules/SimpleServiceDiscovery.jsm",
   TabCrashHandler: "resource:///modules/ContentCrashHandlers.jsm",
   UIState: "resource://services-sync/UIState.jsm",
   UITour: "resource:///modules/UITour.jsm",
   WebChannel: "resource://gre/modules/WebChannel.jsm",
   WindowsRegistry: "resource://gre/modules/WindowsRegistry.jsm",
+  FontsDownloader: "resource:///modules/FontsDownloader.jsm",
 });
 
 /* global AboutHome:false, ContentPrefServiceParent:false, ContentSearch:false,
           UpdateListener:false, webrtcUI:false */
 
 /**
  * IF YOU ADD OR REMOVE FROM THIS LIST, PLEASE UPDATE THE LIST ABOVE AS WELL.
  * XXX Bug 1325373 is for making eslint detect these automatically.
@@ -1009,16 +1010,17 @@ BrowserGlue.prototype = {
     if (Object.prototype.hasOwnProperty.call(this, "pingCentre")) {
       this.pingCentre.uninit();
     }
 
     PageThumbs.uninit();
     NewTabUtils.uninit();
     AutoCompletePopup.uninit();
     DateTimePickerHelper.uninit();
+    FontsDownloader.uninit();
   },
 
   // All initial windows have opened.
   _onWindowsRestored: function BG__onWindowsRestored() {
     if (this._windowsWereRestored) {
       return;
     }
     this._windowsWereRestored = true;
@@ -1161,16 +1163,20 @@ BrowserGlue.prototype = {
       handlerService.asyncInit();
     });
 
     if (AppConstants.platform == "win") {
       Services.tm.idleDispatchToMainThread(() => {
         JawsScreenReaderVersionCheck.onWindowsRestored();
       });
     }
+
+    Services.tm.idleDispatchToMainThread(() => {
+      FontsDownloader.init();
+    });
   },
 
   /**
    * Use this function as an entry point to schedule tasks that need
    * to run once per session, at any arbitrary point in time.
    * This function will be called from an idle observer. Check the value of
    * LATE_TASKS_IDLE_TIME_SEC to see the current value for this idle
    * observer.
--- a/dom/ipc/ContentParent.cpp
+++ b/dom/ipc/ContentParent.cpp
@@ -5432,16 +5432,23 @@ mozilla::ipc::IPCResult
 ContentParent::RecvMaybeReloadPlugins()
 {
   RefPtr<nsPluginHost> pluginHost = nsPluginHost::GetInst();
   pluginHost->ReloadPlugins();
   return IPC_OK();
 }
 
 mozilla::ipc::IPCResult
+ContentParent::RecvRFPDownloadFonts()
+{
+  nsRFPService::GetOrCreate()->MaybeDownloadFonts();
+  return IPC_OK();
+}
+
+mozilla::ipc::IPCResult
 ContentParent::RecvDeviceReset()
 {
   GPUProcessManager* pm = GPUProcessManager::Get();
   if (pm) {
     pm->SimulateDeviceReset();
   }
 
   return IPC_OK();
--- a/dom/ipc/ContentParent.h
+++ b/dom/ipc/ContentParent.h
@@ -298,16 +298,18 @@ public:
   virtual mozilla::ipc::IPCResult RecvCreateGMPService() override;
 
   virtual mozilla::ipc::IPCResult RecvLoadPlugin(const uint32_t& aPluginId, nsresult* aRv,
                                                  uint32_t* aRunID,
                                                  Endpoint<PPluginModuleParent>* aEndpoint) override;
 
   virtual mozilla::ipc::IPCResult RecvMaybeReloadPlugins() override;
 
+  virtual mozilla::ipc::IPCResult RecvRFPDownloadFonts() override;
+
   virtual mozilla::ipc::IPCResult RecvConnectPluginBridge(const uint32_t& aPluginId,
                                                           nsresult* aRv,
                                                           Endpoint<PPluginModuleParent>* aEndpoint) override;
 
   virtual mozilla::ipc::IPCResult RecvUngrabPointer(const uint32_t& aTime) override;
 
   virtual mozilla::ipc::IPCResult RecvRemovePermission(const IPC::Principal& aPrincipal,
                                                        const nsCString& aPermissionType,
--- a/dom/ipc/PContent.ipdl
+++ b/dom/ipc/PContent.ipdl
@@ -1105,16 +1105,18 @@ parent:
                              IHandlerControlHolder aHandlerControl);
 
     async AddMemoryReport(MemoryReport aReport);
     async FinishMemoryReport(uint32_t aGeneration);
 
     async MaybeReloadPlugins();
 
     async BHRThreadHang(HangDetails aHangDetails);
+
+    async RFPDownloadFonts();
 both:
      async AsyncMessage(nsString aMessage, CpowEntry[] aCpows,
                         Principal aPrincipal, ClonedMessageData aData);
 
     /**
      * Notify `push-subscription-modified` observers in the parent and child.
      */
     async NotifyPushSubscriptionModifiedObservers(nsCString scope,
--- a/modules/libpref/init/all.js
+++ b/modules/libpref/init/all.js
@@ -2620,16 +2620,17 @@ pref("services.blocklist.pinning.collect
 pref("services.blocklist.pinning.checked", 0);
 pref("services.blocklist.gfx.collection", "gfx");
 pref("services.blocklist.gfx.checked", 0);
 
 // Fonts downloading for fingerprinting resistance via settings server (Kinto)
 pref("privacy.resistFingerprinting.fonts.bucket", "fingerprinting-defenses");
 pref("privacy.resistFingerprinting.fonts.collection", "fonts");
 pref("privacy.resistFingerprinting.fonts.checked", 0);
+pref("privacy.resistFingerprinting.fonts.server", "https://firefox-settings-attachments.cdn.mozilla.net/");
 
 // Controls whether signing should be enforced on signature-capable blocklist
 // collections.
 pref("services.blocklist.signing.enforced", true);
 
 // Enable blocklists via the services settings mechanism
 pref("services.blocklist.update_enabled", true);
 
new file mode 100644
--- /dev/null
+++ b/toolkit/components/resistfingerprinting/FontsDownloader.jsm
@@ -0,0 +1,236 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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 = ["FontsDownloader"];
+
+const { classes: Cc, Constructor: CC, interfaces: Ci, utils: Cu } = Components;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
+  "resource://gre/modules/NetUtil.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Services",
+  "resource://gre/modules/Services.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "AppConstants",
+  "resource://gre/modules/AppConstants.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "OS",
+  "resource://gre/modules/osfile.jsm");
+
+Cu.importGlobalProperties(["crypto"]);
+
+const RFP_FONTS_LIST_DIR = "fingerprinting-defenses";
+const RFP_FONTS_DIR = "rfp-fonts";
+const RFP_FONTS_STAGING_DIR = "rfp-fonts-staging";
+const RFP_FONTS_LIST_FILE = "fonts.json";
+const RFP_PREF = "privacy.resistFingerprinting";
+const RFP_FONTS_DOWNLOAD_SERVER_PREF = "privacy.resistFingerprinting.fonts.server";
+const RFP_FONTS_NEED_MOVE_PREF = "privacy.resistFingerprinting.fonts.need_move";
+const RFP_FONTS_LAST_UPDATE_TIME_PREF = "privacy.resistFingerprinting.fonts.last_update_time";
+
+/**
+ * A helper function for verifying data with the given hash.
+ *
+ * @param {Uint8Array} aData      The data to be verified.
+ * @param {String}     aHash      The hash value
+ * @param {String}     aAlgorithm The hash algorithm
+ * @return {boolean} A boolean indicates whether hash matches.
+ */
+async function verifyHash(aData, aHash, aAlgorithm) {
+  const hash = await crypto.subtle.digest(aAlgorithm, aData);
+  const hashArray = Array.from(new Uint8Array(hash));
+  const hashHex = hashArray.map(
+    b => ("00" + b.toString(16)).slice(-2)).join(""); // convert bytes to hex string
+
+  return hashHex === aHash;
+}
+
+let FontsDownloader = {
+  init() {
+    this._shutdown = false;
+    this._initialized = true;
+
+    // We will try to download fonts if fingerprinting resistance is enabled during
+    // initialization.
+    if (Services.prefs.getBoolPref(RFP_PREF)) {
+      this.maybeDownloadFonts();
+    }
+
+    Services.obs.addObserver(this, "resist-fingerprinting:start-download-fonts");
+  },
+
+  uninit() {
+    if (!this._initialized) {
+      return;
+    }
+
+    this._shutdown = true;
+    Services.obs.removeObserver(this, "resist-fingerprinting:start-download-fonts");
+  },
+
+  observe(aSubject, aTopic, aData) {
+    if (aTopic === "resist-fingerprinting:start-download-fonts") {
+      this.maybeDownloadFonts();
+    }
+  },
+
+  async maybeDownloadFonts() {
+    if (this.downloading) {
+      return;
+    }
+
+    this.downloading = true;
+
+    try {
+      let fontsList = await this.loadFontsList();
+
+      await this.downloadFonts(fontsList);
+
+      // We set the pref here for telling nsRFPService to move fonts from staging
+      // directory to the font directory.
+      Services.prefs.setBoolPref(RFP_FONTS_NEED_MOVE_PREF, true);
+
+      // Record the last time of the successful downloading.
+      Services.prefs.setIntPref(RFP_FONTS_LAST_UPDATE_TIME_PREF, (Date.now() / 1000));
+    } catch (aError) {
+      Cu.reportError(aError);
+    }
+
+    this.downloading = false;
+  },
+
+  async loadFontsList() {
+    // We first read fonts list file in profile directory. This file is the
+    // up-to-date list which is updated by the blocklist client.
+    let fontsListPath = OS.Path.join(OS.Constants.Path.profileDir,
+                                     RFP_FONTS_LIST_DIR,
+                                     RFP_FONTS_LIST_FILE);
+
+    try {
+      let data = await OS.File.read(fontsListPath, {encoding: "utf-8"});
+      return JSON.parse(data);
+    } catch (ex) {
+      if (!(ex instanceof OS.File.Error) || !ex.becauseNoSuchFile) {
+        throw ex;
+      }
+    }
+
+    // If the fonts list file in the profile folder doesn't exist, this means
+    // the first update has not been triggered by Firefox. So we will use the
+    // default fonts list instead which is shipped with Firefox.
+    let defaultFile = Services.dirsvc.get("DefRt",
+                                          Ci.nsIFile);
+    fontsListPath = OS.Path.join(defaultFile.path,
+                                 RFP_FONTS_LIST_DIR,
+                                 RFP_FONTS_LIST_FILE);
+
+    let data = await OS.File.read(fontsListPath, {encoding: "utf-8"});
+    return JSON.parse(data);
+  },
+
+  async downloadFonts(aFontList) {
+    let fonts = aFontList.data;
+    let lastFontsUpdateTime = Services.prefs.getIntPref(RFP_FONTS_LAST_UPDATE_TIME_PREF, 0) * 1000;
+
+    // If the server url is empty, we will bail out here and throw nothing. So,
+    // we can disable the whole download by clearing this server pref.
+    let serverURL = Services.prefs.getCharPref(RFP_FONTS_DOWNLOAD_SERVER_PREF);
+
+    if (!serverURL) {
+      return;
+    }
+
+    // We only download the font that matches to this platform and skip fonts
+    // that are still up-to-date.
+    let expectedDownloadFonts = fonts.filter(f => f.platforms.includes(AppConstants.platform))
+                                     .filter(f => f.last_modified >= lastFontsUpdateTime);
+
+    for (let font of expectedDownloadFonts) {
+      if (this._shutdown) {
+        return;
+      }
+
+      let fontFilename = font.attachment.filename;
+      let localFontPath = await this._getLocalFontPath(fontFilename);
+      let localFontData;
+
+      // Check the local font file and verify whether it is up-to-date.
+      try {
+        let localFontStat = await OS.File.stat(localFontPath);
+
+        if (localFontStat.size === font.attachment.size) {
+          // If the size matches, we need to further check the hash to know
+          // whether it is up-to-date.
+          localFontData = await OS.File.read(localFontPath);
+        }
+      } catch (ex) {
+        if (!(ex instanceof OS.File.Error) || !ex.becauseNoSuchFile) {
+          throw ex;
+        }
+      }
+
+      if (localFontData &&
+          await verifyHash(localFontData, font.attachment.hash, "SHA-256")) {
+        // The local font file is up-to-date, we skip to the next download.
+        continue;
+      }
+
+      // Download the font and decompress it.
+      let fontURL = serverURL + font.attachment.location;
+      let response = await fetch(fontURL);
+      let fontData = new Uint8Array(await response.arrayBuffer());
+
+      // If the download size is wrong, we skip to the next one.
+      if (fontData.byteLength !== font.attachment.size) {
+        continue;
+      }
+
+      let stagingFontPath = OS.Path.join(OS.Constants.Path.profileDir,
+                                         RFP_FONTS_STAGING_DIR,
+                                         fontFilename);
+
+      await OS.File.makeDir(OS.Path.dirname(stagingFontPath),
+                            {from: OS.Constants.Path.profileDir});
+
+      // Write the downloaded font into the staging font directory. The font file will
+      // be moved into the font directory at the next start-up of Firefox.
+      await OS.File.writeAtomic(stagingFontPath, fontData,
+                                {tmpPath: stagingFontPath + ".tmp"});
+
+    }
+  },
+
+  /**
+   * Find the path to use for rfp fonts. Registering fonts from 2 directories
+   * is likely to cause issues. While we prefer the GreD directory in the application,
+   * the user may not have write permissions for it. If this is the case, fonts will
+   * be downloaded and/or copied into the profile directory. Thus, when looking for
+   * fonts, we check the profile directory first, because if there are fonts there
+   * they are guaranteed to be at least as new as what is in GreD.
+   *
+   * @param {String} aFontFileName The file name of the font.
+   * @return {String} The path of the local font.
+   */
+  async _getLocalFontPath(aFontFileName) {
+    // First, we try to get the path of font directory.
+    if (!this._getLocalFontDirPath) {
+      // We use the font file in profile directory if it exists. Otherwise, we use
+      // the one in GRE directory.
+      let localFontPath = OS.Path.join(OS.Constants.Path.localProfileDir,
+                                       RFP_FONTS_DIR);
+      let isExist = await OS.File.exists(localFontPath);
+
+      if (!isExist) {
+        let greDir = Services.dirsvc.get("GreD",
+                                         Ci.nsIFile);
+        localFontPath = OS.Path.join(greDir.path,
+                                     RFP_FONTS_DIR);
+      }
+
+      this._getLocalFontDirPath = localFontPath;
+    }
+
+    return OS.Path.join(this._getLocalFontDirPath, aFontFileName);
+  }
+};
--- a/toolkit/components/resistfingerprinting/moz.build
+++ b/toolkit/components/resistfingerprinting/moz.build
@@ -3,16 +3,22 @@
 # 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/.
 
 UNIFIED_SOURCES += [
     'nsRFPService.cpp',
 ]
 
+EXTRA_JS_MODULES += [
+    'FontsDownloader.jsm',
+]
+
+include('/ipc/chromium/chromium-config.mozbuild')
+
 FINAL_LIBRARY = 'xul'
 
 FINAL_TARGET_FILES.defaults['fingerprinting-defenses'] += ['fonts.json']
 
 if CONFIG['MOZ_BUILD_APP'] == 'browser':
     DIST_SUBDIR = 'browser'
 
 EXPORTS += [
--- a/toolkit/components/resistfingerprinting/nsRFPService.cpp
+++ b/toolkit/components/resistfingerprinting/nsRFPService.cpp
@@ -8,45 +8,65 @@
 #include <algorithm>
 #include <time.h>
 
 #include "mozilla/ClearOnShutdown.h"
 #include "mozilla/Preferences.h"
 #include "mozilla/Services.h"
 #include "mozilla/StaticPtr.h"
 
+#include "mozilla/dom/ContentChild.h"
+#include "mozilla/dom/ContentParent.h"
+
+#include "gfxPlatform.h"
+
+#include "nsAppDirectoryServiceDefs.h"
 #include "nsCOMPtr.h"
 #include "nsCoord.h"
+#include "nsDirectoryServiceDefs.h"
 #include "nsServiceManagerUtils.h"
 #include "nsString.h"
 #include "nsXULAppAPI.h"
 #include "nsPrintfCString.h"
 
+#include "nsIFontEnumerator.h"
 #include "nsIObserverService.h"
 #include "nsIPrefBranch.h"
 #include "nsIPrefService.h"
+#include "nsISimpleEnumerator.h"
 #include "nsIXULAppInfo.h"
 #include "nsIXULRuntime.h"
 #include "nsJSUtils.h"
 
 #include "prenv.h"
 
 #include "js/Date.h"
 
 using namespace mozilla;
 using namespace std;
 
 #define RESIST_FINGERPRINTING_PREF "privacy.resistFingerprinting"
 #define RFP_SPOOFED_FRAMES_PER_SEC_PREF "privacy.resistFingerprinting.video_frames_per_sec"
 #define RFP_SPOOFED_DROPPED_RATIO_PREF  "privacy.resistFingerprinting.video_dropped_ratio"
 #define RFP_TARGET_VIDEO_RES_PREF "privacy.resistFingerprinting.target_video_res"
+#define RFP_FONTS_DIR "rfp-fonts"
+#define RFP_FONTS_STAGING_DIR "rfp-fonts-staging"
+#define RFP_FONTS_NEED_MOVE_PREF "privacy.resistFingerprinting.fonts.need_move"
 #define RFP_SPOOFED_FRAMES_PER_SEC_DEFAULT 30
 #define RFP_SPOOFED_DROPPED_RATIO_DEFAULT  5
 #define RFP_TARGET_VIDEO_RES_DEFAULT 480
 #define PROFILE_INITIALIZED_TOPIC "profile-initial-state"
+#define PROFILE_DO_CHANGE_TOPIC "profile-do-change"
+
+#define RFP_DOWNLOAD_FONTS_MSG  "resist-fingerprinting:download-fonts"
+#define RFP_DOWNLOAD_FONTS_START_MSG "resist-fingerprinting:start-download-fonts"
+// A message for telling nsRFPService to move fonts, this is for testing purpose.
+#define RFP_MOVE_FONTS_MSG "resist-fingerprinting:move-fonts"
+// A message for notifying the moving of fonts is finished, this is for testing purpose.
+#define RFP_MOVE_FONTS_FINISH "resist-fingerprinting:move-fonts-finish"
 
 NS_IMPL_ISUPPORTS(nsRFPService, nsIObserver)
 
 static StaticRefPtr<nsRFPService> sRFPService;
 static bool sInitialized = false;
 Atomic<bool, ReleaseAcquire> nsRFPService::sPrivacyResistFingerprinting;
 static uint32_t kResolutionUSec = 100000;
 static uint32_t sVideoFramesPerSec;
@@ -231,16 +251,25 @@ nsRFPService::Init()
   rv = obs->AddObserver(this, NS_XPCOM_SHUTDOWN_OBSERVER_ID, false);
   NS_ENSURE_SUCCESS(rv, rv);
 
 #if defined(XP_WIN)
   rv = obs->AddObserver(this, PROFILE_INITIALIZED_TOPIC, false);
   NS_ENSURE_SUCCESS(rv, rv);
 #endif
 
+  rv = obs->AddObserver(this, PROFILE_DO_CHANGE_TOPIC, false);
+  NS_ENSURE_SUCCESS(rv, rv);
+
+  rv = obs->AddObserver(this, RFP_DOWNLOAD_FONTS_MSG, false);
+  NS_ENSURE_SUCCESS(rv, rv);
+
+  rv = obs->AddObserver(this, RFP_MOVE_FONTS_MSG, false);
+  NS_ENSURE_SUCCESS(rv, rv);
+
   nsCOMPtr<nsIPrefBranch> prefs = do_GetService(NS_PREFSERVICE_CONTRACTID);
   NS_ENSURE_TRUE(prefs, NS_ERROR_NOT_AVAILABLE);
 
   rv = prefs->AddObserver(RESIST_FINGERPRINTING_PREF, this, false);
   NS_ENSURE_SUCCESS(rv, rv);
 
   Preferences::AddUintVarCache(&sVideoFramesPerSec,
                                RFP_SPOOFED_FRAMES_PER_SEC_PREF,
@@ -269,18 +298,20 @@ void
 nsRFPService::UpdatePref()
 {
   MOZ_ASSERT(NS_IsMainThread());
   sPrivacyResistFingerprinting = Preferences::GetBool(RESIST_FINGERPRINTING_PREF);
 
   if (sPrivacyResistFingerprinting) {
     PR_SetEnv("TZ=UTC");
     JS::SetTimeResolutionUsec(kResolutionUSec);
+    MaybeDownloadFonts();
   } else if (sInitialized) {
     JS::SetTimeResolutionUsec(0);
+
     // We will not touch the TZ value if 'privacy.resistFingerprinting' is false during
     // the time of initialization.
     if (!mInitialTZValue.IsEmpty()) {
       nsAutoCString tzValue = NS_LITERAL_CSTRING("TZ=") + mInitialTZValue;
       static char* tz = nullptr;
 
       // If the tz has been set before, we free it first since it will be allocated
       // a new value later.
@@ -313,25 +344,143 @@ void
 nsRFPService::StartShutdown()
 {
   MOZ_ASSERT(NS_IsMainThread());
 
   nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService();
 
   if (obs) {
     obs->RemoveObserver(this, NS_XPCOM_SHUTDOWN_OBSERVER_ID);
+    obs->RemoveObserver(this, PROFILE_DO_CHANGE_TOPIC);
+    obs->RemoveObserver(this, RFP_DOWNLOAD_FONTS_MSG);
+    obs->RemoveObserver(this, RFP_MOVE_FONTS_MSG);
 
     nsCOMPtr<nsIPrefBranch> prefs = do_GetService(NS_PREFSERVICE_CONTRACTID);
 
     if (prefs) {
       prefs->RemoveObserver(RESIST_FINGERPRINTING_PREF, this);
     }
   }
 }
 
+void
+nsRFPService::MaybeDownloadFonts()
+{
+  MOZ_ASSERT(NS_IsMainThread());
+
+  if (!XRE_IsParentProcess()) {
+    return;
+  }
+
+  nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService();
+
+  // Notify the observer for telling FontsDownloader to download fonts.
+  if (obs) {
+    obs->NotifyObservers(nullptr, RFP_DOWNLOAD_FONTS_START_MSG, nullptr);
+  }
+}
+
+void
+nsRFPService::MaybeMoveFontFiles()
+{
+  if (!XRE_IsParentProcess()) {
+    return;
+  }
+
+  // First, we check the pref for whether we need to move staging fonts.
+  if (!Preferences::GetBool(RFP_FONTS_NEED_MOVE_PREF)) {
+    return;
+  }
+
+  nsCOMPtr<nsIFile> stagingDir;
+  nsresult rv = NS_GetSpecialDirectory(
+    NS_APP_USER_PROFILE_50_DIR, getter_AddRefs(stagingDir));
+  NS_ENSURE_SUCCESS_VOID(rv);
+
+  rv = stagingDir->Append(NS_LITERAL_STRING(RFP_FONTS_STAGING_DIR));
+  NS_ENSURE_SUCCESS_VOID(rv);
+
+  // We will try to use GRE directory first. If we cannot write into this directory
+  // we will fallback to use profile directory instead.
+  nsCOMPtr<nsIFile> destFontDir;
+  bool usingProfileDir = false;
+  rv = NS_GetSpecialDirectory(NS_GRE_DIR, getter_AddRefs(destFontDir));
+  NS_ENSURE_SUCCESS_VOID(rv);
+
+  rv = destFontDir->Append(NS_LITERAL_STRING(RFP_FONTS_DIR));
+  NS_ENSURE_SUCCESS_VOID(rv);
+
+  nsCOMPtr<nsISimpleEnumerator> entries;
+  rv = stagingDir->GetDirectoryEntries(getter_AddRefs(entries));
+  NS_ENSURE_SUCCESS_VOID(rv);
+
+  bool hasMore;
+
+  while (NS_SUCCEEDED(entries->HasMoreElements(&hasMore)) && hasMore) {
+    nsCOMPtr<nsISupports> next;
+    rv = entries->GetNext(getter_AddRefs(next));
+    NS_ENSURE_SUCCESS_VOID(rv);
+    nsCOMPtr<nsIFile> fontFile = do_QueryInterface(next);
+    NS_ENSURE_TRUE_VOID(fontFile);
+
+    rv = fontFile->MoveToNative(destFontDir, EmptyCString());
+    if (!usingProfileDir &&
+        (rv == NS_ERROR_FILE_ACCESS_DENIED || rv == NS_ERROR_FILE_READ_ONLY)) {
+      // We move to use profile directory if we cannot write into GRE directory.
+      rv = NS_GetSpecialDirectory(
+        NS_APP_USER_PROFILE_50_DIR, getter_AddRefs(destFontDir));
+      NS_ENSURE_SUCCESS_VOID(rv);
+
+      rv = destFontDir->Append(NS_LITERAL_STRING(RFP_FONTS_DIR));
+      NS_ENSURE_SUCCESS_VOID(rv);
+      usingProfileDir = true;
+      bool isExists = false;
+
+      rv = destFontDir->Exists(&isExists);
+      NS_ENSURE_SUCCESS_VOID(rv);
+
+      // If the font directory doesn't exist in the profile directory and this
+      // profile has no right to access GreD, this means it's the first time
+      // this profile tries to move fonts from staging direcotry. In this case,
+      // we need to first copy all fonts into the profile directory from the
+      // GreD directory.
+      if (!isExists) {
+        nsCOMPtr<nsIFile> greDFontDir;
+        rv = NS_GetSpecialDirectory(NS_GRE_DIR, getter_AddRefs(destFontDir));
+        NS_ENSURE_SUCCESS_VOID(rv);
+
+        rv = greDFontDir->Append(NS_LITERAL_STRING(RFP_FONTS_DIR));
+        NS_ENSURE_SUCCESS_VOID(rv);
+
+        nsCOMPtr<nsIFile> profDir;
+        rv = NS_GetSpecialDirectory(
+          NS_APP_USER_PROFILE_50_DIR, getter_AddRefs(profDir));
+        NS_ENSURE_SUCCESS_VOID(rv);
+
+        rv = greDFontDir->CopyToNative(profDir, EmptyCString());
+        if (rv != NS_ERROR_FILE_TARGET_DOES_NOT_EXIST) {
+          NS_ENSURE_SUCCESS_VOID(rv);
+        }
+      }
+
+      // And move again.
+      rv = fontFile->MoveToNative(destFontDir, EmptyCString());
+      NS_ENSURE_SUCCESS_VOID(rv);
+    } else {
+      NS_ENSURE_SUCCESS_VOID(rv);
+    }
+  }
+
+  Preferences::ClearUser(RFP_FONTS_NEED_MOVE_PREF);
+
+  // After moving all fonts, we remove the staging directory.
+  rv = stagingDir->Remove(true);
+  NS_ENSURE_SUCCESS_VOID(rv);
+}
+
 NS_IMETHODIMP
 nsRFPService::Observe(nsISupports* aObject, const char* aTopic,
                       const char16_t* aMessage)
 {
   if (!strcmp(NS_PREFBRANCH_PREFCHANGE_TOPIC_ID, aTopic)) {
     NS_ConvertUTF16toUTF8 pref(aMessage);
 
     if (pref.EqualsLiteral(RESIST_FINGERPRINTING_PREF)) {
@@ -343,16 +492,50 @@ nsRFPService::Observe(nsISupports* aObje
         // are not reflected immediately on that platform as they are on UNIX
         // systems without this call.
         _tzset();
       }
 #endif
     }
   }
 
+  // We will catch this when the fonts list get updated. We only download fonts
+  // when fingerprinting resistance is enabled.
+  if (!strcmp(RFP_DOWNLOAD_FONTS_MSG, aTopic)) {
+    if (sPrivacyResistFingerprinting) {
+      if (XRE_IsParentProcess()) {
+        MaybeDownloadFonts();
+      } else {
+        dom::ContentChild* cc = dom::ContentChild::GetSingleton();
+
+        if (cc) {
+          Unused << cc->SendRFPDownloadFonts();
+        }
+      }
+    }
+  }
+
+  // We will start to move fonts if we receive this. This is for testing purpose.
+  if (!strcmp(RFP_MOVE_FONTS_MSG, aTopic)) {
+    MaybeMoveFontFiles();
+
+    nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService();
+
+    // Notify the observer for telling moving fonts finishes.
+    if (obs) {
+      obs->NotifyObservers(nullptr, RFP_MOVE_FONTS_FINISH, nullptr);
+    }
+  }
+
+  if (!strcmp(PROFILE_DO_CHANGE_TOPIC, aTopic)) {
+    // We move staging font files into the font directory as soon as the profile
+    // is available. This stage is before the initialization of gfxPlatfromFontList.
+    MaybeMoveFontFiles();
+  }
+
   if (!strcmp(NS_XPCOM_SHUTDOWN_OBSERVER_ID, aTopic)) {
     StartShutdown();
   }
 #if defined(XP_WIN)
   else if (!strcmp(PROFILE_INITIALIZED_TOPIC, aTopic)) {
     // If we're e10s, then we don't need to run this, since the child process will
     // simply inherit the environment variable from the parent process, in which
     // case it's unnecessary to call _tzset().
--- a/toolkit/components/resistfingerprinting/nsRFPService.h
+++ b/toolkit/components/resistfingerprinting/nsRFPService.h
@@ -47,25 +47,29 @@ public:
   // depend on the video resolution.
   static uint32_t GetSpoofedTotalFrames(double aTime);
   static uint32_t GetSpoofedDroppedFrames(double aTime, uint32_t aWidth, uint32_t aHeight);
   static uint32_t GetSpoofedPresentedFrames(double aTime, uint32_t aWidth, uint32_t aHeight);
 
   // This method generates the spoofed value of User Agent.
   static nsresult GetSpoofedUserAgent(nsACString &userAgent);
 
+  // This method triggers the font downloading process.
+  void MaybeDownloadFonts();
+
 private:
   nsresult Init();
 
   nsRFPService() {}
 
   ~nsRFPService() {}
 
   void UpdatePref();
   void StartShutdown();
+  void MaybeMoveFontFiles();
 
   static Atomic<bool, ReleaseAcquire> sPrivacyResistFingerprinting;
 
   nsCString mInitialTZValue;
 };
 
 } // mozilla namespace