import { createHash, generateKeyPairSync, type KeyObject, randomBytes, sign } from "node:crypto"; // 测试用软件 WebAuthn 认证器(ES256,attestation=none):生成 @simplewebauthn/browser 形状的 // RegistrationResponseJSON / AuthenticationResponseJSON,让服务端走真实的 @simplewebauthn/server 校验。 type Cbor = number | string | Uint8Array | Map | Record; function head(major: number, n: number): number[] { if (n < 24) return [(major << 5) | n]; if (n < 0x100) return [(major << 5) | 24, n]; if (n < 0x10000) return [(major << 5) | 25, n >> 8, n & 0xff]; return [(major << 5) | 26, (n >>> 24) & 0xff, (n >>> 16) & 0xff, (n >>> 8) & 0xff, n & 0xff]; } /** 极简 CBOR 编码:整数、字节串、文本、map(够编 COSE 公钥与 attestationObject) */ export function cbor(v: Cbor): Uint8Array { const out: number[] = []; const enc = (x: Cbor) => { if (typeof x === "number") out.push(...(x >= 0 ? head(0, x) : head(1, -1 - x))); else if (typeof x === "string") { const b = new TextEncoder().encode(x); out.push(...head(3, b.length), ...b); } else if (x instanceof Uint8Array) out.push(...head(2, x.length), ...x); else if (x instanceof Map) { out.push(...head(5, x.size)); for (const [k, val] of x) { enc(k); enc(val); } } else { const entries = Object.entries(x); out.push(...head(5, entries.length)); for (const [k, val] of entries) { enc(k); enc(val as Cbor); } } }; enc(v); return new Uint8Array(out); } const b64u = (b: Uint8Array | Buffer) => Buffer.from(b).toString("base64url"); const sha256 = (b: Uint8Array | string) => createHash("sha256").update(b).digest(); const u32 = (n: number) => { const b = Buffer.alloc(4); b.writeUInt32BE(n); return b; }; export class SoftAuthenticator { readonly credentialId = randomBytes(16); private readonly privateKey: KeyObject; private readonly cose: Uint8Array; counter = 0; constructor( readonly rpId = "localhost", readonly origin = "http://localhost:5197", ) { const { privateKey, publicKey } = generateKeyPairSync("ec", { namedCurve: "P-256" }); this.privateKey = privateKey; const jwk = publicKey.export({ format: "jwk" }) as { x: string; y: string }; this.cose = cbor( new Map([ [1, 2], [3, -7], [-1, 1], [-2, Buffer.from(jwk.x, "base64url")], [-3, Buffer.from(jwk.y, "base64url")], ]), ); } get id() { return b64u(this.credentialId); } private clientData(type: string, challenge: string, origin: string) { return Buffer.from(JSON.stringify({ type, challenge, origin, crossOrigin: false })); } /** navigator.credentials.create 的等价物 */ register(options: { challenge: string }, over: { origin?: string } = {}) { const clientDataJSON = this.clientData("webauthn.create", options.challenge, over.origin ?? this.origin); const idLen = Buffer.alloc(2); idLen.writeUInt16BE(this.credentialId.length); const authData = Buffer.concat([ sha256(this.rpId), Buffer.from([0x45]), // UP | UV | AT u32(this.counter), Buffer.alloc(16), // aaguid idLen, this.credentialId, this.cose, ]); const attestationObject = cbor({ fmt: "none", attStmt: {}, authData: new Uint8Array(authData) }); return { id: this.id, rawId: this.id, type: "public-key", response: { clientDataJSON: b64u(clientDataJSON), attestationObject: b64u(attestationObject), transports: ["internal"], }, clientExtensionResults: {}, authenticatorAttachment: "platform", }; } /** navigator.credentials.get 的等价物;counter 自增(可传 counter 模拟克隆 / 回退) */ assert(options: { challenge: string }, over: { origin?: string; counter?: number } = {}) { this.counter = over.counter ?? this.counter + 1; const clientDataJSON = this.clientData("webauthn.get", options.challenge, over.origin ?? this.origin); const authData = Buffer.concat([sha256(this.rpId), Buffer.from([0x05]), u32(this.counter)]); const signature = sign("sha256", Buffer.concat([authData, sha256(clientDataJSON)]), this.privateKey); return { id: this.id, rawId: this.id, type: "public-key", response: { clientDataJSON: b64u(clientDataJSON), authenticatorData: b64u(authData), signature: b64u(signature), }, clientExtensionResults: {}, authenticatorAttachment: "platform", }; } }