Skip to content

Commit

Permalink
improve clousre
Browse files Browse the repository at this point in the history
  • Loading branch information
weizman committed Oct 7, 2024
1 parent 5265dd4 commit c8c9cd7
Showing 1 changed file with 33 additions and 46 deletions.
79 changes: 33 additions & 46 deletions packages/core/src/lavadome.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,11 @@ import {
textContentSet,
Blob, ClipboardItem,
write, clipboard,
addEventListener,
ownerDocument,
navigation,
url, destination, includes,
preventDefault, stopPropagation,
} from './native.mjs';
import {distraction, loadable, hardened} from './element.mjs';
import {distraction, hardened} from './element.mjs';
import {getShadow} from './shadow.mjs';

// text-fragments links can be abused to leak shadow internals - block in-app redirection to them
Expand All @@ -31,56 +29,38 @@ navigation.addEventListener('navigate', event => {
}
});

export function LavaDome(host, opts) {
opts = options(opts);

// make exported API tamper-proof
defineProperties(this, {
text: {value: text},
copy: {value: copy},
});

// get/create shadow for host (empty shadow content if there's any already)
const shadow = getShadow(host, opts);
replaceChildren(shadow);

// fire every time instance is reloaded and abort loading for non-top documents
const iframe = loadable();
addEventListener(iframe, 'load', () => {
const ownerDoc = ownerDocument(iframe);
if (ownerDoc !== document) {
replaceChildren(shadow);
throw new Error(`LavaDomeCore: ` +
`The document to which LavaDome was originally introduced ` +
`must be the same as the one this instance is inserted to`);
}
});

// child of the shadow, where the secret is set, must be hardened
const child = hardened();
appendChild(shadow, child);

let secret = '';
function defineCopy(instance, input) {
return async function copy() {
const type = 'text/plain';
const blob = new Blob([input], {type});
const data = [new ClipboardItem({[type]: blob})];
await write(clipboard, data);
}
}

function text(input) {
function defineText(instance, host, opts) {
return function text(input) {
const type = typeof input;
if (type !== 'string') {
throw new Error(
`LavaDomeCore: first argument must be a string, instead got ${type}`);
}

// get/create shadow for host (empty shadow content if there's any already)
const shadow = getShadow(host, opts);
replaceChildren(shadow);

// child of the shadow, where the secret is set, must be hardened
const child = hardened();
appendChild(shadow, child);

// 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(input), 1) === undefined) {
return textContentSet(child, input);
}

secret = input;

// attach loadable only once per instance to avoid excessive load firing
appendChild(shadow, iframe);

// place each char of the secret in its own LavaDome protection instance
map(from(input), char => {
const span = createElement(document, 'span');
Expand All @@ -90,14 +70,21 @@ export function LavaDome(host, opts) {
appendChild(child, span);
});

// make exported API tamper-proof
defineProperties(instance, {
copy: {value: defineCopy(instance, input)},
});

// add a distraction against side channel leaks attack attempts
appendChild(child, distraction());
}

async function copy() {
const type = 'text/plain';
const blob = new Blob([secret], {type});
const data = [new ClipboardItem({[type]: blob})];
await write(clipboard, data);
}
}

export function LavaDome(host, opts) {
opts = options(opts);

// make exported API tamper-proof
defineProperties(this, {
text: {value: defineText(this, host, opts)},
});
}

0 comments on commit c8c9cd7

Please sign in to comment.