Commit d1f7c437 authored by Xunnamius (Luna)'s avatar Xunnamius (Luna)

updates

parent 18a2e684
......@@ -3,15 +3,17 @@
"flow-typed": true
},
"cSpell.words": [
"asyncify",
"authed",
"cleanbuild",
"cleantypes",
"cntinue",
"davidcash",
"devseed",
"haschk's",
"event",
"frame",
"handler",
"haschk's",
"hash",
"interruptible",
"newstring",
......
......@@ -18,21 +18,20 @@ import { transformSync as babel } from '@babel/core'
import { relative as relPath, join as joinPath } from 'path'
import webpack from 'webpack'
import WebpackDevServer from 'webpack-dev-server'
// flow-disable-line
import config from './webpack.config'
// flow-disable-line
import pkg from './package'
require('dotenv').config();
process.env.NODE_ENV = process.env.NODE_ENV || 'development';
const {
WEBPACK_PORT,
DEV_ENDPOINT,
HASHING_ALGORITHM
} = process.env;
const configured = config({ NODE_ENV: process.env.NODE_ENV });
if(typeof WEBPACK_PORT !== 'string')
throw new TypeError('WEBPACK_PORT is improperly defined. Did you copy dist.env -> .env ?');
......@@ -70,6 +69,7 @@ const CLI_BANNER = `/**
*/\n\n`;
const readFileAsync = promisify(readFile);
const generateWebpackConfig = () => config({ NODE_ENV: process.env.NODE_ENV });
// * CLEANTYPES
......@@ -115,12 +115,14 @@ regenerate.description = 'Invokes babel on the files in config, transpiling them
export const build = (): Promise<void> => {
process.env.NODE_ENV = 'production';
const configured = generateWebpackConfig();
return new Promise(resolve => {
webpack(configured, (err, stats) => {
if(err)
{
const details = err.details ? `\n\t${err.details}` : '';
throw `WEBPACK FATAL BUILD ERROR: ${err}${details}`;
throw `WEBPACK FATAL BUILD ERROR: ${err.toString()}${details}`;
}
const info = stats.toJson();
......@@ -151,6 +153,8 @@ bundleZip.description = 'Bundles the build directory into a zip archive for uplo
// * WPDEVSERV
export const wpdevserv = () => {
const configured = generateWebpackConfig();
Object.keys(configured.entry).forEach(entryKey => configured.entry[entryKey] = [
`webpack-dev-server/client?http://${DEV_ENDPOINT}:${DEV_PORT}`,
'webpack/hot/dev-server'
......
......@@ -3,17 +3,14 @@
// flow-disable-line
import pkg from './package'
import webpack from 'webpack'
import { join as joinPath } from 'path'
import { CleanWebpackPlugin } from 'clean-webpack-plugin'
import CopyWebpackPlugin from 'copy-webpack-plugin'
import HtmlWebpackPlugin from 'html-webpack-plugin'
import WriteFileWebpackPlugin from 'write-file-webpack-plugin'
import parseGitIgnore from 'parse-gitignore'
import { readFileSync } from 'fs'
require('dotenv').config();
const { HASHING_ALGORITHM } = process.env;
const { HASHING_ALGORITHM, APPLICATION_LABEL } = process.env;
const paths = {};
......@@ -25,9 +22,12 @@ paths.manifest = `${paths.src}/manifest.json`;
paths.components = `${paths.src}/components`;
paths.universe = `${paths.src}/universe`;
paths.assets = `${paths.src}/assets`;
const assetExtensions = ['jpg', 'jpeg', 'png', `gif`, "eot", 'otf', 'svg', 'ttf', 'woff', 'woff2'];
const configure = (NODE_ENV: ?string) => {
NODE_ENV = NODE_ENV || 'production';
const DEV_ENV = NODE_ENV === 'development';
const options = {};
......@@ -75,8 +75,9 @@ const configure = (NODE_ENV: ?string) => {
// ? Expose desired environment variables in the packed bundle
new webpack.DefinePlugin({
'process.env.NODE_ENV': JSON.stringify(NODE_ENV),
'process.env.HASHING_ALGORITHM': JSON.stringify(HASHING_ALGORITHM)
_NODE_ENV: JSON.stringify(NODE_ENV),
_HASHING_ALGORITHM: JSON.stringify(HASHING_ALGORITHM || 'SHA-256'),
_APPLICATION_LABEL: JSON.stringify(APPLICATION_LABEL || '_haschk')
}),
new CopyWebpackPlugin([{
......
......@@ -5,5 +5,8 @@ WEBPACK_PORT=9000
DEV_ENDPOINT=127.0.0.1
# The algorithm used to hash RI and file data
# Options: SHA-256 SHA-384 SHA-512
# Options: SHA-256 (<- default), SHA-384, SHA-512
HASHING_ALGORITHM=SHA-256
# The Application Label (AL) is a well-defined string used in BD requests
APPLICATION_LABEL=_haschk
This diff is collapsed.
......@@ -10,47 +10,56 @@ import {
FrameworkEventEmitter
} from 'universe/events'
import type { Chrome } from 'components/background'
import { Debug } from 'universe'
import type { Chrome } from 'universe'
const resultHandlerFactory = (eventName, context, port) => (downloadItem) => {
port.postMessage(portEvent(eventName, downloadItem));
};
const webRequestFilter = {
urls: [
'http://*/*',
'https://*/*',
]
};
// ? Essentially, we hook into four browser-level events here:
// ? - when the extension is first installed
// ? - when a tab finishes navigating to a URL
// ? - when a download is started
// ? - when an http/s web request is started
// ? - when an http/s web request results in an error
// ? - when a download first starts
// ? - when a download finishes
export default (oracle: FrameworkEventEmitter, chrome: Chrome, context: Object) => {
// ? These fire when judgements are made about downloads and send them out
// ? to other components of the extension.
// ?
// ? Three events are made available:
// ? * judgement.safe a resource's content is as expected
// ? * judgement.unsafe a resource's content is mutated/corrupted
// ? * judgement.unknown a resource's content cannot be judged (ignored)
chrome.runtime.onConnect.addListener((port) => {
console.log('runtime.onConnect: ', port);
Debug.log('runtime.onConnect: ', port);
const handlerUnsafeResult = resultHandlerFactory('judgement.unsafe', context, port);
const handlerSafeResult = resultHandlerFactory('judgement.safe', context, port);
const handlerUnknownResult = resultHandlerFactory('judgement.unknown', context, port);
port.onDisconnect.addListener(() => {
console.log('(port.onDisconnect)');
Debug.log('(port.onDisconnect)');
oracle.removeListener('judgement.unsafe', handlerUnsafeResult);
oracle.removeListener('judgement.safe', handlerSafeResult);
oracle.removeListener('judgement.unknown', handlerUnknownResult);
});
// ? These fire when judgements are made about downloads and send them out
// ? to other components of the extension.
// ?
// ? Three events are made available:
// ? * judgement.safe a resource's content is as expected
// ? * judgement.unsafe a resource's content is mutated/corrupted
// ? * judgement.unknown a resource's content cannot be judged (ignored)
oracle.addListener('judgement.unsafe', handlerUnsafeResult);
oracle.addListener('judgement.safe', handlerSafeResult);
oracle.addListener('judgement.unknown', handlerUnknownResult);
// TODO: describe me too
port.onMessage.addListener(message => {
console.log('port.onMessage: ', message);
Debug.log('port.onMessage: ', message);
if(message.event.charAt(0) !== '.' && message.event == 'fetch')
{
let values = {};
......@@ -76,59 +85,40 @@ export default (oracle: FrameworkEventEmitter, chrome: Chrome, context: Object)
// ? This event fires when the extension is first installed
chrome.runtime.onInstalled.addListener(details => {
console.log('runtime.onInstalled: ', details);
Debug.log('runtime.onInstalled: ', details);
if(['install', 'update'].includes(details.reason))
chrome.tabs.create({ url: chrome.runtime.getURL('welcome.html') });
});
// ? This event fires whenever a navigation event first begins
chrome.webRequest.onBeforeRequest.addListener(details => {
console.log('webRequest.onBeforeRequest: ', details);
});
Debug.log('webRequest.onBeforeRequest: ', details);
}, webRequestFilter);
// ? This event fires whenever a navigation event completes
chrome.webRequest.onCompleted.addListener(details => {
console.log('webRequest.onCompleted: ', details);
});
// ? This event fires whenever an error occurs during a web request
chrome.webRequest.onErrorOccurred.addListener(details => {
console.log('webRequest.onErrorOccurred: ', details);
});
// ? This event fires whenever a WebRequest error occurs
// * Only fires if we're debugging
Debug.if(() => chrome.webRequest.onErrorOccurred.addListener(details => {
Debug.log('webRequest.onErrorOccurred : ', details);
}, webRequestFilter));
// ? This event fires when a new download begins in chrome
// * Note: the DownloadItem passed at this point is incomplete!
// ! Sometimes this event fires for old downloads far in the past (months,
// ! even years), so we cannot trust that this event
chrome.downloads.onCreated.addListener(downloadItem => {
console.log('downloads.onCreated: ', downloadItem);
const eventFrame = new EventFrame();
extendDownloadItemInstance(downloadItem);
oracle.emit('download.incoming', eventFrame, downloadItem).then(() => {
try {
if(eventFrame.stopped)
context.handledDownloadItems.add(downloadItem.id);
eventFrame.finish();
}
catch(err) {
oracle.emit('error', err);
}
});
return true;
Debug.log('downloads.onCreated: ', downloadItem);
oracle.emit('download.incoming', new EventFrame(), downloadItem);
});
// ? This event fires when some download-related event changes
chrome.downloads.onChanged.addListener(targetItem => {
console.log('downloads.onChanged: ', targetItem);
// ? Only trigger the moment a download completes and only if this event
// ? has not already been cancelled
if(targetItem?.state?.current == 'complete' && !context.handledDownloadItems.has(targetItem.id)) {
// ? We need to ask for the full DownloadItem instance due to
// ? security
Debug.log('downloads.onChanged: ', targetItem);
// ? Only trigger the moment a download completes
if(targetItem?.state?.current == 'complete') {
// ? We need to ask for the most up-to-date DownloadItem object
chrome.downloads.search({ id: targetItem.id }, ([ downloadItem ]) => {
const eventFrame = new EventFrame(oracle, context);
oracle.emit('download.completed', eventFrame, extendDownloadItemInstance(downloadItem));
oracle.emit('download.completed', new EventFrame(), extendDownloadItemInstance(downloadItem));
});
}
});
......
......@@ -7,10 +7,11 @@ import ParsedUrl from 'url-parse'
import {
bufferToHex,
HASHING_ALGORITHM,
GOOGLE_DNS_HTTPS_RI_FN,
JUDGEMENT_UNKNOWN,
JUDGEMENT_UNSAFE,
JUDGEMENT_SAFE
JUDGEMENT_SAFE,
} from 'universe'
import {
......@@ -18,17 +19,11 @@ import {
FrameworkEventEmitter
} from 'universe/events'
import type { Chrome } from 'components/background'
import type { Chrome } from 'universe'
export default (oracle: FrameworkEventEmitter, chrome: Chrome, context: Object) => {
oracle.addListener('download.incoming', async (e, downloadItem) => {
oracle.addListener('download.incoming', async () => {
const eventFrame = new DownloadEventFrame(oracle, context);
const startTime = (new Date(downloadItem.startTime)).getTime();
const tabLoadTime = context.navHistory[downloadItem.referrer] || startTime;
// ! This step protects against certain timing attacks (see issue #3)
if(startTime - tabLoadTime <= DANGER_THRESHOLD)
await oracle.emit('download.suspiciousOrigin', eventFrame, downloadItem);
// ? If the event was ended prematurely, assume downloadItem was handled
// ? elsewhere in the event flow
......@@ -52,6 +47,7 @@ export default (oracle: FrameworkEventEmitter, chrome: Chrome, context: Object)
const $file = await http.get(`file://${downloadItem.filename}`, { responseType: 'arraybuffer' });
// ? Hash file data with proper algorithm
// flow-disable-line
nonauthedHash = bufferToHex(await crypto.subtle.digest('SHA-256', $file.data));
// ? Determine resource identifier and prepare for DNS request
......
......@@ -42,7 +42,8 @@
import { setBadge } from 'universe/ui'
import { FrameworkEventEmitter } from 'universe/events'
import type { Chrome } from 'components/background'
import { Debug } from 'universe'
import type { Chrome } from 'universe'
export default (oracle: FrameworkEventEmitter, chrome: Chrome, context: Object) => {
// ? This is our generic error handler that fires whenever an error occurs
......@@ -55,41 +56,30 @@ export default (oracle: FrameworkEventEmitter, chrome: Chrome, context: Object)
oracle.addListener('download.incoming', async (e, downloadItem) => {
// TODO: add the new download to popup UI (tell popup UI to update)
// ! Note how this function is async, which means you can await Promises
console.log(`file "${downloadItem.filename}" incoming`);
});
// ? This event fires whenever a suspicious originDomain needs to be
// ? verified manually by the user
oracle.addListener('download.suspiciousOrigin', async (e, downloadItem) => {
// TODO: must ask user to approve downloadItem.urlDomain via the UI
// ! Note how this function is async, i.e. means you can await Promises!
// ! You should use async/await syntax to ensure the extension waits for
// ! the UI to finish before continuing
console.log(`file "${downloadItem.filename}" flagged for suspicious origin`);
Debug.log(`file "${downloadItem.filename}" incoming`);
});
// ? This event fires whenever haschk decides it cannot judge a download
oracle.addListener('judgement.unknown', downloadItem => {
setBadge(chrome)(' ', '#D0D6B5');
console.log(`file "${downloadItem.filename}" judgement: UNKNOWN`);
Debug.log(`file "${downloadItem.filename}" judgement: UNKNOWN`);
});
// ? This event fires whenever haschk decides a download is safe
oracle.addListener('judgement.safe', downloadItem => {
setBadge(chrome)(' ', '#6EEB83');
console.log(`file "${downloadItem.filename}" judgement: SAFE`);
Debug.log(`file "${downloadItem.filename}" judgement: SAFE`);
});
// ? This event fires whenever haschk decides a download is NOT safe
oracle.addListener('judgement.unsafe', downloadItem => {
setBadge(chrome)(' ', '#FF3C38');
console.log(`file "${downloadItem.filename}" judgement: UNSAFE`);
Debug.log(`file "${downloadItem.filename}" judgement: UNSAFE`);
});
oracle.addListener('ui.clear', () => {
setBadge(chrome)('');
context.judgedDownloadItems = [];
console.clear();
});
};
......@@ -3,24 +3,31 @@
* @name Background
*/
import { FRAMEWORK_EVENTS } from 'universe'
import {
Debug,
FRAMEWORK_EVENTS
} from 'universe'
import { EventEmitter } from 'universe/events'
import registerChromeEvents from 'components/background/events.chrome'
import registerCoreEvents from 'components/background/events.core'
import registerUIEvents from 'components/background/events.ui'
declare var chrome: any;
export type Chrome = chrome;
const oracle = new EventEmitter(FRAMEWORK_EVENTS);
const context = {
handledDownloadItems: new Set(),
judgedDownloadItems: [],
seenPorts: {},
navHistory: {}
// ? (download) id -> DownloadItem
downloadItems: new Map(),
// ? requestId -> request stack [req n, ..., req 1]
// ! Limited to 1000 requests!
navHistory: new Map()
};
declare var chrome: any;
Debug.if(() => console.warn('!! == HASCHK IS IN DEVELOPER MODE == !!'));
registerChromeEvents(oracle, chrome, context);
registerCoreEvents(oracle, chrome, context);
registerUIEvents(oracle, chrome, context);
......@@ -9,22 +9,31 @@ export default class EventFrame {
_continueFn: Function;
_finishedFn: Function;
constructor(continueFn: Function, finishedFn: Function) {
this._continueFn = continueFn;
this._finishedFn = finishedFn;
constructor(continueFn: ?Function, finishedFn: ?Function) {
this._continueFn = continueFn || (() => {});
this._finishedFn = finishedFn || (() => {});
}
continue(...args: Array<any>) {
this._continueFn(...args);
}
/**
* Stop the event from finishing. The finish() method will become a noop.
*/
stop() {
this.stopped = true;
}
// ? Should always be called eventually; is idempotent
/**
* Finish the event after it has been passed around. This should always be
* called eventually. This method is idempotent.
*
* @param {...any} args
*/
finish(...args: Array<any>) {
if(!this.finished)
if(!this.stopped && !this.finished)
{
this.finished = true;
this._finished = this._finishedFn(...args);
......
......@@ -6,74 +6,63 @@ import type { ListenerFn } from 'universe/events'
// TODO: document me!
const asyncEmit = async (index: number, listeners: Array<ListenerFn>, args: Array<any>) => {
const asyncEmit = async (index: number, listeners: Array<ListenerFn>, eventFrame: EventFrame, args: Array<any>) => {
const handler: ?ListenerFn = listeners[index];
if(!handler)
return Promise.resolve();
await Promise.resolve(handler(...args));
await asyncEmit(index + 1, listeners, args); // ? I've unwound the loop here and made each iteration async!
await Promise.resolve(handler(eventFrame, ...args));
await asyncEmit(index + 1, listeners, eventFrame, args); // ? I've unwound the loop here and made each iteration async!
};
const checkEventFrame = (eventName: string, eventFrame: EventFrame) => {
if(typeof eventFrame.stopped !== 'boolean')
{
throw new TypeError(
`first argument (arg1) passed to handlers via emit('${eventName}', arg1, ...) `
+`must be an EventFrame instance or expose compatible interface, got ${eventFrame.toString()} instead`
);
}
}
/**
* Modify a listener to interrupt event loop when EventFrames are stop()ed
*
* @param {ListenerFn} listener
*/
const asyncifyListener = (listener: ListenerFn) => {
const handlerActual = listener;
return async (eventFrame: EventFrame, ...args: Array<any>) => {
await Promise.resolve(eventFrame.stopped || handlerActual(eventFrame, ...args));
await Promise.resolve(eventFrame.continue());
};
};
export default class FrameworkEventEmitter extends EventEmitter {
_frameworkEvents = [];
constructor(frameworkEvents: ?Array<string>, ...args: Array<any>) {
constructor(...args: Array<any>) {
super(...args);
this._frameworkEvents = frameworkEvents || [];
}
// ? Modify emit to handle async handlers and errors in handlers
async emit(event: string, ...args: Array<any>) {
async emit(event: string, eventFrame: EventFrame, ...args: Array<any>) {
try {
await asyncEmit(0, this.listeners(event), args);
await asyncEmit(0, this.listeners(event), eventFrame, args);
await Promise.resolve(eventFrame.finish());
}
catch(error) {
error.eventName = event;
error.eventArgs = args;
super.emit('error', error);
}
}
// ? Modify addListener to interrupt event loop when EventFrames are stop()ed
addListener(eventName: string | Symbol, eventHandler: ListenerFn, context: ?{}, prepend: ?boolean) {
if(this._frameworkEvents.includes(eventName)) {
const handlerActual = eventHandler;
eventHandler = async (eventFrame: EventFrame, ...args: Array<any>) => {
checkEventFrame(eventName.toString(), eventFrame);
await Promise.resolve(eventFrame.stopped || handlerActual(eventFrame, ...args));
}
}
super.addListener(eventName, eventHandler, context);
if(prepend && !this._events[eventName].fn)
{
this._events[eventName] = [this._events[eventName].pop(), ...this._events[eventName]];
return super.emit('error', error);
}
return this;
}
prependListener(eventName: string | Symbol, eventHandler: ListenerFn, context: ?{}) {
return this.addListener(eventName, eventHandler, context, true);
}
appendListener(...args: Array<any>) {
return this.addListener(...args);
/**
* See https://nodejs.org/api/events.html#events_emitter_addlistener_eventname_listener
*
* Modifies a listener to interrupt event loop when EventFrames are
* stop()ed.
*
* The listener should have the following function signature:
* (eventFrame: EventFrame, ...args: Array<any>) => { ... }
*
* @param {string} eventName
* @param {ListenerFn} listener
*/
addListener(eventName: string | Symbol, listener: ListenerFn) {
return super.addListener(eventName, asyncifyListener(listener));
}
}
......@@ -2,33 +2,56 @@
* @description global utility functions and constants
*/
export const JUDGEMENT_SAFE = 'safe';
export const JUDGEMENT_UNSAFE = 'unsafe';
export const JUDGEMENT_UNKNOWN = 'unknown';
declare var chrome: any;
export type Chrome = chrome;
declare var _NODE_ENV: string;
declare var _HASHING_ALGORITHM: string;
declare var _APPLICATION_LABEL: string;
export const NODE_ENV = _NODE_ENV;
export const HASHING_ALGORITHM = _HASHING_ALGORITHM;
export const APPLICATION_LABEL = _APPLICATION_LABEL;
// ? Returns a string HTTPS endpoint URI that will yield the desired resource
// ? identifier hash
export const GOOGLE_DNS_HTTPS_RI_FN = (riHashLeft: string, riHashRight: string, originDomain: string) =>
`https://dns.google.com/resolve?name=${riHashLeft}.${riHashRight}.${haschk}.${originDomain}&type=TXT`;
`https://dns.google.com/resolve?name=${riHashLeft}.${riHashRight}.${APPLICATION_LABEL}.${originDomain}&type=TXT`;
// ? Returns a string HTTPS endpoint URI that will yield the desired resource
// ? range string
export const GOOGLE_DNS_HTTPS_RR_FN = (originDomain: string) =>
`https://dns.google.com/resolve?name=_rr._haschk.${originDomain}&type=TXT`;
export const FRAMEWORK_EVENTS = ['download.incoming', 'download.completed', 'download.suspiciousOrigin'];
export const FRAMEWORK_EVENTS = ['download.incoming', 'download.completed', ];
export const extractDomainFromURI = (url: string) => (new URL(url)).hostname.split('.').slice(-2).join('.');
export const Debug = {
log(...args: Array<any>) {
NODE_ENV === 'development' && console.log(...args);
},
if(fn: Function) {
NODE_ENV === 'development' && fn();
}
};
export const extendDownloadItemInstance = (downloadItem: any) => {
const uri: string = downloadItem.referrer || downloadItem.url;
if(!uri) throw new Error('cannot determine originDomain');
downloadItem.originDomain = extractDomainFromURI(uri);
downloadItem.judgement = JUDGEMENT_UNKNOWN;
return downloadItem;
};
export const JUDGEMENT_SAFE = 'safe';
export const JUDGEMENT_UNSAFE = 'unsafe';
export const JUDGEMENT_UNKNOWN = 'unknown';
export const bufferToHex = (buffer: ArrayBuffer) => {
let hexCodes = [];
let view = new DataView(buffer);
......
......@@ -4,11 +4,13 @@ import { guaranteeElementById } from 'universe/ui'
test('guaranteeElementById returns HTMLElement', () => {
const el = {};
document.getElementById = id => el;
// flow-disable-line
document.getElementById = () => el;
expect(guaranteeElementById('id')).toBe(el);
});
test("guaranteeElementById throws if it can't return HTMLElement", () => {
document.getElementById = id => null;
// flow-disable-line
document.getElementById = () => null;
expect(() => guaranteeElementById('id')).toThrow();
});
This diff is collapsed.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment