Compare commits
9 Commits
8f7bd620b1
...
e39251cafc
Author | SHA1 | Date | |
---|---|---|---|
|
e39251cafc | ||
|
866777686c | ||
|
b3c0097270 | ||
|
d70732d12b | ||
|
a63066edbd | ||
|
9dc7131f81 | ||
|
bd81c3c582 | ||
|
b84a28f062 | ||
|
9afe3ac2ab |
13
README.md
13
README.md
@ -12,13 +12,13 @@ There are spy libraries that can determine how long keys are held down and the t
|
||||
|
||||
This addon was created to skew the key press timing by limiting the speed of presses. The speed is changed randomly on each page load, with a min of 150ms and a max of 300ms.
|
||||
|
||||
It uses a less than ideal thread locking solution, as simply buffering text does not stop event listeners from spies.
|
||||
It uses a less than ideal thread locking solution, as simply buffering text does not stop event listeners from spies. **Now it also has a non-default setting to use prompt() on non-password single line input elements. This reduces CPU usage and increase typing speed while offering better protection than the delay approach.**
|
||||
|
||||
You can whitelist domains that you trust on the addon settings page, but it was decided not to support changing the key speed manually as that would add another fingerprinting avenue.
|
||||
|
||||
Development Roadmap:
|
||||
|
||||
* Menu button to add whitelisted domain more quickly
|
||||
* ~~Menu button to add whitelisted domain more quickly~~
|
||||
* Buffered window/menu/popup option to type quickly but prevent page surveillance
|
||||
* More throrough testing against different avenues of keyboard surveillance
|
||||
* Stylometry protection
|
||||
@ -29,6 +29,7 @@ Development Roadmap:
|
||||
|
||||
* **This addon may not defeat all types of keyboard biometric surveillance**, however it was tested against the Keytrac and TypingDNA demos and it worked well.
|
||||
* **This addon does not yet deal with stylometry**
|
||||
* Some websites override all key events in which case it is not (yet) possible to protect against keyboarding analysis there
|
||||
* You may see higher CPU usage while typing. This is due do the unideal locking solution described above
|
||||
* Do not confuse this with spying keyboard apps on mobile devices, this cannot address that.
|
||||
* Not tested on Firefox Android
|
||||
@ -36,9 +37,9 @@ Development Roadmap:
|
||||
|
||||
Sources:
|
||||
|
||||
https://www.keytrac.net/en/
|
||||
https://www.typingdna.com/
|
||||
www dot keytrac dot net/en/
|
||||
|
||||
www dot typingdna dot com/
|
||||
|
||||
https://www.whonix.org/wiki/Stylometry
|
||||
[https://www.whonix.org/wiki/Keystroke_Deanonymization](https://www.whonix.org/wiki/Keystroke_Deanonymization)
|
||||
|
||||
(Spy companies not linked to avoid helping their SEO)
|
106
background.js
106
background.js
@ -16,13 +16,70 @@
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
*/
|
||||
let lastActive = null
|
||||
const defaultHosts = "<all_urls>";
|
||||
|
||||
let appCode = function (){
|
||||
let lastActive = document.createElement("p")
|
||||
let popupEnabled = false
|
||||
let popupGetter = browser.storage.sync.get("keyboardprivacyprompt")
|
||||
popupGetter.then(function(val){
|
||||
popupEnabled = val['keyboardprivacyprompt']
|
||||
console.debug(popupEnabled)
|
||||
}, function(){})
|
||||
|
||||
let clearSelect = function()
|
||||
{
|
||||
if (window.getSelection) {window.getSelection().removeAllRanges()}
|
||||
else if
|
||||
(document.selection) {
|
||||
document.selection.empty()
|
||||
}
|
||||
}
|
||||
let minValue = 75
|
||||
let maxValue = 150
|
||||
let time = 0
|
||||
let last = null
|
||||
|
||||
let doPopup = function(e) {
|
||||
let text = ""
|
||||
let inputType = ""
|
||||
|
||||
if (e.target.tagName === "INPUT"){
|
||||
inputType = e.target.getAttribute("type")
|
||||
|
||||
if (Date.now() - time < 1000 && e.target === last){
|
||||
return
|
||||
}
|
||||
if (! inputType){
|
||||
text = prompt("[PrivateKeyboard]\n\nEnter text for the text field:", e.target.value)
|
||||
}
|
||||
else if (! ["text", "search", "email", "number"].includes(inputType.toLowerCase())){
|
||||
return
|
||||
}
|
||||
else{
|
||||
text = prompt("[PrivateKeyboard]\n\nEnter text for the " + inputType + " field:", e.target.value)
|
||||
}
|
||||
if (text !== null) {
|
||||
e.target.value = text
|
||||
}
|
||||
last = e.target
|
||||
clearSelect()
|
||||
time = Date.now()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
let popupMode = function(){
|
||||
|
||||
if (popupEnabled){
|
||||
document.addEventListener('focusin', doPopup, true)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
let mainKeyboardPrivacy = function(){
|
||||
popupMode()
|
||||
|
||||
function getRandNum(){
|
||||
let buf = new Uint8Array(1)
|
||||
@ -63,9 +120,24 @@ let appCode = function (){
|
||||
}, )
|
||||
|
||||
document.addEventListener('keydown', function(e){
|
||||
if (e.key.startsWith('Arrow') || e.key.startsWith('Page')){
|
||||
if (e.key.startsWith('Arrow') || e.key.startsWith('Page') ||
|
||||
|
||||
e.key == "Enter" || e.key == "Control" || e.key == "Tab"
|
||||
){
|
||||
return true;
|
||||
}
|
||||
else if (e.key.length > 1 && e.key.startsWith("F")){
|
||||
// Ignore function keys (unicode can also be >1 length so check for f)
|
||||
return true;
|
||||
}
|
||||
else{
|
||||
}
|
||||
|
||||
if (popupEnabled){
|
||||
setTimeout(
|
||||
function(){doPopup(e)}, 10)
|
||||
}
|
||||
|
||||
pausecomp(down);
|
||||
return true;
|
||||
})
|
||||
@ -126,6 +198,38 @@ let appCode = function (){
|
||||
let whitelist = browser.storage.sync.get("keyboardprivacywhitelist");
|
||||
whitelist.then(shouldRunKeyboardPrivacy, noKeyboardPrivacySettings)
|
||||
|
||||
|
||||
document.addEventListener('focus', function(e){
|
||||
let active = document.activeElement;
|
||||
if (active.tagName == "INPUT" || active.tagName == "TEXTAREA") {
|
||||
lastActive = active
|
||||
}
|
||||
})
|
||||
|
||||
browser.runtime.onMessage.addListener(request => {
|
||||
let active = document.activeElement
|
||||
|
||||
if (request.getCurrent){
|
||||
if (active.tagName != "INPUT" && active.tagName != "TEXTAREA") {
|
||||
if (lastActive.tagName != "INPUT" && lastActive.tagName != "TEXTAREA"){
|
||||
console.debug("no current active or last active")
|
||||
return Promise.resolve({response: false});
|
||||
}
|
||||
return Promise.resolve({response: lastActive.value});
|
||||
}
|
||||
return Promise.resolve({response: active.value});
|
||||
}
|
||||
if (active.tagName != "INPUT" && active.tagName != "TEXTAREA"){
|
||||
|
||||
lastActive.value = request.keys
|
||||
}
|
||||
else{
|
||||
document.activeElement.value = request.keys
|
||||
lastActive = document.activeElement
|
||||
}
|
||||
return Promise.resolve({response: "ack"});
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
const dummyStr = ''
|
||||
|
@ -2,7 +2,7 @@
|
||||
|
||||
"manifest_version": 2,
|
||||
"name": "Private Keyboard",
|
||||
"version": "1.8",
|
||||
"version": "2.0",
|
||||
|
||||
|
||||
"description": "Protect against keyboard biometrics",
|
||||
@ -32,6 +32,15 @@
|
||||
"default_popup": "settings/button.html"
|
||||
},
|
||||
|
||||
"commands": {
|
||||
"_execute_browser_action": {
|
||||
"suggested_key": {
|
||||
"default": "Ctrl+1"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
|
||||
"browser_specific_settings": {
|
||||
"gecko": {
|
||||
|
@ -5,10 +5,14 @@
|
||||
<title>Private Keyboard</title>
|
||||
<link rel="stylesheet" href="./button.css">
|
||||
<script src="./button.js" defer></script>
|
||||
<script src="./keybuffer2.js" defer></script>
|
||||
</head>
|
||||
<body>
|
||||
<header id="siteDomain"></header>
|
||||
<button id="toggleSite">Disable Keyboard Privacy</button>
|
||||
<p id="reloadPage">Refresh the page to apply</p>
|
||||
|
||||
<br>
|
||||
<textarea tabindex="0" id="keyBuffer" cols="20" rows="10" placeholder="Text typed here will be sent to active input/textarea elements. Breaks on many sites, but is faster than typing directly on the page"></textarea>
|
||||
</body>
|
||||
</html>
|
@ -116,4 +116,4 @@ document.getElementById('toggleSite').onclick = async function(){
|
||||
document.getElementById('toggleSite').removeAttribute('disabled')
|
||||
}, 3000)
|
||||
document.getElementById('reloadPage').style.display = 'block'
|
||||
}
|
||||
}
|
||||
|
76
settings/keybuffer2.js
Normal file
76
settings/keybuffer2.js
Normal file
@ -0,0 +1,76 @@
|
||||
let started = false
|
||||
|
||||
function onError(error) {
|
||||
console.error(`Error: ${error}`);
|
||||
}
|
||||
|
||||
function doSendMsg(msg, tabs) {
|
||||
|
||||
for (let tab of tabs) {
|
||||
browser.tabs.sendMessage(
|
||||
tab.id,
|
||||
{keys: msg, getCurrent: false}
|
||||
).then(response => {
|
||||
console.log("Message from the content script:");
|
||||
console.log(response.response);
|
||||
}).catch(onError);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
let sender = async function(e){
|
||||
|
||||
if (e.key == 'Tab') {
|
||||
e.preventDefault();
|
||||
let start = this.selectionStart;
|
||||
let end = this.selectionEnd;
|
||||
|
||||
// set textarea value to: text before caret + tab + text after caret
|
||||
this.value = this.value.substring(0, start) +
|
||||
"\t" + this.value.substring(end);
|
||||
|
||||
// put caret at right position again
|
||||
this.selectionStart =
|
||||
this.selectionEnd = start + 1;
|
||||
}
|
||||
|
||||
let sendMessageToTabs = function(tabs){
|
||||
doSendMsg(document.getElementById('keyBuffer').value, tabs)
|
||||
}
|
||||
|
||||
browser.tabs.query({
|
||||
currentWindow: true,
|
||||
active: true
|
||||
}).then(sendMessageToTabs).catch(onError);
|
||||
|
||||
}
|
||||
|
||||
function getCurrent(){
|
||||
browser.tabs.query({
|
||||
currentWindow: true,
|
||||
active: true
|
||||
}).then(function(tabs){
|
||||
|
||||
for (let tab of tabs) {
|
||||
browser.tabs.sendMessage(
|
||||
tab.id,
|
||||
{keys: "", getCurrent: true}
|
||||
).then(response => {
|
||||
console.debug(response)
|
||||
if (response.response === undefined || response.response === false){
|
||||
//document.getElementById('keyBuffer').value = "No element in focus. Focus an element and reopen this."
|
||||
return
|
||||
}
|
||||
document.getElementById('keyBuffer').value = response.response
|
||||
}).catch(onError);
|
||||
}
|
||||
|
||||
}).catch(onError);
|
||||
}
|
||||
|
||||
getCurrent()
|
||||
|
||||
document.getElementById("keyBuffer").focus()
|
||||
|
||||
document.getElementById('keyBuffer').onkeydown = sender
|
||||
document.getElementById('keyBuffer').onpaste = sender
|
@ -9,6 +9,9 @@
|
||||
<body>
|
||||
|
||||
<form>
|
||||
<input type="checkbox" id="usePrompt">
|
||||
<label for="usePrompt">Use prompt() dialogs on single-line inputs. Faster typing and less CPU usage, but can break some websites and features like autocomplete.</label>
|
||||
<br>
|
||||
<input type="checkbox" id="whitelistLAN">
|
||||
<label for="whitelistLAN">Whitelist private IP + localhost range hostnames (e.g. routers)</label>
|
||||
<h1>Trusted domains (comma delimited)</h1>
|
||||
|
@ -24,6 +24,9 @@ function saveOptions(e) {
|
||||
browser.storage.sync.set({
|
||||
keyboardprivacylan: document.querySelector("#whitelistLAN").checked
|
||||
})
|
||||
browser.storage.sync.set({
|
||||
keyboardprivacyprompt: document.querySelector("#usePrompt").checked
|
||||
})
|
||||
document.getElementById('saved').innerHTML = '<br><b>Saved</b>'
|
||||
setTimeout(function(){
|
||||
document.getElementById('saved').innerHTML = '<br>'
|
||||
@ -42,6 +45,10 @@ function saveOptions(e) {
|
||||
document.querySelector("#whitelistLAN").checked = result['keyboardprivacylan']
|
||||
}
|
||||
|
||||
function setCurrentPrompt(result){
|
||||
document.querySelector("#usePrompt").checked = result['keyboardprivacyprompt']
|
||||
}
|
||||
|
||||
function onError(error) {
|
||||
console.log(`Error: ${error}`);
|
||||
}
|
||||
@ -51,6 +58,9 @@ function saveOptions(e) {
|
||||
|
||||
let gettingLAN = browser.storage.sync.get("keyboardprivacylan");
|
||||
gettingLAN.then(setCurrentLAN, onError);
|
||||
|
||||
let gettingPrompt = browser.storage.sync.get("keyboardprivacyprompt");
|
||||
gettingPrompt.then(setCurrentPrompt, onError);
|
||||
}
|
||||
|
||||
document.addEventListener("DOMContentLoaded", restoreOptions);
|
||||
|
Loading…
Reference in New Issue
Block a user