""" 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 ``#``. 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: ``#`` (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