From f01069a392b69fed64a87361ea88179a6a38f584 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philipp=20Kr=C3=BCger?= Date: Fri, 10 Sep 2021 15:59:58 +0200 Subject: [PATCH] Improve login flow resilience against extensions (#284) * Add retrying to retrieving login secrets * Bump version, changelog * Detect malformed data earlier * Don't put errors in the console if these are warnings --- CHANGELOG.md | 5 +-- package.json | 2 +- src/common/version.ts | 2 +- src/index.ts | 90 +++++++++++++++++++++++++++++++++++-------- 4 files changed, 79 insertions(+), 20 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6a03ea8d2..937814165 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,10 +1,9 @@ # Changelog - ### v0.28.0 -Added the `fs.addPublicExchangeKey()` function which adds the public exchange key of that domain/browser/device to your filesystem at `/public/.well-known/exchange/DID_KEY`. Along with the `fs.hasPublicExchangeKey()` to check if it's there. - +- Added the `fs.addPublicExchangeKey()` function which adds the public exchange key of that domain/browser/device to your filesystem at `/public/.well-known/exchange/DID_KEY`. Along with the `fs.hasPublicExchangeKey()` to check if it's there. +- Made the login low more resilient. Should work better with extensions triggering `postMessage`s now. ### v0.27.0 diff --git a/package.json b/package.json index 26c4639e1..0ffa1ce74 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "webnative", - "version": "0.27.0", + "version": "0.27.1", "description": "Fission Webnative SDK", "keywords": [ "WebCrypto", diff --git a/src/common/version.ts b/src/common/version.ts index 03f3ccbd3..1bf4a4ca2 100644 --- a/src/common/version.ts +++ b/src/common/version.ts @@ -1 +1 @@ -export const VERSION = "0.27.0" +export const VERSION = "0.27.1" diff --git a/src/index.ts b/src/index.ts index 6018d2228..ac1ce7a0b 100644 --- a/src/index.ts +++ b/src/index.ts @@ -139,11 +139,11 @@ export async function initialise( const newUser = url.searchParams.get("newUser") === "t" const username = url.searchParams.get("username") || "" - await importClassifiedInfo( + await retry(async () => importClassifiedInfo( authorised === "via-postmessage" ? await getClassifiedViaPostMessage() - : await ipfs.cat(authorised) // in any other case we expect it to be a CID - ) + : JSON.parse(await ipfs.cat(authorised)) // in any other case we expect it to be a CID + ), { tries: 10, timeout: 10000, timeoutMessage: "Trying to retrieve UCAN(s) and readKey(s) from the auth lobby timed out after 10 seconds." }) await storage.setItem(USERNAME_STORAGE_KEY, username) @@ -335,15 +335,13 @@ interface AuthLobbyClassifiedInfo { async function importClassifiedInfo( - classified : string + classifiedInfo: AuthLobbyClassifiedInfo ): Promise { - const info: AuthLobbyClassifiedInfo = JSON.parse(classified) - // Extract session key and its iv - const rawSessionKey = await crypto.keystore.decrypt(info.sessionKey) + const rawSessionKey = await crypto.keystore.decrypt(classifiedInfo.sessionKey) // Decrypt secrets - const secretsStr = await crypto.aes.decryptGCM(info.secrets, rawSessionKey, info.iv) + const secretsStr = await crypto.aes.decryptGCM(classifiedInfo.secrets, rawSessionKey, classifiedInfo.iv) const secrets = JSON.parse(secretsStr) const fsSecrets: Record = secrets.fs @@ -365,7 +363,7 @@ async function importClassifiedInfo( await ucan.store(ucans) } -async function getClassifiedViaPostMessage(): Promise { +async function getClassifiedViaPostMessage(): Promise { const iframe: HTMLIFrameElement = await new Promise(resolve => { const iframe = document.createElement("iframe") iframe.id = "webnative-secret-exchange" @@ -384,16 +382,44 @@ async function getClassifiedViaPostMessage(): Promise { try { - const answer: Promise = new Promise((resolve, reject) => { + const answer: Promise = new Promise((resolve, reject) => { + let tries = 10 window.addEventListener("message", listen) + function retryOrReject(eventData?: string) { + console.warn(`When importing UCANs & readKey(s): Can't parse: ${eventData}. Might be due to extensions.`) + if (--tries === 0) { + window.removeEventListener("message", listen) + reject(new Error("Couldn't parse message from auth lobby after 10 tries. See warnings above.")) + } + } + function listen(event: MessageEvent) { - window.removeEventListener("message", listen) - if (event.data) { - resolve(event.data) - } else { - reject(new Error("Can't import UCANs & readKey(s): Missing data")) + if (new URL(event.origin).host !== new URL(setup.endpoints.lobby).host) { + console.log(`Got a message from ${event.origin} while waiting for login credentials. Ignoring.`) + return + } + + if (event.data == null) { + // Might be an extension sending a message without data + return + } + + let classifiedInfo: unknown = null + try { + classifiedInfo = JSON.parse(event.data) + } catch { + retryOrReject(event.data) + return } + + if (!isAuthLobbyClassifiedInfo(classifiedInfo)) { + retryOrReject(event.data) + return + } + + window.removeEventListener("message", listen) + resolve(classifiedInfo) } }) @@ -423,3 +449,37 @@ async function validateSecrets(permissions: Permissions): Promise { Promise.resolve(true) ) } + +async function retry(action: () => Promise, options: { tries: number; timeout: number; timeoutMessage: string }): Promise { + return await Promise.race([ + (async () => { + let tryNum = 1 + while (tryNum <= options.tries) { + try { + await action() + return + } catch (e) { + if (tryNum == options.tries) { + throw e + } + } + tryNum++ + } + })(), + new Promise((resolve, reject) => setTimeout(() => reject(new Error(options.timeoutMessage)), options.timeout)) + ]) +} + + +interface AuthLobbyClassifiedInfo { + sessionKey: string + secrets: string + iv: string +} + +function isAuthLobbyClassifiedInfo(obj: unknown): obj is AuthLobbyClassifiedInfo { + return common.isObject(obj) + && common.isString(obj.sessionKey) + && common.isString(obj.secrets) + && common.isString(obj.iv) +}