Skip to content

Commit

Permalink
feat: leverage partitioned and unpartitioned storage
Browse files Browse the repository at this point in the history
  • Loading branch information
ditoglez committed Jul 4, 2024
1 parent af5ee5a commit a383ec6
Show file tree
Hide file tree
Showing 3 changed files with 78 additions and 38 deletions.
92 changes: 64 additions & 28 deletions apps/idos-enclave/src/lib/enclave.js
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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"),
Expand All @@ -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;
Expand Down Expand Up @@ -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"));
Expand All @@ -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 }));
Expand Down Expand Up @@ -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") || "[]",
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
23 changes: 13 additions & 10 deletions packages/idos-store/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,11 @@ interface PipeCodecArgs<T> {

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();
}

Expand All @@ -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;
Expand Down Expand Up @@ -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) {
Expand All @@ -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;
Expand All @@ -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);
}
}
}

0 comments on commit a383ec6

Please sign in to comment.