From: csagan5 <32685696+csagan5@users.noreply.github.com>
Date: Fri, 30 Mar 2018 10:09:03 +0200
Subject: Multiple fingerprinting mitigations

1. getClientRects, getBoundingClientRect, measureText: add fingerprinting mitigation

Scale the result of Range::getClientRects, Element::getBoundingClientRect and
Canvas::measureText by a random +/-3/1000000th of the original value for each
float in the returned Rect/Quad.

It contains improvements from ungoogled-chromium which add two flags:
1. --fingerprinting-client-rects-noise to enable fingerprinting deception for Range::getClientRects and Element::getBoundingClientRect
2. --fingerprinting-canvas-measuretext-noise to enable fingerprinting deception for Canvas::measureText

2. Canvas: fingerprinting mitigations for image data

Modify the color data returned by ToBlob, ToDataURL and getImageData so that
it will contain randomly manipulated pixels (maximum 20) that slightly change
the color of the R,G,B components without a visible effect.

Credits to Slaviro (https://github.com/Slaviro) for coming up with a better
approach to change color components.

Added flag --fingerprinting-canvas-image-data-noise to disable Canvas image data fingerprinting deception
Add noise also to SVGs (credits to uazo)

License: GPL-3.0-only - https://spdx.org/licenses/GPL-3.0-only.html
---
 chrome/browser/BUILD.gn                       |   1 +
 chrome/browser/about_flags.cc                 |  16 ++
 content/browser/BUILD.gn                      |   1 +
 .../renderer_host/render_process_host_impl.cc |   4 +
 content/child/BUILD.gn                        |   1 +
 content/child/runtime_features.cc             |   9 +
 .../public/platform/web_runtime_features.h    |   3 +
 .../blink/renderer/core/dom/document.cc       |  18 ++
 .../blink/renderer/core/dom/document.h        |   7 +
 .../blink/renderer/core/dom/element.cc        |   9 +
 third_party/blink/renderer/core/dom/range.cc  |  12 +-
 .../html/canvas/canvas_async_blob_creator.cc  |   5 +
 .../renderer/core/html/canvas/text_metrics.cc |  18 ++
 .../renderer/core/html/canvas/text_metrics.h  |   2 +
 .../renderer/core/svg/svg_graphics_element.cc |   1 +
 .../core/svg/svg_text_content_element.cc      |  23 ++-
 .../canvas2d/base_rendering_context_2d.cc     |   5 +
 .../canvas2d/canvas_rendering_context_2d.cc   |   8 +-
 third_party/blink/renderer/platform/BUILD.gn  |   5 +-
 .../platform/exported/web_runtime_features.cc |  12 ++
 .../platform/graphics/image_data_buffer.cc    |   7 +
 .../platform/graphics/static_bitmap_image.cc  | 155 ++++++++++++++++++
 .../platform/graphics/static_bitmap_image.h   |   2 +
 .../platform/runtime_enabled_features.json5   |   9 +
 third_party/ungoogled/BUILD.gn                |  10 ++
 third_party/ungoogled/ungoogled_switches.cc   |  18 ++
 third_party/ungoogled/ungoogled_switches.h    |  18 ++
 27 files changed, 370 insertions(+), 9 deletions(-)
 create mode 100644 third_party/ungoogled/BUILD.gn
 create mode 100644 third_party/ungoogled/ungoogled_switches.cc
 create mode 100644 third_party/ungoogled/ungoogled_switches.h

diff --git a/chrome/browser/BUILD.gn b/chrome/browser/BUILD.gn
--- a/chrome/browser/BUILD.gn
+++ b/chrome/browser/BUILD.gn
@@ -2395,6 +2395,7 @@ static_library("browser") {
     "//services/device/public/cpp:device_features",
     "//services/device/public/cpp/serial:switches",
     "//services/device/public/cpp/usb",
+    "//third_party/ungoogled:switches",
     "//services/device/public/mojom",
     "//services/device/public/mojom:usb",
     "//services/image_annotation:service",
diff --git a/chrome/browser/about_flags.cc b/chrome/browser/about_flags.cc
--- a/chrome/browser/about_flags.cc
+++ b/chrome/browser/about_flags.cc
@@ -216,6 +216,8 @@
 #include "ui/native_theme/native_theme_features.h"
 #include "ui/ui_features.h"
 
+#include "third_party/ungoogled/ungoogled_switches.h"
+
 #if BUILDFLAG(IS_LINUX) || BUILDFLAG(IS_CHROMEOS)
 #include "base/allocator/buildflags.h"
 #endif
@@ -3646,6 +3648,20 @@ const FeatureEntry kFeatureEntries[] = {
      flag_descriptions::kWebrtcUseMinMaxVEADimensionsName,
      flag_descriptions::kWebrtcUseMinMaxVEADimensionsDescription, kOsAll,
      FEATURE_VALUE_TYPE(blink::features::kWebRtcUseMinMaxVEADimensions)},
+
+    {"fingerprinting-canvas-image-data-noise",
+     "Disable Canvas image data fingerprint deception",
+     "Slightly modifies at most 20 pixels in Canvas image data extracted via JS APIs",
+     kOsAll, SINGLE_DISABLE_VALUE_TYPE(switches::kFingerprintingCanvasImageDataNoise)},
+    {"fingerprinting-client-rects-noise",
+     "Disable get*ClientRects() fingerprint deception",
+     "Scale the output values of Range::getClientRects() and Element::getBoundingClientRect() with a randomly selected factor in the range -0.0003% to 0.0003%, which are recomputed on every document initialization.",
+     kOsAll, SINGLE_DISABLE_VALUE_TYPE(switches::kFingerprintingClientRectsNoise)},
+    {"fingerprinting-canvas-measuretext-noise",
+     "Disable Canvas::measureText() fingerprint deception",
+     "Scale the output values of Canvas::measureText() with a randomly selected factor in the range -0.0003% to 0.0003%, which are recomputed on every document initialization.",
+     kOsAll, SINGLE_DISABLE_VALUE_TYPE(switches::kFingerprintingCanvasMeasureTextNoise)},
+
 #if BUILDFLAG(ENABLE_NACL)
     {"enable-nacl", flag_descriptions::kNaclName,
      flag_descriptions::kNaclDescription, kOsAll,
diff --git a/content/browser/BUILD.gn b/content/browser/BUILD.gn
--- a/content/browser/BUILD.gn
+++ b/content/browser/BUILD.gn
@@ -247,6 +247,7 @@ source_set("browser") {
     "//third_party/libyuv",
     "//third_party/re2",
     "//third_party/sqlite",
+    "//third_party/ungoogled:switches",
     "//third_party/webrtc_overrides:webrtc_component",
     "//third_party/wtl",
     "//third_party/zlib",
diff --git a/content/browser/renderer_host/render_process_host_impl.cc b/content/browser/renderer_host/render_process_host_impl.cc
--- a/content/browser/renderer_host/render_process_host_impl.cc
+++ b/content/browser/renderer_host/render_process_host_impl.cc
@@ -204,6 +204,7 @@
 #include "url/gurl.h"
 #include "url/origin.h"
 
+#include "third_party/ungoogled/ungoogled_switches.h"
 #if BUILDFLAG(IS_ANDROID)
 #include "base/android/child_process_binding_types.h"
 #include "content/browser/android/java_interfaces_impl.h"
@@ -3250,6 +3251,9 @@ void RenderProcessHostImpl::PropagateBrowserCommandLineToRenderer(
     switches::kDisableBreakpad,
     switches::kDisableDatabases,
     switches::kDisableFileSystem,
+    switches::kFingerprintingClientRectsNoise,
+    switches::kFingerprintingCanvasMeasureTextNoise,
+    switches::kFingerprintingCanvasImageDataNoise,
     switches::kDisableFrameRateLimit,
     switches::kDisableGpuMemoryBufferVideoFrames,
     switches::kDisableHistogramCustomizer,
diff --git a/content/child/BUILD.gn b/content/child/BUILD.gn
--- a/content/child/BUILD.gn
+++ b/content/child/BUILD.gn
@@ -101,6 +101,7 @@ target(link_target_type, "child") {
     "//third_party/blink/public/common",
     "//third_party/blink/public/strings",
     "//third_party/ced",
+    "//third_party/ungoogled:switches",
     "//third_party/zlib/google:compression_utils",
     "//ui/base",
     "//ui/events/blink",
diff --git a/content/child/runtime_features.cc b/content/child/runtime_features.cc
--- a/content/child/runtime_features.cc
+++ b/content/child/runtime_features.cc
@@ -41,6 +41,8 @@
 #include "ui/gl/gl_switches.h"
 #include "ui/native_theme/native_theme_features.h"
 
+#include "third_party/ungoogled/ungoogled_switches.h"
+
 #if BUILDFLAG(IS_ANDROID)
 #include "base/android/build_info.h"
 #endif
@@ -456,6 +458,13 @@ void SetCustomizedRuntimeFeaturesFromCombinedArgs(
   if (!command_line.HasSwitch(switches::kDisableYUVImageDecoding) &&
       base::FeatureList::IsEnabled(
           blink::features::kDecodeLossyWebPImagesToYUV)) {
+  WebRuntimeFeatures::EnableFingerprintingClientRectsNoise(
+      !command_line.HasSwitch(switches::kFingerprintingClientRectsNoise));
+  WebRuntimeFeatures::EnableFingerprintingCanvasMeasureTextNoise(
+      !command_line.HasSwitch(switches::kFingerprintingCanvasMeasureTextNoise));
+  WebRuntimeFeatures::EnableFingerprintingCanvasImageDataNoise(
+      !command_line.HasSwitch(switches::kFingerprintingCanvasImageDataNoise));
+
     WebRuntimeFeatures::EnableDecodeLossyWebPImagesToYUV(true);
   }
 
diff --git a/third_party/blink/public/platform/web_runtime_features.h b/third_party/blink/public/platform/web_runtime_features.h
--- a/third_party/blink/public/platform/web_runtime_features.h
+++ b/third_party/blink/public/platform/web_runtime_features.h
@@ -66,6 +66,9 @@ class BLINK_PLATFORM_EXPORT WebRuntimeFeatures : public WebRuntimeFeaturesBase {
   static void UpdateStatusFromBaseFeatures();
 
   static void EnableOverlayScrollbars(bool);
+  static void EnableFingerprintingClientRectsNoise(bool);
+  static void EnableFingerprintingCanvasMeasureTextNoise(bool);
+  static void EnableFingerprintingCanvasImageDataNoise(bool);
   static void EnableFluentScrollbars(bool);
 
   WebRuntimeFeatures() = delete;
diff --git a/third_party/blink/renderer/core/dom/document.cc b/third_party/blink/renderer/core/dom/document.cc
--- a/third_party/blink/renderer/core/dom/document.cc
+++ b/third_party/blink/renderer/core/dom/document.cc
@@ -37,6 +37,7 @@
 #include "base/containers/contains.h"
 #include "base/debug/dump_without_crashing.h"
 #include "base/metrics/histogram_functions.h"
+#include "base/rand_util.h"
 #include "base/notreached.h"
 #include "base/ranges/algorithm.h"
 #include "base/time/time.h"
@@ -820,6 +821,15 @@ Document::Document(const DocumentInit& initializer,
               ? MakeGarbageCollected<RenderBlockingResourceManager>(*this)
               : nullptr),
       data_(MakeGarbageCollected<DocumentData>(GetExecutionContext())) {
+  if (RuntimeEnabledFeatures::FingerprintingClientRectsNoiseEnabled()) {
+    // Precompute -0.0003% to 0.0003% noise factor for get*ClientRect*() fingerprinting
+    noise_factor_x_ = 1 + (base::RandDouble() - 0.5) * 0.000003;
+    noise_factor_y_ = 1 + (base::RandDouble() - 0.5) * 0.000003;
+  } else {
+    noise_factor_x_ = 1;
+    noise_factor_y_ = 1;
+  }
+
   if (base::FeatureList::IsEnabled(features::kDelayAsyncScriptExecution))
     script_runner_delayer_->Activate();
 
@@ -2243,6 +2253,14 @@ void Document::UpdateStyleAndLayoutTreeForThisDocument() {
 #endif
 }
 
+double Document::GetNoiseFactorX() {
+  return noise_factor_x_;
+}
+
+double Document::GetNoiseFactorY() {
+  return noise_factor_y_;
+}
+
 void Document::InvalidateStyleAndLayoutForFontUpdates() {
   DCHECK(IsActive());
   DCHECK(IsMainThread());
diff --git a/third_party/blink/renderer/core/dom/document.h b/third_party/blink/renderer/core/dom/document.h
--- a/third_party/blink/renderer/core/dom/document.h
+++ b/third_party/blink/renderer/core/dom/document.h
@@ -513,6 +513,10 @@ class CORE_EXPORT Document : public ContainerNode,
     has_xml_declaration_ = has_xml_declaration ? 1 : 0;
   }
 
+  // Values for get*ClientRect fingerprint deception
+  double GetNoiseFactorX();
+  double GetNoiseFactorY();
+
   AtomicString visibilityState() const;
   bool IsPageVisible() const;
   bool hidden() const;
@@ -2332,6 +2336,9 @@ class CORE_EXPORT Document : public ContainerNode,
 
   base::ElapsedTimer start_time_;
 
+  double noise_factor_x_;
+  double noise_factor_y_;
+
   Member<ScriptRunner> script_runner_;
   Member<ScriptRunnerDelayer> script_runner_delayer_;
 
diff --git a/third_party/blink/renderer/core/dom/element.cc b/third_party/blink/renderer/core/dom/element.cc
--- a/third_party/blink/renderer/core/dom/element.cc
+++ b/third_party/blink/renderer/core/dom/element.cc
@@ -2053,6 +2053,7 @@ void Element::ClientQuads(Vector<gfx::QuadF>& quads) const {
       quads.push_back(element_layout_object->LocalToAbsoluteQuad(
           gfx::QuadF(element_layout_object->ObjectBoundingBox())));
     }
+    //TODO: cover this as well
     return;
   }
 
@@ -2060,6 +2061,11 @@ void Element::ClientQuads(Vector<gfx::QuadF>& quads) const {
   if (element_layout_object->IsBoxModelObject() ||
       element_layout_object->IsBR())
     element_layout_object->AbsoluteQuads(quads);
+
+  if (RuntimeEnabledFeatures::FingerprintingClientRectsNoiseEnabled()) {
+    for (auto& quad : quads)
+     quad.Scale(GetDocument().GetNoiseFactorX(), GetDocument().GetNoiseFactorY());
+  }
 }
 
 DOMRectList* Element::getClientRects() {
@@ -2098,6 +2104,9 @@ gfx::RectF Element::GetBoundingClientRectNoLifecycleUpdate() const {
   DCHECK(element_layout_object);
   GetDocument().AdjustRectForScrollAndAbsoluteZoom(result,
                                                    *element_layout_object);
+  if (RuntimeEnabledFeatures::FingerprintingClientRectsNoiseEnabled()) {
+    result.Scale(GetDocument().GetNoiseFactorX(), GetDocument().GetNoiseFactorY());
+  }
   return result;
 }
 
diff --git a/third_party/blink/renderer/core/dom/range.cc b/third_party/blink/renderer/core/dom/range.cc
--- a/third_party/blink/renderer/core/dom/range.cc
+++ b/third_party/blink/renderer/core/dom/range.cc
@@ -1604,6 +1604,12 @@ DOMRectList* Range::getClientRects() const {
   Vector<gfx::QuadF> quads;
   GetBorderAndTextQuads(quads);
 
+  if (RuntimeEnabledFeatures::FingerprintingClientRectsNoiseEnabled()) {
+    for (auto& quad : quads) {
+      quad.Scale(owner_document_->GetNoiseFactorX(), owner_document_->GetNoiseFactorY());
+    }
+  }
+
   return MakeGarbageCollected<DOMRectList>(quads);
 }
 
@@ -1743,7 +1749,11 @@ gfx::RectF Range::BoundingRect() const {
 
   // If all rects are empty, return the first rect.
   if (result.IsEmpty() && !quads.empty())
-    return quads.front().BoundingBox();
+    result = quads.front().BoundingBox();
+
+  if (!result.IsEmpty() && RuntimeEnabledFeatures::FingerprintingClientRectsNoiseEnabled()) {
+    result.Scale(owner_document_->GetNoiseFactorX(), owner_document_->GetNoiseFactorY());
+  }
 
   return result;
 }
diff --git a/third_party/blink/renderer/core/html/canvas/canvas_async_blob_creator.cc b/third_party/blink/renderer/core/html/canvas/canvas_async_blob_creator.cc
--- a/third_party/blink/renderer/core/html/canvas/canvas_async_blob_creator.cc
+++ b/third_party/blink/renderer/core/html/canvas/canvas_async_blob_creator.cc
@@ -20,6 +20,7 @@
 #include "third_party/blink/renderer/core/fileapi/blob.h"
 #include "third_party/blink/renderer/core/html/canvas/canvas_rendering_context.h"
 #include "third_party/blink/renderer/platform/graphics/image_data_buffer.h"
+#include "third_party/blink/renderer/platform/graphics/static_bitmap_image.h"
 #include "third_party/blink/renderer/platform/graphics/skia/skia_utils.h"
 #include "third_party/blink/renderer/platform/graphics/unaccelerated_static_bitmap_image.h"
 #include "third_party/blink/renderer/platform/heap/garbage_collected.h"
@@ -262,6 +263,10 @@ CanvasAsyncBlobCreator::CanvasAsyncBlobCreator(
                          std::min(info.height(), max_dimension));
       src_data_.reset(info, src_data_.addr(), src_data_.rowBytes());
     }
+    if (RuntimeEnabledFeatures::FingerprintingCanvasImageDataNoiseEnabled()) {
+      StaticBitmapImage::ShuffleSubchannelColorData(src_data_.writable_addr(),
+        src_data_.info(), 0, 0);
+    }
   }
 
   idle_task_status_ = kIdleTaskNotSupported;
diff --git a/third_party/blink/renderer/core/html/canvas/text_metrics.cc b/third_party/blink/renderer/core/html/canvas/text_metrics.cc
--- a/third_party/blink/renderer/core/html/canvas/text_metrics.cc
+++ b/third_party/blink/renderer/core/html/canvas/text_metrics.cc
@@ -55,6 +55,24 @@ TextMetrics::TextMetrics(const Font& font,
   Update(font, direction, baseline, align, text);
 }
 
+void TextMetrics::Shuffle(const double factor) {
+  // x-direction
+  width_ *= factor;
+  actual_bounding_box_left_ *= factor;
+  actual_bounding_box_right_ *= factor;
+
+  // y-direction
+  font_bounding_box_ascent_ *= factor;
+  font_bounding_box_descent_ *= factor;
+  actual_bounding_box_ascent_ *= factor;
+  actual_bounding_box_descent_ *= factor;
+  em_height_ascent_ *= factor;
+  em_height_descent_ *= factor;
+  baselines_->setAlphabetic(baselines_->alphabetic() * factor);
+  baselines_->setHanging(baselines_->hanging() * factor);
+  baselines_->setIdeographic(baselines_->ideographic() * factor);
+}
+
 void TextMetrics::Update(const Font& font,
                          const TextDirection& direction,
                          const TextBaseline& baseline,
diff --git a/third_party/blink/renderer/core/html/canvas/text_metrics.h b/third_party/blink/renderer/core/html/canvas/text_metrics.h
--- a/third_party/blink/renderer/core/html/canvas/text_metrics.h
+++ b/third_party/blink/renderer/core/html/canvas/text_metrics.h
@@ -64,6 +64,8 @@ class CORE_EXPORT TextMetrics final : public ScriptWrappable {
 
   void Trace(Visitor*) const override;
 
+  void Shuffle(const double factor);
+
  private:
   void Update(const Font&,
               const TextDirection&,
diff --git a/third_party/blink/renderer/core/svg/svg_graphics_element.cc b/third_party/blink/renderer/core/svg/svg_graphics_element.cc
--- a/third_party/blink/renderer/core/svg/svg_graphics_element.cc
+++ b/third_party/blink/renderer/core/svg/svg_graphics_element.cc
@@ -191,6 +191,7 @@ SVGRectTearOff* SVGGraphicsElement::getBBoxFromJavascript() {
 
     if (layout_object->IsSVGText() || layout_object->IsSVGInline())
       UseCounter::Count(GetDocument(), WebFeature::kGetBBoxForText);
+    bounding_box.Scale(GetDocument().GetNoiseFactorX(), GetDocument().GetNoiseFactorY());
   }
   return SVGRectTearOff::CreateDetached(bounding_box);
 }
diff --git a/third_party/blink/renderer/core/svg/svg_text_content_element.cc b/third_party/blink/renderer/core/svg/svg_text_content_element.cc
--- a/third_party/blink/renderer/core/svg/svg_text_content_element.cc
+++ b/third_party/blink/renderer/core/svg/svg_text_content_element.cc
@@ -117,11 +117,14 @@ float SVGTextContentElement::getComputedTextLength() {
   GetDocument().UpdateStyleAndLayoutForNode(this,
                                             DocumentUpdateReason::kJavaScript);
   auto* layout_object = GetLayoutObject();
+  float value = 0;
   if (IsNGTextOrInline(layout_object)) {
     NGSvgTextQuery query(*layout_object);
-    return query.SubStringLength(0, query.NumberOfCharacters());
+    value = query.SubStringLength(0, query.NumberOfCharacters());
+  } else {
+    value = SVGTextQuery(layout_object).TextLength();
   }
-  return SVGTextQuery(layout_object).TextLength();
+  return value * GetDocument().GetNoiseFactorX();
 }
 
 float SVGTextContentElement::getSubStringLength(
@@ -144,9 +147,12 @@ float SVGTextContentElement::getSubStringLength(
     nchars = number_of_chars - charnum;
 
   auto* layout_object = GetLayoutObject();
+  float value = 0;
   if (IsNGTextOrInline(layout_object))
-    return NGSvgTextQuery(*layout_object).SubStringLength(charnum, nchars);
-  return SVGTextQuery(layout_object).SubStringLength(charnum, nchars);
+    value = NGSvgTextQuery(*layout_object).SubStringLength(charnum, nchars);
+  else
+    value = SVGTextQuery(layout_object).SubStringLength(charnum, nchars);
+  return value * GetDocument().GetNoiseFactorX();
 }
 
 SVGPointTearOff* SVGTextContentElement::getStartPositionOfChar(
@@ -170,6 +176,7 @@ SVGPointTearOff* SVGTextContentElement::getStartPositionOfChar(
   } else {
     point = SVGTextQuery(layout_object).StartPositionOfCharacter(charnum);
   }
+  point.Scale(GetDocument().GetNoiseFactorX(), GetDocument().GetNoiseFactorY());
   return SVGPointTearOff::CreateDetached(point);
 }
 
@@ -194,6 +201,7 @@ SVGPointTearOff* SVGTextContentElement::getEndPositionOfChar(
   } else {
     point = SVGTextQuery(layout_object).EndPositionOfCharacter(charnum);
   }
+  point.Scale(GetDocument().GetNoiseFactorX(), GetDocument().GetNoiseFactorY());
   return SVGPointTearOff::CreateDetached(point);
 }
 
@@ -218,6 +226,7 @@ SVGRectTearOff* SVGTextContentElement::getExtentOfChar(
   } else {
     rect = SVGTextQuery(layout_object).ExtentOfCharacter(charnum);
   }
+  rect.Scale(GetDocument().GetNoiseFactorX(), GetDocument().GetNoiseFactorY());
   return SVGRectTearOff::CreateDetached(rect);
 }
 
@@ -247,12 +256,14 @@ int SVGTextContentElement::getCharNumAtPosition(
   GetDocument().UpdateStyleAndLayoutForNode(this,
                                             DocumentUpdateReason::kJavaScript);
   auto* layout_object = GetLayoutObject();
+  gfx::PointF target = gfx::PointF(point->Target()->Value());
+  target.Scale(GetDocument().GetNoiseFactorX(), GetDocument().GetNoiseFactorY());
   if (IsNGTextOrInline(layout_object)) {
     return NGSvgTextQuery(*layout_object)
-        .CharacterNumberAtPosition(point->Target()->Value());
+        .CharacterNumberAtPosition(target);
   }
   return SVGTextQuery(layout_object)
-      .CharacterNumberAtPosition(point->Target()->Value());
+      .CharacterNumberAtPosition(target);
 }
 
 void SVGTextContentElement::selectSubString(unsigned charnum,
diff --git a/third_party/blink/renderer/modules/canvas/canvas2d/base_rendering_context_2d.cc b/third_party/blink/renderer/modules/canvas/canvas2d/base_rendering_context_2d.cc
--- a/third_party/blink/renderer/modules/canvas/canvas2d/base_rendering_context_2d.cc
+++ b/third_party/blink/renderer/modules/canvas/canvas2d/base_rendering_context_2d.cc
@@ -29,6 +29,7 @@
 #include "third_party/blink/renderer/modules/webcodecs/video_frame.h"
 #include "third_party/blink/renderer/platform/graphics/bitmap_image.h"
 #include "third_party/blink/renderer/platform/graphics/graphics_context.h"
+#include "third_party/blink/renderer/platform/graphics/static_bitmap_image.h"
 #include "third_party/blink/renderer/platform/graphics/skia/skia_utils.h"
 #include "third_party/blink/renderer/platform/graphics/stroke_data.h"
 #include "third_party/blink/renderer/platform/graphics/video_frame_image_util.h"
@@ -2070,6 +2071,10 @@ ImageData* BaseRenderingContext2D::getImageDataInternal(
           snapshot->PaintImageForCurrentFrame().GetSkImageInfo().bounds();
       DCHECK(!bounds.intersect(SkIRect::MakeXYWH(sx, sy, sw, sh)));
     }
+
+    if (read_pixels_successful && RuntimeEnabledFeatures::FingerprintingCanvasImageDataNoiseEnabled()) {
+      StaticBitmapImage::ShuffleSubchannelColorData(image_data_pixmap.addr(), image_data_pixmap.info(), sx, sy);
+    }
   }
 
   return image_data;
diff --git a/third_party/blink/renderer/modules/canvas/canvas2d/canvas_rendering_context_2d.cc b/third_party/blink/renderer/modules/canvas/canvas2d/canvas_rendering_context_2d.cc
--- a/third_party/blink/renderer/modules/canvas/canvas2d/canvas_rendering_context_2d.cc
+++ b/third_party/blink/renderer/modules/canvas/canvas2d/canvas_rendering_context_2d.cc
@@ -920,9 +920,15 @@ TextMetrics* CanvasRenderingContext2D::measureText(const String& text) {
   TextDirection direction =
       ToTextDirection(GetState().GetDirection(), canvas());
 
-  return MakeGarbageCollected<TextMetrics>(font, direction,
+  TextMetrics* text_metrics = MakeGarbageCollected<TextMetrics>(font, direction,
                                            GetState().GetTextBaseline(),
                                            GetState().GetTextAlign(), text);
+
+  // Scale text metrics if enabled
+  if (RuntimeEnabledFeatures::FingerprintingCanvasMeasureTextNoiseEnabled()) {
+    text_metrics->Shuffle(canvas()->GetDocument().GetNoiseFactorX());
+  }
+  return text_metrics;
 }
 
 void CanvasRenderingContext2D::drawFormattedText(
diff --git a/third_party/blink/renderer/platform/BUILD.gn b/third_party/blink/renderer/platform/BUILD.gn
--- a/third_party/blink/renderer/platform/BUILD.gn
+++ b/third_party/blink/renderer/platform/BUILD.gn
@@ -1613,7 +1613,9 @@ component("platform") {
     "//third_party/blink/renderer:non_test_config",
   ]
 
-  include_dirs = []
+  include_dirs = [
+    "//third_party/skia/include/private", # For shuffler in graphics/static_bitmap_image.cc
+  ]
 
   allow_circular_includes_from = [
     "//third_party/blink/renderer/platform/blob",
@@ -1682,6 +1684,7 @@ component("platform") {
     "//third_party/blink/public/strings",
     "//third_party/blink/renderer/platform/wtf",
     "//third_party/ced",
+    "//third_party/ungoogled:switches",
     "//third_party/emoji-segmenter",
     "//third_party/icu",
     "//third_party/libyuv",
diff --git a/third_party/blink/renderer/platform/exported/web_runtime_features.cc b/third_party/blink/renderer/platform/exported/web_runtime_features.cc
--- a/third_party/blink/renderer/platform/exported/web_runtime_features.cc
+++ b/third_party/blink/renderer/platform/exported/web_runtime_features.cc
@@ -65,4 +65,16 @@ void WebRuntimeFeatures::EnableFluentScrollbars(bool enable) {
   ScrollbarThemeSettings::SetFluentScrollbarsEnabled(enable);
 }
 
+void WebRuntimeFeatures::EnableFingerprintingClientRectsNoise(bool enable) {
+  RuntimeEnabledFeatures::SetFingerprintingClientRectsNoiseEnabled(enable);
+}
+
+void WebRuntimeFeatures::EnableFingerprintingCanvasMeasureTextNoise(bool enable) {
+  RuntimeEnabledFeatures::SetFingerprintingCanvasMeasureTextNoiseEnabled(enable);
+}
+
+void WebRuntimeFeatures::EnableFingerprintingCanvasImageDataNoise(bool enable) {
+  RuntimeEnabledFeatures::SetFingerprintingCanvasImageDataNoiseEnabled(enable);
+}
+
 }  // namespace blink
diff --git a/third_party/blink/renderer/platform/graphics/image_data_buffer.cc b/third_party/blink/renderer/platform/graphics/image_data_buffer.cc
--- a/third_party/blink/renderer/platform/graphics/image_data_buffer.cc
+++ b/third_party/blink/renderer/platform/graphics/image_data_buffer.cc
@@ -36,6 +36,8 @@
 
 #include "base/compiler_specific.h"
 #include "base/memory/ptr_util.h"
+#include "base/rand_util.h"
+#include "base/logging.h"
 #include "third_party/blink/renderer/platform/graphics/static_bitmap_image.h"
 #include "third_party/blink/renderer/platform/image-encoders/image_encoder.h"
 #include "third_party/blink/renderer/platform/runtime_enabled_features.h"
@@ -145,6 +147,11 @@ bool ImageDataBuffer::EncodeImageInternal(const ImageEncodingMimeType mime_type,
                                           const SkPixmap& pixmap) const {
   DCHECK(is_valid_);
 
+  if (RuntimeEnabledFeatures::FingerprintingCanvasImageDataNoiseEnabled()) {
+    // shuffle subchannel color data within the pixmap
+    StaticBitmapImage::ShuffleSubchannelColorData(pixmap_.writable_addr(), pixmap_.info(), 0, 0);
+  }
+
   if (mime_type == kMimeTypeJpeg) {
     SkJpegEncoder::Options options;
     options.fQuality = ImageEncoder::ComputeJpegQuality(quality);
diff --git a/third_party/blink/renderer/platform/graphics/static_bitmap_image.cc b/third_party/blink/renderer/platform/graphics/static_bitmap_image.cc
--- a/third_party/blink/renderer/platform/graphics/static_bitmap_image.cc
+++ b/third_party/blink/renderer/platform/graphics/static_bitmap_image.cc
@@ -4,6 +4,8 @@
 
 #include "third_party/blink/renderer/platform/graphics/static_bitmap_image.h"
 
+#include "base/logging.h"
+#include "base/rand_util.h"
 #include "base/numerics/checked_math.h"
 #include "gpu/command_buffer/client/gles2_interface.h"
 #include "third_party/blink/renderer/platform/graphics/accelerated_static_bitmap_image.h"
@@ -18,6 +20,9 @@
 #include "ui/gfx/geometry/skia_conversions.h"
 #include "v8/include/v8.h"
 
+#include "third_party/blink/renderer/platform/runtime_enabled_features.h"
+#include "third_party/skia/include/private/SkColorData.h"
+
 namespace blink {
 
 scoped_refptr<StaticBitmapImage> StaticBitmapImage::Create(
@@ -115,4 +120,154 @@ void StaticBitmapImage::DrawHelper(cc::PaintCanvas* canvas,
       WebCoreClampingModeToSkiaRectConstraint(draw_options.clamping_mode));
 }
 
+// set the component to maximum-delta if it is >= maximum, or add to existing color component (color + delta)
+#define shuffleComponent(color, max, delta) ( (color) >= (max) ? ((max)-(delta)) : ((color)+(delta)) )
+
+#define writable_addr(T, p, stride, x, y) (T*)((const char *)p + y * stride + x * sizeof(T))
+
+void StaticBitmapImage::ShuffleSubchannelColorData(const void *addr, const SkImageInfo& info, int srcX, int srcY) {
+  auto w = info.width() - srcX, h = info.height() - srcY;
+
+  // skip tiny images; info.width()/height() can also be 0
+  if ((w < 8) || (h < 8)) {
+    return;
+  }
+
+  // generate the first random number here
+  double shuffleX = base::RandDouble();
+
+  // cap maximum pixels to change
+  auto pixels = (w + h) / 128;
+  if (pixels > 20) {
+    pixels = 20;
+  } else if (pixels < 2) {
+    pixels = 2;
+  }
+
+  auto colorType = info.colorType();
+  auto fRowBytes = info.minRowBytes(); // stride
+
+  DLOG(INFO) << "BRM: ShuffleSubchannelColorData() w=" << w << " h=" << h << " colorType=" << colorType << " fRowBytes=" << fRowBytes;
+
+  // second random number (for y/height)
+  double shuffleY = base::RandDouble();
+
+  // calculate random coordinates using bisection
+  auto currentW = w, currentH = h;
+  for(;pixels >= 0; pixels--) {
+    int x = currentW * shuffleX, y = currentH * shuffleY;
+
+    // calculate randomisation amounts for each RGB component
+    uint8_t shuffleR = base::RandInt(0, 4);
+    uint8_t shuffleG = (shuffleR + x) % 4;
+    uint8_t shuffleB = (shuffleG + y) % 4;
+
+    // manipulate pixel data to slightly change the R, G, B components
+    switch (colorType) {
+      case kAlpha_8_SkColorType:
+      {
+         auto *pixel = writable_addr(uint8_t, addr, fRowBytes, x, y);
+         auto r = SkColorGetR(*pixel), g = SkColorGetG(*pixel), b = SkColorGetB(*pixel), a = SkColorGetA(*pixel);
+
+         r = shuffleComponent(r, UINT8_MAX-1, shuffleR);
+         g = shuffleComponent(g, UINT8_MAX-1, shuffleG);
+         b = shuffleComponent(b, UINT8_MAX-1, shuffleB);
+         // alpha is left unchanged
+
+         *pixel = SkColorSetARGB(a, r, g, b);
+      }
+      break;
+      case kGray_8_SkColorType:
+      {
+         auto *pixel = writable_addr(uint8_t, addr, fRowBytes, x, y);
+         *pixel = shuffleComponent(*pixel, UINT8_MAX-1, shuffleB);
+      }
+      break;
+      case kRGB_565_SkColorType:
+      {
+         auto *pixel = writable_addr(uint16_t, addr, fRowBytes, x, y);
+         unsigned    r = SkPacked16ToR32(*pixel);
+         unsigned    g = SkPacked16ToG32(*pixel);
+         unsigned    b = SkPacked16ToB32(*pixel);
+
+         r = shuffleComponent(r, 31, shuffleR);
+         g = shuffleComponent(g, 63, shuffleG);
+         b = shuffleComponent(b, 31, shuffleB);
+
+         unsigned r16 = (r & SK_R16_MASK) << SK_R16_SHIFT;
+         unsigned g16 = (g & SK_G16_MASK) << SK_G16_SHIFT;
+         unsigned b16 = (b & SK_B16_MASK) << SK_B16_SHIFT;
+
+         *pixel = r16 | g16 | b16;
+      }
+      break;
+      case kARGB_4444_SkColorType:
+      {
+         auto *pixel = writable_addr(uint16_t, addr, fRowBytes, x, y);
+         auto a = SkGetPackedA4444(*pixel), r = SkGetPackedR4444(*pixel), g = SkGetPackedG4444(*pixel), b = SkGetPackedB4444(*pixel);
+
+         r = shuffleComponent(r, 15, shuffleR);
+         g = shuffleComponent(g, 15, shuffleG);
+         b = shuffleComponent(b, 15, shuffleB);
+         // alpha is left unchanged
+
+         unsigned a4 = (a & 0xF) << SK_A4444_SHIFT;
+         unsigned r4 = (r & 0xF) << SK_R4444_SHIFT;
+         unsigned g4 = (g & 0xF) << SK_G4444_SHIFT;
+         unsigned b4 = (b & 0xF) << SK_B4444_SHIFT;
+
+         *pixel = r4 | b4 | g4 | a4;
+      }
+      break;
+      case kRGBA_8888_SkColorType:
+      {
+         auto *pixel = writable_addr(uint32_t, addr, fRowBytes, x, y);
+         auto a = SkGetPackedA32(*pixel), r = SkGetPackedR32(*pixel), g = SkGetPackedG32(*pixel), b = SkGetPackedB32(*pixel);
+
+         r = shuffleComponent(r, UINT8_MAX-1, shuffleR);
+         g = shuffleComponent(g, UINT8_MAX-1, shuffleG);
+         b = shuffleComponent(b, UINT8_MAX-1, shuffleB);
+         // alpha is left unchanged
+
+         *pixel = (a << SK_A32_SHIFT) | (r << SK_R32_SHIFT) |
+                  (g << SK_G32_SHIFT) | (b << SK_B32_SHIFT);
+      }
+      break;
+      case kBGRA_8888_SkColorType:
+      {
+         auto *pixel = writable_addr(uint32_t, addr, fRowBytes, x, y);
+         auto a = SkGetPackedA32(*pixel), b = SkGetPackedR32(*pixel), g = SkGetPackedG32(*pixel), r = SkGetPackedB32(*pixel);
+
+         r = shuffleComponent(r, UINT8_MAX-1, shuffleR);
+         g = shuffleComponent(g, UINT8_MAX-1, shuffleG);
+         b = shuffleComponent(b, UINT8_MAX-1, shuffleB);
+         // alpha is left unchanged
+
+         *pixel = (a << SK_BGRA_A32_SHIFT) | (r << SK_BGRA_R32_SHIFT) |
+                  (g << SK_BGRA_G32_SHIFT) | (b << SK_BGRA_B32_SHIFT);
+      }
+      break;
+      default:
+         // the remaining formats are not expected to be used in Chromium
+         LOG(WARNING) << "BRM: ShuffleSubchannelColorData(): Ignoring pixel format";
+         return;
+    }
+
+    // keep bisecting or reset current width/height as needed
+    if (x == 0) {
+       currentW = w;
+    } else {
+       currentW = x;
+    }
+    if (y == 0) {
+       currentH = h;
+    } else {
+       currentH = y;
+    }
+  }
+}
+
+#undef writable_addr
+#undef shuffleComponent
+
 }  // namespace blink
diff --git a/third_party/blink/renderer/platform/graphics/static_bitmap_image.h b/third_party/blink/renderer/platform/graphics/static_bitmap_image.h
--- a/third_party/blink/renderer/platform/graphics/static_bitmap_image.h
+++ b/third_party/blink/renderer/platform/graphics/static_bitmap_image.h
@@ -37,6 +37,8 @@ class PLATFORM_EXPORT StaticBitmapImage : public Image {
 
   StaticBitmapImage(ImageOrientation orientation) : orientation_(orientation) {}
 
+  static void ShuffleSubchannelColorData(const void *addr, const SkImageInfo& info, int srcX, int srcY);
+
   bool IsStaticBitmapImage() const override { return true; }
 
   // Methods overridden by all sub-classes
diff --git a/third_party/blink/renderer/platform/runtime_enabled_features.json5 b/third_party/blink/renderer/platform/runtime_enabled_features.json5
--- a/third_party/blink/renderer/platform/runtime_enabled_features.json5
+++ b/third_party/blink/renderer/platform/runtime_enabled_features.json5
@@ -1447,6 +1447,15 @@
       // No plan to support complex UI for date/time INPUT types on Android.
       status: {"Android": "test", "default": "stable"},
     },
+    {
+      name: "FingerprintingClientRectsNoise",
+    },
+    {
+      name: "FingerprintingCanvasMeasureTextNoise",
+    },
+    {
+      name: "FingerprintingCanvasImageDataNoise",
+    },
     {
       name: "InstalledApp",
       public: true,
diff --git a/third_party/ungoogled/BUILD.gn b/third_party/ungoogled/BUILD.gn
new file mode 100644
--- /dev/null
+++ b/third_party/ungoogled/BUILD.gn
@@ -0,0 +1,10 @@
+# Copyright (c) 2018 The ungoogled-chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+component("switches") {
+  sources = [
+    "ungoogled_switches.h",
+    "ungoogled_switches.cc",
+  ]
+}
diff --git a/third_party/ungoogled/ungoogled_switches.cc b/third_party/ungoogled/ungoogled_switches.cc
new file mode 100644
--- /dev/null
+++ b/third_party/ungoogled/ungoogled_switches.cc
@@ -0,0 +1,18 @@
+// Copyright (c) 2018 The ungoogled-chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "third_party/ungoogled/ungoogled_switches.h"
+
+namespace switches {
+
+// Enable fingerprinting deception for getClientRects and getBoundingClientRect
+const char kFingerprintingClientRectsNoise[] = "fingerprinting-client-rects-noise";
+
+// Enable fingerprinting deception for measureText
+const char kFingerprintingCanvasMeasureTextNoise[] = "fingerprinting-canvas-measuretext-noise";
+
+// Enable fingerprinting deception for Canvas image data
+const char kFingerprintingCanvasImageDataNoise[] = "fingerprinting-canvas-image-data-noise";
+
+}  // namespace switches
diff --git a/third_party/ungoogled/ungoogled_switches.h b/third_party/ungoogled/ungoogled_switches.h
new file mode 100644
--- /dev/null
+++ b/third_party/ungoogled/ungoogled_switches.h
@@ -0,0 +1,18 @@
+// Copyright (c) 2018 The ungoogled-chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+// Defines all the fingerprinting command-line switches.
+
+#ifndef THIRD_PARTY_UNGOOGLED_FINGERPRINTING_SWITCHES_H_
+#define THIRD_PARTY_UNGOOGLED_FINGERPRINTING_SWITCHES_H_
+
+namespace switches {
+
+extern const char kFingerprintingClientRectsNoise[];
+extern const char kFingerprintingCanvasMeasureTextNoise[];
+extern const char kFingerprintingCanvasImageDataNoise[];
+
+}
+
+#endif  // THIRD_PARTY_UNGOOGLED_FINGERPRINTING_SWITCHES_H_
--
2.25.1
