feat(web): add account workspaces and signed content flows

This commit is contained in:
justme committed 2026-10-14 04:06:03 +08:00
1 parent 7b8d61d1ba
commit b5cf3d5958
117 files changed
+2789 -1674

No files matched your search

+15 -6
View File
@@ -8,17 +8,19 @@ export function b64(value: Uint8Array): string {
return btoa(text);
}
export function unb64(value: string): Uint8Array { return Uint8Array.from(atob(value), (c) => c.charCodeAt(0)); }
export async function deriveAccountKeys(password: string, salt: Uint8Array): Promise<{ verifier: Uint8Array; key: CryptoKey }> {
export async function deriveAccountKeys(password: string, salt: Uint8Array, exportMaterial = false): Promise<{ verifier: Uint8Array; key: CryptoKey; material?: Uint8Array }> {
if (salt.length !== 16 || password.length > 1024) throw new Error('账号凭据格式无效');
const base = await crypto.subtle.importKey('raw', encoder.encode(password), 'PBKDF2', false, ['deriveBits']);
const root = new Uint8Array(await crypto.subtle.deriveBits({ name: 'PBKDF2', hash: 'SHA-256', salt: bytes(salt), iterations: 600_000 }, base, 256));
try {
const hmac = await crypto.subtle.importKey('raw', root, { name: 'HMAC', hash: 'SHA-256' }, false, ['sign']);
const verifier = new Uint8Array(await crypto.subtle.sign('HMAC', hmac, encoder.encode('webdrop-account-login-v1')));
const hkdf = await crypto.subtle.importKey('raw', root, 'HKDF', false, ['deriveKey']);
const key = await crypto.subtle.deriveKey({ name: 'HKDF', hash: 'SHA-256', salt: bytes(salt), info: encoder.encode(vaultLabel) }, hkdf,
{ name: 'AES-GCM', length: 256 }, false, ['encrypt', 'decrypt']);
return { verifier, key };
const hkdf = await crypto.subtle.importKey('raw', root, 'HKDF', false, ['deriveBits']);
const material = new Uint8Array(await crypto.subtle.deriveBits({ name: 'HKDF', hash: 'SHA-256', salt: bytes(salt), info: encoder.encode(vaultLabel) }, hkdf, 256));
try {
const key = await crypto.subtle.importKey('raw', bytes(material), 'AES-GCM', false, ['encrypt', 'decrypt']);
return { verifier, key, ...(exportMaterial ? { material: Uint8Array.from(material) } : {}) };
} finally { material.fill(0); }
} finally { root.fill(0); }
}
export async function sealJson(value: unknown, key: CryptoKey, context = vaultLabel): Promise<string> {
@@ -36,11 +38,18 @@ export async function openJson(value: string, key: CryptoKey, context = vaultLab
return JSON.parse(new TextDecoder('utf-8', { fatal: true }).decode(plain));
} catch { throw new Error('加密数据校验失败,已拒绝导入'); }
}
export interface InvitationKeyPair { publicKey: JsonWebKey; privateKey: JsonWebKey }
export type { InvitationKeyPair } from '../protocol/account';
import type { InvitationKeyPair } from '../protocol/account';
import type { SigningKeyPair } from '../protocol/account';
import { generateSigningKeyPair } from '@webdrop/core';
export async function generateInvitationKeys(): Promise<InvitationKeyPair> {
const pair = await crypto.subtle.generateKey({ name: 'ECDH', namedCurve: 'P-256' }, true, ['deriveBits']);
return { publicKey: await crypto.subtle.exportKey('jwk', pair.publicKey), privateKey: await crypto.subtle.exportKey('jwk', pair.privateKey) };
}
export async function generateAccountSigningKeys(): Promise<SigningKeyPair> {
const pair = await generateSigningKeyPair(true);
return { publicKey: await crypto.subtle.exportKey('jwk', pair.publicKey), privateKey: await crypto.subtle.exportKey('jwk', pair.privateKey) };
}
async function invitationKey(privateKey: JsonWebKey, publicKey: JsonWebKey, context: string): Promise<CryptoKey> {
const own = await crypto.subtle.importKey('jwk', privateKey, { name: 'ECDH', namedCurve: 'P-256' }, false, ['deriveBits']);
const peer = await crypto.subtle.importKey('jwk', publicKey, { name: 'ECDH', namedCurve: 'P-256' }, false, []);