From 8ffcf769052461e03a2c69220392cb37a623f3f4 Mon Sep 17 00:00:00 2001 From: Adam Soltys Date: Fri, 27 Feb 2026 14:46:27 -0800 Subject: 2 step register --- src/lib/deriveAuthKey.ts | 5 +--- src/lib/keyWrapping.ts | 61 ++++++++++++++++++++++++++++++++++++++++++++++++ src/lib/passkey.ts | 7 ++++-- src/lib/utils.ts | 2 ++ src/lib/walletEntropy.ts | 10 +++----- 5 files changed, 72 insertions(+), 13 deletions(-) create mode 100644 src/lib/keyWrapping.ts (limited to 'src/lib') diff --git a/src/lib/deriveAuthKey.ts b/src/lib/deriveAuthKey.ts index 9215b515..eb498e0a 100644 --- a/src/lib/deriveAuthKey.ts +++ b/src/lib/deriveAuthKey.ts @@ -1,7 +1,6 @@ import { scryptAsync } from "@noble/hashes/scrypt.js"; import { bytesToHex } from "@noble/hashes/utils.js"; import { getPublicKey } from "nostr-tools"; -import { rememberPrfKey, defaultRememberForMs } from "$lib/passwordCache"; export async function deriveAuthKeypair(username: string, password: string) { const salt = `coinos:auth:${username.toLowerCase().replace(/\s/g, "")}`; @@ -17,7 +16,5 @@ export async function deriveWalletEntropy(sk: Uint8Array): Promise const sigBytes = new Uint8Array(signed.sig.length / 2); for (let i = 0; i < signed.sig.length; i += 2) sigBytes[i / 2] = parseInt(signed.sig.slice(i, i + 2), 16); - const entropy = await crypto.subtle.digest("SHA-256", sigBytes); - rememberPrfKey(entropy, defaultRememberForMs); - return entropy; + return crypto.subtle.digest("SHA-256", sigBytes); } diff --git a/src/lib/keyWrapping.ts b/src/lib/keyWrapping.ts new file mode 100644 index 00000000..b176e741 --- /dev/null +++ b/src/lib/keyWrapping.ts @@ -0,0 +1,61 @@ +import { prfEncrypt, prfDecrypt } from "$lib/crypto"; +import { rememberPrfKey, defaultRememberForMs, getCachedPrfKey } from "$lib/passwordCache"; +import { post } from "$lib/utils"; + +export function generateCanonicalKey(): ArrayBuffer { + return crypto.getRandomValues(new Uint8Array(32)).buffer as ArrayBuffer; +} + +export async function wrapCanonicalKey( + wrappingKey: ArrayBuffer, + canonicalKey: ArrayBuffer, +): Promise { + return prfEncrypt(wrappingKey, new Uint8Array(canonicalKey)); +} + +export async function unwrapCanonicalKey( + wrappingKey: ArrayBuffer, + encrypted: string, +): Promise { + const bytes = await prfDecrypt(wrappingKey, encrypted); + return bytes.buffer as ArrayBuffer; +} + +export async function resolveCanonicalKey( + methodId: string, + wrappingKey: ArrayBuffer, + encryptedKeys: Record | null | undefined, + oldPrfKey: ArrayBuffer, +): Promise<{ canonicalKey: ArrayBuffer; encryptedKeys: Record }> { + if (encryptedKeys && encryptedKeys[methodId]) { + const canonicalKey = await unwrapCanonicalKey(wrappingKey, encryptedKeys[methodId]); + rememberPrfKey(canonicalKey, defaultRememberForMs); + return { canonicalKey, encryptedKeys }; + } + + // Migration: old PRF key becomes canonical key + const canonicalKey = oldPrfKey; + const wrapped = await wrapCanonicalKey(wrappingKey, canonicalKey); + const updated = { ...(encryptedKeys || {}), [methodId]: wrapped }; + await saveEncryptedKeys(updated); + rememberPrfKey(canonicalKey, defaultRememberForMs); + return { canonicalKey, encryptedKeys: updated }; +} + +export async function saveEncryptedKeys( + encryptedKeys: Record, +): Promise { + await post("/post/user", { encryptedKeys }); +} + +export function passwordMethodId(): string { + return "password"; +} + +export function passkeyMethodId(credentialId: string): string { + return `passkey:${credentialId}`; +} + +export function nostrMethodId(pubkey: string): string { + return `nostr:${pubkey}`; +} diff --git a/src/lib/passkey.ts b/src/lib/passkey.ts index 053eb06a..7c9e98cc 100644 --- a/src/lib/passkey.ts +++ b/src/lib/passkey.ts @@ -10,12 +10,15 @@ function authHeader(token: string) { return { authorization: `Bearer ${token}` }; } -export async function registerPasskey(token?: string): Promise { +export async function registerPasskey( + token?: string, +): Promise<{ prfKey: ArrayBuffer; credentialId: string }> { const headers = token ? authHeader(token) : undefined; const options = await post("/passkey/register/options", {}, headers); const registration = await startRegistration({ optionsJSON: options }); await post("/passkey/register/verify", registration, headers); - return deriveKey(registration.id); + const prfKey = await deriveKey(registration.id); + return { prfKey, credentialId: registration.id }; } export async function loginWithPasskey(): Promise<{ diff --git a/src/lib/utils.ts b/src/lib/utils.ts index eee6903e..e37556e3 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -214,6 +214,8 @@ export const login = async ( }); } catch {} } + + return u; }; export const auth = (cookies) => ({ diff --git a/src/lib/walletEntropy.ts b/src/lib/walletEntropy.ts index 25bd49b8..95b3e903 100644 --- a/src/lib/walletEntropy.ts +++ b/src/lib/walletEntropy.ts @@ -1,5 +1,5 @@ import { browser } from "$app/environment"; -import { getCachedPrfKey, rememberPrfKey, defaultRememberForMs } from "$lib/passwordCache"; +import { getCachedPrfKey } from "$lib/passwordCache"; const DERIVATION_EVENT = { kind: 1, @@ -27,9 +27,7 @@ export async function tryDeriveFromNsec(): Promise { const { nip19, finalizeEvent } = await import("nostr-tools"); const sk = nip19.decode(nsec).data as Uint8Array; const signed = finalizeEvent({ ...DERIVATION_EVENT } as any, sk); - const entropy = await hashSignature(signed.sig); - rememberPrfKey(entropy, defaultRememberForMs); - return entropy; + return await hashSignature(signed.sig); } catch (e) { console.log("nsec entropy derivation failed", e); return null; @@ -60,7 +58,5 @@ export async function deriveNostrEntropy(): Promise { const { sign } = await import("$lib/nostr"); const signed = await sign({ ...DERIVATION_EVENT }); - const entropy = await hashSignature(signed.sig); - rememberPrfKey(entropy, defaultRememberForMs); - return entropy; + return hashSignature(signed.sig); } -- cgit v1.2.3