import { etc as secpEtc, getPublicKey, sign, Signature, utils as secpUtils, verify, } from '@noble/secp256k1'; import { hmac } from '@noble/hashes/hmac'; import { sha256 } from '@noble/hashes/sha256'; import { bytesToHex, hexToBytes } from '@noble/hashes/utils'; /** secp256k1 curve order (number of points on the curve). */ const SECP256K1_ORDER = 0xfffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141n; // Require hmacSha256Sync for secp256k1 sign() (RFC6979). Set once at module load. if (secpEtc.hmacSha256Sync === undefined) { secpEtc.hmacSha256Sync = (k: Uint8Array, ...m: Uint8Array[]): Uint8Array => hmac(sha256, k, secpEtc.concatBytes(...m)); } export interface KeyPair { privateKey: string; publicKey: string; } /** * Generates a new secp256k1 key pair for authentication. * The private key is kept secret, the public key is used for verification. */ export function generateKeyPair(): KeyPair { const privateKey = secpUtils.randomPrivateKey(); const publicKey = getPublicKey(privateKey, true); return { privateKey: bytesToHex(privateKey), publicKey: bytesToHex(publicKey), }; } /** * Derives the secp256k1 public key (compressed, hex) from a raw hex private key. * Input must be 64 hex characters (32 bytes). Throws if invalid. */ export function publicKeyFromPrivateKey(privateKeyHex: string): string { const key = hexToBytes(privateKeyHex); const pub = getPublicKey(key, true); return bytesToHex(pub); } const DERIVE_DOMAIN = 'userwallet-derive-v1'; /** * Derives a child key pair deterministically from a parent private key. * Index must be >= 0. Same index always yields the same key pair. * Uses HMAC-SHA256(parentKey, domain || index) then reduces to valid secp256k1 scalar. */ export function deriveChildKeyPair( parentPrivateKeyHex: string, index: number, ): KeyPair { const parentBytes = hexToBytes(parentPrivateKeyHex); const indexBytes = new TextEncoder().encode(`${DERIVE_DOMAIN}-${index}`); const h = hmac(sha256, parentBytes, indexBytes); const num = BigInt('0x' + bytesToHex(h)); const scalar = (num % (SECP256K1_ORDER - 1n)) + 1n; const privHex = scalar.toString(16).padStart(64, '0'); const privateKeyBytes = hexToBytes(privHex); const publicKey = bytesToHex(getPublicKey(privateKeyBytes, true)); return { privateKey: privHex, publicKey }; } /** * Returns the main public key plus derived public keys for indices 0..count-1. * Index 0 is the first derived key, etc. Total length is 1 + count. */ export function getDerivedPublicKeys( parentPrivateKeyHex: string, count: number, ): string[] { const main = publicKeyFromPrivateKey(parentPrivateKeyHex); const out: string[] = [main]; for (let i = 0; i < count; i++) { const { publicKey } = deriveChildKeyPair(parentPrivateKeyHex, i); out.push(publicKey); } return out; } /** * Returns true if the given public key is the main key or any derived key (indices 0..maxDerived-1). * Fast: main key check O(1), then at most maxDerived derivations. Pass maxDerived to limit search. */ export function publicKeyBelongsToIdentity( identityPrivateKeyHex: string, publicKeyHex: string, maxDerived: number = 0, ): boolean { const main = publicKeyFromPrivateKey(identityPrivateKeyHex); if (main === publicKeyHex) { return true; } for (let i = 0; i < maxDerived; i++) { const { publicKey } = deriveChildKeyPair(identityPrivateKeyHex, i); if (publicKey === publicKeyHex) { return true; } } return false; } /** * Signs a message with a private key using secp256k1. * The message is hashed with SHA-256 before signing. */ export function signMessage(message: string, privateKeyHex: string): string { const messageHash = sha256(message); const privateKey = hexToBytes(privateKeyHex); const sig = sign(messageHash, privateKey); return bytesToHex(sig.toCompactRawBytes()); } /** * Verifies a signature against a message and public key. * Returns false if verification fails or if any error occurs. */ export function verifySignature( message: string, signatureHex: string, publicKeyHex: string, ): boolean { try { const messageHash = sha256(message); const sig = Signature.fromCompact(hexToBytes(signatureHex)); const pub = hexToBytes(publicKeyHex); return verify(sig, messageHash, pub); } catch (error) { console.error('Signature verification error:', error); return false; } } export function generateChallenge(): string { const timestamp = Date.now(); const random = crypto.getRandomValues(new Uint8Array(16)); return `auth-challenge-${timestamp}-${bytesToHex(random)}`; }