/* global chrome, browser */ import {parse_certs, verifyChain, getCommonName, getAltNames} from './verifychain.js'; import {ba2str, b64decode, concatTA, int2ba, sha256, b64encode, verifySig, assert, ba2int, xor, eq, wait, AESECBencrypt, wildcardTest, pubkeyPEM2raw, import_resource} from './utils.js'; import {getPref, getSessionBlob, getSession, getAllSessions, saveNewSession, init_db, addNewPreference, setPref, renameSession, deleteSession} from './indexeddb.js'; import {globals} from './globals.js'; import {Socket} from './Socket.js'; import {TLS, getExpandedKeys, decrypt_tls_responseV6} from './TLS.js'; import {verifyNotary, getURLFetcherDoc} from './oracles.js'; import {TLSNotarySession} from './TLSNotarySession.js'; import {TLSprobe} from './TLSprobe.js'; import {ProgressMonitor} from './ProgressMonitor.js'; import {FirstTimeSetup} from './FirstTimeSetup.js'; export class Main{ constructor(){ this.messageListener; this.notarization_in_progress = false; this.isFirstTimeSetupNeeded = false; this.waiting_for_click = false; // popupError will be set to non-null when there is some error message that must be shown // via the popup this.popupError = null; // tabid set to 0 is a sign that this is the main window when querying with .getViews() this.tabid = 0; // pendingAction is Firefox only: the action which must be taken as soon as the user allows // access to the website (either notarize or notarizeAfter) this.pendingAction = null; // trustedOracle is an object {'IP':, 'pubkeyPEM':} // describing the oracle server which was verified and can be used for notarization. this.trustedOracle = null; if (typeof(window) != 'undefined') { this.is_chrome = window.navigator.userAgent.match('Chrome') ? true : false; this.is_firefox = window.navigator.userAgent.match('Firefox') ? true : false; this.is_edge = window.navigator.userAgent.match('Edg') ? true : false; this.is_opera = window.navigator.userAgent.match('OPR') ? true : false; // pm is the only instance of ProgressMonitor that is reused between // notarization sesions this.pm = new ProgressMonitor(); } // trustedOracleReady will be set to true after we performed AWS HTTP queries // and verified that an oracle is trusted (we verify only once) this.trustedOracleReady = false; } async main() { // perform browser-specific init first if (this.is_edge || this.is_firefox || this.is_opera){ globals.usePythonBackend = true; } if (this.is_firefox){ // Firefox asks user for permission to access the current website. // Listen when the permission was given and run the pending action. // This way user doesnt have to click notarize->allow->notarize const listener = function(permissions){ if (permissions.origins.length != 1) { // unknown permission granted return; } if (this.pendingAction == 'notarize'){ this.prepareNotarizing(false); } else if (this.pendingAction == 'notarizeAfter'){ this.prepareNotarizing(true); } }; browser.permissions.onAdded.addListener(listener); } // browser-agnostic init const that = this; this.messageListener = chrome.runtime.onMessage.addListener(function(data) { // processMessages is the main entrypoint for all extension's logic that.processMessages(data); }); await init_db(); // Some preferences may not exist if we are upgrading from // a previous PageSigner version. Create the preferences. if (await getPref('firstTimeInitCompletedv2') === null){ await addNewPreference('firstTimeInitCompletedv2', false); await setPref('parsedCircuits', {}); } if (await getPref('trustedOracle') === null){ await addNewPreference('trustedOracle', {}); } // check if we need to communicate with a new version of the notary server const notaryVersion = await getPref('notaryServerVersion'); if (notaryVersion === null){ await addNewPreference('notaryServerVersion', 16); await setPref('trustedOracle', {}); } else if (notaryVersion < 16) { // the notary server was upgraded await setPref('notaryServerVersion', 16); await setPref('trustedOracle', {}); } const text = await import_resource('core/third-party/certs.txt'); await parse_certs(text); if (globals.useNotaryNoSandbox){ const obj = await this.queryNotaryNoSandbox(globals.defaultNotaryIP); this.trustedOracle = obj; this.trustedOracleReady = true; await setPref('trustedOracle', obj); return; } this.trustedOracle = await getPref('trustedOracle'); if (Object.keys(this.trustedOracle).length !== 0){ if (await this.pingNotary(this.trustedOracle.IP) !== true) { await this.tryBackupNotary(this.trustedOracle.IP); } else { this.trustedOracleReady = true; } return; } // on first launch trustedOracle is not set, verify the default one asynchronously if (await this.pingNotary(globals.defaultNotaryIP) !== true){ await this.tryBackupNotary(globals.defaultNotaryIP); return; } // notary is online const URLFetcherDoc = await getURLFetcherDoc(globals.defaultNotaryIP, globals.defaultNotaryPort); const trustedPubkeyPEM = await verifyNotary(URLFetcherDoc); assert(trustedPubkeyPEM != undefined); // verification was successful const obj = { 'IP': globals.defaultNotaryIP, 'pubkeyPEM': trustedPubkeyPEM, 'URLFetcherDoc': URLFetcherDoc }; await setPref('trustedOracle', obj); that.trustedOracle = obj; that.trustedOracleReady = true; } async queryNotaryNoSandbox(IP){ // just fetch the pubkey and trust it const resp = await fetch('http://'+IP+':' + globals.defaultNotaryPort + '/getPubKey', { method: 'POST', mode: 'cors', cache: 'no-store', }); const trustedPubkeyPEM = await resp.text(); return { 'IP': IP, 'pubkeyPEM': trustedPubkeyPEM, }; } // pingNotary returns true if notary's IP address is reachable async pingNotary(IP){ // ping the notary, it should respond with 404 not found const fProm = fetch('http://'+IP+':'+ globals.defaultNotaryPort + '/ping', { mode: 'no-cors' }); const out = await Promise.race([fProm, wait(5000)]) // eslint-disable-next-line no-unused-vars .catch(err => { // fetch got 404; do nothing, just prevent exception from propagating }); if (out === 'wait'){ console.log('Notary is unreachable at IP: ', IP); return false; } return true; } // tryBackupNotary tries to use a backup notary. It checks that the backup notary // is not the same as failedNotaryIP async tryBackupNotary(failedNotaryIP){ const resp = await fetch(globals.backupUrl); const backupIP = await resp.text(); if (backupIP === failedNotaryIP){ throw('Notary is unreachable. Please let the Pagesigner devs know about this.'); } if (this.pingNotary(backupIP) !== true){ console.log('Backup notary is unreachable.'); throw('Notary is unreachable. Please let the Pagesigner devs know about this.'); } const URLFetcherDoc = await getURLFetcherDoc(backupIP, globals.defaultNotaryPort); const trustedPubkeyPEM = await verifyNotary(URLFetcherDoc); assert(trustedPubkeyPEM != undefined); const obj = { 'IP': backupIP, 'pubkeyPEM': trustedPubkeyPEM, 'URLFetcherDoc': URLFetcherDoc }; console.log('backup oracle verified'); await setPref('trustedOracle', obj); this.trustedOracle = obj; this.trustedOracleReady = true; } openChromeExtensions(){ chrome.tabs.query({url: 'chrome://extensions/*'}, function(tabs) { if (tabs.length == 0) { chrome.tabs.create({url: 'chrome://extensions'}); return; } chrome.tabs.update(tabs[0].id, {active: true}); }); } // Pagesigner's popup has been clicked async popupProcess(){ if (this.notarization_in_progress) { chrome.runtime.sendMessage({ destination: 'popup', message: 'notarization_in_progress', firstTime: this.isFirstTimeSetupNeeded }); return; } if (this.waiting_for_click) { chrome.runtime.sendMessage({ destination: 'popup', message: 'waiting_for_click' }); return; } // else{} the checks below are only for Chrome // probe a non-existent port, if reject()ed with undefined, then the helper app is not running try{ const dummySock = new Socket('127.0.0.1', -1); dummySock.connectTimeout = 200; // 200 ms await dummySock.connect(); } catch(error){ if (error == undefined || error == 'connection error'){ chrome.runtime.sendMessage({ destination: 'popup', message: 'app_not_installed' }); return; } } if (this.popupError) { chrome.runtime.sendMessage({ destination: 'popup', message: 'popup error', data: this.popupError }); this.popupError = null; this.loadDefaultIcon(); } else { chrome.runtime.sendMessage({ destination: 'popup', message: 'show_menu' }); } } // checkIfTabOpened checks if a "tab" containing window[property] has signalled that it has // been loaded and its message listeners are ready // openTabs is an optional array of tab ids to skip when checking because they were already // opened BEFORE we initiated the opening of "tab" // we abort if tab doesn't open after 5 secs. checkIfTabOpened(tab, property, openTabs){ // eslint-disable-next-line no-unused-vars openTabs = openTabs || []; let isTimeoutTriggered = false; setTimeout(function(){ isTimeoutTriggered = true; }, 5 * 1000); // eslint-disable-next-line no-async-promise-executor return new Promise(async function(resolve) { function tryAgain(){ console.log('checking if '+property+' is ready...'); const views = chrome.extension.getViews(); // sometimes the View for the newly opened tab may not yet be available // so we must wait a little longer for (const win of views){ if (win[property] == undefined) continue; // found some viewer. Our viewer's tabid is null if (win.tabid != null) continue; if (win.isReady !== true) continue; // else is ready // save the tab id inside the window object win.tabid = tab.id; resolve(win); return true; } } while (tryAgain() !== true){ if (isTimeoutTriggered){ console.log('isTimeoutTriggered', isTimeoutTriggered); return; } await wait(10); } }); } openFileChooser(){ const myTabs = []; const views = chrome.extension.getViews(); for (const win of views){ if (win.isFileChooser){ // re-focus if already opened chrome.tabs.update(win.tabid, {active: true}); return; } myTabs.push(win.tabid); } const that = this; // #filechooser is in the URL only so that the user is not confused. We do not // create a separate filechooser.html because as soon as a file is chosen, the // same tab will be reused as a viewer. // Otherwise we would have to close filechooser tab and instantly open a // viewer tab with an unpleasant flicker. const url = chrome.extension.getURL('ui/html/viewer.html#filechooser'); chrome.tabs.create({url: url}, async function(t){ const win = await that.checkIfTabOpened(t, 'isViewer', myTabs); win.viewer.showFileChooser(); }); } openManager() { const url = chrome.extension.getURL('ui/html/manager.html'); for (const win of chrome.extension.getViews()){ if (win.isManager){ // re-focus tab if manager already open console.log('will refocus manger tab', win.tabid); chrome.tabs.update(win.tabid, {active: true}); return; } } const that = this; chrome.tabs.create({url: url}, function(t){ that.checkIfTabOpened(t, 'is_manager'); }); } async prepareNotarization(after_click = false, isPreview = false) { if (!isPreview && !this.trustedOracleReady) { this.sendAlert({ title: 'PageSigner error.', text: 'Cannot notarize because something is wrong with PageSigner server. Please try again later' }); return; } let clickTimeout = null; const that = this; const active_tab = await new Promise(function(resolve) { chrome.tabs.query({active: true}, function(t) { resolve(t[0]); }); }); if (! active_tab.url.startsWith('https://')) { this.sendAlert({ 'title': 'PageSigner error.', 'text': 'You can only notarize pages which start with https://' }); return; } if (after_click){ const url = chrome.extension.getURL('ui/img/arrow24.png'); chrome.browserAction.setIcon({path: url}); this.waiting_for_click = true; clickTimeout = setTimeout(function() { that.waiting_for_click = false; that.loadDefaultIcon(); that.sendAlert({ title: 'PageSigner error.', text: 'You haven\'t clicked any https:// links in 30 seconds. Please try again. If this error persists it may mean that the website you are trying to notarize is not compatible with PageSigner.' }); }, 30 * 1000); } let oBR_details; const oBR_handler = function(details){ console.log('in onBeforeRequest', details); chrome.webRequest.onBeforeRequest.removeListener(oBR_handler); oBR_details = details; }; chrome.webRequest.onBeforeRequest.addListener( oBR_handler, { urls: [''], tabId: active_tab.id, types: ['main_frame', 'xmlhttprequest'] // types: ["main_frame", "sub_frame", "stylesheet", "script", // "image", "font", "object", "xmlhttprequest", "ping", "csp_report", "media", "websocket", "other"] }, ['requestBody']); let oBSH_details; const oBSH_handler = function(details){ console.log('in onBeforeSendHeaders', details); chrome.webRequest.onBeforeSendHeaders.removeListener(oBSH_handler); oBSH_details = details; console.log(oBR_details, oBSH_details); }; const extraInfoSpec = ['requestHeaders']; if (this.is_chrome) extraInfoSpec.push('extraHeaders'); chrome.webRequest.onBeforeSendHeaders.addListener( oBSH_handler, { urls: [''], tabId: active_tab.id, types: ['main_frame', 'xmlhttprequest'] }, extraInfoSpec); // wait for the request to pass oBR and oBHS and reach onSendHeaders await new Promise(function(resolve) { const oSH_handler = function(details){ console.log('in onSendHeaders'); chrome.webRequest.onSendHeaders.removeListener(oSH_handler); resolve(details); }; chrome.webRequest.onSendHeaders.addListener( oSH_handler, { urls: [''], tabId: active_tab.id, types: ['main_frame', 'xmlhttprequest'] }); // if not Notarize After Click mode, // reload current tab in order to trigger the HTTP request if (!that.waiting_for_click) chrome.tabs.reload(active_tab.id); // otherwise just wait for the user to click smth and trigger onBeforeRequest }); if (this.waiting_for_click) { clearTimeout(clickTimeout); this.waiting_for_click = false; } if (oBR_details.url !== oBSH_details.url) return; if (oBR_details.requestId !== oBSH_details.requestId) return; if (oBR_details.method == 'POST') { // POST payload is only available from onBeforeRequest oBSH_details['requestBody'] = oBR_details.requestBody; } const rv = this.getHeaders(oBSH_details); this.startNotarization(rv.headers, rv.server, rv.port, isPreview) .catch(err => { console.log('Notarization aborted.', err); console.trace(); let errmsg = err; if (typeof(err) === 'object'){ errmsg = err.message; if (err.message == 'Failed to fetch'){ errmsg = 'Unable to connect to the notary server. Please check your internet connection.'; } } this.sendAlert({ title: 'PageSigner error.', text: errmsg }); }) .finally(()=> { this.notarization_in_progress = false; this.loadDefaultIcon(); }); } getHeaders(obj) { console.log('headers are', obj); const x = obj.url.split('/'); const host = x[2].split(':')[0]; x.splice(0, 3); const resource_url = x.join('/'); const http_version = globals.useHTTP11 ? ' HTTP/1.1':' HTTP/1.0'; let headers = obj.method + ' /' + resource_url + http_version + '\r\n'; // Chrome doesnt add Host header. Firefox does if (this.is_chrome){ headers += 'Host: ' + host + '\r\n'; } for (let h of obj.requestHeaders) { // we dont want any "br" encoding if (h.name == 'Accept-Encoding') { // h.value = 'gzip, deflate' h.value = 'identity;q=1, *;q=0'; } headers += h.name + ': ' + h.value + '\r\n'; } if (obj.method == 'GET') { headers += '\r\n'; } else if (obj.method == 'POST') { let content; if (obj.requestBody.raw != undefined) { content = ba2str(new Uint8Array(obj.requestBody.raw[0].bytes)); } else{ const keys = Object.keys(obj.requestBody.formData); content = ''; for (var key of keys) { content += key + '=' + obj.requestBody.formData[key] + '&'; } // get rid of the last & content = content.slice(0, -1); // Chrome doesn't expose Content-Length which chokes nginx headers += 'Content-Length: ' + parseInt(content.length) + '\r\n\r\n'; headers += content; } } let port = 443; if (obj.url.split(':').length === 3) { // the port is explicitely provided in URL port = parseInt(obj.url.split(':')[2].split('/')[0]); } return { 'headers': headers, 'server': host, 'port': port }; } async getPGSG(sid){ const blob = await getSessionBlob(sid); return blob.pgsg; } // processMessages is the main entrypoint for all extension's logic async processMessages(data) { if (data.destination !== 'extension') return; console.log('ext got msg', data); switch (data.message){ case 'rename': await renameSession(data.sid, data.newname); this.sendSessions(await getAllSessions()); break; case 'delete': await deleteSession(data.sid); this.sendSessions(await getAllSessions()); break; case 'import': // data is js array this.importPgsgAndShow(new Uint8Array(data.data)); break; case 'export': this.sendToManager({'pgsg': JSON.stringify(await this.getPGSG(data.sid)), 'name': (await getSession(data.sid)).sessionName}, 'export'); break; case 'notarize': this.prepareNotarization(false); break; case 'notarizeAfter': this.prepareNotarization(true); break; case 'preview': this.prepareNotarization(false, true); break; case 'manage': this.openManager(); break; case 'refresh': this.sendSessions(await getAllSessions()); break; case 'openLink1': chrome.tabs.create({url: 'https://www.tlsnotary.org'}); break; case 'showSession': this.showSession(data.sid); break; case 'showDetails': this.openDetails(data.sid); break; case 'showPreviewDetails': this.openPreviewDetails(data.serverName, data.request, data.response); break; case 'fileChooser': this.openFileChooser(); break; case 'openChromeExtensions': this.openChromeExtensions(); break; case 'popup active': this.popupProcess(); break; case 'open python script': this.openPythonScript(); break; case 'pendingAction': this.pendingAction = data.action; break; case 'useNotaryNoSandbox': this.useNotaryNoSandbox(data.IP); break; case 'removeNotary': await setPref('trustedOracle', {}); } } // startNotarization starts a TLSNotary session, saves the session result // and displays it to the user. If we are in the preview mode, then we send // the request directly to the server and display the result. async startNotarization(headers, server, port, isPreview=false) { if (isPreview){ await this.startPreview(headers, server, port); return; } this.notarization_in_progress = true; this.pm.init(); this.isFirstTimeSetupNeeded = ! await getPref('firstTimeInitCompletedv2'); chrome.runtime.sendMessage({ destination: 'popup', message: 'notarization_in_progress', firstTime: this.isFirstTimeSetupNeeded }); if (this.isFirstTimeSetupNeeded){ const obj = await new FirstTimeSetup().start(this.pm); console.time('setPref'); await setPref('parsedCircuits', obj); console.timeEnd('setPref'); await setPref('firstTimeInitCompletedv2', true); } const circuits = await getPref('parsedCircuits'); const session = new TLSNotarySession( server, port, headers, this.trustedOracle, globals.sessionOptions, circuits, this.pm); const obj = await session.start(); obj['title'] = 'PageSigner notarization file'; obj['version'] = 6; if (! globals.useNotaryNoSandbox){ obj['URLFetcher attestation'] = this.trustedOracle.URLFetcherDoc; } const [host, request, response, date] = await this.verifyPgsgV6(obj); const serializedPgsg = this.serializePgsg(obj); await saveNewSession (date, host, request, response, serializedPgsg); // date uniquely identifies a session this.showSession(date); } // startPreview does not perfrom a TLSNotary session but simply fetches the // resource from the webserver and shows the user a preview of what the // notarization result will look like, were the user to initiate a notarization. // This is especially useful when the the user picks which headers/resources // to include in the notarization and wants to have a quick preview. async startPreview(headers, server, port) { const preview = new TLSprobe(server, port, headers, globals.sessionOptions); const response = await preview.start(); console.log('response was: ', response); await this.openViewer(server, headers, response); } loadDefaultIcon(){ const url = chrome.extension.getURL('ui/img/icon.png'); chrome.browserAction.setIcon({path: url}); } // opens a tab showing the session. sid is a unique session id // creation time is sid. async showSession (sid){ const data = await getSession(sid); const blob = await getSessionBlob(sid); if (data === null) {throw('failed to get index', sid);} await this.openViewer(data.serverName, blob.request, blob.response, sid); this.sendSessions( await getAllSessions()); // refresh manager } openPythonScript(){ const url = chrome.extension.getURL('pagesigner.py'); chrome.tabs.create({url: url}, function(t){ chrome.tabs.executeScript(t.id, {file: ('ui/python_script_header.js')}); }); } async verifyPgsg(json){ if (json['version'] == 6){ return await this.verifyPgsgV6(json); } else { throw ('Unrecognized version of the imported pgsg file.'); } } // Serialize fields of json object serializePgsg(json){ // b64encode every field const newjson = {}; const keys = Object.keys(json); for (const key of keys){ if (['title', 'version'].includes(key)){ newjson[key] = json[key]; } else if (key === 'certificates' || key === 'server response records'){ // turn an array into obj with key as index i.e {"0": elem0, "1": elem1, ...} const obj = {}; for (let i=0; i { console.log('Error in main: ', err); m.sendAlert({ title: 'PageSigner error.', text: err }); }); }