/** \file
* \brief Wrappers for Web Audio API
*
* \see https://webaudio.github.io/web-audio-api
*
* \author Copyright (C) 2021 Matus Svancar
*
* \license SPDX-License-Identifier: GPL-3.0-or-later
* \license SPDX-License-Identifier: MPL-2.0
*/
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see .
//
// Alternatively, the contents of this file may be used under the terms
// of the Mozilla Public License, v. 2.0, as described below:
//
// 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/.
//
// \copyright Copyright (c) 2020 The Brave Authors.
/** \file
* This file contains wrappers for AudioBuffer and AnalyserNode related calls
* * https://developer.mozilla.org/en-US/docs/Web/API/AudioBuffer
* * https://developer.mozilla.org/en-US/docs/Web/API/AnalyserNode
* \ingroup wrappers
*
* The goal is to prevent fingerprinting by modifying the values from functions which are reading/copying from AudioBuffer and AnalyserNode.
* So the audio content of wrapped objects is the same as intended.
*
* The modified content can be either a white noise based on domain key or a fake audio data that is modified according to
* domain key to be different than the original albeit very similar (i.e. the approach
* inspired by the algorithms created by Brave Software
* available at https://github.com/brave/brave-core/blob/master/chromium_src/third_party/blink/renderer/core/execution_context/execution_context.cc.)
*
* \note Both approaches are detectable by a fingerprinter that checks if a predetermined audio
* is the same as the read one. Nevertheless, the aim of the wrappers is
* to limit the finerprintability.
*
* \bug Possibly inconsistant data between AudioBuffer and AnalyserNode wrappers.
*
* \bug Inconsistency between AudioBuffer.prototype.copyFromChannel and AudioBuffer.prototype.getChannelData.
* AudioBuffer.prototype.copyFromChannel should call AudioBuffer.prototype.getChannelData wrapper
* and then return result of the original call.
*/
/*
* Create private namespace
*/
(function() {
/**
* \brief Modifies audio data
*
* \param arr typed array with data - Uint8Array or Float32Array
*
* Depending on level chosen this function modifies arr content:
* * (0) - multiplies values by fudge factor based on domain key
* * (1) - replace values by white noise based on domain key
*/
function audioFarble(array){
// PRNG function needs to depend on the original audio, so that the same
// audio is farbled the same way but different audio is farbled differently
// See https://pagure.io/JShelter/webextension/issue/23
const MAXUINT32 = 4294967295;
let crc = new CRC16();
for (value of array) {
crc.single(value * MAXUINT32);
}
var thisaudio_prng = alea(domainHash, "AudioFarbling", crc.crc);
for (i in array) {
// Possible improvements:
// Copy a neighbor data (possibly with modifications
// Make bigger canges than xoring with 1
array[i] *= 0.99 + thisaudio_prng() / 100;
}
};
function audioFarbleInt(array) {
// PRNG function needs to depend on the original audio, so that the same
// audio is farbled the same way but different audio is farbled differently
// See https://pagure.io/JShelter/webextension/issue/23
let crc = new CRC16();
for (value of array) {
crc.single(value);
}
var thisaudio_prng = alea(domainHash, "AudioFarbling", crc.crc);
for (i in array) {
if (thisaudio_prng.get_bits(1)) { // Modify data with probability of 0.5
// Possible improvements:
// Copy a neighbor data (possibly with modifications
// Make bigger canges than xoring with 1
array[i] ^= 1;
}
}
}
function whiteNoiseInt(array) {
noise_prng = alea(Date.now(), prng());
for (i in array) {
array[i] = (noise_prng() * 256) | 0;
}
}
function whiteNoiseFloat(array) {
noise_prng = alea(Date.now(), prng());
for (i in array) {
array[i] = (noise_prng() * 2) -1;
}
}
/** @var String audioFarbleBody.
*
* Contains functions for modyfing audio data according to chosen level of protection -
* (0) - replace by white noise (range <0,0.1>) based on domain key
* (1) - multiply array by fudge factor based on domain key
*/
var audioFarbleBody = strToUint + audioFarble;
var wrappers = [
{
parent_object: "AudioBuffer.prototype",
parent_object_property: "getChannelData",
wrapped_objects: [
{
original_name: "AudioBuffer.prototype.getChannelData",
wrapped_name: "origGetChannelData",
}
],
helping_code: "var behaviour = args[0]; var modified = new Set();" + audioFarbleBody + whiteNoiseFloat,
original_function: "parent.AudioBuffer.prototype.getChannelData",
wrapping_function_args: "channel",
/** \fn fake AudioBuffer.prototype.getChannelData
* \brief Returns modified channel data.
*
* Calls original function, which returns array with result, then calls function
* audioFarble with returned array as argument - which changes array values according to chosen level.
*/
wrapping_function_body: `
var floatArr = origGetChannelData.call(this, channel);
if (modified.has(floatArr)) {
return floatArr;
}
if (behaviour == 0) {
audioFarble(floatArr);
}
else if (behaviour == 1) {
whiteNoiseFloat(floatArr);
}
modified.add(floatArr);
setTimeout(function() {
modified.delete(floatArr);
}, 300000); // Remove the information after 5 minutes, this might need tweaking
return floatArr;
`,
},
{
parent_object: "AudioBuffer.prototype",
parent_object_property: "copyFromChannel",
wrapped_objects: [
{
original_name: "AudioBuffer.prototype.copyFromChannel",
wrapped_name: "origCopyFromChannel",
}
],
helping_code: "var behaviour = args[0];" + audioFarbleBody + whiteNoiseFloat,
original_function: "parent.AudioBuffer.prototype.copyFromChannel",
wrapping_function_args: "destination, channel, start",
/** \fn fake AudioBuffer.prototype.copyFromChannel
* \brief Modifies destination array after calling original function.
*
* Calls original function, which writes data to destination array, then calls function
* audioFarble with destination array as argument - which changes array values according to chosen level.
*/
wrapping_function_body: `
if (behaviour == 0) {
origCopyFromChannel.call(this, destination, channel, start);
audioFarble(destination);
}
else if (behaviour == 1) {
whiteNoiseFloat(destination);
}
`,
},
{
parent_object: "AnalyserNode.prototype",
parent_object_property: "getByteTimeDomainData",
wrapped_objects: [
{
original_name: "AnalyserNode.prototype.getByteTimeDomainData",
wrapped_name: "origGetByteTimeDomainData",
}
],
helping_code: "var behaviour = args[0];" + audioFarbleInt + whiteNoiseInt,
wrapping_function_args: "destination",
/** \fn fake AnalyserNode.prototype.getByteTimeDomainData
* \brief Modifies destination array after calling original function.
*
* Calls original function, which writes data to destination array, then calls function
* audioFarble with destination array as argument - which changes array values according to chosen level.
*/
wrapping_function_body: `
if (behaviour == 0) {
origGetByteTimeDomainData.call(this, destination);
audioFarbleInt(destination);
}
else if (behaviour == 1) {
whiteNoiseInt(destination);
}
`,
},
{
parent_object: "AnalyserNode.prototype",
parent_object_property: "getFloatTimeDomainData",
wrapped_objects: [
{
original_name: "AnalyserNode.prototype.getFloatTimeDomainData",
wrapped_name: "origGetFloatTimeDomainData",
}
],
helping_code: "var behaviour = args[0];" + audioFarbleBody + whiteNoiseFloat,
wrapping_function_args: "destination",
/** \fn fake AnalyserNode.prototype.getFloatTimeDomainData
* \brief Modifies destination array after calling original function.
*
* Calls original function, which writes data to destination array, then calls function
* audioFarble with destination array as argument - which changes array values according to chosen level.
*/
wrapping_function_body: `
if (behaviour == 0) {
origGetFloatTimeDomainData.call(this, destination);
audioFarble(destination);
}
else if (behaviour == 1) {
whiteNoiseFloat(destination);
}
`,
},
{
parent_object: "AnalyserNode.prototype",
parent_object_property: "getByteFrequencyData",
wrapped_objects: [
{
original_name: "AnalyserNode.prototype.getByteFrequencyData",
wrapped_name: "origGetByteFrequencyData",
}
],
helping_code: "var behaviour = args[0];" + audioFarbleInt + whiteNoiseInt,
wrapping_function_args: "destination",
/** \fn fake AnalyserNode.prototype.getByteFrequencyData
* \brief Modifies destination array after calling original function.
*
* Calls original function, which writes data to destination array, then calls function
* audioFarble with destination array as argument - which changes array values according to chosen level.
*/
wrapping_function_body: `
if (behaviour == 0) {
origGetByteFrequencyData.call(this, destination);
audioFarbleInt(destination);
}
else if (behaviour == 1) {
whiteNoiseInt(destination);
}
`,
},
{
parent_object: "AnalyserNode.prototype",
parent_object_property: "getFloatFrequencyData",
wrapped_objects: [
{
original_name: "AnalyserNode.prototype.getFloatFrequencyData",
wrapped_name: "origGetFloatFrequencyData",
}
],
helping_code: "var behaviour = args[0];" + audioFarbleBody + whiteNoiseFloat,
wrapping_function_args: "destination",
/** \fn fake AnalyserNode.prototype.getFloatFrequencyData
* \brief Modifies destination array after calling original function.
*
* Calls original function, which writes data to destination array, then calls function
* audioFarble with destination array as argument - which changes array values according to chosen level.
*/
wrapping_function_body: `
if (behaviour == 0) {
origGetFloatFrequencyData.call(this, destination);
audioFarble(destination);
}
else if (behaviour == 1) {
whiteNoiseFloat(destination);
}
`,
}
];
add_wrappers(wrappers);
})();