Files
Steam-Panel-Simple/pysteamauth/auth/steam.py
T
Manchik 9b7001407f v3.0.4
2026-04-25 04:17:13 +03:00

388 lines
14 KiB
Python

import base64
import binascii
import hashlib
import hmac
import json
import math
from struct import pack
from typing import (
Any,
Mapping,
Optional,
)
import rsa
from aiohttp import FormData
from bitstring import BitArray
from urllib3.util import parse_url
from pysteamauth.abstract import (
CookieStorageAbstract,
RequestStrategyAbstract,
)
from pysteamauth.base import (
BaseCookieStorage,
BaseRequestStrategy,
)
from pysteamauth.pb2.enums_pb2 import ESessionPersistence
from pysteamauth.pb2.steammessages_auth.steamclient_pb2 import (
CAuthentication_AllowedConfirmation,
CAuthentication_BeginAuthSessionViaCredentials_Request,
CAuthentication_BeginAuthSessionViaCredentials_Response,
CAuthentication_GetPasswordRSAPublicKey_Request,
CAuthentication_GetPasswordRSAPublicKey_Response,
CAuthentication_PollAuthSessionStatus_Request,
CAuthentication_PollAuthSessionStatus_Response,
CAuthentication_UpdateAuthSessionWithSteamGuardCode_Request,
EAuthSessionGuardType,
EAuthTokenPlatformType,
)
from .schemas import FinalizeLoginStatus
class Steam:
def __init__(
self,
login: str,
password: str,
steamid: Optional[int] = None,
shared_secret: Optional[str] = None,
identity_secret: Optional[str] = None,
device_id: Optional[str] = None,
cookie_storage: Optional[CookieStorageAbstract] = None,
request_strategy: Optional[RequestStrategyAbstract] = None,
):
self._login = login
self._steamid = steamid
self._password = password
self._shared_secret = shared_secret
self._identity_secret = identity_secret
self._device_id = device_id
self._requests = request_strategy if request_strategy is not None else BaseRequestStrategy()
self._storage = cookie_storage if cookie_storage is not None else BaseCookieStorage()
self._refresh_token: Optional[str] = None
@property
def steamid(self) -> int:
if not self._steamid:
raise ValueError('steamid is not specified')
return self._steamid
@property
def shared_secret(self) -> str:
if not self._shared_secret:
raise ValueError('shared_secret is not specified')
return self._shared_secret
@property
def identity_secret(self) -> str:
if not self._identity_secret:
raise ValueError('identity_secret is not specified')
return self._identity_secret
@property
def device_id(self) -> str:
if not self._device_id:
raise ValueError('device_id is not specified')
return self._device_id
@property
def login(self) -> str:
return self._login
@property
def partner_id(self) -> int:
return self.steamid - 76561197960265728
async def cookies(self, domain: str = 'steamcommunity.com') -> Mapping[str, str]:
return await self._storage.get(
login=self._login,
domain=domain,
)
async def sessionid(self, domain: str = 'steamcommunity.com') -> str:
return (await self.cookies(domain))['sessionid']
async def request(self, url: str, method: str = 'GET', **kwargs: Any) -> str:
cookies = await self._storage.get(
login=self._login,
domain=parse_url(url).host,
)
return await self._requests.text(
url=url,
method=method,
cookies={
**cookies,
**kwargs.pop('cookies', {}),
},
**kwargs,
)
async def is_authorized(self) -> bool:
response: str = await self.request(
url='https://steamcommunity.com/chat/clientjstoken',
)
data = json.loads(response)
logged_in = data['logged_in']
if logged_in:
if not self._steamid:
try:
self._steamid = int(data['steamid'])
except KeyError:
...
return logged_in
async def _getrsakey(self) -> CAuthentication_GetPasswordRSAPublicKey_Response:
message = CAuthentication_GetPasswordRSAPublicKey_Request(
account_name=self._login,
)
response = await self._requests.bytes(
method='GET',
url='https://api.steampowered.com/IAuthenticationService/GetPasswordRSAPublicKey/v1',
params={
'input_protobuf_encoded': str(base64.b64encode(message.SerializeToString()), 'utf8'),
},
)
return CAuthentication_GetPasswordRSAPublicKey_Response.FromString(response)
async def _begin_auth_session(
self,
encrypted_password: str,
rsa_timestamp: int,
) -> CAuthentication_BeginAuthSessionViaCredentials_Response:
message = CAuthentication_BeginAuthSessionViaCredentials_Request(
account_name=self._login,
encrypted_password=encrypted_password,
encryption_timestamp=rsa_timestamp,
remember_login=True,
platform_type=EAuthTokenPlatformType.k_EAuthTokenPlatformType_MobileApp,
website_id='Mobile',
persistence=ESessionPersistence.k_ESessionPersistence_Persistent,
device_friendly_name=self._device_id or 'android:unknown',
)
response = await self._requests.bytes(
method='POST',
url='https://api.steampowered.com/IAuthenticationService/BeginAuthSessionViaCredentials/v1',
data=FormData(
fields=[
('input_protobuf_encoded', str(base64.b64encode(message.SerializeToString()), 'utf8')),
],
),
)
return CAuthentication_BeginAuthSessionViaCredentials_Response.FromString(response)
async def get_server_time(self) -> int:
response = await self._requests.text(
method='POST',
url='https://api.steampowered.com/ITwoFactorService/QueryTime/v0001',
)
data = json.loads(response)
return int(data['response']['server_time'])
@classmethod
async def get_steam_guard(cls, shared_secret: str, server_time: int) -> str:
data = binascii.unhexlify(
hmac.new(
key=base64.b64decode(shared_secret),
msg=pack('!L', 0) + pack('!L', math.floor(server_time // 30)),
digestmod=hashlib.sha1,
).hexdigest(),
)
value = data[19] & 0xF
full_code = (
(data[value] & 0x7F) << 24 | # noqa:W504
(data[value + 1] & 0xFF) << 16 | # noqa:W504
(data[value + 2] & 0xFF) << 8 | # noqa:W504
(data[value + 3] & 0xFF)
)
code = ''
chars = '23456789BCDFGHJKMNPQRTVWXY'
for _ in range(5):
code += chars[full_code % len(chars)]
full_code //= len(chars)
return code
def get_confirmation_hash(self, server_time: int, tag: str = 'conf') -> str:
identitysecret = base64.b64decode(
self.identity_secret,
)
secret = BitArray(
bytes=identitysecret,
length=len(identitysecret) * 8,
)
tag_buffer = BitArray(
bytes=tag.encode('utf8'),
length=len(tag) * 8,
)
buffer = BitArray(4 * 8)
buffer.append(BitArray(int=server_time, length=32))
buffer.append(tag_buffer)
confirmation = hmac.new(
key=secret.tobytes(),
msg=buffer.tobytes(),
digestmod=hashlib.sha1,
)
return str(base64.b64encode(confirmation.digest()), 'utf8')
def _encrypt_password(self, keys: CAuthentication_GetPasswordRSAPublicKey_Response) -> str:
publickey_exp = int(keys.publickey_exp, 16) # type:ignore
publickey_mod = int(keys.publickey_mod, 16) # type:ignore
public_key = rsa.PublicKey(
n=publickey_mod,
e=publickey_exp,
)
encrypted_password = rsa.encrypt(
message=self._password.encode('ascii'),
pub_key=public_key,
)
return str(base64.b64encode(encrypted_password), 'utf8')
async def _update_auth_session(
self,
client_id: int,
steamid: int,
code: str,
code_type: int,
) -> None:
message = CAuthentication_UpdateAuthSessionWithSteamGuardCode_Request(
client_id=client_id,
steamid=steamid,
code=code,
code_type=code_type,
)
await self._requests.request(
method='POST',
url='https://api.steampowered.com/IAuthenticationService/UpdateAuthSessionWithSteamGuardCode/v1',
data=FormData(
fields=[
('input_protobuf_encoded', str(base64.b64encode(message.SerializeToString()), 'utf8')),
],
),
)
async def _poll_auth_session_status(
self,
client_id: int,
request_id: bytes,
) -> CAuthentication_PollAuthSessionStatus_Response:
message = CAuthentication_PollAuthSessionStatus_Request(
client_id=client_id,
request_id=request_id,
)
response = await self._requests.bytes(
method='POST',
url='https://api.steampowered.com/IAuthenticationService/PollAuthSessionStatus/v1',
data=FormData(
fields=[
('input_protobuf_encoded', str(base64.b64encode(message.SerializeToString()), 'utf8')),
],
),
)
return CAuthentication_PollAuthSessionStatus_Response.FromString(response)
async def _finalize_login(self, refresh_token: str, sessionid: str) -> FinalizeLoginStatus:
response = await self._requests.text(
method='POST',
url='https://login.steampowered.com/jwt/finalizelogin',
data=FormData(
fields=[
('nonce', refresh_token),
('sessionid', sessionid),
('redir', 'https://steamcommunity.com/login/home/?goto='),
],
),
headers={
'Origin': 'https://steamcommunity.com'
}
)
import json as _json
from app.core.exceptions import SteamAuthError
try:
data = _json.loads(response)
except Exception:
data = {}
if not data.get('success', True) or 'steamID' not in data:
error_code = data.get('error', 0)
from pysteamauth.errors.codes import STEAM_ERROR_CODES
error_name = STEAM_ERROR_CODES.get(error_code, f'code {error_code}')
raise SteamAuthError(
f"finalizelogin failed at step 'nonce': {error_name} (EResult={error_code})"
)
return FinalizeLoginStatus.model_validate(data)
async def _set_token(self, url: str, nonce: str, auth: str, steamid: int) -> None:
await self._requests.request(
method='POST',
url=url,
data=FormData(
fields=[
('nonce', nonce),
('auth', auth),
('steamID', str(steamid)),
],
),
)
def _is_twofactor_required(self, confirmation: CAuthentication_AllowedConfirmation) -> bool:
return confirmation.confirmation_type in (
EAuthSessionGuardType.k_EAuthSessionGuardType_DeviceCode,
EAuthSessionGuardType.k_EAuthSessionGuardType_DeviceConfirmation,
)
async def login_to_steam(self) -> None:
if await self.is_authorized():
return
if not self._requests.cookies().get('sessionid'):
await self._requests.bytes(
method='GET',
url='https://steamcommunity.com',
)
keys = await self._getrsakey()
encrypted_password = self._encrypt_password(keys)
auth_session = await self._begin_auth_session(
encrypted_password=encrypted_password,
rsa_timestamp=keys.timestamp,
)
if auth_session.allowed_confirmations:
if self._is_twofactor_required(auth_session.allowed_confirmations[0]):
server_time = await self.get_server_time()
code = await Steam.get_steam_guard(self.shared_secret, server_time)
await self._update_auth_session(
client_id=auth_session.client_id,
steamid=auth_session.steamid,
code=code,
code_type=EAuthSessionGuardType.k_EAuthSessionGuardType_DeviceCode,
)
session = await self._poll_auth_session_status(
client_id=auth_session.client_id,
request_id=auth_session.request_id,
)
self._refresh_token = session.refresh_token
tokens = await self._finalize_login(
refresh_token=session.refresh_token,
sessionid=self._requests.cookies()['sessionid'],
)
for token in tokens.transfer_info:
await self._set_token(
url=token.url,
nonce=token.params.nonce,
auth=token.params.auth,
steamid=auth_session.steamid,
)
if not self._steamid and tokens.steamID:
self._steamid = int(tokens.steamID)
cookies = {
'steamcommunity.com': self._requests.cookies('steamcommunity.com'),
}
for url in ('https://store.steampowered.com', 'https://help.steampowered.com'):
await self._requests.bytes(url, 'GET')
cookies.update({
parse_url(url).host: self._requests.cookies(parse_url(url).host),
})
await self._storage.set(login=self._login, cookies=cookies)