4a7a970206
Se admite cualquier dominio de correo. Quedaba en dos validaciones (registro y cambio de correo) y en tres textos: - Register.email «Correo electrónico de Gmail» -> «Correo electrónico» - Register.emailRule «debe pertenecer a Gmail y con acceso al mismo» -> «debe ser válido y tener acceso al mismo» (el correo de activación sigue siendo real, así que se conserva el «con acceso») - ChangeEmail.info se cae la frase «El nuevo correo debe ser de Gmail» `GMAIL_RE` se queda sin uso y desaparece: ahora `EMAIL_RE` es la ÚNICA validación de correo del sitio (registro, cambio, recuperación y login), así que no pueden volver a divergir como pasaba (recuperar exigía Gmail y dejaba fuera a 16 de las 17 cuentas existentes). Verificado: pasan inna@inna.cl, hotmail, outlook, proton, nightspire.gg y dominios con doble punto; siguen fuera 15#1, textos sin @, dobles arrobas y espacios. En la /es/create-account que sirve producción ya no aparece «Gmail». Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
193 lines
7.2 KiB
TypeScript
193 lines
7.2 KiB
TypeScript
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}`
|
|
}
|
|
|
|
/**
|
|
* Inverso de `makeGameAccountUsername` para enseñárselo al usuario: «15#1» → «WOW1».
|
|
* El número tras # es la cuenta de juego (WOW1…WOW8). El `15#1` es interno y no le
|
|
* dice nada a nadie.
|
|
*/
|
|
export function gameAccountDisplayName(username: string): string {
|
|
const i = username.indexOf('#')
|
|
return i >= 0 ? `WOW${username.slice(i + 1)}` : username
|
|
}
|
|
|
|
/**
|
|
* Forma de correo. La ÚNICA validación de correo del sitio: registro, cambio de
|
|
* correo, recuperación y login. Antes el registro/cambio/recuperación exigían
|
|
* Gmail; ya no, se admite cualquier dominio.
|
|
*
|
|
* Deliberadamente laxa. No exige punto en el dominio porque hay cuentas creadas
|
|
* con correos de todo tipo (INNA@INNA.CL, e incluso Q@Q) y validar de más al
|
|
* entrar o al recuperar las dejaría fuera. Lo único que hace falta es separar
|
|
* «correo» de «nombre de cuenta» («15#1», que no tiene @). Es el mismo criterio
|
|
* que aplica el navegador a un <input type="email">.
|
|
*
|
|
* Sin distinguir mayúsculas por definición (no hay letras literales): el dominio
|
|
* de un correo no las distingue, y los nuestros muestran la cuenta en MAYÚSCULAS,
|
|
* así que quien la copie de ahí y la pegue tiene que poder usarla.
|
|
*/
|
|
export const EMAIL_RE = /^[^\s@]+@[^\s@]+$/
|
|
|
|
/**
|
|
* Cuentas de juego por cuenta Battle.net (WoW1…WoW8). Vive aquí, y no en la ruta
|
|
* que la aplica, para que el correo de activación cite el límite REAL: el texto
|
|
* anterior prometía 10 mientras el código ya cortaba en 8.
|
|
*/
|
|
export const MAX_GAME_ACCOUNTS = 8
|
|
|
|
/**
|
|
* Nombre visible de una cuenta de juego: «15#1» → «WOW1» (el nº tras # es la
|
|
* cuenta de juego bajo la Battle.net; con varias serían WOW1, WOW2, WOW3…).
|
|
* El «15» es el id de la cuenta Battle.net y no debe mostrarse al usuario.
|
|
*/
|
|
export function wowAccountLabel(username: string): string {
|
|
const i = username.indexOf('#')
|
|
return i >= 0 ? `WOW${username.slice(i + 1)}` : username
|
|
}
|