forked from FOSS/Steam-Panel
388 lines
14 KiB
Python
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)
|