feat: WebGL fingerprint spoofing

Experimental WebGL fingerprint injection.
- Allows the ability to set all WebGL parameters, supported extension list, shader precision formats, & context attributes.
- Added a demo website under scripts/examples/webgl.html that can be used to generate Camoufox config data
This commit is contained in:
daijro 2024-10-14 02:12:13 -05:00
parent df755def7f
commit 02bc15161a
10 changed files with 880 additions and 58 deletions

View file

@ -109,7 +109,7 @@ build:
@if [ ! -f $(cf_source_dir)/_READY ]; then \
make dir; \
fi
cd $(cf_source_dir) && ./mach build
cd $(cf_source_dir) && ./mach build $(_ARGS)
edits:
python ./scripts/developer.py $(version) $(release)

View file

@ -14,6 +14,7 @@ Written by daijro.
#include "mozilla/glue/Debug.h"
#include <stdlib.h>
#include <stdio.h>
#include <variant>
#ifdef _WIN32
# include <windows.h>
@ -76,11 +77,9 @@ inline const nlohmann::json& GetJson() {
}
inline bool HasKey(const std::string& key, nlohmann::json& data) {
if (!data.contains(key)) {
// printf_stderr("WARNING: Key not found: %s\n", key.c_str());
return false;
}
return true;
// printf_stderr("Property: %s\n", key.c_str());
// printf_stderr("WARNING: Key not found: %s\n", key.c_str());
return data.contains(key);
}
inline std::optional<std::string> GetString(const std::string& key) {
@ -163,8 +162,8 @@ inline std::optional<std::array<uint32_t, 4>> GetRect(
const std::string& height) {
// Make top and left default to 0
std::array<std::optional<uint32_t>, 4> values = {
GetUint32(left).value_or(0), GetUint32(top).value_or(0),
GetUint32(width), GetUint32(height)};
GetUint32(left).value_or(0), GetUint32(top).value_or(0), GetUint32(width),
GetUint32(height)};
// If height or width is std::nullopt, return std::nullopt
if (!values[2].has_value() || !values[3].has_value()) {
@ -197,4 +196,88 @@ inline std::optional<std::array<int32_t, 4>> GetInt32Rect(
return std::nullopt;
}
// Helpers for WebGL
inline std::optional<nlohmann::json> GetNested(const std::string& domain,
std::string keyStr) {
auto data = GetJson();
if (!data.contains(domain)) return std::nullopt;
if (!data[domain].contains(keyStr)) return std::nullopt;
return data[domain][keyStr];
}
template <typename T>
inline std::optional<T> GetAttribute(const std::string attrib, bool isWebGL2) {
auto value = MaskConfig::GetNested(
isWebGL2 ? "webgl2:contextAttributes" : "webgl:contextAttributes",
attrib);
if (!value) return std::nullopt;
return value.value().get<T>();
}
inline std::optional<std::variant<int64_t, bool, double, std::string>> GLParam(
uint32_t pname, bool isWebGL2) {
auto value =
MaskConfig::GetNested(isWebGL2 ? "webgl2:parameters" : "webgl:parameters",
std::to_string(pname));
if (!value) return std::nullopt;
auto data = value.value();
// cast the data and return
if (data.is_number_integer()) return data.get<int64_t>();
if (data.is_boolean()) return data.get<bool>();
if (data.is_number_float()) return data.get<double>();
if (data.is_string()) return data.get<std::string>();
return std::nullopt;
}
template <typename T>
inline T MParamGL(uint32_t pname, T defaultValue, bool isWebGL2) {
if (auto value = MaskConfig::GetNested(
isWebGL2 ? "webgl2:parameters" : "webgl:parameters",
std::to_string(pname));
value.has_value()) {
return value.value().get<T>();
}
return defaultValue;
}
template <typename T>
inline std::vector<T> MParamGLVector(uint32_t pname,
std::vector<T> defaultValue,
bool isWebGL2) {
if (auto value = MaskConfig::GetNested(
isWebGL2 ? "webgl2:parameters" : "webgl:parameters",
std::to_string(pname)); value.has_value()) {
if (value.value().is_array()) {
std::array<T, 4UL> result = value.value().get<std::array<T, 4UL>>();
return std::vector<T>(result.begin(), result.end());
}
}
return defaultValue;
}
inline std::optional<std::array<int32_t, 3UL>> MShaderData(
uint32_t shaderType, uint32_t precisionType, bool isWebGL2) {
std::string valueName =
std::to_string(shaderType) + "," + std::to_string(precisionType);
if (auto value =
MaskConfig::GetNested(isWebGL2 ? "webgl2:shaderPrecisionFormats"
: "webgl:shaderPrecisionFormats",
valueName)) {
// Convert {rangeMin: int, rangeMax: int, precision: int} to array
auto data = value.value();
// Assert rangeMin, rangeMax, and precision are present
if (!data.contains("rangeMin") || !data.contains("rangeMax") ||
!data.contains("precision")) {
return std::nullopt;
}
return std::array<int32_t, 3U>{data["rangeMin"].get<int32_t>(),
data["rangeMax"].get<int32_t>(),
data["precision"].get<int32_t>()};
}
return std::nullopt;
}
} // namespace MaskConfig

View file

@ -463,52 +463,6 @@ index 3a90c93c01..91d673039b 100644
+# DOM Mask
+LOCAL_INCLUDES += ["/camoucfg"]
\ No newline at end of file
diff --git a/dom/canvas/ClientWebGLContext.cpp b/dom/canvas/ClientWebGLContext.cpp
index 4e43425ef9..26bbfdc868 100644
--- a/dom/canvas/ClientWebGLContext.cpp
+++ b/dom/canvas/ClientWebGLContext.cpp
@@ -4,6 +4,7 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
#include "ClientWebGLContext.h"
+#include "MaskConfig.hpp"
#include <bitset>
@@ -2399,6 +2400,10 @@ void ClientWebGLContext::GetParameter(JSContext* cx, GLenum pname,
switch (pname) {
case dom::WEBGL_debug_renderer_info_Binding::UNMASKED_RENDERER_WEBGL:
+ if (auto value = MaskConfig::GetString("webGl:renderer")) {
+ ret = Some(value.value());
+ break;
+ }
ret = GetUnmaskedRenderer();
if (ret && StaticPrefs::webgl_sanitize_unmasked_renderer()) {
*ret = webgl::SanitizeRenderer(*ret);
@@ -2406,6 +2411,10 @@ void ClientWebGLContext::GetParameter(JSContext* cx, GLenum pname,
break;
case dom::WEBGL_debug_renderer_info_Binding::UNMASKED_VENDOR_WEBGL:
+ if (auto value = MaskConfig::GetString("webGl:vendor")) {
+ ret = Some(value.value());
+ break;
+ }
ret = GetUnmaskedVendor();
break;
diff --git a/dom/canvas/moz.build b/dom/canvas/moz.build
index 3a533d36d1..41d74d9b3f 100644
--- a/dom/canvas/moz.build
+++ b/dom/canvas/moz.build
@@ -221,3 +221,6 @@ if CONFIG["CC_TYPE"] == "gcc":
# Add libFuzzer configuration directives
include("/tools/fuzzing/libfuzzer-config.mozbuild")
+
+# DOM Mask
+LOCAL_INCLUDES += ["/camoucfg"]
\ No newline at end of file
diff --git a/dom/workers/WorkerNavigator.cpp b/dom/workers/WorkerNavigator.cpp
index b05e409a84..8e0f7ea657 100644
--- a/dom/workers/WorkerNavigator.cpp

View file

@ -0,0 +1,360 @@
diff --git a/dom/canvas/ClientWebGLContext.cpp b/dom/canvas/ClientWebGLContext.cpp
index db60868f65..afed6eeb7c 100644
--- a/dom/canvas/ClientWebGLContext.cpp
+++ b/dom/canvas/ClientWebGLContext.cpp
@@ -4,6 +4,8 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
#include "ClientWebGLContext.h"
+#include "MaskConfig.hpp"
+#include <algorithm>
#include <bitset>
@@ -744,6 +746,13 @@ void ClientWebGLContext::SetDrawingBufferColorSpace(
Run<RPROC(SetDrawingBufferColorSpace)>(*mDrawingBufferColorSpace);
}
+bool ClientWebGLContext::MBoolVal(const std::string& key, bool defaultValue) {
+ if (auto value = MaskConfig::GetAttribute<bool>(key, mIsWebGL2);
+ value.has_value())
+ return value.value();
+ return defaultValue;
+}
+
void ClientWebGLContext::GetContextAttributes(
dom::Nullable<dom::WebGLContextAttributes>& retval) {
retval.SetNull();
@@ -754,14 +763,38 @@ void ClientWebGLContext::GetContextAttributes(
const auto& options = mNotLost->info.options;
- result.mAlpha.Construct(options.alpha);
- result.mDepth = options.depth;
- result.mStencil = options.stencil;
- result.mAntialias.Construct(options.antialias);
- result.mPremultipliedAlpha = options.premultipliedAlpha;
- result.mPreserveDrawingBuffer = options.preserveDrawingBuffer;
- result.mFailIfMajorPerformanceCaveat = options.failIfMajorPerformanceCaveat;
- result.mPowerPreference = options.powerPreference;
+ result.mAlpha.Construct(MBoolVal("alpha", options.alpha));
+ result.mDepth = MBoolVal("depth", options.depth);
+ result.mStencil = MBoolVal("stencil", options.stencil);
+ result.mAntialias.Construct(MBoolVal("antialias", options.antialias));
+ result.mPremultipliedAlpha = MBoolVal(
+ "webgl:contextAttributes.premultipliedAlpha", options.premultipliedAlpha);
+ result.mPreserveDrawingBuffer =
+ MBoolVal("preserveDrawingBuffer", options.preserveDrawingBuffer);
+ result.mFailIfMajorPerformanceCaveat = MBoolVal(
+ "failIfMajorPerformanceCaveat", options.failIfMajorPerformanceCaveat);
+ if (auto value =
+ MaskConfig::GetAttribute<std::string>("powerPreference", mIsWebGL2);
+ value.has_value()) {
+ // Convert to enum
+ switch (value.value()[0]) {
+ case 'd':
+ result.mPowerPreference = dom::WebGLPowerPreference::Default;
+ break;
+ case 'l':
+ result.mPowerPreference = dom::WebGLPowerPreference::Low_power;
+ break;
+ case 'h':
+ result.mPowerPreference = dom::WebGLPowerPreference::High_performance;
+ break;
+ default:
+ // Invalid value
+ result.mPowerPreference = options.powerPreference;
+ break;
+ }
+ } else {
+ result.mPowerPreference = options.powerPreference;
+ }
}
// -----------------------
@@ -979,18 +1012,28 @@ bool ClientWebGLContext::CreateHostContext(const uvec2& requestedSize) {
std::unordered_map<GLenum, bool> webgl::MakeIsEnabledMap(const bool webgl2) {
auto ret = std::unordered_map<GLenum, bool>{};
- ret[LOCAL_GL_BLEND] = false;
- ret[LOCAL_GL_CULL_FACE] = false;
- ret[LOCAL_GL_DEPTH_TEST] = false;
- ret[LOCAL_GL_DITHER] = true;
- ret[LOCAL_GL_POLYGON_OFFSET_FILL] = false;
- ret[LOCAL_GL_SAMPLE_ALPHA_TO_COVERAGE] = false;
- ret[LOCAL_GL_SAMPLE_COVERAGE] = false;
- ret[LOCAL_GL_SCISSOR_TEST] = false;
- ret[LOCAL_GL_STENCIL_TEST] = false;
+ ret[LOCAL_GL_BLEND] =
+ MaskConfig::MParamGL<bool>(LOCAL_GL_BLEND, false, webgl2);
+ ret[LOCAL_GL_CULL_FACE] =
+ MaskConfig::MParamGL<bool>(LOCAL_GL_CULL_FACE, false, webgl2);
+ ret[LOCAL_GL_DEPTH_TEST] =
+ MaskConfig::MParamGL<bool>(LOCAL_GL_DEPTH_TEST, false, webgl2);
+ ret[LOCAL_GL_DITHER] =
+ MaskConfig::MParamGL<bool>(LOCAL_GL_DITHER, true, webgl2);
+ ret[LOCAL_GL_POLYGON_OFFSET_FILL] =
+ MaskConfig::MParamGL<bool>(LOCAL_GL_POLYGON_OFFSET_FILL, false, webgl2);
+ ret[LOCAL_GL_SAMPLE_ALPHA_TO_COVERAGE] = MaskConfig::MParamGL<bool>(
+ LOCAL_GL_SAMPLE_ALPHA_TO_COVERAGE, false, webgl2);
+ ret[LOCAL_GL_SAMPLE_COVERAGE] =
+ MaskConfig::MParamGL<bool>(LOCAL_GL_SAMPLE_COVERAGE, false, webgl2);
+ ret[LOCAL_GL_SCISSOR_TEST] =
+ MaskConfig::MParamGL<bool>(LOCAL_GL_SCISSOR_TEST, false, webgl2);
+ ret[LOCAL_GL_STENCIL_TEST] =
+ MaskConfig::MParamGL<bool>(LOCAL_GL_STENCIL_TEST, false, webgl2);
if (webgl2) {
- ret[LOCAL_GL_RASTERIZER_DISCARD] = false;
+ ret[LOCAL_GL_RASTERIZER_DISCARD] =
+ MaskConfig::MParamGL<bool>(LOCAL_GL_RASTERIZER_DISCARD, false, webgl2);
}
return ret;
@@ -2058,6 +2101,28 @@ void ClientWebGLContext::GetParameter(JSContext* cx, GLenum pname,
const auto& state = State();
// -
+ std::optional<std::variant<int64_t, bool, double, std::string>> data;
+ data = MaskConfig::GLParam(pname, mIsWebGL2);
+
+ if (data.has_value()) {
+ const auto& value = data.value();
+ if (std::holds_alternative<int64_t>(value)) {
+ retval.set(JS::NumberValue(double(std::get<int64_t>(value))));
+ return;
+ }
+ if (std::holds_alternative<double>(value)) {
+ retval.set(JS::NumberValue(std::get<double>(value)));
+ return;
+ }
+ if (std::holds_alternative<bool>(value)) {
+ retval.set(JS::BooleanValue(std::get<bool>(value)));
+ return;
+ }
+ if (std::holds_alternative<std::string>(value)) {
+ retval.set(StringValue(cx, std::get<std::string>(value), rv));
+ return;
+ }
+ }
const auto fnSetRetval_Buffer = [&](const GLenum target) {
const auto buffer = *MaybeFind(state.mBoundBufferByTarget, target);
@@ -2163,49 +2228,84 @@ void ClientWebGLContext::GetParameter(JSContext* cx, GLenum pname,
// 2 floats
case LOCAL_GL_DEPTH_RANGE:
- retval.set(Create<dom::Float32Array>(cx, this, state.mDepthRange, rv));
+ retval.set(Create<dom::Float32Array>(
+ cx, this,
+ MaskConfig::MParamGL<std::array<float, 2UL>>(pname, state.mDepthRange,
+ mIsWebGL2),
+ rv));
return;
case LOCAL_GL_ALIASED_POINT_SIZE_RANGE:
- retval.set(
- Create<dom::Float32Array>(cx, this, limits.pointSizeRange, rv));
+ retval.set(Create<dom::Float32Array>(
+ cx, this,
+ MaskConfig::MParamGL<std::array<float, 2UL>>(
+ pname, limits.pointSizeRange, mIsWebGL2),
+ rv));
return;
case LOCAL_GL_ALIASED_LINE_WIDTH_RANGE:
- retval.set(
- Create<dom::Float32Array>(cx, this, limits.lineWidthRange, rv));
+ retval.set(Create<dom::Float32Array>(
+ cx, this,
+ MaskConfig::MParamGL<std::array<float, 2UL>>(
+ pname, limits.lineWidthRange, mIsWebGL2),
+ rv));
return;
// 4 floats
case LOCAL_GL_COLOR_CLEAR_VALUE:
- retval.set(Create<dom::Float32Array>(cx, this, state.mClearColor, rv));
+ retval.set(Create<dom::Float32Array>(
+ cx, this,
+ MaskConfig::MParamGL<std::array<float, 4UL>>(pname, state.mClearColor,
+ mIsWebGL2),
+ rv));
return;
case LOCAL_GL_BLEND_COLOR:
- retval.set(Create<dom::Float32Array>(cx, this, state.mBlendColor, rv));
+ retval.set(Create<dom::Float32Array>(
+ cx, this,
+ MaskConfig::MParamGL<std::array<float, 4UL>>(pname, state.mBlendColor,
+ mIsWebGL2),
+ rv));
return;
// 2 ints
case LOCAL_GL_MAX_VIEWPORT_DIMS: {
auto maxViewportDim = BitwiseCast<int32_t>(limits.maxViewportDim);
const auto dims = std::array<int32_t, 2>{maxViewportDim, maxViewportDim};
- retval.set(Create<dom::Int32Array>(cx, this, dims, rv));
+ retval.set(Create<dom::Int32Array>(
+ cx, this,
+ MaskConfig::MParamGL<std::array<int32_t, 2UL>>(pname, dims,
+ mIsWebGL2),
+ rv));
return;
}
// 4 ints
case LOCAL_GL_SCISSOR_BOX:
- retval.set(Create<dom::Int32Array>(cx, this, state.mScissor, rv));
+ retval.set(Create<dom::Int32Array>(
+ cx, this,
+ MaskConfig::MParamGL<std::array<int32_t, 4UL>>(pname, state.mScissor,
+ mIsWebGL2),
+ rv));
return;
case LOCAL_GL_VIEWPORT:
- retval.set(Create<dom::Int32Array>(cx, this, state.mViewport, rv));
+ retval.set(Create<dom::Int32Array>(
+ cx, this,
+ MaskConfig::MParamGL<std::array<int32_t, 4UL>>(pname, state.mViewport,
+ mIsWebGL2),
+ rv));
return;
- // any
case LOCAL_GL_COMPRESSED_TEXTURE_FORMATS:
- retval.set(Create<dom::Uint32Array>(cx, this,
- state.mCompressedTextureFormats, rv));
+ std::vector<uint32_t> compressedTextureUint32(
+ state.mCompressedTextureFormats.begin(),
+ state.mCompressedTextureFormats.end());
+ retval.set(Create<dom::Uint32Array>(
+ cx, this,
+ MaskConfig::MParamGLVector<uint32_t>(pname, compressedTextureUint32,
+ mIsWebGL2),
+ rv));
return;
}
@@ -2385,6 +2485,10 @@ void ClientWebGLContext::GetParameter(JSContext* cx, GLenum pname,
switch (pname) {
case dom::WEBGL_debug_renderer_info_Binding::UNMASKED_RENDERER_WEBGL:
+ if (auto value = MaskConfig::GetString("webGl:renderer")) {
+ ret = Some(value.value());
+ break;
+ }
ret = GetUnmaskedRenderer();
if (ret && StaticPrefs::webgl_sanitize_unmasked_renderer()) {
*ret = webgl::SanitizeRenderer(*ret);
@@ -2392,6 +2496,10 @@ void ClientWebGLContext::GetParameter(JSContext* cx, GLenum pname,
break;
case dom::WEBGL_debug_renderer_info_Binding::UNMASKED_VENDOR_WEBGL:
+ if (auto value = MaskConfig::GetString("webGl:vendor")) {
+ ret = Some(value.value());
+ break;
+ }
ret = GetUnmaskedVendor();
break;
@@ -2482,7 +2590,9 @@ void ClientWebGLContext::GetParameter(JSContext* cx, GLenum pname,
case LOCAL_GL_COLOR_WRITEMASK: {
const auto mask = uint8_t(*maybe);
const auto bs = std::bitset<4>(mask);
- const auto src = std::array<bool, 4>{bs[0], bs[1], bs[2], bs[3]};
+ const auto src = MaskConfig::MParamGL<std::array<bool, 4>>(
+ pname, std::array<bool, 4>{bs[0], bs[1], bs[2], bs[3]},
+ mIsWebGL2);
JS::Rooted<JS::Value> arr(cx);
if (!dom::ToJSValue(cx, src.data(), src.size(), &arr)) {
rv = NS_ERROR_OUT_OF_MEMORY;
@@ -2865,6 +2975,24 @@ ClientWebGLContext::GetShaderPrecisionFormat(const GLenum shadertype,
const GLenum precisiontype) {
if (IsContextLost()) return nullptr;
const auto info = [&]() {
+ // Check for spoofed value
+ if (auto value =
+ MaskConfig::MShaderData(shadertype, precisiontype, mIsWebGL2)) {
+ const auto& format = value.value();
+ return Some(webgl::ShaderPrecisionFormat{
+ format[0], // rangeMin
+ format[1], // rangeMax
+ format[2] // precision
+ });
+ }
+ // Check if block if not defined is on
+ if (MaskConfig::GetBool(
+ mIsWebGL2 ? "webgl2:shaderPrecisionFormats:blockIfNotDefined"
+ : "webgl:shaderPrecisionFormats:blockIfNotDefined")) {
+ Maybe<webgl::ShaderPrecisionFormat> ret;
+ return ret;
+ }
+
const auto& inProcess = mNotLost->inProcess;
if (inProcess) {
return inProcess->GetShaderPrecisionFormat(shadertype, precisiontype);
@@ -5822,6 +5950,17 @@ bool ClientWebGLContext::IsSupported(const WebGLExtensionID ext,
return false;
}
+ if (std::vector<std::string> maskValues =
+ MaskConfig::GetStringList(mIsWebGL2 ? "webgl2:supportedExtensions"
+ : "webgl:supportedExtensions");
+ !maskValues.empty()) {
+ if (std::find(maskValues.begin(), maskValues.end(),
+ GetExtensionName(ext)) != maskValues.end()) {
+ return true;
+ }
+ return false;
+ }
+
const auto& limits = Limits();
return limits.supportedExtensions[ext];
}
@@ -5833,6 +5972,17 @@ void ClientWebGLContext::GetSupportedExtensions(
if (!mNotLost) return;
auto& retarr = retval.SetValue();
+
+ // Implement separately to prevent O(n^2) timing
+ if (std::vector<std::string> maskValues =
+ MaskConfig::GetStringList("webgl:supportedExtensions");
+ !maskValues.empty()) {
+ for (const auto& ext : maskValues) {
+ retarr.AppendElement(NS_ConvertUTF8toUTF16(ext));
+ }
+ return;
+ }
+
for (const auto i : MakeEnumeratedRange(WebGLExtensionID::Max)) {
if (!IsSupported(i, callerType)) continue;
diff --git a/dom/canvas/ClientWebGLContext.h b/dom/canvas/ClientWebGLContext.h
index 9e8b3c0f00..16e3fc7aa5 100644
--- a/dom/canvas/ClientWebGLContext.h
+++ b/dom/canvas/ClientWebGLContext.h
@@ -1067,6 +1067,9 @@ class ClientWebGLContext final : public nsICanvasRenderingContextInternal,
// -
+ // Helper to get booleans set in MaskConfig
+ bool MBoolVal(const std::string& key, bool defaultValue);
+
void GetContextAttributes(dom::Nullable<dom::WebGLContextAttributes>& retval);
private:
diff --git a/dom/canvas/moz.build b/dom/canvas/moz.build
index 3a533d36d1..41d74d9b3f 100644
--- a/dom/canvas/moz.build
+++ b/dom/canvas/moz.build
@@ -221,3 +221,6 @@ if CONFIG["CC_TYPE"] == "gcc":
# Add libFuzzer configuration directives
include("/tools/fuzzing/libfuzzer-config.mozbuild")
+
+# DOM Mask
+LOCAL_INCLUDES += ["/camoucfg"]
\ No newline at end of file

View file

@ -0,0 +1,63 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Random Button Clicker</title>
<style>
body {
margin: 0;
padding: 0;
height: 100vh;
overflow: hidden;
}
.button {
position: absolute;
padding: 10px 20px;
background-color: #007bff;
color: white;
border: none;
cursor: pointer;
font-size: 16px;
}
#cursor-highlight {
position: fixed;
width: 20px;
height: 20px;
border-radius: 50%;
background-color: rgba(255, 0, 0, 0.5);
pointer-events: none;
transform: translate(-50%, -50%);
}
</style>
</head>
<body>
<script>
const body = document.body;
function createButton() {
const button = document.createElement("button");
button.classList.add("button");
button.textContent = "Click me!";
const maxX = window.innerWidth - 100;
const maxY = window.innerHeight - 50;
const randomX = Math.floor(Math.random() * maxX);
const randomY = Math.floor(Math.random() * maxY);
button.style.left = `${randomX}px`;
button.style.top = `${randomY}px`;
button.addEventListener("click", () => {
button.remove();
createButton();
});
body.appendChild(button);
}
// Create the first button
createButton();
</script>
</body>
</html>

View file

@ -0,0 +1 @@
python -m http.server

345
scripts/examples/webgl.html Normal file
View file

@ -0,0 +1,345 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>WebGL Fingerprinting - Camoufox</title>
<style>
html,
body {
height: 100%;
margin: 0;
padding: 0;
overflow: hidden;
}
body {
font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;
background-color: #1e1e1e;
color: #d4d4d4;
display: flex;
flex-direction: column;
}
.container {
flex: 1;
display: flex;
flex-direction: column;
padding: 20px;
overflow: hidden;
}
h1 {
color: #569cd6;
margin-top: 0;
}
#output {
flex: 1;
background-color: #252526;
border: 1px solid #3e3e42;
border-radius: 4px;
padding: 15px;
white-space: pre-wrap;
word-wrap: break-word;
font-family: "Consolas", "Courier New", monospace;
font-size: 14px;
overflow-y: auto;
}
.button-container {
display: flex;
gap: 10px;
margin-top: 5px;
}
.btn {
background-color: #0e639c;
color: white;
border: none;
padding: 3px 8px;
cursor: pointer;
border-radius: 4px;
font-size: 16px;
}
.btn:hover {
background-color: #1177bb;
}
.string {
color: #ce9178;
}
.number {
color: #b5cea8;
}
.boolean {
color: #569cd6;
}
.null {
color: #569cd6;
}
.key {
color: #9cdcfe;
}
#hash {
margin-bottom: 5px;
font-family: "Consolas", "Courier New", monospace;
font-size: 14px;
color: #569cd6;
}
#warning {
background-color: #ff9800;
color: #000;
margin-bottom: 10px;
border-radius: 4px;
display: none;
position: relative;
transition: opacity 0.3s ease-out, max-height 0.3s ease-out, padding 0.3s ease-out;
opacity: 1;
max-height: 100px;
overflow: hidden;
padding: 10px;
}
#warning.hide {
opacity: 0;
max-height: 0;
padding: 0;
}
#close-warning {
position: absolute;
top: 50%;
right: 5px;
transform: translateY(-50%);
background: none;
border: none;
color: #000;
font-size: 16px;
cursor: pointer;
}
</style>
</head>
<body>
<div class="container">
<h1>Your WebGL Information (in Camoufox format)</h1>
<div id="warning">Warning: This browser is not Firefox. The fingerprint below will not run properly on Camoufox.<button id="close-warning">&times;</button></div>
<div id="hash"></div>
<pre id="output"></pre>
<div class="button-container">
<button id="copy-btn" class="btn">Copy data</button>
<button id="copy-minified-btn" class="btn">Copy data (minified)</button>
</div>
</div>
<script>
const dumpWebGLCore = async (
webglContextId,
experimentalWebglContextId
) => {
function getWebGLContext() {
const canvas = new OffscreenCanvas(256, 256);
let result = null;
try {
result =
canvas.getContext(webglContextId, {
preserveDrawingBuffer: false,
}) ||
canvas.getContext(experimentalWebglContextId, {
preserveDrawingBuffer: false,
});
} catch (ex) {}
result || (result = null);
return result;
}
const webglContext = getWebGLContext();
if (!webglContext) {
return {};
}
function getMaxAnisotropy(ctx) {
if (ctx) {
const ext =
ctx.getExtension("EXT_texture_filter_anisotropic") ||
ctx.getExtension("WEBKIT_EXT_texture_filter_anisotropic") ||
ctx.getExtension("MOZ_EXT_texture_filter_anisotropic");
if (ext) {
return ctx.getParameter(ext.MAX_TEXTURE_MAX_ANISOTROPY_EXT);
}
}
return null;
}
const glEnums = [
2849, 2884, 2885, 2886, 2928, 2929, 2930, 2931, 2932, 2960, 2961,
2962, 2963, 2964, 2965, 2966, 2967, 2968, 2978, 3024, 3042, 3088,
3089, 3106, 3107, 3317, 3333, 3379, 3386, 3408, 3410, 3411, 3412,
3413, 3414, 3415, 7936, 7937, 7938, 10752, 32773, 32777, 32823, 32824,
32873, 32883, 32936, 32937, 32938, 32939, 32968, 32969, 32970, 32971,
33170, 33901, 33902, 34016, 34024, 34045, 34047, 34068, 34076, 34467,
34816, 34817, 34818, 34819, 34852, 34877, 34921, 34930, 34964, 34965,
35071, 35076, 35077, 35371, 35373, 35374, 35375, 35376, 35377, 35379,
35380, 35657, 35658, 35659, 35660, 35661, 35724, 35725, 35968, 35978,
35979, 36003, 36004, 36005, 36006, 36007, 36063, 36183, 36347, 36348,
36349, 37154, 37157, 37440, 37441, 37443, 37444, 37445, 37446,
];
let rendererInfo = null;
const debug_ext = webglContext.getExtension(
"WEBGL_debug_renderer_info"
);
if (debug_ext) {
rendererInfo = {
webglVendor: webglContext.getParameter(
debug_ext.UNMASKED_VENDOR_WEBGL
),
webglRenderer: webglContext.getParameter(
debug_ext.UNMASKED_RENDERER_WEBGL
),
};
}
const result = {
[`${webglContextId}:renderer`]: rendererInfo
? rendererInfo.webglRenderer
: "Not available",
[`${webglContextId}:vendor`]: rendererInfo
? rendererInfo.webglVendor
: "Not available",
[`${webglContextId}:contextAttributes`]:
webglContext.getContextAttributes(),
[`${webglContextId}:supportedExtensions`]:
webglContext.getSupportedExtensions() || [],
[`${webglContextId}:parameters`]: {},
[`${webglContextId}:shaderPrecisionFormats`]: {},
};
for (const glEnum of glEnums) {
try {
const parmValue = webglContext.getParameter(glEnum);
if (
Array.isArray(parmValue) ||
[Float32Array, Int32Array, Uint32Array].some(
(type) => parmValue instanceof type
)
) {
const arrayValue = Array.from(parmValue);
result[`${webglContextId}:parameters`][glEnum] =
arrayValue.length > 0 ? arrayValue : null;
} else {
result[`${webglContextId}:parameters`][glEnum] = parmValue;
}
} catch (err) {
console.log(err);
}
}
const shaderTypes = [
webglContext.VERTEX_SHADER,
webglContext.FRAGMENT_SHADER,
];
const precisionTypes = [
webglContext.LOW_FLOAT,
webglContext.MEDIUM_FLOAT,
webglContext.HIGH_FLOAT,
webglContext.LOW_INT,
webglContext.MEDIUM_INT,
webglContext.HIGH_INT,
];
for (let shaderType of shaderTypes) {
for (let precisionType of precisionTypes) {
const r = webglContext.getShaderPrecisionFormat(
shaderType,
precisionType
);
const key = `${shaderType},${precisionType}`;
result[`${webglContextId}:shaderPrecisionFormats`][key] = {
rangeMin: r.rangeMin,
rangeMax: r.rangeMax,
precision: r.precision,
};
}
}
return result;
};
function syntaxHighlight(json) {
json = json
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;");
return json.replace(
/("(\\u[a-zA-Z0-9]{4}|\\[^u]|[^\\"])*"(\s*:)?|\b(true|false|null)\b|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?)/g,
function (match) {
var cls = "number";
if (/^"/.test(match)) {
if (/:$/.test(match)) {
cls = "key";
} else {
cls = "string";
}
} else if (/true|false/.test(match)) {
cls = "boolean";
} else if (/null/.test(match)) {
cls = "null";
}
return '<span class="' + cls + '">' + match + "</span>";
}
);
}
async function sha256(message) {
const msgBuffer = new TextEncoder().encode(message);
const hashBuffer = await crypto.subtle.digest("SHA-256", msgBuffer);
const hashArray = Array.from(new Uint8Array(hashBuffer));
const hashHex = hashArray
.map((b) => b.toString(16).padStart(2, "0"))
.join("");
return hashHex;
}
document.addEventListener(
"DOMContentLoaded",
async () => {
// Check if the user agent is Firefox
const isFirefox = navigator.userAgent.toLowerCase().indexOf('firefox') > -1;
if (!isFirefox) {
document.getElementById('warning').style.display = 'block';
}
// Add event listener for close button
document.getElementById('close-warning').addEventListener('click', function() {
document.getElementById('warning').classList.add('hide');
});
const [webglDetail, webgl2Detail] = await Promise.all([
dumpWebGLCore("webgl", "experimental-webgl"),
dumpWebGLCore("webgl2", "experimental-webgl2"),
]);
const combinedInfo = { ...webglDetail, ...webgl2Detail };
const jsonString = JSON.stringify(combinedInfo, null, 2);
const minifiedJson = JSON.stringify(combinedInfo);
const hashValue = await sha256(minifiedJson);
document.getElementById("hash").textContent = `SHA256: ${hashValue}`;
document.getElementById("output").innerHTML =
syntaxHighlight(jsonString);
document.getElementById("copy-btn").addEventListener("click", () => {
navigator.clipboard.writeText(jsonString).then(() => {
alert("Copied formatted JSON to clipboard!");
});
});
document
.getElementById("copy-minified-btn")
.addEventListener("click", () => {
navigator.clipboard.writeText(minifiedJson).then(() => {
alert("Copied minified JSON to clipboard!");
});
});
},
false
);
</script>
</body>
</html>

View file

@ -194,6 +194,9 @@ pref("layout.css.always_underline_links", true);
// No WebGL
pref("webgl.disabled", true);
// Force software rendering
pref("webgl.forbid-hardware", true);
pref("webgl.forbid-software", false);
// Debloat pt.2 (from Librewolf)

View file

@ -48,8 +48,6 @@
{ "property": "webrtc:ipv4", "type": "str" },
{ "property": "webrtc:ipv6", "type": "str" },
{ "property": "pdfViewerEnabled", "type": "bool" },
{ "property": "webGl:renderer", "type": "str" },
{ "property": "webGl:vendor", "type": "str" },
{ "property": "battery:charging", "type": "bool" },
{ "property": "battery:chargingTime", "type": "double" },
{ "property": "battery:dischargingTime", "type": "double" },
@ -64,6 +62,21 @@
{ "property": "locale:script", "type": "str" },
{ "property": "humanize", "type": "bool" },
{ "property": "humanize:maxTime", "type": "double" },
{ "property": "humanize:minTime", "type": "double" },
{ "property": "showcursor", "type": "bool" },
{ "property": "AudioContext:sampleRate", "type": "uint" },
{ "property": "AudioContext:outputLatency", "type": "double" },
{ "property": "AudioContext:maxChannelCount", "type": "uint" },
{ "property": "webGl:renderer", "type": "str" },
{ "property": "webGl:vendor", "type": "str" },
{ "property": "webgl:supportedExtensions", "type": "array" },
{ "property": "webgl:parameters", "type": "dict" },
{ "property": "webgl2:parameters", "type": "dict" },
{ "property": "webgl:shaderPrecisionFormats", "type": "dict" },
{ "property": "webgl:shaderPrecisionFormats:blockIfNotDefined", "type": "bool" },
{ "property": "webgl2:shaderPrecisionFormats", "type": "dict" },
{ "property": "webgl2:shaderPrecisionFormats:blockIfNotDefined", "type": "bool" },
{ "property": "webgl:contextAttributes", "type": "dict" },
{ "property": "webgl2:contextAttributes", "type": "dict" },
{ "property": "debug", "type": "bool" }
]

View file

@ -1,2 +1,2 @@
version=130.0.1
release=beta.11
release=beta.12