Skip to content
This repository has been archived by the owner on Dec 11, 2019. It is now read-only.

Commit

Permalink
Merge pull request #1354 from brave/fix/canvas-fingerprint
Browse files Browse the repository at this point in the history
Block (most) canvas and webgl fingerprinting by default
  • Loading branch information
bbondy committed Apr 17, 2016
2 parents 2e196e4 + b7daf57 commit 1dd2048
Show file tree
Hide file tree
Showing 9 changed files with 235 additions and 0 deletions.
221 changes: 221 additions & 0 deletions app/extensions/brave/brave-default.js
Original file line number Diff line number Diff line change
Expand Up @@ -763,4 +763,225 @@ if (typeof KeyEvent === 'undefined') {
ipcRenderer.on('post-page-load-run', function () {
ipcRenderer.sendToHost('theme-color-computed', computeThemeColor())
})

/** Begin canvas fingerprinting detection **/
/**
* @return {string}
*/
function getPageScript () {
return '(' + Function.prototype.toString.call(function (ERROR) {
ERROR.stackTraceLimit = Infinity // collect all frames
var event_id = document.currentScript ? document.currentScript.getAttribute('data-event-id') : ''

// from Underscore v1.6.0
function debounce (func, wait, immediate) {
var timeout, args, context, timestamp, result

var later = function () {
var last = Date.now() - timestamp
if (last < wait) {
timeout = setTimeout(later, wait - last)
} else {
timeout = null
if (!immediate) {
result = func.apply(context, args)
context = args = null
}
}
}

return function () {
context = this
args = arguments
timestamp = Date.now()
var callNow = immediate && !timeout
if (!timeout) {
timeout = setTimeout(later, wait)
}
if (callNow) {
result = func.apply(context, args)
context = args = null
}
return result
}
}

// messages the injected script
var send = (function () {
var messages = []
// debounce sending queued messages
var _send = debounce(function () {
document.dispatchEvent(new window.CustomEvent(event_id, {
detail: messages
}))
// clear the queue
messages = []
}, 100)
return function (msg) {
// queue the message
messages.push(msg)
_send()
}
}())

// https://code.google.com/p/v8-wiki/wiki/JavaScriptStackTraceApi
/**
* Customize the stack trace
* @param structured If true, change to customized version
* @returns {*} Returns the stack trace
*/
function getStackTrace (structured) {
var errObj = {}
var origFormatter
var stack

if (structured) {
origFormatter = ERROR.prepareStackTrace
ERROR.prepareStackTrace = function (errObj, structuredStackTrace) {
return structuredStackTrace
}
}

ERROR.captureStackTrace(errObj, getStackTrace)
stack = errObj.stack

if (structured) {
ERROR.prepareStackTrace = origFormatter
}

return stack
}

/**
* Checks the stack trace for the originating URL
* @returns {String} The URL of the originating script (URL:Line number:Column number)
*/
function getOriginatingScriptUrl () {
var trace = getStackTrace(true)

if (trace.length < 2) {
return ''
}

// this script is at 0 and 1
var callSite = trace[2]

if (callSite.isEval()) {
// argh, getEvalOrigin returns a string ...
var eval_origin = callSite.getEvalOrigin()
var script_url_matches = eval_origin.match(/\((http.*:\d+:\d+)/)

return script_url_matches && script_url_matches[1] || eval_origin
} else {
return callSite.getFileName() + ':' + callSite.getLineNumber() + ':' + callSite.getColumnNumber()
}
}

/**
* Strip away the line and column number (from stack trace urls)
* @param script_url The stack trace url to strip
* @returns {String} the pure URL
*/
function stripLineAndColumnNumbers (script_url) {
return script_url.replace(/:\d+:\d+$/, '')
}

/**
* Monitor the reads from a canvas instance
* @param item special item objects
*/
function trapInstanceMethod (item) {
item.obj[item.propName] = (function (orig) {
return function () {
var script_url = getOriginatingScriptUrl()
var msg = {
obj: item.objName,
prop: item.propName,
scriptUrl: stripLineAndColumnNumbers(script_url)
}

// Block the read from occuring; send info to background page instead
console.log('blocking canvas read', msg)
send(msg)
}
}(item.obj[item.propName]))
}

var methods = []
var canvasMethods = ['getImageData', 'getLineDash', 'measureText']
canvasMethods.forEach(function (method) {
var item = {
objName: 'CanvasRenderingContext2D.prototype',
propName: method,
obj: window.CanvasRenderingContext2D.prototype
}

methods.push(item)
})

var canvasElementMethods = ['toDataURL', 'toBlob']
canvasElementMethods.forEach(function (method) {
var item = {
objName: 'HTMLCanvasElement.prototype',
propName: method,
obj: window.HTMLCanvasElement.prototype
}
methods.push(item)
})

var webglMethods = ['getSupportedExtensions', 'getParameter', 'getContextAttributes',
'getShaderPrecisionFormat', 'getExtension']
webglMethods.forEach(function (method) {
var item = {
objName: 'WebGLRenderingContext.prototype',
propName: method,
obj: window.WebGLRenderingContext.prototype
}
methods.push(item)
})

methods.forEach(trapInstanceMethod)

// save locally to keep from getting overwritten by site code
}) + '(Error));'
}

/**
* Executes a script in the page DOM context
*
* @param text The content of the script to insert
* @param data attributes to set in the inserted script tag
*/
function insertScript (text, data) {
var parent = document.documentElement
var script = document.createElement('script')

script.text = text
script.async = false

for (var key in data) {
script.setAttribute('data-' + key.replace('_', '-'), data[key])
}

parent.insertBefore(script, parent.firstChild)
parent.removeChild(script)
}

ipcRenderer.on('block-canvas-fingerprinting', function () {
var event_id = Math.random().toString()

// listen for messages from the script we are about to insert
document.addEventListener(event_id, function (e) {
if (!e.detail) {
return
}
// pass these on to the background page
ipcRenderer.send('got-canvas-fingerprinting', e.detail)
})

insertScript(getPageScript(), {
event_id: event_id
})
})
/* End canvas fingerprinting detection */
}).apply(this)
1 change: 1 addition & 0 deletions app/extensions/brave/locales/en-US/preferences.l20n
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
<bookmarks "Bookmarks">
<openedTabs "Open tabs">
<doNotTrack "Send a 'Do Not Track' header with browsing requests (requires browser restart)">
<blockCanvasFingerprinting "Block HTML canvas and WebGL fingerprinting">
<advancedPrivacySettings "Advanced privacy settings:">

/* Security settings page */
Expand Down
4 changes: 4 additions & 0 deletions app/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -506,6 +506,10 @@ app.on('ready', () => {
})
})

ipcMain.on(messages.GOT_CANVAS_FINGERPRINTING, (e, details) => {
console.log('got canvas fingerprint block', details)
})

// Setup the crash handling
CrashHerald.init()

Expand Down
1 change: 1 addition & 0 deletions docs/state.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ AppStore
'privacy.history-suggestions': boolean, // Auto suggest for history enabled
'privacy.bookmark-suggestions': boolean, // Auto suggest for bookmarks enabled
'privacy.opened-tab-suggestions': boolean, // Auto suggest for opened tabs enabled
'privacy.block-canvas-fingerprinting': boolean, // Canvas fingerprinting defense
'security.passwords.manager-enabled': boolean // whether to use default password manager
}]
}
Expand Down
1 change: 1 addition & 0 deletions js/about/preferences.js
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,7 @@ class PrivacyTab extends ImmutableComponent {
</SettingsList>
<SettingsList dataL10nId='advancedPrivacySettings'>
<SettingCheckbox dataL10nId='doNotTrack' prefKey={settings.DO_NOT_TRACK} settings={this.props.settings} onChangeSetting={this.props.onChangeSetting}/>
<SettingCheckbox dataL10nId='blockCanvasFingerprinting' prefKey={settings.BLOCK_CANVAS_FINGERPRINTING} settings={this.props.settings} onChangeSetting={this.props.onChangeSetting}/>
</SettingsList>
</div>
}
Expand Down
3 changes: 3 additions & 0 deletions js/components/frame.js
Original file line number Diff line number Diff line change
Expand Up @@ -305,6 +305,9 @@ class Frame extends ImmutableComponent {
ipc.send(messages.CHECK_CERT_ERROR_ACCEPTED, parsedUrl.host, this.props.frame.get('key'))
}
}
if (getSetting(settings.BLOCK_CANVAS_FINGERPRINTING)) {
this.webview.send(messages.BLOCK_CANVAS_FINGERPRINTING)
}
windowActions.updateBackForwardState(
this.props.frame,
this.webview.canGoBack(),
Expand Down
1 change: 1 addition & 0 deletions js/constants/appConfig.js
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ module.exports = {
'privacy.bookmark-suggestions': true,
'privacy.opened-tab-suggestions': true,
'privacy.autocomplete.history-size': 500,
'privacy.block-canvas-fingerprinting': false,
'bookmarks.toolbar.show': false,
'privacy.do-not-track': false,
'security.passwords.manager-enabled': true,
Expand Down
2 changes: 2 additions & 0 deletions js/constants/messages.js
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,8 @@ const messages = {
LEAVE_FULL_SCREEN: _,
SET_CLIPBOARD: _,
AUTOFILL_PASSWORD: _,
BLOCK_CANVAS_FINGERPRINTING: _,
GOT_CANVAS_FINGERPRINTING: _,
// Password manager
GET_PASSWORDS: _, /** @arg {string} formOrigin, @arg {string} action */
GOT_PASSWORD: _, /** @arg {string} username, @arg {string} password, @arg {string} origin, @arg {string} action, @arg {boolean} isUnique */
Expand Down
1 change: 1 addition & 0 deletions js/constants/settings.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ const settings = {
OPENED_TAB_SUGGESTIONS: 'privacy.opened-tab-suggestions',
AUTOCOMPLETE_HISTORY_SIZE: 'privacy.autocomplete.history-size',
DO_NOT_TRACK: 'privacy.do-not-track',
BLOCK_CANVAS_FINGERPRINTING: 'privacy.block-canvas-fingerprinting',
// Security Tab
PASSWORD_MANAGER_ENABLED: 'security.passwords.manager-enabled',
ONE_PASSWORD_ENABLED: 'security.passwords.one-password-enabled',
Expand Down

0 comments on commit 1dd2048

Please sign in to comment.