From a383ec69832591c3940d3062b9599f8618cc0b35 Mon Sep 17 00:00:00 2001 From: Fernando Gonzalez Goncharov Date: Tue, 4 Jun 2024 19:36:43 +0300 Subject: [PATCH] feat: leverage partitioned and unpartitioned storage --- apps/idos-enclave/src/lib/enclave.js | 92 +++++++++++++------ .../lib/enclave-providers/iframe-enclave.ts | 1 + packages/idos-store/index.ts | 23 +++-- 3 files changed, 78 insertions(+), 38 deletions(-) diff --git a/apps/idos-enclave/src/lib/enclave.js b/apps/idos-enclave/src/lib/enclave.js index 38248e300..57688c4dd 100644 --- a/apps/idos-enclave/src/lib/enclave.js +++ b/apps/idos-enclave/src/lib/enclave.js @@ -6,16 +6,20 @@ import nacl from "tweetnacl"; import { idOSKeyDerivation } from "./idOSKeyDerivation"; export class Enclave { + unpartitionedStore; + constructor({ parentOrigin }) { this.parentOrigin = parentOrigin; this.store = new Store(); - this.authorizedOrigins = JSON.parse(this.store.get("enclave-authorized-origins") ?? "[]"); + this.authorizedOrigins = []; this.unlockButton = document.querySelector("button#unlock"); this.confirmButton = document.querySelector("button#confirm"); + this.store = new Store(); const storeWithCodec = this.store.pipeCodec(Base64Codec); const secretKey = storeWithCodec.get("encryption-private-key"); + if (secretKey) this.keyPair = nacl.box.keyPair.fromSecretKey(secretKey); this.#listenToRequests(); @@ -29,22 +33,30 @@ export class Enclave { this.store.reset(); } - storage(humanId, signerAddress, signerPublicKey) { + async storage(humanId, signerAddress, signerPublicKey) { + const permission = await navigator.permissions.query({ + name: "storage-access", + }); + + if (permission.state === "granted") { + if (!this.unpartitionedStore) await this.#initUnpartitionedStore(); + + if (!this.isAuthorizedOrigin) { + return { + humanId: "", + encryptionPublicKey: "", + signerAddress: "", + signerPublicKey: "", + }; + } + } + humanId && this.store.set("human-id", humanId); signerAddress && this.store.set("signer-address", signerAddress); signerPublicKey && this.store.set("signer-public-key", signerPublicKey); const storeWithCodec = this.store.pipeCodec(Base64Codec); - if (!this.isAuthorizedOrigin) { - return { - humanId: "", - encryptionPublicKey: "", - signerAddress: "", - signerPublicKey: "", - }; - } - return { humanId: this.store.get("human-id"), encryptionPublicKey: storeWithCodec.get("encryption-public-key"), @@ -60,15 +72,18 @@ export class Enclave { return this.keyPair?.publicKey; } - async authWithPassword() { - const { password, duration } = await this.#openDialog("password"); - this.store.set("password", password); - this.store.setRememberDuration(duration); - return { password, duration }; - } - async ensurePassword() { - if (this.isAuthorizedOrigin && this.store.get("password")) return Promise.resolve; + const permission = await navigator.permissions.query({ + name: "storage-access", + }); + + if (permission.state !== "denied") { + if (!this.unpartitionedStore) await this.#initUnpartitionedStore(); + + const password = this.unpartitionedStore.get("password"); + + if (password && this.isAuthorizedOrigin) return Promise.resolve; + } this.unlockButton.style.display = "block"; this.unlockButton.disabled = false; @@ -99,15 +114,19 @@ export class Enclave { return new Promise((resolve, reject) => this.unlockButton.addEventListener("click", async () => { + if (!this.unpartitionedStore) await this.#initUnpartitionedStore(); + + if (this.unpartitionedStore.get("password") && this.isAuthorizedOrigin) return resolve(); + this.unlockButton.disabled = true; - const storedCredentialId = this.store.get("credential-id"); + const storedCredentialId = this.unpartitionedStore.get("credential-id"); const preferredAuthMethod = this.store.get("preferred-auth-method"); try { if (storedCredentialId) { ({ password, credentialId } = await getWebAuthnCredential(storedCredentialId)); - } else if (!!preferredAuthMethod) { + } else if (preferredAuthMethod) { ({ password, duration } = await this.#openDialog(preferredAuthMethod)); } else { ({ password, duration, credentialId } = await this.#openDialog("auth")); @@ -116,29 +135,37 @@ export class Enclave { return reject(e); } - this.store.set("password", password); + this.unpartitionedStore.set("password", password); this.authorizedOrigins = [...new Set([...this.authorizedOrigins, this.parentOrigin])]; - this.store.set("enclave-authorized-origins", JSON.stringify(this.authorizedOrigins)); + + this.unpartitionedStore.set( + "enclave-authorized-origins", + JSON.stringify(this.authorizedOrigins), + ); if (credentialId) { - this.store.set("credential-id", credentialId); + this.unpartitionedStore.set("credential-id", credentialId); this.store.set("preferred-auth-method", "passkey"); } else { - this.store.set("preferred-auth-method", "password"); + this.unpartitionedStore.set("preferred-auth-method", "password"); this.store.setRememberDuration(duration); } - return password ? resolve() : reject(); + if (!password) return reject(); + + return resolve(); }), ); } async ensureKeyPair() { - const password = this.store.get("password"); + if (!this.unpartitionedStore) await this.#initUnpartitionedStore(); + + const password = this.unpartitionedStore.get("password"); const salt = this.store.get("human-id"); - const storeWithCodec = this.store.pipeCodec(Base64Codec); + const storeWithCodec = this.unpartitionedStore.pipeCodec(Base64Codec); const secretKey = storeWithCodec.get("encryption-private-key") || (await idOSKeyDerivation({ password, salt })); @@ -315,4 +342,13 @@ export class Enclave { ); }); } + + async #initUnpartitionedStore() { + const handle = await document.requestStorageAccess({ localStorage: true }); + this.unpartitionedStore = new Store(handle.localStorage); + + this.authorizedOrigins = JSON.parse( + this.unpartitionedStore.get("enclave-authorized-origins") || "[]", + ); + } } diff --git a/packages/idos-sdk-js/src/lib/enclave-providers/iframe-enclave.ts b/packages/idos-sdk-js/src/lib/enclave-providers/iframe-enclave.ts index c9e26d640..0a1967069 100644 --- a/packages/idos-sdk-js/src/lib/enclave-providers/iframe-enclave.ts +++ b/packages/idos-sdk-js/src/lib/enclave-providers/iframe-enclave.ts @@ -87,6 +87,7 @@ export class IframeEnclave implements EnclaveProvider { "popups-to-escape-sandbox", "same-origin", "scripts", + "storage-access-by-user-activation", ].map((toLift) => `allow-${toLift}`); // https://developer.mozilla.org/en-US/docs/Web/HTML/Element/iframe#referrerpolicy diff --git a/packages/idos-store/index.ts b/packages/idos-store/index.ts index 6462676f6..75d850509 100644 --- a/packages/idos-store/index.ts +++ b/packages/idos-store/index.ts @@ -5,10 +5,11 @@ interface PipeCodecArgs { export class Store { keyPrefix = "idOS-"; - + device: Storage; readonly REMEMBER_DURATION_KEY = "storage-expiration"; - constructor() { + constructor(device = window.localStorage) { + this.device = device; if (this.hasRememberDurationElapsed()) this.reset(); } @@ -19,11 +20,12 @@ export class Store { const result = this.get(key); if (result) return decode(result); }, - set: (key: string, value: any, days: string | number) => - this.set.call(this, key, encode(value), days), + // biome-ignore lint/suspicious/noExplicitAny: This is fine. We want to allow any value. + set: (key: string, value: any) => this.set.call(this, key, encode(value)), }; } + // biome-ignore lint/suspicious/noExplicitAny: We are fine with `any` here. get(key: string): any { const value = this.#getLocalStorage(key); if (!value) return undefined; @@ -52,8 +54,8 @@ export class Store { // If the value doesn't decode right, we're going to assume that somebody messed around with it. // The absence of a value means `false` today. So, we're following suit on the reasoning: consider it absent. // Furthermore, since this is not really a recoverable situation, we're going to clean up that stored value. + let str: string; - let str; try { str = JSON.parse(value); } catch (error) { @@ -70,6 +72,7 @@ export class Store { return expires < Date.now(); } + // biome-ignore lint/suspicious/noExplicitAny: We are fine with `any` here. set(key: string, value: any) { if (!key || typeof key !== "string") throw new Error(`Bad key: ${key}`); if (!value) return; @@ -78,21 +81,21 @@ export class Store { } #getLocalStorage(key: string) { - return window.localStorage.getItem(`${this.keyPrefix}${key}`); + return this.device.getItem(`${this.keyPrefix}${key}`); } #setLocalStorage(key: string, value: string) { - return window.localStorage.setItem(`${this.keyPrefix}${key}`, value); + return this.device.setItem(`${this.keyPrefix}${key}`, value); } #removeLocalStorage(key: string) { - return window.localStorage.removeItem(`${this.keyPrefix}${key}`); + return this.device.removeItem(`${this.keyPrefix}${key}`); } reset() { - for (const key of Object.keys(window.localStorage)) { + for (const key of Object.keys(this.device)) { if (key === "idOS-credential-id") continue; - if (key.startsWith(this.keyPrefix)) window.localStorage.removeItem(key); + if (key.startsWith(this.keyPrefix)) this.device.removeItem(key); } } }