const encoder = new TextEncoder(); const vaultLabel = 'webdrop-account-vault-v1'; const invitationLabel = 'webdrop-invitation-v2'; export function bytes(value: Uint8Array): ArrayBuffer { return Uint8Array.from(value).buffer; } export function b64(value: Uint8Array): string { let text = ''; for (let i = 0; i < value.length; i += 32768) text += String.fromCharCode(...value.subarray(i, i + 32768)); 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, 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, ['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 { const nonce = crypto.getRandomValues(new Uint8Array(12)); const cipher = new Uint8Array(await crypto.subtle.encrypt({ name: 'AES-GCM', iv: nonce, additionalData: encoder.encode(context) }, key, encoder.encode(JSON.stringify(value)))); const result = new Uint8Array(nonce.length + cipher.length); result.set(nonce); result.set(cipher, 12); if (result.length > 1_300_000) throw new Error('账号保险库已达到大小上限'); return b64(result); } export async function openJson(value: string, key: CryptoKey, context = vaultLabel): Promise { const input = unb64(value); if (input.length < 28 || input.length > 1_300_000) throw new Error('加密数据格式无效'); try { const plain = await crypto.subtle.decrypt({ name: 'AES-GCM', iv: bytes(input.subarray(0, 12)), additionalData: encoder.encode(context) }, key, bytes(input.subarray(12))); return JSON.parse(new TextDecoder('utf-8', { fatal: true }).decode(plain)); } catch { throw new Error('加密数据校验失败,已拒绝导入'); } } 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 { 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 { 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 { 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, []); const shared = new Uint8Array(await crypto.subtle.deriveBits({ name: 'ECDH', public: peer }, own, 256)); try { const root = await crypto.subtle.importKey('raw', shared, 'HKDF', false, ['deriveKey']); return await crypto.subtle.deriveKey({ name: 'HKDF', hash: 'SHA-256', salt: encoder.encode(context), info: encoder.encode(invitationLabel) }, root, { name: 'AES-GCM', length: 256 }, false, ['encrypt', 'decrypt']); } finally { shared.fill(0); } } export async function encryptInvitation(link: string, recipient: string, channelId: string, publicKey: JsonWebKey): Promise { const pair = await generateInvitationKeys(); const context = `${invitationLabel}:${recipient}:${channelId}`; const key = await invitationKey(pair.privateKey, publicKey, context); return JSON.stringify({ v: 2, publicKey: pair.publicKey, cipher: await sealJson({ link }, key, context) }); } export async function decryptInvitation(payload: string, recipient: string, channelId: string, privateKey: JsonWebKey): Promise { const envelope = JSON.parse(payload) as { v: number; publicKey: JsonWebKey; cipher: string }; if (envelope.v !== 2 || typeof envelope.cipher !== 'string') throw new Error('邀请格式无效'); const context = `${invitationLabel}:${recipient}:${channelId}`; const key = await invitationKey(privateKey, envelope.publicKey, context); const value = await openJson(envelope.cipher, key, context) as { link?: unknown }; if (typeof value.link !== 'string') throw new Error('邀请格式无效'); return value.link; }