Files
Inna 4edd08cd1a Mi cuenta: enlace a Battlepay y saldo de créditos bnet
- my_account: añade battlepay_credits (leído de battlenet_accounts.battlePayCredits)
- bnet.get_battlepay_credits() defensivo (0 si la columna no existe)
- partials/my-account.html: muestra 'Créditos Battlepay' y añade botón 'Tienda del cliente' que enlaza a /es/battlepay/

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 16:51:03 +00:00

217 lines
7.9 KiB
Python

"""
Utilidades de autenticación para servidores 3.4.3 (TrinityCore) con cuentas
Battle.net.
Dos algoritmos SRP6 distintos conviven:
* ``battlenet_accounts`` -> SRP6 **v2**: PBKDF2-HMAC-SHA512, N de 2048 bits,
g = 2. El "usuario" SRP es HEX(SHA256(UPPER(email))). Es el que usa el
cliente 3.4.3 para conectarse (login por email).
* ``account`` (cuenta de juego) -> SRP6 **Grunt/SHA1**: el clásico de
AzerothCore/3.3.5a. Cada cuenta bnet tiene 1..N cuentas de juego con
username ``<bnetId>#<index>``.
Referencias del propio source 3.4.3:
src/common/Cryptography/Authentication/SRP6.cpp (CalculateX / CalculateVerifier)
src/server/game/Accounts/BattlenetAccountMgr.cpp (CreateBattlenetAccount)
"""
import hashlib
import os
# ---------------------------------------------------------------------------
# SRP6 v2 (battlenet_accounts) — PBKDF2-HMAC-SHA512
# ---------------------------------------------------------------------------
# BnetSRP6v2Base::N (2048 bits) y g, tal cual en SRP6.cpp
_BNET_N = int(
"AC6BDB41324A9A9BF166DE5E1389582FAF72B6651987EE07FC3192943DB56050"
"A37329CBB4A099ED8193E0757767A13DD52312AB4B03310DCD7F48A9DA04FD50"
"E8083969EDB767B0CF6095179A163AB3661A05FBD5FAAAE82918A9962F0B93B8"
"55F97993EC975EEAA80D740ADBF4FF747359D041D5C33EA71D281E446B14773B"
"CA97B43A23FB801676BD207A436C6481F1D2B9078717461A5B9D32E688F87748"
"544523B524B0D57D5EA77A2775D2ECFA032CFBDBF52FB3786160279004E57AE6"
"AF874E7303CE53299CCC041C7BC308D82A5698F3A8D0C38271AE35F8E9DBFBB6"
"94B5C803D89F7AE435DE236D525F54759B65E372FCD68EF20FA7111F9E4AFF73",
16,
)
_BNET_g = 2
_BNET_SALT_LEN = 32
_BNET_ITERATIONS = 15000 # GetXIterations() para v2
_BNET_SRP_VERSION = 2
_TWO_POW_512 = 1 << 512
# TrinityCore almacena el verifier con BigNumber::ToByteVector(), que por
# defecto es little-endian. Si al comparar contra una cuenta creada por el
# propio servidor (`.bnetaccount create`) el login del CLIENTE fallara, prueba a
# poner esto en False (big-endian). El login del PORTAL funciona en cualquier
# caso porque registra y verifica con el mismo criterio.
_BNET_VERIFIER_LITTLE_ENDIAN = True
def bnet_srp_username(email):
"""SRP username de bnet = HEX(SHA256(UPPER(email))) en mayúsculas."""
email_up = email.strip().upper()
return hashlib.sha256(email_up.encode("utf-8")).hexdigest().upper()
def _bnet_calculate_x(email, password, salt):
"""Replica BnetSRP6v2Base::CalculateX (SRP6.cpp)."""
srp_user = bnet_srp_username(email)
tmp = (srp_user + ":" + password).encode("utf-8")
x_bytes = hashlib.pbkdf2_hmac("sha512", tmp, salt, _BNET_ITERATIONS, dklen=64)
x = int.from_bytes(x_bytes, "big")
if x_bytes[0] & 0x80: # si el bit alto está puesto, resta 2^512
x -= _TWO_POW_512
return x % (_BNET_N - 1)
def _int_to_verifier_bytes(value):
"""Emula BigNumber::ToByteVector(): longitud mínima, endianness configurable."""
length = max(1, (value.bit_length() + 7) // 8)
order = "little" if _BNET_VERIFIER_LITTLE_ENDIAN else "big"
return value.to_bytes(length, order)
def _verifier_bytes_to_int(data):
order = "little" if _BNET_VERIFIER_LITTLE_ENDIAN else "big"
return int.from_bytes(data, order)
def bnet_make_registration(email, password):
"""
Genera (salt, verifier, srp_version) para una nueva cuenta Battle.net.
salt: 32 bytes aleatorios (se guarda tal cual).
verifier: g^x mod N en longitud mínima (como TrinityCore::ToByteVector).
"""
salt = os.urandom(_BNET_SALT_LEN)
x = _bnet_calculate_x(email, password, salt)
v = pow(_BNET_g, x, _BNET_N)
return salt, _int_to_verifier_bytes(v), _BNET_SRP_VERSION
def bnet_verify(email, password, salt, stored_verifier):
"""Comprueba la contraseña de una cuenta Battle.net (login por email)."""
if salt is None or stored_verifier is None:
return False
if isinstance(salt, memoryview):
salt = bytes(salt)
if isinstance(stored_verifier, memoryview):
stored_verifier = bytes(stored_verifier)
x = _bnet_calculate_x(email, password, salt)
v = pow(_BNET_g, x, _BNET_N)
# Comparación numérica (robusta frente a padding).
return v == _verifier_bytes_to_int(stored_verifier)
# ---------------------------------------------------------------------------
# SRP6 Grunt/SHA1 (cuenta de juego `account`) — el clásico de 3.3.5a
# ---------------------------------------------------------------------------
_GRUNT_g = 7
_GRUNT_N = int("894B645E89E1535BBDAD5B8B290650530801B18EBFBF5E8FAB3C82872A3E9BB7", 16)
_GRUNT_SALT_LEN = 32
def _sha1(data):
return hashlib.sha1(data).digest()
def game_calculate_verifier(username, password, salt):
"""
SRP6 Grunt (SHA1) para la tabla `account`. ``username`` y ``password`` se
pasan en mayúsculas (TrinityCore hace Utf8ToUpperOnlyLatin en ambos).
Devuelve el verifier en bytes little-endian (32).
"""
if isinstance(salt, memoryview):
salt = bytes(salt)
h1 = _sha1((username.upper() + ":" + password.upper()).encode("utf-8"))
h2 = _sha1(salt + h1)
h2_int = int.from_bytes(h2, "little")
verifier = pow(_GRUNT_g, h2_int, _GRUNT_N)
return verifier.to_bytes(32, "little")
def game_make_registration(username, password):
"""Genera (salt, verifier) Grunt para una cuenta de juego nueva."""
salt = os.urandom(_GRUNT_SALT_LEN)
verifier = game_calculate_verifier(username, password[:16], salt)
return salt, verifier
def game_verify(username, password, salt, stored_verifier):
"""Comprueba la contraseña de una cuenta de juego (SRP6 Grunt)."""
if salt is None or stored_verifier is None:
return False
if isinstance(stored_verifier, memoryview):
stored_verifier = bytes(stored_verifier)
calc = game_calculate_verifier(username, password[:16], salt)
return int.from_bytes(calc, "little") == int.from_bytes(stored_verifier, "little")
# ---------------------------------------------------------------------------
# Helpers de acceso a la BD auth (bnet <-> cuentas de juego)
# ---------------------------------------------------------------------------
def normalize_email(email):
"""El email en battlenet_accounts se almacena en MAYÚSCULAS."""
return email.strip().upper()
def get_bnet_account_by_email(cursor, email):
"""Devuelve dict {id, email, srp_version, salt, verifier} o None."""
cursor.execute(
"SELECT id, email, srp_version, salt, verifier "
"FROM battlenet_accounts WHERE email = %s",
[normalize_email(email)],
)
row = cursor.fetchone()
if not row:
return None
return {
"id": row[0],
"email": row[1],
"srp_version": row[2],
"salt": row[3],
"verifier": row[4],
}
def get_game_accounts_for_bnet(cursor, bnet_id):
"""Lista de cuentas de juego de una cuenta bnet: [{id, username, index}]."""
cursor.execute(
"SELECT id, username, battlenet_index "
"FROM account WHERE battlenet_account = %s ORDER BY battlenet_index",
[bnet_id],
)
return [{"id": r[0], "username": r[1], "index": r[2]} for r in cursor.fetchall()]
def get_next_battlenet_index(cursor, bnet_id):
"""Siguiente battlenet_index libre para una cuenta bnet (1-based)."""
cursor.execute(
"SELECT MAX(battlenet_index) FROM account WHERE battlenet_account = %s",
[bnet_id],
)
row = cursor.fetchone()
return (row[0] or 0) + 1
def make_game_account_username(bnet_id, index=1):
"""Nombre de la cuenta de juego: ``<bnetId>#<index>`` (como TrinityCore)."""
return f"{bnet_id}#{index}"
def get_battlepay_credits(cursor, bnet_id):
"""Créditos Battlepay de la cuenta bnet (0 si la columna no existe)."""
if not bnet_id:
return 0
try:
cursor.execute("SELECT battlePayCredits FROM battlenet_accounts WHERE id = %s", [bnet_id])
row = cursor.fetchone()
return row[0] if row and row[0] is not None else 0
except Exception:
return 0