-
Notifications
You must be signed in to change notification settings - Fork 4
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Defend against font-face side channeling attack, and refactor (#17)
- Loading branch information
Showing
5 changed files
with
163 additions
and
106 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,54 @@ | ||
'use strict'; | ||
|
||
import { | ||
Array, | ||
createElement, | ||
entries, from, | ||
join, keys, map, | ||
parseInt, random, | ||
setAttribute, | ||
textContentSet, | ||
toFixed, | ||
toUpperCase, | ||
} from './native.mjs'; | ||
|
||
const letters = 'abcdefghijklmnopqrstuvwxyz'; | ||
const digits = '0123456789'; | ||
const symbols = '!@#$%^&*()?.;:"\'[]{}+=-_/'; | ||
const alphanumeric = letters + digits; | ||
const all = letters + toUpperCase(letters) + digits + symbols; | ||
|
||
const randChar = (f, n) => f[parseInt(toFixed(random() * n))]; | ||
const rand = len => randChar(letters, 26) + | ||
join(map(from(keys(Array(len))), () => randChar(alphanumeric, 36)), ''); | ||
|
||
// secure and generic creator for elements with specific characteristics | ||
function creator(style, tag, text = '') { | ||
// turn style object to string asap to prevent later pollution attack attempts | ||
style = join(map(entries(style), ([k,v]) => `${k}: ${v} !important`), '; '); | ||
return function () { | ||
const node = createElement(document, tag()); | ||
setAttribute(node, 'style', style); | ||
textContentSet(node, text); | ||
return node; | ||
}; | ||
} | ||
|
||
const invoker = creator => () => creator(); | ||
|
||
// an element that is hard to find/select | ||
export const unselectable = invoker(creator({ | ||
// makes element uneditable to prevent document.execCommand HTML injection attacks | ||
'-webkit-user-modify': 'unset', | ||
// makes element unselectable to prevent getSelection attacks | ||
'-webkit-user-select': 'none', 'user-select': 'none', | ||
}, () => rand(7))); | ||
|
||
// an element that includes all possible chars to distract side channel leaks attempts | ||
export const distraction = invoker(creator({ | ||
// place element so that it isn't intractable nor viewable | ||
// for the user, but still a distraction for attackers | ||
'top': '-10px', 'right': '-10px', 'position': 'fixed', | ||
// font-size smaller than 1px fails to be a distraction on Firefox | ||
'font-size': '1px', | ||
}, () => 'span', all)); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,110 +1,55 @@ | ||
"use strict"; | ||
|
||
const { | ||
Object, | ||
Array, | ||
Function, | ||
Math, | ||
parseInt, | ||
Map, | ||
} = window; | ||
|
||
const { | ||
getOwnPropertyDescriptor, | ||
entries, | ||
} = Object; | ||
|
||
const { | ||
random, | ||
} = Math; | ||
|
||
const css = Object.create(null); | ||
css['-webkit-user-modify'] = 'unset'; | ||
css['user-select'] = 'none'; | ||
|
||
const opts = Object.create(null); | ||
opts.mode = 'closed'; | ||
|
||
const letters = 'abcdefghijklmnopqrstuvwxyz'; | ||
const nums = '0123456789'; | ||
|
||
// p stands for primordial | ||
function p(obj, prop, accessor, type = 'function') { | ||
const desc = getOwnPropertyDescriptor(obj, prop); | ||
switch (type) { | ||
case 'function': | ||
return Function.prototype.call.bind(desc[accessor]); | ||
break; | ||
} | ||
} | ||
|
||
function generateRandomString(len = 1) { | ||
let tag = letters[parseInt(random() * (26))]; | ||
for (let i = 1; i < len; i++) { | ||
const r = parseInt(random() * (36)); | ||
tag += (letters+nums)[r]; | ||
} | ||
return tag; | ||
} | ||
|
||
const attachShadow = p(Element.prototype, 'attachShadow', 'value'); | ||
const createElement = p(Document.prototype, 'createElement', 'value'); | ||
const appendChild = p(Node.prototype, 'appendChild', 'value'); | ||
const textContentSet = p(Node.prototype, 'textContent', 'set'); | ||
const innerHTMLSet = p(ShadowRoot.prototype, 'innerHTML', 'set'); | ||
const setAttribute = p(Element.prototype, 'setAttribute', 'value'); | ||
const map = p(Array.prototype, 'map', 'value'); | ||
const split = p(String.prototype, 'split', 'value'); | ||
const join = p(Array.prototype, 'join', 'value'); | ||
const get = p(Map.prototype, 'get', 'value'); | ||
const set = p(Map.prototype, 'set', 'value'); | ||
'use strict'; | ||
|
||
import { | ||
Map, Error, | ||
defineProperties, | ||
from, stringify, | ||
attachShadow, | ||
createElement, | ||
appendChild, | ||
textContentSet, | ||
map, at, get, set, | ||
} from './native.mjs'; | ||
import {distraction, unselectable} from './element.mjs'; | ||
|
||
const shadows = new Map(); | ||
|
||
export function LavaDome(root) { | ||
let host = root, inner = null; | ||
|
||
this.text = text; this.char = char; | ||
export function LavaDome(host) { | ||
// make exported API tamper-proof | ||
defineProperties(this, {text: {value: text}}); | ||
|
||
// cache shadows for efficient reuse | ||
let shadow = get(shadows, host); | ||
if (!shadow) { | ||
shadow = attachShadow(host, opts); | ||
shadow = attachShadow(host, {mode:'closed'}); | ||
set(shadows, host, shadow); | ||
} | ||
|
||
empty(); | ||
|
||
function empty() { | ||
innerHTMLSet(shadow, ''); | ||
} | ||
// child of the shadow, where the secret is set, must be unselectable | ||
const child = unselectable(); | ||
appendChild(shadow, child); | ||
|
||
function reset() { | ||
empty(); | ||
const tag = generateRandomString(7); | ||
const style = join(map(entries(css), ([k,v]) => `${k}: ${v} !important`), '; '); | ||
inner = createElement(document, tag); | ||
setAttribute(inner, 'style', style); | ||
} | ||
function text(text) { | ||
if (typeof text !== 'string') { | ||
throw new Error( | ||
`LavaDome: first argument must be a string, instead got ${stringify(text)}`); | ||
} | ||
|
||
function init() { | ||
reset(); | ||
appendChild(shadow, inner); | ||
} | ||
// check if text is a single char and if so, either is part of a longer secret | ||
// which is protected by the parent LavaDome, or simply a single char provided by | ||
// consumer either way - not worth attempting to secure | ||
if (at(from(text), 1) === undefined) { | ||
return textContentSet(child, text); | ||
} | ||
|
||
function char(char) { | ||
init(); | ||
textContentSet(inner, char); | ||
} | ||
// place each char of the secret in its own LavaDome protection instance | ||
map(from(text), char => { | ||
const span = createElement(document, 'span'); | ||
new LavaDome(span).text(char); | ||
appendChild(child, span); | ||
}); | ||
|
||
function text(text) { | ||
init(); | ||
const chars = split(text, ''); | ||
for (let i = 0; i < chars.length; i++) { | ||
const char = chars[i]; | ||
const s = createElement(document, 'span'); | ||
const ld = new LavaDome(s); | ||
ld.char(char); | ||
appendChild(inner, s); | ||
} | ||
// add a distraction against side channel leaks attack attempts | ||
appendChild(child, distraction()); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,52 @@ | ||
'use strict'; | ||
|
||
const { | ||
Object, Array, | ||
Function, Math, | ||
parseInt, Map, | ||
Error, JSON, | ||
} = window; | ||
const { | ||
defineProperties, | ||
getOwnPropertyDescriptor, | ||
entries, | ||
} = Object; | ||
const { from } = Array; | ||
const {random } = Math; | ||
const { stringify } = JSON; | ||
|
||
// native generation util | ||
const n = (obj, prop, accessor) => | ||
Function.prototype.call.bind(getOwnPropertyDescriptor(obj, prop)[accessor]); | ||
|
||
export const attachShadow = n(Element.prototype, 'attachShadow', 'value'); | ||
export const createElement = n(Document.prototype, 'createElement', 'value'); | ||
export const appendChild = n(Node.prototype, 'appendChild', 'value'); | ||
export const textContentSet = n(Node.prototype, 'textContent', 'set'); | ||
export const setAttribute = n(Element.prototype, 'setAttribute', 'value'); | ||
export const toUpperCase = n(String.prototype, 'toUpperCase', 'value'); | ||
export const map = n(Array.prototype, 'map', 'value'); | ||
export const join = n(Array.prototype, 'join', 'value'); | ||
export const keys = n(Array.prototype, 'keys', 'value'); | ||
export const at = n(Array.prototype, 'at', 'value'); | ||
export const get = n(Map.prototype, 'get', 'value'); | ||
export const set = n(Map.prototype, 'set', 'value'); | ||
export const toFixed = n(Number.prototype, 'toFixed', 'value') | ||
|
||
export { | ||
// window | ||
Object, Array, | ||
Function, Math, | ||
parseInt, Map, | ||
Error, JSON, | ||
// Object | ||
defineProperties, | ||
getOwnPropertyDescriptor, | ||
entries, | ||
// Array | ||
from, | ||
// Math | ||
random, | ||
// JSON | ||
stringify, | ||
} |