import crypto from 'node:crypto' /** * Port TS de home/bnet.py (autenticación 3.4.3 / Battle.net). * Debe producir EXACTAMENTE el mismo salt/verifier que la implementación Python * (validado con vectores idénticos) o los logins contra AzerothCore fallan. * * - battlenet_accounts -> SRP6 v2 (PBKDF2-HMAC-SHA512, N de 2048 bits, g=2). * - account (cuenta de juego) -> SRP6 Grunt/SHA1 (clásico 3.3.5a). */ // --- SRP6 v2 (battlenet_accounts) --- const BNET_N = BigInt( '0x' + 'AC6BDB41324A9A9BF166DE5E1389582FAF72B6651987EE07FC3192943DB56050' + 'A37329CBB4A099ED8193E0757767A13DD52312AB4B03310DCD7F48A9DA04FD50' + 'E8083969EDB767B0CF6095179A163AB3661A05FBD5FAAAE82918A9962F0B93B8' + '55F97993EC975EEAA80D740ADBF4FF747359D041D5C33EA71D281E446B14773B' + 'CA97B43A23FB801676BD207A436C6481F1D2B9078717461A5B9D32E688F87748' + '544523B524B0D57D5EA77A2775D2ECFA032CFBDBF52FB3786160279004E57AE6' + 'AF874E7303CE53299CCC041C7BC308D82A5698F3A8D0C38271AE35F8E9DBFBB6' + '94B5C803D89F7AE435DE236D525F54759B65E372FCD68EF20FA7111F9E4AFF73', ) const BNET_g = 2n const BNET_SALT_LEN = 32 const BNET_ITERATIONS = 15000 const BNET_SRP_VERSION = 2 const TWO_POW_512 = 1n << 512n // TrinityCore guarda el verifier little-endian (BigNumber::ToByteVector). const BNET_VERIFIER_LITTLE_ENDIAN = true // --- SRP6 Grunt/SHA1 (cuenta de juego) --- const GRUNT_g = 7n const GRUNT_N = BigInt('0x894B645E89E1535BBDAD5B8B290650530801B18EBFBF5E8FAB3C82872A3E9BB7') const GRUNT_SALT_LEN = 32 // --- utilidades BigInt <-> bytes --- function modpow(base: bigint, exp: bigint, mod: bigint): bigint { let result = 1n base %= mod while (exp > 0n) { if (exp & 1n) result = (result * base) % mod exp >>= 1n base = (base * base) % mod } return result } /** Módulo no negativo (como el `%` de Python). */ function pyMod(a: bigint, m: bigint): bigint { return ((a % m) + m) % m } function bytesToBigInt(buf: Buffer, littleEndian = false): bigint { const b = littleEndian ? Buffer.from(buf).reverse() : buf const hex = b.toString('hex') return hex.length ? BigInt('0x' + hex) : 0n } /** Emula int.to_bytes: longitud mínima (o fija si se da length), endianness dada. */ function bigIntToBytes(value: bigint, littleEndian = false, length?: number): Buffer { let hex = value.toString(16) if (hex.length % 2) hex = '0' + hex let buf = Buffer.from(hex, 'hex') // big-endian, longitud mínima if (buf.length === 0) buf = Buffer.from([0]) if (length && buf.length < length) { buf = Buffer.concat([Buffer.alloc(length - buf.length), buf]) // pad izquierda (BE) } return littleEndian ? Buffer.from(buf).reverse() : buf } // --- SRP6 v2 (bnet) --- export function bnetSrpUsername(email: string): string { return crypto.createHash('sha256').update(email.trim().toUpperCase(), 'utf8').digest('hex').toUpperCase() } function bnetCalculateX(email: string, password: string, salt: Buffer): bigint { const srpUser = bnetSrpUsername(email) const tmp = Buffer.from(srpUser + ':' + password, 'utf8') const xBytes = crypto.pbkdf2Sync(tmp, salt, BNET_ITERATIONS, 64, 'sha512') let x = bytesToBigInt(xBytes, false) // big-endian if (xBytes[0] & 0x80) x -= TWO_POW_512 return pyMod(x, BNET_N - 1n) } export interface BnetRegistration { salt: Buffer verifier: Buffer srpVersion: number } export function bnetMakeRegistration(email: string, password: string): BnetRegistration { const salt = crypto.randomBytes(BNET_SALT_LEN) const x = bnetCalculateX(email, password, salt) const v = modpow(BNET_g, x, BNET_N) return { salt, verifier: bigIntToBytes(v, BNET_VERIFIER_LITTLE_ENDIAN), srpVersion: BNET_SRP_VERSION } } export function bnetVerify( email: string, password: string, salt: Buffer | null, storedVerifier: Buffer | null, ): boolean { if (!salt || !storedVerifier) return false const x = bnetCalculateX(email, password, salt) const v = modpow(BNET_g, x, BNET_N) return v === bytesToBigInt(storedVerifier, BNET_VERIFIER_LITTLE_ENDIAN) } // --- SRP6 Grunt/SHA1 (cuenta de juego) --- function sha1(data: Buffer): Buffer { return crypto.createHash('sha1').update(data).digest() } export function gameCalculateVerifier(username: string, password: string, salt: Buffer): Buffer { const h1 = sha1(Buffer.from(username.toUpperCase() + ':' + password.toUpperCase(), 'utf8')) const h2 = sha1(Buffer.concat([salt, h1])) const h2int = bytesToBigInt(h2, true) // little-endian const v = modpow(GRUNT_g, h2int, GRUNT_N) return bigIntToBytes(v, true, 32) } export function gameMakeRegistration(username: string, password: string): { salt: Buffer; verifier: Buffer } { const salt = crypto.randomBytes(GRUNT_SALT_LEN) const verifier = gameCalculateVerifier(username, password.slice(0, 16), salt) return { salt, verifier } } export function gameVerify( username: string, password: string, salt: Buffer | null, storedVerifier: Buffer | null, ): boolean { if (!salt || !storedVerifier) return false const calc = gameCalculateVerifier(username, password.slice(0, 16), salt) return bytesToBigInt(calc, true) === bytesToBigInt(storedVerifier, true) } // --- helpers varios --- export function normalizeEmail(email: string): string { return email.trim().toUpperCase() } export function makeGameAccountUsername(bnetId: number, index = 1): string { return `${bnetId}#${index}` }