# NOTE: Changes made: # * Added flag --fingerprinting-canvas-image-data-noise to enable/disable # Canvas image data fingerprinting deception # * Removed WebGLDebugRendererInfo disabling in favor of an alternative # implementation in ungoogled-chromium/disable-webgl-renderer-info.patch # * Tweak subchannel noise generation to require fewer random number generation From: csagan5 <32685696+csagan5@users.noreply.github.com> Date: Sat, 24 Mar 2018 05:18:03 +0100 Subject: Canvas: fingerprinting mitigations for image data and webGL Disable webGL renderer info and modify the color data returned by ToBlob, ToDataURL and getImageData so that it will contain randomly manipulated pixels (maximum 10) 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. --- .../platform/graphics/image_data_buffer.cc | 5 + .../platform/graphics/static_bitmap_image.cc | 154 +++++++++++++++++++++ .../platform/graphics/static_bitmap_image.h | 2 + 4 files changed, 163 insertions(+), 2 deletions(-) --- a/chrome/browser/bromite_flag_entries.h +++ b/chrome/browser/bromite_flag_entries.h @@ -16,4 +16,8 @@ flag_descriptions::kMaxConnectionsPerHostName, flag_descriptions::kMaxConnectionsPerHostDescription, kOsAll, MULTI_VALUE_TYPE(kMaxConnectionsPerHostChoices)}, + {"fingerprinting-canvas-image-data-noise", + "Enable Canvas image data fingerprint deception", + "Slightly modifies at most 10 pixels in Canvas image data extracted via JS APIs. ungoogled-chromium flag, Bromite feature.", + kOsAll, SINGLE_VALUE_TYPE(switches::kFingerprintingCanvasImageDataNoise)}, #endif // CHROME_BROWSER_BROMITE_FLAG_ENTRIES_H_ --- a/content/browser/renderer_host/render_process_host_impl.cc +++ b/content/browser/renderer_host/render_process_host_impl.cc @@ -3341,6 +3341,7 @@ void RenderProcessHostImpl::PropagateBro switches::kFileUrlPathAlias, switches::kFingerprintingClientRectsNoise, switches::kFingerprintingCanvasMeasureTextNoise, + switches::kFingerprintingCanvasImageDataNoise, switches::kForceDeviceScaleFactor, switches::kForceDisplayColorProfile, switches::kForceGpuMemAvailableMb, --- a/content/child/runtime_features.cc +++ b/content/child/runtime_features.cc @@ -489,6 +489,8 @@ void SetRuntimeFeaturesFromCommandLine(c switches::kFingerprintingClientRectsNoise, true}, {wrf::EnableFingerprintingCanvasMeasureTextNoise, switches::kFingerprintingCanvasMeasureTextNoise, true}, + {wrf::EnableFingerprintingCanvasImageDataNoise, + switches::kFingerprintingCanvasImageDataNoise, true}, }; for (const auto& mapping : switchToFeatureMapping) { if (command_line.HasSwitch(mapping.switch_name)) --- a/third_party/blink/public/platform/web_runtime_features.h +++ b/third_party/blink/public/platform/web_runtime_features.h @@ -223,6 +223,7 @@ class WebRuntimeFeatures { BLINK_PLATFORM_EXPORT static void EnableAllowSyncXHRInPageDismissal(bool); BLINK_PLATFORM_EXPORT static void EnableFingerprintingClientRectsNoise(bool); BLINK_PLATFORM_EXPORT static void EnableFingerprintingCanvasMeasureTextNoise(bool); + BLINK_PLATFORM_EXPORT static void EnableFingerprintingCanvasImageDataNoise(bool); BLINK_PLATFORM_EXPORT static void EnableSignedExchangeSubresourcePrefetch( bool); BLINK_PLATFORM_EXPORT static void EnableSubresourceWebBundles(bool); --- 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 @@ -30,6 +30,7 @@ #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/skia/skia_utils.h" +#include "third_party/blink/renderer/platform/graphics/static_bitmap_image.h" #include "third_party/blink/renderer/platform/graphics/stroke_data.h" #include "third_party/blink/renderer/platform/graphics/video_frame_image_util.h" #include "third_party/blink/renderer/platform/heap/garbage_collected.h" @@ -2055,6 +2056,9 @@ ImageData* BaseRenderingContext2D::getIm 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; --- a/third_party/blink/renderer/platform/BUILD.gn +++ b/third_party/blink/renderer/platform/BUILD.gn @@ -1641,7 +1641,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", --- a/third_party/blink/renderer/platform/exported/web_runtime_features.cc +++ b/third_party/blink/renderer/platform/exported/web_runtime_features.cc @@ -705,4 +705,8 @@ void WebRuntimeFeatures::EnableFingerpri RuntimeEnabledFeatures::SetFingerprintingCanvasMeasureTextNoiseEnabled(enable); } +void WebRuntimeFeatures::EnableFingerprintingCanvasImageDataNoise(bool enable) { + RuntimeEnabledFeatures::SetFingerprintingCanvasImageDataNoiseEnabled(enable); +} + } // namespace blink --- 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::EncodeImageInterna 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); --- 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/rand_util.h" +#include "base/logging.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" @@ -11,10 +13,12 @@ #include "third_party/blink/renderer/platform/graphics/image_observer.h" #include "third_party/blink/renderer/platform/graphics/paint/paint_image.h" #include "third_party/blink/renderer/platform/graphics/unaccelerated_static_bitmap_image.h" +#include "third_party/blink/renderer/platform/runtime_enabled_features.h" #include "third_party/skia/include/core/SkCanvas.h" #include "third_party/skia/include/core/SkImage.h" #include "third_party/skia/include/core/SkPaint.h" #include "third_party/skia/include/core/SkSurface.h" +#include "third_party/skia/include/private/SkColorData.h" #include "ui/gfx/geometry/skia_conversions.h" #include "v8/include/v8.h" @@ -79,4 +83,154 @@ void StaticBitmapImage::DrawHelper(cc::P 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 > 10) { + pixels = 10; + } 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 --- a/third_party/blink/renderer/platform/graphics/static_bitmap_image.h +++ b/third_party/blink/renderer/platform/graphics/static_bitmap_image.h @@ -36,6 +36,8 @@ class PLATFORM_EXPORT StaticBitmapImage 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 --- a/third_party/blink/renderer/platform/runtime_enabled_features.json5 +++ b/third_party/blink/renderer/platform/runtime_enabled_features.json5 @@ -1108,6 +1108,9 @@ name: "FingerprintingCanvasMeasureTextNoise", }, { + name: "FingerprintingCanvasImageDataNoise", + }, + { name: "FocuslessSpatialNavigation", settable_from_internals: true, },