Files
WebDrop/web/src/core-bridge/worker/accountCrypto.ts
T

77 lines
5.6 KiB
TypeScript

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<string> {
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<unknown> {
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<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, []);
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<string> {
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<string> {
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;
}