Skip to content

Commit

Permalink
Improve login flow resilience against extensions (#284)
Browse files Browse the repository at this point in the history
* Add retrying to retrieving login secrets

* Bump version, changelog

* Detect malformed data earlier

* Don't put errors in the console if these are warnings
  • Loading branch information
matheus23 committed Sep 10, 2021
1 parent ed5ee65 commit f01069a
Show file tree
Hide file tree
Showing 4 changed files with 79 additions and 20 deletions.
5 changes: 2 additions & 3 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "webnative",
"version": "0.27.0",
"version": "0.27.1",
"description": "Fission Webnative SDK",
"keywords": [
"WebCrypto",
Expand Down
2 changes: 1 addition & 1 deletion src/common/version.ts
Original file line number Diff line number Diff line change
@@ -1 +1 @@
export const VERSION = "0.27.0"
export const VERSION = "0.27.1"
90 changes: 75 additions & 15 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down Expand Up @@ -335,15 +335,13 @@ interface AuthLobbyClassifiedInfo {


async function importClassifiedInfo(
classified : string
classifiedInfo: AuthLobbyClassifiedInfo
): Promise<void> {
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<string, { key: string; bareNameFilter: string }> = secrets.fs
Expand All @@ -365,7 +363,7 @@ async function importClassifiedInfo(
await ucan.store(ucans)
}

async function getClassifiedViaPostMessage(): Promise<string> {
async function getClassifiedViaPostMessage(): Promise<AuthLobbyClassifiedInfo> {
const iframe: HTMLIFrameElement = await new Promise(resolve => {
const iframe = document.createElement("iframe")
iframe.id = "webnative-secret-exchange"
Expand All @@ -384,16 +382,44 @@ async function getClassifiedViaPostMessage(): Promise<string> {

try {

const answer: Promise<string> = new Promise((resolve, reject) => {
const answer: Promise<AuthLobbyClassifiedInfo> = 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<string>) {
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)
}
})

Expand Down Expand Up @@ -423,3 +449,37 @@ async function validateSecrets(permissions: Permissions): Promise<boolean> {
Promise.resolve(true)
)
}

async function retry(action: () => Promise<void>, options: { tries: number; timeout: number; timeoutMessage: string }): Promise<void> {
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<void>((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)
}

0 comments on commit f01069a

Please sign in to comment.