4edd08cd1a
- 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>
217 lines
7.9 KiB
Python
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
|