diff --git a/docs/changelog.md b/docs/changelog.md new file mode 100644 index 0000000..02fedc9 --- /dev/null +++ b/docs/changelog.md @@ -0,0 +1,11 @@ +# Changelog + +> Only notable changes are documented here. You can find the changelog for the `@ssasy-auth/extension` package at [src/bridge/docs/CHANGELOG.md](../src/bridge/docs/changelog.md). + +## `1.0.0` + +- updates `@ssasy-auth/core` to `2.2.3` which enables SSASy URIs + +## `0.23.10` + +- renames `REQUEST_SOLUTION` message type (in [types.ts](../src/bridge/src/types.ts)) to `REQUEST_CHALLENGE_RESPONSE` to better reflect the purpose of the message. diff --git a/package.json b/package.json index b655863..cc0ebf1 100644 --- a/package.json +++ b/package.json @@ -1,18 +1,18 @@ { "name": "@ssasy-auth/extension", "nickname": "ssasy", - "version": "0.23.9", + "version": "1.0.0", "license": "MIT", "description": "A browser extension that offers a secure and usable alternative to passwords and federated identity providers.", "repository": "this-oliver/ssasy-ext", "author": "hello@oliverrr.net", "scripts": { "bridge:build": "tsc -p src/bridge/tsconfig.json", - "bridge:pack": "esno scripts/prepare-bridge.ts", - "bridge:prepare": "run-s test bridge:build bridge:pack", - "bridge:release": "pnpm bridge:prepare && cd src/bridge && npm publish --access public && cd ../../ && pnpm bridge:clear", - "bridge:preview": "pnpm bridge:prepare && cd src/bridge && npm pack && cd ../../ && pnpm bridge:clear", - "bridge:clear": "rimraf src/bridge/package.json src/bridge/*-lock.* src/bridge/lib src/bridge/node_modules", + "bridge:prepare": "esno scripts/prepare-bridge.ts", + "bridge:pack": "run-s test bridge:clear bridge:prepare bridge:build", + "bridge:release": "pnpm bridge:pack && cd src/bridge && npm publish --access public && cd ../../ && pnpm bridge:clear", + "bridge:preview": "pnpm bridge:pack && cd src/bridge && npm pack && cd ../../ && pnpm bridge:clear", + "bridge:clear": "rimraf src/bridge/lib src/bridge/node_modules src/bridge/package.json src/bridge/*-lock.*", "build": "cross-env NODE_ENV=production run-s clear test build:web build:prepare build:js", "build:mv3": "cross-env NODE_ENV=production MANIFEST_VERSION=3 run-s clear test build:web build:prepare build:js build:bg", "build:prepare": "esno scripts/prepare.ts", @@ -43,7 +43,7 @@ }, "dependencies": { "@mdi/js": "^7.2.96", - "@ssasy-auth/core": "1.9.1", + "@ssasy-auth/core": "^3.0.0", "pinia": "^2.0.33", "vue": "^3.2.47", "vue-router": "4", @@ -97,4 +97,4 @@ "*.ts": "eslint --fix" }, "packageManager": "pnpm@7.29.0" -} +} \ No newline at end of file diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0d9b8b9..1754119 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -3,7 +3,7 @@ lockfileVersion: 5.4 specifiers: '@ffflorian/jszip-cli': ^3.1.9 '@mdi/js': ^7.2.96 - '@ssasy-auth/core': 1.9.1 + '@ssasy-auth/core': ^3.0.0 '@types/fs-extra': ^11.0.1 '@types/node': ^18.14.6 '@types/webextension-polyfill': ^0.10.0 @@ -49,7 +49,7 @@ specifiers: dependencies: '@mdi/js': 7.2.96 - '@ssasy-auth/core': 1.9.1 + '@ssasy-auth/core': 3.0.0 pinia: 2.0.33_hmuptsblhheur2tugfgucj7gc4 vue: 3.2.47 vue-router: 4.1.6_vue@3.2.47 @@ -1026,8 +1026,8 @@ packages: engines: {node: '>=14.16'} dev: true - /@ssasy-auth/core/1.9.1: - resolution: {integrity: sha512-ywmGjbHZLFRTMMEfq4uJXp47zIJpvBYjLxA7dzjmR3OhLFeqHyClBta9XsMRT50uzP25bMPMPQtiKSy6WDItsQ==} + /@ssasy-auth/core/3.0.0: + resolution: {integrity: sha512-cAPXBJu0/KdKQq7uIFJE/9+AKs6Zgh6qr3OzralcQtJxC6kjHB1pt0FSwTg8f4vO8HPJ2oSjz0y8vKy0cFt8TA==} dependencies: buffer: 6.0.3 dev: false diff --git a/shim.d.ts b/shim.d.ts index 9cdaeab..09ee7e9 100644 --- a/shim.d.ts +++ b/shim.d.ts @@ -3,11 +3,11 @@ import { MessageType } from '~/bridge' import type { KeyRequest, PublicKeyResponse, ChallengeRequest, ChallengeResponse, ErrorResponse } from '~/bridge' declare module 'webext-bridge' { - + // define message protocol types below (see https://github.com/antfu/webext-bridge#type-safe-protocols) export interface ProtocolMap { - 'close-request-tab': ErrorResponse | undefined + 'close-popup': ErrorResponse | undefined [MessageType.REQUEST_PUBLIC_KEY]: ProtocolWithReturn - [MessageType.REQUEST_SOLUTION]: ProtocolWithReturn + [MessageType.REQUEST_CHALLENGE_RESPONSE]: ProtocolWithReturn } } diff --git a/src/background/index.ts b/src/background/index.ts index ba9ee5f..dedb236 100644 --- a/src/background/index.ts +++ b/src/background/index.ts @@ -28,29 +28,29 @@ export interface Session { } /** - * Current request tab id + * Popup window id. `-1` means no popup window is open, `-2` + * means popup window is locked (i.e. user is in the middle of a request) */ -let requestTab: number = -1; +let popupWindowId: number = -1; /** - * Close current request tab and reset `requestTab` to `-1` + * Close current request tab and reset `popupWindowId` to `-1` */ -function _closeRequestTab(): void { +function _closePopupWindow(): void { + if (popupWindowId === -1 || popupWindowId === -2) return; + // close popup window if it is open - if (requestTab !== -1) { - PopupPage.close({ id: requestTab }); - } + PopupPage.close({ id: popupWindowId }); // reset current request tab - requestTab = -1; - + popupWindowId = -1; } /** - * Listen for current tab requests from content scripts + * Listen for current tab requests from content scripts (client) */ -onMessage('close-request-tab', ({ data }) => { - Logger.info('close-request-tab channel', 'close popup message recieved', 'background'); +onMessage('close-popup', ({ data }) => { + Logger.info('close-popup channel', 'close popup message recieved', 'background'); if (data) { // show error message to user @@ -59,7 +59,7 @@ onMessage('close-request-tab', ({ data }) => { } // close current request tab - _closeRequestTab(); + _closePopupWindow(); }) /** @@ -82,8 +82,8 @@ onMessage(MessageType.REQUEST_PUBLIC_KEY, async ({ data }) => { popupPage: await PopupPage.open({ queryString: query }) } - requestTab = session.popupPage.id || 0; - Logger.info('requestTab set to ', requestTab, 'background'); + popupWindowId = session.popupPage.id || 0; + Logger.info('popupWindowId set to ', popupWindowId, 'background'); // wait for response from popup window return new Promise((resolve) => { @@ -113,6 +113,9 @@ onMessage(MessageType.REQUEST_PUBLIC_KEY, async ({ data }) => { key: msg.key }; + // lock popup window to prevent broadcasts from closing the popup window + popupWindowId = -2; + // respond to content script return resolve(response); } @@ -124,10 +127,10 @@ onMessage(MessageType.REQUEST_PUBLIC_KEY, async ({ data }) => { * Listen for challenge requests from content scripts and * responds with a challenge response after user makes a decision (approve/deny) */ -onMessage(MessageType.REQUEST_SOLUTION, async ({ data }) => { +onMessage(MessageType.REQUEST_CHALLENGE_RESPONSE, async ({ data }) => { const request: ChallengeRequest = { origin: data.origin, - type: MessageType.REQUEST_SOLUTION, + type: MessageType.REQUEST_CHALLENGE_RESPONSE, mode: data.mode, challenge: data.challenge }; @@ -137,16 +140,19 @@ onMessage(MessageType.REQUEST_SOLUTION, async ({ data }) => { // wait for response from popup window return new Promise((resolve) => { - // broadcast undefined response, if the messageSession is still active and popup window is closed + + // listen for popup window close event browser.windows.onRemoved.addListener(async (windowId) => { - if (requestTab === windowId) { + + // close current popup window if it matches the windowId + if (popupWindowId === windowId) { const response: ChallengeResponse = { - type: MessageType.RESPONSE_SOLUTION, - solution: null + type: MessageType.RESPONSE_CHALLENGE_RESPONSE, + challengeResponse: null }; // close current request tab - _closeRequestTab(); + _closePopupWindow(); return resolve(response); } @@ -158,10 +164,10 @@ onMessage(MessageType.REQUEST_SOLUTION, async ({ data }) => { type: msg.type }; - if (message.type === MessageType.RESPONSE_SOLUTION) { + if (message.type === MessageType.RESPONSE_CHALLENGE_RESPONSE) { const response: ChallengeResponse = { - type: MessageType.RESPONSE_SOLUTION, - solution: msg.solution + type: MessageType.RESPONSE_CHALLENGE_RESPONSE, + challengeResponse: (msg as ChallengeResponse).challengeResponse }; // respond to content script diff --git a/src/bridge/README.md b/src/bridge/README.md index d105347..7bf2740 100644 --- a/src/bridge/README.md +++ b/src/bridge/README.md @@ -9,23 +9,23 @@ The `bridge` component abstracts the complex logic that web applications would n ## usage -```js +```ts import { Bridge } from 'ssasy-ext-bridge'; // check if the extension is installed -const extensionInstalled = await Bridge.isExtensionInstalled(); // returns true or false +const extensionInstalled: boolean = await Bridge.isExtensionInstalled(); // returns true or false if(extensionInstalled === true){ - const requestMode = 'login'; // or 'registration' + const requestMode: string = 'login'; // or 'registration' // request the user's public key - const publicKey = await Bridge.requestPublicKey(requestMode); // returns the user's public key + const publicKey: string = await Bridge.requestPublicKey(requestMode); // returns the user's public key // ... generate a challenge with the user's public key // initiate challenge-response - const challengeResponse = await Bridge.requestSolution(requestMode, challenge); // returns the challenge response + const challengeResponse: string = await Bridge.requestChallengeResponse(requestMode, challenge); // returns the challenge response // ... verify the challenge response } diff --git a/src/bridge/__tests__/bridge.test.ts b/src/bridge/__tests__/bridge.test.ts index 9e3a647..5aaf6fb 100644 --- a/src/bridge/__tests__/bridge.test.ts +++ b/src/bridge/__tests__/bridge.test.ts @@ -1,28 +1,29 @@ import { beforeEach, describe, expect, it } from 'vitest' import { Bridge } from '~/bridge/src/bridge' import { MessageType } from '~/bridge/src/types' -import type { ChallengeResponse } from '~/bridge/src/types' +import type { PublicKeyResponse, ChallengeResponse } from '~/bridge/src/types' describe('Bridge', () => { - describe('isExtensionInstalled') - describe('requestPublicKey') - - describe.only('requestSolution', () => { + describe('isExtensionInstalled', () => { + it('should return true if extension is installed'); + it('should return false if extension is not installed'); + }) + describe('requestPublicKey', () => { /** * Dispatches an event message with a challenge response. Used to simulate * the message event from the extension. */ function _dispatchSuccessResponse() { - const challengeResponse: ChallengeResponse = { type: MessageType.RESPONSE_SOLUTION, solution: 'solution' }; + const response: PublicKeyResponse = { type: MessageType.RESPONSE_PUBLIC_KEY, key: 'key' }; window.dispatchEvent(new MessageEvent( 'message', - { data: challengeResponse } + { data: response } )); } - // mock window.addEventListener response for each test in `requestSolution` + // mock window.addEventListener response for each test in `requestChallengeResponse` beforeEach(() => { setTimeout(() => { _dispatchSuccessResponse(); @@ -33,25 +34,60 @@ describe('Bridge', () => { let errorThrown = false; try { - await Bridge.requestSolution(1 as any, 'challengeString'); + await Bridge.requestPublicKey(1 as any); } catch (error) { errorThrown = true; } - expect(errorThrown).toBe(true); + expect(errorThrown).to.be.true; }) - + }) + + describe('requestChallengeResponse', () => { + + /** + * Dispatches an event message with a challenge response. Used to simulate + * the message event from the extension. + */ + function _dispatchSuccessResponse() { + const response: ChallengeResponse = { type: MessageType.RESPONSE_CHALLENGE_RESPONSE, challengeResponse: 'challenge response' }; + + window.dispatchEvent(new MessageEvent( + 'message', + { data: response } + )); + } + + // mock window.addEventListener response for each test in `requestChallengeResponse` + beforeEach(() => { + setTimeout(() => { + _dispatchSuccessResponse(); + }, 500); + }) + + it('should throw error if requestMode is not a string', async () => { + let errorThrown = false; + + try { + await Bridge.requestChallengeResponse(1 as any, 'challengeString'); + } catch (error) { + errorThrown = true; + } + + expect(errorThrown).to.be.true; + }) + it('should throw error if challengeString is a string', async () => { let errorThrown = false; try { - await Bridge.requestSolution('login', 1 as any); + await Bridge.requestChallengeResponse('login', 1 as any); } catch (error) { errorThrown = true; } - expect(errorThrown).toBe(true); + expect(errorThrown).to.be.true; }) }) }) diff --git a/src/bridge/docs/changelog.md b/src/bridge/docs/changelog.md new file mode 100644 index 0000000..791339e --- /dev/null +++ b/src/bridge/docs/changelog.md @@ -0,0 +1,11 @@ +# Changelog + +> Only notable changes are documented here. + +## `0.23.10` + +- [breaking] renames `requestSolution()` to `requestChallengeResponse()` to better reflect the purpose of the method. + +### migrating from `0.23.9` to `0.23.10` + +- replace all instances of `Bridge.requestSolution()` with `Bridge.requestChallengeResponse()` diff --git a/src/bridge/src/bridge.ts b/src/bridge/src/bridge.ts index b12614f..816a49e 100644 --- a/src/bridge/src/bridge.ts +++ b/src/bridge/src/bridge.ts @@ -1,5 +1,14 @@ -import type { RawKey } from '@ssasy-auth/core'; -import { +/** + * This file acts as a 'bridge' between the SSASy extension and web applications + * that want to request a public key or chalenge-response from a user. It exposes + * three main functions: + * + * - `isExtensionInstalled`: Checks if the SSASy extension is installed in the browser. + * - `requestPublicKey`: Requests the public key of the user from the SSASy extension. + * - `requestChallengeResponse`: Requests the solution from the SSASy extension. + */ + +import { MessageType, RequestMode, BaseMessage, @@ -11,6 +20,23 @@ import { ErrorResponse } from './types'; +const LOGGING_EMOJI = '🌉'; +const LOGGING_CONTEXT = '[ssasy-bridge]'; +const LOGGING_PREFIX = `${LOGGING_CONTEXT} ${LOGGING_EMOJI}`; + +/** + * Logs a message to the console. + */ +function _log(message: string, config?: { error?: boolean }) { + const log = `${LOGGING_PREFIX} ${message}`; + + if (config?.error) { + console.error(log); + } else { + console.info(log); + } +} + /** * Returns true if the browser has the Ssasy extension installed. */ @@ -27,13 +53,13 @@ async function isExtensionInstalled(): Promise { const interval = setInterval(() => { if (rounds >= MAX_ROUNDS) { clearInterval(interval); - console.info('[ssasy-bridge]', 'Timeout...') + _log('Timeout...', { error: true }); resolve(false); } - + // check again window.postMessage(request, '*'); - console.info('[ssasy-bridge]', 'Pinging...') + _log('Pinging...'); // increment rounds rounds++; @@ -42,19 +68,19 @@ async function isExtensionInstalled(): Promise { // listen for response from extension window.addEventListener('message', (event) => { - const message: BaseMessage = { - type: event.data.type, + const message: BaseMessage = { + type: event.data.type, description: event.data.description }; if (message.type === MessageType.RESPONSE_PING) { - console.info('[ssasy-bridge] Extension installed') + _log('Extension installed'); clearInterval(interval); return resolve(true); } - if(message.type === MessageType.RESPONSE_ERROR){ + if (message.type === MessageType.RESPONSE_ERROR) { const errorResponse: ErrorResponse = { type: event.data.type, error: event.data.error @@ -64,9 +90,10 @@ async function isExtensionInstalled(): Promise { return reject(errorResponse.error); } }); - + } catch (error) { - console.error('[ssasy-bridge]', (error as Error).message || 'Failed to ping extension'); + const message = (error as Error).message || 'Failed to ping extension'; + _log(message, { error: true }); resolve(false); } }); @@ -75,109 +102,110 @@ async function isExtensionInstalled(): Promise { /** * Returns the public key of the user from the Ssasy extension. */ -async function requestPublicKey(mode: RequestMode): Promise { +async function requestPublicKey(mode: RequestMode): Promise { return new Promise((resolve, reject) => { + + // throw error if requestMode is not a string + if (typeof mode !== 'string') { + reject(new Error('requestMode must be a string')); + } + try { // listen for response from extension window.addEventListener('message', (event) => { - const message: BaseMessage = { + const message: BaseMessage = { type: event.data.type }; - + if (message.type === MessageType.RESPONSE_PUBLIC_KEY) { - console.info('[ssasy-bridge] Recieved public key from extension') + _log('Recieved public key from extension'); + + const response: PublicKeyResponse = event.data; const keyResponse: PublicKeyResponse = { - type: event.data.type, - key: event.data.key + type: response.type, + key: response.key }; - return keyResponse.key - ? resolve(JSON.parse(keyResponse.key) as RawKey) + return keyResponse.key + ? resolve(keyResponse.key) : resolve(null); } - if(message.type === MessageType.RESPONSE_ERROR){ - const errorResponse: ErrorResponse = { - type: event.data.type, - error: event.data.error - }; - - reject(errorResponse.error); + if (message.type === MessageType.RESPONSE_ERROR) { + + const response: ErrorResponse = event.data; + reject(response.error); } }); - + // send message to extension const request: PublicKeyRequest = { origin: '*', mode: mode, type: MessageType.REQUEST_PUBLIC_KEY }; window.postMessage(request, '*'); - + } catch (error) { - console.error('[ssasy-bridge]', (error as Error).message || 'Failed to request public key'); + const message = (error as Error).message || 'Failed to request public key'; + _log(message, { error: true }); resolve(null); } }); } /** - * Returns the solution to the challenge from the Ssasy extension. + * Returns the challenge response from the Ssasy extension. */ -async function requestSolution(mode: RequestMode, encryptedChallenge: string): Promise { - +async function requestChallengeResponse(mode: RequestMode, encryptedChallengeUri: string): Promise { return new Promise((resolve, reject) => { + // throw error if requestMode is not a string - if(typeof mode !== 'string') { + if (typeof mode !== 'string') { reject(new Error('requestMode must be a string')); } // throw error if challengeString is not a string - if(typeof encryptedChallenge !== 'string') { + if (typeof encryptedChallengeUri !== 'string') { reject(new Error('challengeString must be a string')); } - + // listen for response from extension window.addEventListener('message', (event) => { + const message: BaseMessage = { type: event.data.type }; - if(message.type === MessageType.RESPONSE_SOLUTION){ - console.info('[ssasy-bridge] Recieved solution from extension'); + if (message.type === MessageType.RESPONSE_CHALLENGE_RESPONSE) { + _log('Recieved challenge response from extension'); - const response: ChallengeResponse = { - type: event.data.type, - solution: event.data.solution - }; + const response: ChallengeResponse = event.data; - if(response.solution){ - resolve(response.solution); + if (response.challengeResponse) { + resolve(response.challengeResponse); } else { resolve(null); } } - if(message.type === MessageType.RESPONSE_ERROR){ - const errorResponse: ErrorResponse = { - type: event.data.type, - error: event.data.error - }; - - reject(errorResponse.error); + if (message.type === MessageType.RESPONSE_ERROR) { + + const response: ErrorResponse = event.data; + reject(response.error); } }); - + // send message to extension - const request: ChallengeRequest = { - origin: '', + const request: ChallengeRequest = { + origin: '', mode: mode, - type: MessageType.REQUEST_SOLUTION, - challenge: encryptedChallenge + type: MessageType.REQUEST_CHALLENGE_RESPONSE, + challenge: encryptedChallengeUri }; window.postMessage(request, '*'); - }); + }); } -export const Bridge = { +export const Bridge = { isExtensionInstalled, requestPublicKey, - requestSolution + requestChallengeResponse }; \ No newline at end of file diff --git a/src/bridge/src/types.ts b/src/bridge/src/types.ts index 8b64043..dacb18a 100644 --- a/src/bridge/src/types.ts +++ b/src/bridge/src/types.ts @@ -1,9 +1,9 @@ enum MessageType { REQUEST_PUBLIC_KEY = 'request-public-key', - REQUEST_SOLUTION = 'request-solution', + REQUEST_CHALLENGE_RESPONSE = 'request-challenge-response', REQUEST_PING = 'request-ping', RESPONSE_PUBLIC_KEY = 'response-public-key', - RESPONSE_SOLUTION = 'response-solution', + RESPONSE_CHALLENGE_RESPONSE = 'response-challenge-response', RESPONSE_PING = 'response-ping', RESPONSE_ERROR = 'response-error', } @@ -30,14 +30,14 @@ interface PublicKeyResponse extends BaseMessage { } interface ChallengeRequest extends BaseRequest { - type: MessageType.REQUEST_SOLUTION; + type: MessageType.REQUEST_CHALLENGE_RESPONSE; mode: RequestMode; challenge: string; } interface ChallengeResponse extends BaseMessage { - type: MessageType.RESPONSE_SOLUTION; - solution: string | null; + type: MessageType.RESPONSE_CHALLENGE_RESPONSE; + challengeResponse: string | null; } interface ErrorResponse extends BaseMessage { diff --git a/src/components/forms/VaultAuthForm.vue b/src/components/forms/VaultAuthForm.vue index 5dc0f9a..bc26172 100644 --- a/src/components/forms/VaultAuthForm.vue +++ b/src/components/forms/VaultAuthForm.vue @@ -110,8 +110,8 @@ async function _unwrapKey(password: string){ // emit the key emits('input', privateKey); } catch (error) { - const message = (error as Error).message || 'Invalid password'; - notificationStore.error('Authentication', message, { toast: true }); + const message = (error as Error).message || 'Failed to unwrap key'; + notificationStore.error('Authentication Form', message, { toast: true }); } } diff --git a/src/composables/useMessenger.ts b/src/composables/useMessenger.ts index 6cacc13..1b2803d 100644 --- a/src/composables/useMessenger.ts +++ b/src/composables/useMessenger.ts @@ -2,7 +2,7 @@ import { MessageType } from '~/bridge'; import type { PublicKeyResponse, ChallengeResponse } from '~/bridge'; /** - * Responds to a public key request with the user's public key and closes the popup window. + * Broadcasts public key response to background * * @param origin - the origin of the website that started the message * @param key - the public key of the user @@ -20,16 +20,16 @@ function broadcastPublicKeyResponse(key: string | null, error?: string) { } /** - * Responds to a challenge request with the solution to the challenge and closes the popup window. + * Broadcasts challenge response to background * - * @param solution - the solution to the challenge + * @param challengeResponse - challenge response * @param error - an error message */ -function broadcastChallengeResponse(solution: string | null, error?: string) { +function broadcastChallengeResponse(challengeResponse: string | null, error?: string) { // define message const message: ChallengeResponse = { - type: MessageType.RESPONSE_SOLUTION, - solution, + type: MessageType.RESPONSE_CHALLENGE_RESPONSE, + challengeResponse, description: error }; diff --git a/src/contentScripts/index.ts b/src/contentScripts/index.ts index 9186bfd..8949bad 100644 --- a/src/contentScripts/index.ts +++ b/src/contentScripts/index.ts @@ -28,12 +28,42 @@ import App from './App.vue'; const SESSION_DURATION = 1000 * 60 * 2; // 2 minute(s) /** - * The origin of the website that started a message. There can only be one - * website at a time that is requesting a public key to mitigate the risk - * of denial of service attacks. - */ + * The origin of the website that started a message. There can only be one + * website at a time that is requesting a public key to mitigate the risk + * of denial of service attacks. + */ let currentSession: BaseRequest | undefined = undefined; + let timeout: NodeJS.Timeout | undefined = undefined; + + /** + * Resets the current session and broadcasts message to background + * script to close the popup window. + */ + function resetSession(config?: { message?: string, error?: boolean }) { + // reset session + currentSession = undefined; + + // clear timer + if (timeout) { + clearTimeout(timeout); + + timeout = undefined; + } + + const errorResponse: ErrorResponse | undefined = config?.message && config?.error + ? { type: MessageType.RESPONSE_ERROR, error: config.message } + : undefined; + + // log that session ended successfully + if(!errorResponse) { + Logger.info(config?.message || 'Session completed.', null, 'content-script'); + } + + // broadcast to close popup + sendMessage('close-popup', errorResponse); + } + /** * Sets the current session and starts a timer to reset the session * after the maximum duration has passed. @@ -45,68 +75,37 @@ import App from './App.vue'; function setSession(request: BaseRequest) { currentSession = request; - Logger.info('starting session timer', null, 'content-script') + Logger.info(`starting session timer (${SESSION_DURATION} milliseconds)`, null, 'content-script') + // start a timer - setTimeout(async () => { - + timeout = setTimeout(async () => { Logger.info('resetting session timer', null, 'content-script') - resetSession(); - + resetSession({ message: 'Session timed out.', error: true }); }, SESSION_DURATION); } /** - * Resets the current session to undefined. - */ - function resetSession(message?: string) { - currentSession = undefined; - - closeSessionWindow(message); - } - - /** - * Closes the popup window that is requesting a user response. + * Listen for public key and challenge-response requests from websites. + * For every request, ask the user to approve/deny the request and + * return the response. */ - async function closeSessionWindow(message?: string){ - if(message) { - const error: ErrorResponse = { - type: MessageType.RESPONSE_ERROR, - error: message - }; - - sendMessage('close-request-tab', error); - } - - else { - sendMessage('close-request-tab', undefined); - } - } - - /** - * Listen for public key requests from websites. For every request, - * ask the user to approve/deny the request and return the response. - * - */ window.addEventListener('message', async (event: MessageEvent) => { const request: BaseRequest = { origin: event.origin, type: event.data.type }; + const isUserRequest: boolean = ( + request.type === MessageType.REQUEST_PUBLIC_KEY || + request.type === MessageType.REQUEST_CHALLENGE_RESPONSE + ); + try { - /** - * the request require user interaction - */ - const isUserRequest = ( - request.type === MessageType.REQUEST_PUBLIC_KEY || - request.type === MessageType.REQUEST_SOLUTION - ); if (isUserRequest && currentSession === undefined) { - setSession(request); - } - + } + else if (isUserRequest && currentSession !== undefined) { throw new Error( 'Another website is already requesting a response. Please try again later.' @@ -125,7 +124,7 @@ import App from './App.vue'; // listen for [public key request] from website if (request.type === MessageType.REQUEST_PUBLIC_KEY) { - Logger.info('SSASy Channel', 'public key request received', 'content-script') + Logger.info('Public key request received', null, 'content-script') const keyRequest: PublicKeyRequest = { origin: request.origin, @@ -143,25 +142,25 @@ import App from './App.vue'; } else { // send message to website window.postMessage(response, request.origin); - + // reset session - return resetSession(); + return resetSession({ message: 'Public key request session completed.' }); } } // listen for [challenge request] from website - if (request.type === MessageType.REQUEST_SOLUTION) { - Logger.info('SSASy Channel', 'solution request received', 'content-script') + if (request.type === MessageType.REQUEST_CHALLENGE_RESPONSE) { + Logger.info('Challenge-response request received', null, 'content-script') const challengeRequest: ChallengeRequest = { origin: request.origin, - type: MessageType.REQUEST_SOLUTION, + type: MessageType.REQUEST_CHALLENGE_RESPONSE, mode: event.data.mode, challenge: event.data.challenge }; const response: ChallengeResponse | ErrorResponse = await sendMessage( - MessageType.REQUEST_SOLUTION, + MessageType.REQUEST_CHALLENGE_RESPONSE, challengeRequest ); @@ -170,15 +169,15 @@ import App from './App.vue'; } else { // send message to website window.postMessage(response, request.origin); - + // reset session - return resetSession(); + return resetSession({ message: 'Challenge-response session completed.' }); } } } catch (error) { const errorMessage = (error as Error).message || `Failed to process request ${request.type}`; - Logger.error('SSASy Channel', errorMessage, 'content-script'); + Logger.error('Error', errorMessage, 'content-script'); // response to website const errorResponse: ErrorResponse = { @@ -190,7 +189,7 @@ import App from './App.vue'; window.postMessage(errorResponse, request.origin); // reset session - resetSession(); + resetSession({ message: errorMessage, error: true }); } }); diff --git a/src/pages/Auth.vue b/src/pages/Auth.vue index 2a00e39..5ef6771 100644 --- a/src/pages/Auth.vue +++ b/src/pages/Auth.vue @@ -25,7 +25,7 @@ async function setSession(privateKey: PrivateKey) { try { // set wallet - walletStore.setWallet(privateKey); + await walletStore.setWallet(privateKey); // extract public key const publicKey = await walletStore.getPublicKey(); @@ -36,7 +36,7 @@ async function setSession(privateKey: PrivateKey) { // kill the wallet walletStore.reset(); } catch (error) { - const message = (error as Error).message || 'Invalid password'; + const message = (error as Error).message || 'Failed to set session'; return notificationStore.error('Authentication', message, { toast: true }); } diff --git a/src/pages/Request.vue b/src/pages/Request.vue index 892ba20..8870105 100644 --- a/src/pages/Request.vue +++ b/src/pages/Request.vue @@ -1,4 +1,18 @@