├── requirements.txt ├── .DS_Store ├── maFiles ├── .DS_Store ├── 76561199434561197.maFile ├── 76561199059431362.maFile └── 76561199142269051.maFile ├── refresh_all_accounts.py ├── README.md └── SteamGuard.py /requirements.txt: -------------------------------------------------------------------------------- 1 | requests>=2.31.0 2 | rsa>=4.9 3 | 4 | -------------------------------------------------------------------------------- /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sporoid/SteamAuthenticator/HEAD/.DS_Store -------------------------------------------------------------------------------- /maFiles/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sporoid/SteamAuthenticator/HEAD/maFiles/.DS_Store -------------------------------------------------------------------------------- /maFiles/76561199434561197.maFile: -------------------------------------------------------------------------------- 1 | {"shared_secret": "j+I5oZKIqk+KXx2JVR3ViFZcXsI=", "serial_number": "2559780196828876947", "revocation_code": "R72812", "uri": "otpauth://totp/Steam:trikce?secret=R7RDTIMSRCVE7CS7DWEVKHOVRBLFYXWC&issuer=Steam", "server_time": 1682318645, "account_name": "trikce", "token_gid": "31d626a162b91073", "identity_secret": "I3RXClXcrsVV/Ng1XFoTi7RPOQA=", "secret_1": "aZnwGTQ6bSrogijtqr4UpTBCVpI=", "status": 1, "device_id": "iphone:11", "fully_enrolled": true, "Session": {"SteamID": 76561199434561197, "AccessToken": "eyAidHlwIjogIkpXVCIsICJhbGciOiAiRWREU0EiIH0.eyAiaXNzIjogInI6MDAwRF8yNzRGRERBRl80QTNGNCIsICJzdWIiOiAiNzY1NjExOTk0MzQ1NjExOTciLCAiYXVkIjogWyAid2ViIiBdLCAiZXhwIjogMTc2NDM3NjY1NiwgIm5iZiI6IDE3NTU2NTAxMDksICJpYXQiOiAxNzY0MjkwMTA5LCAianRpIjogIjAwMTRfMjc0RkREQURfRTkzMEEiLCAib2F0IjogMTc2NDI5MDEwOSwgInJ0X2V4cCI6IDE3ODIxNzk4NTEsICJwZXIiOiAwLCAiaXBfc3ViamVjdCI6ICIxODYuMTUxLjk3LjYxIiwgImlwX2NvbmZpcm1lciI6ICIxODYuMTUxLjk3LjYxIiB9.F6LxlFalgc3mQpM4aJrFOiuYlcb_Z9nneUb7w0YhO9TcIknu5WJigu55nuX1ZQVY74VZq3GJc5t7SlGWlmvbAw", "RefreshToken": "eyAidHlwIjogIkpXVCIsICJhbGciOiAiRWREU0EiIH0.eyAiaXNzIjogInN0ZWFtIiwgInN1YiI6ICI3NjU2MTE5OTQzNDU2MTE5NyIsICJhdWQiOiBbICJ3ZWIiLCAicmVuZXciLCAiZGVyaXZlIiBdLCAiZXhwIjogMTc4MjE3OTg1MSwgIm5iZiI6IDE3NTU2NTAxMDksICJpYXQiOiAxNzY0MjkwMTA5LCAianRpIjogIjAwMERfMjc0RkREQUZfNEEzRjQiLCAib2F0IjogMTc2NDI5MDEwOSwgInBlciI6IDEsICJpcF9zdWJqZWN0IjogIjE4Ni4xNTEuOTcuNjEiLCAiaXBfY29uZmlybWVyIjogIjE4Ni4xNTEuOTcuNjEiIH0.Ir65GCYOG5iutyWwR7Z14gB4ULdmL6AR3uXBebzzGHrbIToswvP1ipz846nJCRKfEE5XyEsyXhvcEN-cFhymAA", "SessionID": null}} -------------------------------------------------------------------------------- /maFiles/76561199059431362.maFile: -------------------------------------------------------------------------------- 1 | {"shared_secret": "ZDd3P0j/buiKCCKn7Y2ORTVhz+U=", "serial_number": "16268458850825034469", "revocation_code": "R63052", "uri": "otpauth://totp/Steam:juanpi1588?secret=MQ3XOP2I75XORCQIEKT63DMOIU2WDT7F&issuer=Steam", "server_time": 1682318436, "account_name": "juanpi1588", "token_gid": "312e26a1622e44b6", "identity_secret": "O2di38hJglgpu6etLZUzxfPnues=", "secret_1": "Pt6YKG3wpeR08JoAapZlZhiVBBM", "status": 1, "device_id": "iphone:11", "fully_enrolled": true, "Session": {"SteamID": 76561199059431362, "AccessToken": "eyAidHlwIjogIkpXVCIsICJhbGciOiAiRWREU0EiIH0.eyAiaXNzIjogInI6MDAwQV8yNzRGRERBQV9FRTYwMyIsICJzdWIiOiAiNzY1NjExOTkwNTk0MzEzNjIiLCAiYXVkIjogWyAid2ViIiBdLCAiZXhwIjogMTc2NDM3NzU3MSwgIm5iZiI6IDE3NTU2NTAxOTUsICJpYXQiOiAxNzY0MjkwMTk1LCAianRpIjogIjAwMEFfMjc0RkREQUFfRUU3NUIiLCAib2F0IjogMTc2NDI5MDE5NSwgInJ0X2V4cCI6IDE3ODIxODEzMTcsICJwZXIiOiAwLCAiaXBfc3ViamVjdCI6ICIxODYuMTUxLjk3LjYxIiwgImlwX2NvbmZpcm1lciI6ICIxODYuMTUxLjk3LjYxIiB9.kYoL__6G41JtlmSp_rN3gUruBlZYtsg71BlPKf87K6nDXMIB6rH0TiZgKSAsfmdXpbfQznke6bDEYmFQGdsiAw", "RefreshToken": "eyAidHlwIjogIkpXVCIsICJhbGciOiAiRWREU0EiIH0.eyAiaXNzIjogInN0ZWFtIiwgInN1YiI6ICI3NjU2MTE5OTA1OTQzMTM2MiIsICJhdWQiOiBbICJ3ZWIiLCAicmVuZXciLCAiZGVyaXZlIiBdLCAiZXhwIjogMTc4MjE4MTMxNywgIm5iZiI6IDE3NTU2NTAxOTUsICJpYXQiOiAxNzY0MjkwMTk1LCAianRpIjogIjAwMEFfMjc0RkREQUFfRUU2MDMiLCAib2F0IjogMTc2NDI5MDE5NSwgInBlciI6IDEsICJpcF9zdWJqZWN0IjogIjE4Ni4xNTEuOTcuNjEiLCAiaXBfY29uZmlybWVyIjogIjE4Ni4xNTEuOTcuNjEiIH0.xgzXC4EHtG7rdbrq_nlJTRjwgZefq0AxrdEEHv3CKNMlD0rM62fUaiu3Q4tSBe_xOmZoVYAUuYz1L5DC81-_AA", "SessionID": null}} -------------------------------------------------------------------------------- /maFiles/76561199142269051.maFile: -------------------------------------------------------------------------------- 1 | {"shared_secret": "JN4RTw5G+AEZXxAuEKaUEBVD6Ro=", "serial_number": "9389873414837843452", "revocation_code": "R09729", "uri": "otpauth://totp/Steam:xkce27?secret=ETPBCTYOI34ACGK7CAXBBJUUCAKUH2I2&issuer=Steam", "server_time": 1747602968, "account_name": "xkce27", "token_gid": "3a664d698f75a7d", "identity_secret": "HNGxRYzy9ncAOHy1nhG26cIoc5E=", "secret_1": "76YlGMianGoLiozIY2i4lZC2rpM=", "status": 1, "device_id": "android:bf5c553e-001c-49e1-b3d6-000aed54a120", "fully_enrolled": true, "Session": {"SteamID": 76561199142269051, "AccessToken": "eyAidHlwIjogIkpXVCIsICJhbGciOiAiRWREU0EiIH0.eyAiaXNzIjogInI6MDAwRV8yNzRGRERBRF8yNkRDQyIsICJzdWIiOiAiNzY1NjExOTkxNDIyNjkwNTEiLCAiYXVkIjogWyAid2ViIiBdLCAiZXhwIjogMTc2NDM3NzI2OCwgIm5iZiI6IDE3NTU2NTAxOTcsICJpYXQiOiAxNzY0MjkwMTk3LCAianRpIjogIjAwMThfMjc0RkREQUNfRERGRjkiLCAib2F0IjogMTc2NDI5MDE5NywgInJ0X2V4cCI6IDE3ODI3ODM3OTMsICJwZXIiOiAwLCAiaXBfc3ViamVjdCI6ICIxODYuMTUxLjk3LjYxIiwgImlwX2NvbmZpcm1lciI6ICIxODYuMTUxLjk3LjYxIiB9.RUuKqWl3OLqYPAWMMAbCGuHiQPQM002EwQWlpRQFiBo-zISbLuSBXgFLHsda3i55FOFJ68B9qPLHUPWa_b_4BQ", "RefreshToken": "eyAidHlwIjogIkpXVCIsICJhbGciOiAiRWREU0EiIH0.eyAiaXNzIjogInN0ZWFtIiwgInN1YiI6ICI3NjU2MTE5OTE0MjI2OTA1MSIsICJhdWQiOiBbICJ3ZWIiLCAicmVuZXciLCAiZGVyaXZlIiBdLCAiZXhwIjogMTc4Mjc4Mzc5MywgIm5iZiI6IDE3NTU2NTAxOTcsICJpYXQiOiAxNzY0MjkwMTk3LCAianRpIjogIjAwMEVfMjc0RkREQURfMjZEQ0MiLCAib2F0IjogMTc2NDI5MDE5NywgInBlciI6IDEsICJpcF9zdWJqZWN0IjogIjE4Ni4xNTEuOTcuNjEiLCAiaXBfY29uZmlybWVyIjogIjE4Ni4xNTEuOTcuNjEiIH0.PqGcmEGCCqGvEHzPPUxRNNwWIPcfYqgnmGarqQYtps02udIr3Vp-n8PyQUgnlrK7-vOW7s558-gGwxi-_oxXBg", "SessionID": null}} -------------------------------------------------------------------------------- /refresh_all_accounts.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import os 3 | import time 4 | import hmac 5 | import json 6 | import struct 7 | import base64 8 | import requests 9 | from hashlib import sha1 10 | 11 | symbols = '23456789BCDFGHJKMNPQRTVWXY' 12 | 13 | def getQueryTime(): 14 | try: 15 | request = requests.post('https://api.steampowered.com/ITwoFactorService/QueryTime/v0001', timeout=30) 16 | json_data = request.json() 17 | server_time = int(json_data['response']['server_time']) - time.time() 18 | return server_time 19 | except: 20 | return 0 21 | 22 | def getGuardCode(shared_secret): 23 | code = '' 24 | timestamp = time.time() + getQueryTime() 25 | _hmac = hmac.new(base64.b64decode(shared_secret), struct.pack('>Q', int(timestamp/30)), sha1).digest() 26 | _ord = ord(_hmac[19:20]) & 0xF 27 | value = struct.unpack('>I', _hmac[_ord:_ord+4])[0] & 0x7fffffff 28 | for i in range(5): 29 | code += symbols[value % len(symbols)] 30 | value = int(value / len(symbols)) 31 | return code 32 | 33 | def is_token_expired(token): 34 | try: 35 | token_parts = token.split('.') 36 | if len(token_parts) < 2: 37 | return True 38 | 39 | payload = token_parts[1] 40 | payload = payload.replace('-', '+').replace('_', '/') 41 | 42 | padding = 4 - len(payload) % 4 43 | if padding != 4: 44 | payload += '=' * padding 45 | 46 | payload_bytes = base64.b64decode(payload) 47 | payload_data = json.loads(payload_bytes.decode('utf-8')) 48 | 49 | exp_time = payload_data.get('exp', 0) 50 | current_time = int(time.time()) 51 | 52 | return current_time > exp_time 53 | except: 54 | return True 55 | 56 | def steam_login_with_tokens(username, password, shared_secret): 57 | try: 58 | import rsa 59 | 60 | session = requests.Session() 61 | 62 | rsa_response = session.get( 63 | 'https://api.steampowered.com/IAuthenticationService/GetPasswordRSAPublicKey/v1/', 64 | params={'account_name': username}, 65 | timeout=30 66 | ) 67 | 68 | if rsa_response.status_code != 200: 69 | return None, None 70 | 71 | rsa_data = rsa_response.json() 72 | rsa_mod = int(rsa_data['response']['publickey_mod'], 16) 73 | rsa_exp = int(rsa_data['response']['publickey_exp'], 16) 74 | rsa_timestamp = rsa_data['response']['timestamp'] 75 | 76 | public_key = rsa.PublicKey(rsa_mod, rsa_exp) 77 | encrypted_password = rsa.encrypt(password.encode('utf-8'), public_key) 78 | encrypted_password_b64 = base64.b64encode(encrypted_password).decode('utf-8') 79 | 80 | begin_auth_data = { 81 | 'account_name': username, 82 | 'encrypted_password': encrypted_password_b64, 83 | 'encryption_timestamp': rsa_timestamp, 84 | 'remember_login': 'false', 85 | 'platform_type': '2', 86 | 'persistence': '1', 87 | 'website_id': 'Mobile' 88 | } 89 | 90 | begin_response = session.post( 91 | 'https://api.steampowered.com/IAuthenticationService/BeginAuthSessionViaCredentials/v1/', 92 | data=begin_auth_data, 93 | timeout=30 94 | ) 95 | 96 | if begin_response.status_code != 200: 97 | return None, None 98 | 99 | begin_result = begin_response.json() 100 | 101 | if 'response' not in begin_result: 102 | return None, None 103 | 104 | response_data = begin_result['response'] 105 | client_id = response_data.get('client_id') 106 | request_id = response_data.get('request_id') 107 | allowed_confirmations = response_data.get('allowed_confirmations', []) 108 | 109 | if not client_id or not request_id: 110 | return None, None 111 | 112 | needs_twofactor = any(c.get('confirmation_type') == 3 for c in allowed_confirmations) 113 | 114 | if needs_twofactor: 115 | code = getGuardCode(shared_secret) 116 | 117 | update_data = { 118 | 'client_id': client_id, 119 | 'steamid': response_data.get('steamid'), 120 | 'code': code, 121 | 'code_type': '3' 122 | } 123 | 124 | update_response = session.post( 125 | 'https://api.steampowered.com/IAuthenticationService/UpdateAuthSessionWithSteamGuardCode/v1/', 126 | data=update_data, 127 | timeout=30 128 | ) 129 | 130 | if update_response.status_code != 200: 131 | return None, None 132 | 133 | for attempt in range(30): 134 | time.sleep(1) 135 | 136 | poll_data = { 137 | 'client_id': client_id, 138 | 'request_id': request_id 139 | } 140 | 141 | poll_response = session.post( 142 | 'https://api.steampowered.com/IAuthenticationService/PollAuthSessionStatus/v1/', 143 | data=poll_data, 144 | timeout=30 145 | ) 146 | 147 | if poll_response.status_code != 200: 148 | continue 149 | 150 | poll_result = poll_response.json() 151 | 152 | if 'response' not in poll_result: 153 | continue 154 | 155 | tokens = poll_result['response'] 156 | access_token = tokens.get('access_token') 157 | refresh_token = tokens.get('refresh_token') 158 | 159 | if access_token and refresh_token: 160 | return access_token, refresh_token 161 | 162 | return None, None 163 | except Exception as e: 164 | print(f"Exception: {e}") 165 | return None, None 166 | 167 | # Credentials 168 | credentials_map = { 169 | 'xkce27': ('xkce27', 'ymw@ntz3xau2gtk-VCT'), 170 | 'juanpi1588': ('juanpi1588', 'EHD_xtx-cnc0drw0wgp'), 171 | 'trikce': ('trikce', 'eqb6UYR5hxa1pvc-qna') 172 | } 173 | 174 | script_dir = os.path.dirname(os.path.abspath(__file__)) 175 | mafiles_dir = os.path.join(script_dir, 'maFiles') 176 | 177 | print("="*60) 178 | print("Refreshing All Accounts") 179 | print("="*60) 180 | 181 | success_count = 0 182 | fail_count = 0 183 | 184 | with os.scandir(mafiles_dir) as files: 185 | for file in files: 186 | if file.is_file() and file.name.endswith('.maFile'): 187 | with open(file, 'r') as f: 188 | mafile_data = json.loads(f.read()) 189 | account_name = mafile_data['account_name'] 190 | 191 | # Check if token is expired 192 | access_token = mafile_data['Session']['AccessToken'] 193 | if not is_token_expired(access_token): 194 | print(f"\n✓ {account_name}: Token is still valid") 195 | success_count += 1 196 | continue 197 | 198 | print(f"\n⟳ {account_name}: Refreshing expired token...") 199 | 200 | if account_name not in credentials_map: 201 | print(f" ✗ No credentials found") 202 | fail_count += 1 203 | continue 204 | 205 | username, password = credentials_map[account_name] 206 | shared_secret = mafile_data['shared_secret'] 207 | 208 | new_access_token, new_refresh_token = steam_login_with_tokens(username, password, shared_secret) 209 | 210 | if new_access_token and new_refresh_token: 211 | mafile_data['Session']['AccessToken'] = new_access_token 212 | mafile_data['Session']['RefreshToken'] = new_refresh_token 213 | 214 | with open(file.path, 'w') as fw: 215 | json.dump(mafile_data, fw) 216 | 217 | print(f" ✓ Token refreshed successfully") 218 | success_count += 1 219 | else: 220 | print(f" ✗ Failed to refresh token") 221 | fail_count += 1 222 | 223 | print(f"\n{'='*60}") 224 | print(f"Summary: {success_count} successful, {fail_count} failed") 225 | print(f"{'='*60}\n") 226 | 227 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # STPY - Steam Guard Authenticator 2 | 3 | A Python-based Steam Guard authenticator that generates time-based one-time passwords (TOTP) and manages Steam mobile confirmations directly from your terminal. 4 | 5 | ## Features 6 | 7 | ### 🔐 TOTP Code Generation 8 | - Generates 5-character Steam Guard codes 9 | - Syncs with Steam's time server for accuracy 10 | - Supports multiple Steam accounts simultaneously 11 | - Auto-refreshes every 30 seconds 12 | 13 | ### ✅ Mobile Confirmations 14 | - View pending trade confirmations 15 | - Approve individual confirmations 16 | - Batch approve all confirmations 17 | - Multi-account confirmation management 18 | 19 | ### 🎨 User Interface 20 | - Clean terminal-based interface 21 | - Animated progress bar synced to 30-second TOTP cycle 22 | - Real-time status updates 23 | - Cross-platform support (macOS, Linux, Windows) 24 | 25 | ## Installation 26 | 27 | ### Prerequisites 28 | - Python 3.x 29 | - Required libraries: `requests`, `rsa` 30 | 31 | ### Setup 32 | ```bash 33 | # Install dependencies 34 | pip install -r requirements.txt 35 | 36 | # Or install manually 37 | pip install requests rsa 38 | 39 | # Run the application 40 | python SteamGuard.py 41 | ``` 42 | 43 | ## Usage 44 | 45 | ### Normal Mode (TOTP Codes) 46 | The application displays a live dashboard with individual progress bars for each account: 47 | 48 | ``` 49 | ============================================================ 50 | STEAM GUARD AUTHENTICATOR 51 | ============================================================ 52 | 53 | Press 'Ctrl+C' to stop | Press '2' to enter verification mode 54 | 55 | trikce | Code: B2C4D | [████████░░░░░░░] 18s 56 | juanpi1588 | Code: HV3GR | [████████░░░░░░░] 18s 57 | xkce27 | Code: G97QR | [████████░░░░░░░] 18s 58 | 59 | ──────────────────────────────────────────────────────────── 60 | [████████████████████████──────────────────────────────────] 61 | ──────────────────────────────────────────────────────────── 62 | ``` 63 | 64 | **Features:** 65 | - ⚡ **Real-time Updates** - Updates every second with current codes and progress 66 | - 🔄 **Synchronized Accounts** - All accounts share the same Steam server time 67 | - 🎯 **30-Second Cycles** - Progress bars match TOTP 30-second intervals 68 | - 📊 **Visual Progress** - Bar shows exact time remaining until next code 69 | - 🔒 **Steam Time Sync** - Syncs with Steam's official time server on startup 70 | 71 | ### Verification Mode (Confirmations) 72 | Press `2` during the progress bar to enter verification mode: 73 | 74 | ``` 75 | ============================================================ 76 | PENDING CONFIRMATIONS: 3 77 | ============================================================ 78 | 79 | 1. [account1] Confirm Trade Offer 80 | Creator ID: 76561198012345678 81 | Verification Code: [45678] 82 | Trade with User123 - 5 items 83 | 84 | 2. [account2] Market Listing 85 | Creator ID: 76561198087654321 86 | Verification Code: [54321] 87 | Sell Item XYZ for $10.50 88 | 89 | 3. [account3] Account Recovery 90 | Creator ID: 5329691627136527629 91 | Verification Code: [27629] 92 | Verify account recovery request 93 | 94 | ============================================================ 95 | SELECT ACTION: 96 | ============================================================ 97 | [Number] - Select confirmation 98 | [A] - Approve All 99 | [D] - Deny All 100 | [Q] - Quit 101 | ============================================================ 102 | 103 | Your choice: 1 104 | 105 | ============================================================ 106 | SELECTED: [account1] Confirm Trade Offer 107 | ============================================================ 108 | 109 | ACTION: 110 | [1] - APPROVE 111 | [2] - DENY 112 | [Q] - Cancel 113 | ============================================================ 114 | 115 | Your choice: 1 116 | 117 | Approving... 118 | ✓ Successfully approved! 119 | 120 | Press Enter to return... 121 | ``` 122 | 123 | **Two-Step Process:** 124 | 125 | 1. **Select** the confirmation number, or choose Approve/Deny All 126 | 2. **Confirm** your action (with warning for batch operations) 127 | 128 | **Safety Features:** 129 | - Warning prompts before approving/denying all 130 | - Per-confirmation verification code in brackets [XXXXX] 131 | - Verification codes match Steam's confirmation page exactly (last 5 digits of Creator ID) 132 | - Two-step action confirmation prevents accidents 133 | - Detailed information to make informed decisions 134 | 135 | **Confirmation Types:** 136 | - **Trade** - Trading items with another user 137 | - **Market Listing** - Selling items on Community Market 138 | - **Account Recovery** - Account security verification 139 | - **Phone Number Change** - Changing account phone number 140 | - **Feature Opt-Out** - Opting out of features 141 | 142 | *Types are from Steam's official API (EMobileConfirmationType enum)* 143 | 144 | ## File Structure 145 | 146 | ``` 147 | STPY/ 148 | ├── SteamGuard.py # Main application 149 | ├── refresh_all_accounts.py # Token refresh utility 150 | ├── requirements.txt # Python dependencies 151 | ├── maFiles/ # Steam authenticator files (NOT included in git) 152 | │ └── *.maFile # Account authentication data 153 | ├── README.md # This file 154 | └── .gitignore # Git ignore rules 155 | ``` 156 | 157 | ## Utility Scripts 158 | 159 | ### refresh_all_accounts.py 160 | Manually refresh all expired tokens for all accounts: 161 | ```bash 162 | python refresh_all_accounts.py 163 | ``` 164 | 165 | This script: 166 | - Checks all accounts for expired tokens 167 | - Automatically logs in with stored credentials 168 | - Submits 2FA codes automatically 169 | - Updates all .maFile files with fresh tokens 170 | - Shows summary of successes/failures 171 | 172 | ## Configuration 173 | 174 | ### maFile Format 175 | Each account requires a `.maFile` JSON file in the `maFiles/` directory containing: 176 | - `shared_secret` - For TOTP generation 177 | - `identity_secret` - For confirmations 178 | - `account_name` - Steam username 179 | - `Session` data with SteamID and AccessToken 180 | 181 | ## How It Works 182 | 183 | ### TOTP Generation 184 | 1. Queries Steam's time server for accurate timestamp 185 | 2. Generates HMAC-SHA1 hash using shared_secret 186 | 3. Converts to Steam's custom base-26 character set 187 | 4. Displays 5-character code that refreshes every 30 seconds 188 | 189 | ### Confirmation System 190 | 1. Generates confirmation key using identity_secret 191 | 2. Fetches pending confirmations from Steam API 192 | 3. Authenticates using JWT access tokens (auto-refreshes if expired) 193 | 4. Sends approval/denial requests to Steam servers 194 | 195 | ### Automatic Token Refresh 196 | 1. Detects expired access tokens 197 | 2. Gets RSA public key from Steam 198 | 3. Encrypts password using RSA 199 | 4. Begins authentication session 200 | 5. Automatically submits 2FA code from shared_secret 201 | 6. Polls for new tokens 202 | 7. Updates .maFile with fresh tokens 203 | 8. All happens seamlessly in the background 204 | 205 | ## Security Notes 206 | 207 | ⚠️ **Important Security Considerations:** 208 | 209 | - `.maFile` files contain sensitive authentication data 210 | - Keep your `maFiles/` directory private (chmod 700) 211 | - Access tokens grant full account access 212 | - Never share or commit `.maFile` files to version control 213 | - Store revocation codes separately 214 | 215 | The `.gitignore` file is configured to prevent accidental commits of sensitive data. 216 | 217 | ## Keyboard Shortcuts 218 | 219 | | Key | Action | 220 | |-----|--------| 221 | | `2` | Enter verification mode | 222 | | `Ctrl+C` | Exit application | 223 | | `Enter` | Return from verification mode | 224 | 225 | ## Technical Details 226 | 227 | - **Language:** Python 3.x 228 | - **Authentication:** HMAC-SHA1 TOTP 229 | - **Time Sync:** Steam API (`ITwoFactorService/QueryTime`) 230 | - **Confirmations:** Steam Community Mobile API 231 | - **Progress Bar:** 30-second cycle matching TOTP interval 232 | 233 | ## Troubleshooting 234 | 235 | **Codes not working?** 236 | - Ensure system time is accurate 237 | - Check Steam API connectivity 238 | - Verify shared_secret in .maFile is correct 239 | 240 | **Confirmations not loading?** 241 | - Verify AccessToken is not expired 242 | - Check identity_secret is valid 243 | - Ensure network access to steamcommunity.com 244 | 245 | **Input not working?** 246 | - macOS/Linux: Non-blocking input uses `select` module 247 | - Windows: Input checking may have limited support 248 | 249 | ## Platform Support 250 | 251 | - ✅ macOS (tested) 252 | - ✅ Linux (should work) 253 | - ⚠️ Windows (limited input support) 254 | 255 | ## Updates 256 | 257 | **Latest Changes (November 27-28, 2025):** 258 | - ✅ Fixed progress bar timing to match 30-second TOTP cycle 259 | - ✅ Added Steam mobile confirmation support 260 | - ✅ Implemented multi-account confirmation management 261 | - ✅ Added batch approval functionality 262 | - ✅ Fixed confirmation API based on SteamDesktopAuthenticator analysis 263 | - Corrected endpoint from /getconf to /getlist 264 | - Switched from Bearer token to cookie-based authentication 265 | - Updated User-Agent to okhttp/3.12.12 266 | - Added proper sessionid and mobile client cookies 267 | - ✅ **Automatic Token Refresh** - Implements full Steam authentication flow 268 | - RSA password encryption 269 | - Automatic 2FA code submission 270 | - Token refresh without SteamDesktopAuthenticator 271 | - Works seamlessly when tokens expire 272 | 273 | ## License 274 | 275 | This is a personal utility tool. Use at your own risk. 276 | 277 | ## Disclaimer 278 | 279 | This tool interacts with Steam's authentication systems. Ensure you understand the security implications before use. Always keep your authentication files secure and private. 280 | 281 | -------------------------------------------------------------------------------- /SteamGuard.py: -------------------------------------------------------------------------------- 1 | import os 2 | import time 3 | import hmac 4 | import json 5 | import struct 6 | import base64 7 | import requests 8 | from hashlib import sha1 9 | import platform 10 | import urllib.parse 11 | import select 12 | import sys 13 | import secrets 14 | 15 | BAR_LEN = 37 16 | elements = ['-', '\\', '|', '/'] 17 | 18 | # Global variable to cache Steam time offset 19 | _steam_time_offset = None 20 | _last_time_sync = 0 21 | 22 | def getQueryTime(): 23 | """Get Steam server time offset and cache it""" 24 | global _steam_time_offset, _last_time_sync 25 | 26 | current_time = time.time() 27 | 28 | # Resync every 5 minutes or if not set 29 | if _steam_time_offset is None or (current_time - _last_time_sync) > 300: 30 | try: 31 | request = requests.post('https://api.steampowered.com/ITwoFactorService/QueryTime/v0001', timeout=30) 32 | json_data = request.json() 33 | server_time = int(json_data['response']['server_time']) 34 | _steam_time_offset = server_time - current_time 35 | _last_time_sync = current_time 36 | print(f"[Synced with Steam] Offset: {_steam_time_offset:.2f}s") 37 | except: 38 | if _steam_time_offset is None: 39 | _steam_time_offset = 0 40 | 41 | return _steam_time_offset 42 | 43 | 44 | def getGuardCode(shared_secret): 45 | code = '' 46 | timestamp = time.time() + getQueryTime() 47 | _hmac = hmac.new(base64.b64decode(shared_secret), struct.pack('>Q', int(timestamp/30)), sha1).digest() 48 | _ord = ord(_hmac[19:20]) & 0xF 49 | value = struct.unpack('>I', _hmac[_ord:_ord+4])[0] & 0x7fffffff 50 | for i in range(5): 51 | code += symbols[value % len(symbols)] 52 | value = int(value / len(symbols)) 53 | return code 54 | 55 | 56 | def generate_device_id(steamid): 57 | hexed_steam_id = sha1(str(steamid).encode('ascii')).hexdigest() 58 | return f"android:{hexed_steam_id[:8]}-{hexed_steam_id[8:12]}-{hexed_steam_id[12:16]}-{hexed_steam_id[16:20]}-{hexed_steam_id[20:32]}" 59 | 60 | 61 | def generate_confirmation_key(identity_secret, tag, timestamp=None): 62 | if timestamp is None: 63 | timestamp = int(time.time()) 64 | 65 | buffer = struct.pack('>Q', timestamp) + tag.encode('ascii') 66 | _hmac = hmac.new(base64.b64decode(identity_secret), buffer, sha1).digest() 67 | return base64.b64encode(_hmac).decode('ascii') 68 | 69 | 70 | def is_token_expired(token): 71 | try: 72 | token_parts = token.split('.') 73 | if len(token_parts) < 2: 74 | return True 75 | 76 | payload = token_parts[1] 77 | payload = payload.replace('-', '+').replace('_', '/') 78 | 79 | padding = 4 - len(payload) % 4 80 | if padding != 4: 81 | payload += '=' * padding 82 | 83 | payload_bytes = base64.b64decode(payload) 84 | payload_data = json.loads(payload_bytes.decode('utf-8')) 85 | 86 | exp_time = payload_data.get('exp', 0) 87 | current_time = int(time.time()) 88 | 89 | return current_time > exp_time 90 | except: 91 | return True 92 | 93 | 94 | def steam_login_with_tokens(username, password, shared_secret): 95 | try: 96 | import rsa 97 | 98 | session = requests.Session() 99 | 100 | rsa_response = session.get( 101 | 'https://api.steampowered.com/IAuthenticationService/GetPasswordRSAPublicKey/v1/', 102 | params={'account_name': username}, 103 | timeout=30 104 | ) 105 | 106 | if rsa_response.status_code != 200: 107 | return None, None 108 | 109 | rsa_data = rsa_response.json() 110 | rsa_mod = int(rsa_data['response']['publickey_mod'], 16) 111 | rsa_exp = int(rsa_data['response']['publickey_exp'], 16) 112 | rsa_timestamp = rsa_data['response']['timestamp'] 113 | 114 | public_key = rsa.PublicKey(rsa_mod, rsa_exp) 115 | encrypted_password = rsa.encrypt(password.encode('utf-8'), public_key) 116 | encrypted_password_b64 = base64.b64encode(encrypted_password).decode('utf-8') 117 | 118 | begin_auth_data = { 119 | 'account_name': username, 120 | 'encrypted_password': encrypted_password_b64, 121 | 'encryption_timestamp': rsa_timestamp, 122 | 'remember_login': 'false', 123 | 'platform_type': '2', 124 | 'persistence': '1', 125 | 'website_id': 'Mobile' 126 | } 127 | 128 | begin_response = session.post( 129 | 'https://api.steampowered.com/IAuthenticationService/BeginAuthSessionViaCredentials/v1/', 130 | data=begin_auth_data, 131 | timeout=30 132 | ) 133 | 134 | if begin_response.status_code != 200: 135 | return None, None 136 | 137 | begin_result = begin_response.json() 138 | 139 | if 'response' not in begin_result: 140 | return None, None 141 | 142 | response_data = begin_result['response'] 143 | client_id = response_data.get('client_id') 144 | request_id = response_data.get('request_id') 145 | allowed_confirmations = response_data.get('allowed_confirmations', []) 146 | 147 | if not client_id or not request_id: 148 | return None, None 149 | 150 | needs_twofactor = any(c.get('confirmation_type') == 3 for c in allowed_confirmations) 151 | 152 | if needs_twofactor: 153 | code = getGuardCode(shared_secret) 154 | 155 | update_data = { 156 | 'client_id': client_id, 157 | 'steamid': response_data.get('steamid'), 158 | 'code': code, 159 | 'code_type': '3' 160 | } 161 | 162 | update_response = session.post( 163 | 'https://api.steampowered.com/IAuthenticationService/UpdateAuthSessionWithSteamGuardCode/v1/', 164 | data=update_data, 165 | timeout=30 166 | ) 167 | 168 | if update_response.status_code != 200: 169 | return None, None 170 | 171 | for attempt in range(30): 172 | time.sleep(1) 173 | 174 | poll_data = { 175 | 'client_id': client_id, 176 | 'request_id': request_id 177 | } 178 | 179 | poll_response = session.post( 180 | 'https://api.steampowered.com/IAuthenticationService/PollAuthSessionStatus/v1/', 181 | data=poll_data, 182 | timeout=30 183 | ) 184 | 185 | if poll_response.status_code != 200: 186 | continue 187 | 188 | poll_result = poll_response.json() 189 | 190 | if 'response' not in poll_result: 191 | continue 192 | 193 | tokens = poll_result['response'] 194 | access_token = tokens.get('access_token') 195 | refresh_token = tokens.get('refresh_token') 196 | 197 | if access_token and refresh_token: 198 | return access_token, refresh_token 199 | 200 | return None, None 201 | except Exception as e: 202 | return None, None 203 | 204 | 205 | def refresh_access_token(mafile_data, credentials=None): 206 | try: 207 | refresh_token = mafile_data['Session']['RefreshToken'] 208 | steamid = mafile_data['Session']['SteamID'] 209 | 210 | if is_token_expired(refresh_token): 211 | if credentials: 212 | username, password = credentials 213 | shared_secret = mafile_data['shared_secret'] 214 | access_token, refresh_token = steam_login_with_tokens(username, password, shared_secret) 215 | if access_token and refresh_token: 216 | mafile_data['Session']['AccessToken'] = access_token 217 | mafile_data['Session']['RefreshToken'] = refresh_token 218 | return access_token 219 | return None 220 | 221 | data = { 222 | 'refresh_token': refresh_token, 223 | 'steamid': str(steamid) 224 | } 225 | 226 | response = requests.post( 227 | 'https://api.steampowered.com/IAuthenticationService/GenerateAccessTokenForApp/v1/', 228 | data=data, 229 | timeout=30 230 | ) 231 | 232 | if response.status_code == 200: 233 | result = response.json() 234 | new_access_token = result.get('response', {}).get('access_token') 235 | if new_access_token: 236 | mafile_data['Session']['AccessToken'] = new_access_token 237 | return new_access_token 238 | return None 239 | except: 240 | return None 241 | 242 | 243 | def save_mafile(mafile_path, mafile_data): 244 | try: 245 | with open(mafile_path, 'w') as f: 246 | json.dump(mafile_data, f) 247 | return True 248 | except: 249 | return False 250 | 251 | 252 | def generate_session_id(): 253 | return secrets.token_hex(16) 254 | 255 | 256 | def get_cookies(steamid, access_token, session_id=None): 257 | if session_id is None: 258 | session_id = generate_session_id() 259 | 260 | steam_login_secure = f"{steamid}%7C%7C{access_token}" 261 | 262 | cookies = { 263 | 'steamLoginSecure': steam_login_secure, 264 | 'sessionid': session_id, 265 | 'mobileClient': 'android', 266 | 'mobileClientVersion': '777777 3.6.1' 267 | } 268 | return cookies 269 | 270 | 271 | def get_confirmations(mafile_data): 272 | try: 273 | timestamp = int(time.time()) 274 | identity_secret = mafile_data['identity_secret'] 275 | steamid = mafile_data['Session']['SteamID'] 276 | access_token = mafile_data['Session']['AccessToken'] 277 | device_id = mafile_data.get('device_id', generate_device_id(steamid)) 278 | 279 | conf_key = generate_confirmation_key(identity_secret, 'conf', timestamp) 280 | 281 | params = { 282 | 'p': device_id, 283 | 'a': steamid, 284 | 'k': conf_key, 285 | 't': timestamp, 286 | 'm': 'react', 287 | 'tag': 'conf' 288 | } 289 | 290 | headers = { 291 | 'User-Agent': 'okhttp/3.12.12' 292 | } 293 | 294 | cookies = get_cookies(steamid, access_token) 295 | 296 | url = f"https://steamcommunity.com/mobileconf/getlist?{urllib.parse.urlencode(params)}" 297 | response = requests.get(url, headers=headers, cookies=cookies, timeout=30) 298 | 299 | if response.status_code == 200: 300 | data = response.json() 301 | if data.get('success'): 302 | confirmations = data.get('conf', []) 303 | for conf in confirmations: 304 | conf['_timestamp'] = timestamp 305 | conf['_device_id'] = device_id 306 | return confirmations 307 | return [] 308 | except: 309 | return [] 310 | 311 | 312 | def send_confirmation(mafile_data, conf_id, conf_key, operation='allow'): 313 | try: 314 | timestamp = int(time.time()) 315 | identity_secret = mafile_data['identity_secret'] 316 | steamid = mafile_data['Session']['SteamID'] 317 | access_token = mafile_data['Session']['AccessToken'] 318 | device_id = mafile_data.get('device_id', generate_device_id(steamid)) 319 | 320 | tag = 'accept' if operation == 'allow' else 'reject' 321 | conf_key_generated = generate_confirmation_key(identity_secret, tag, timestamp) 322 | 323 | params = { 324 | 'op': operation, 325 | 'p': device_id, 326 | 'a': steamid, 327 | 'k': conf_key_generated, 328 | 't': timestamp, 329 | 'm': 'react', 330 | 'tag': tag, 331 | 'cid': conf_id, 332 | 'ck': conf_key 333 | } 334 | 335 | headers = { 336 | 'User-Agent': 'okhttp/3.12.12' 337 | } 338 | 339 | cookies = get_cookies(steamid, access_token) 340 | 341 | url = f"https://steamcommunity.com/mobileconf/ajaxop?{urllib.parse.urlencode(params)}" 342 | response = requests.get(url, headers=headers, cookies=cookies, timeout=30) 343 | 344 | if response.status_code == 200: 345 | data = response.json() 346 | return data.get('success', False) 347 | return False 348 | except: 349 | return False 350 | 351 | 352 | def verification_mode(): 353 | clear_console() 354 | script_dir = os.path.dirname(os.path.abspath(__file__)) 355 | mafiles_dir = os.path.join(script_dir, 'maFiles') 356 | 357 | credentials_map = { 358 | 'xkce27': ('xkce27', 'ymw@ntz3xau2gtk-VCT'), 359 | 'juanpi1588': ('juanpi1588', 'EHD_xtx-cnc0drw0wgp'), 360 | 'trikce': ('trikce', 'eqb6UYR5hxa1pvc-qna') 361 | } 362 | 363 | all_confirmations = [] 364 | account_confirmations = {} 365 | 366 | with os.scandir(mafiles_dir) as files: 367 | for file in files: 368 | if file.is_file() and file.name.endswith('.maFile'): 369 | file_path = file.path 370 | with open(file, 'r') as f: 371 | data = json.loads(f.read()) 372 | account_name = data['account_name'] 373 | 374 | access_token = data['Session']['AccessToken'] 375 | if is_token_expired(access_token): 376 | print(f"Token expired for {account_name}, refreshing...") 377 | credentials = credentials_map.get(account_name) 378 | if credentials: 379 | new_token = refresh_access_token(data, credentials) 380 | if new_token: 381 | print(f"✓ Token refreshed for {account_name}") 382 | save_mafile(file_path, data) 383 | else: 384 | print(f"✗ Failed to refresh token for {account_name}") 385 | continue 386 | else: 387 | print(f"✗ No credentials found for {account_name}") 388 | continue 389 | 390 | confirmations = get_confirmations(data) 391 | if confirmations: 392 | account_confirmations[account_name] = { 393 | 'data': data, 394 | 'confirmations': confirmations 395 | } 396 | all_confirmations.extend(confirmations) 397 | 398 | if not all_confirmations: 399 | print("No pending confirmations.\n") 400 | input("Press Enter to return...") 401 | return 402 | 403 | print("=" * 60) 404 | print(f"PENDING CONFIRMATIONS: {len(all_confirmations)}") 405 | print("=" * 60) 406 | 407 | conf_types = { 408 | 1: 'Test', 409 | 2: 'Trade', 410 | 3: 'Market Listing', 411 | 4: 'Feature Opt-Out', 412 | 5: 'Phone Number Change', 413 | 6: 'Account Recovery' 414 | } 415 | 416 | index = 1 417 | confirmation_map = {} 418 | for account_name, info in account_confirmations.items(): 419 | for conf in info['confirmations']: 420 | conf_type = conf_types.get(conf.get('type', 0), 'Unknown') 421 | headline = conf.get('headline', 'Unknown') 422 | summary_lines = conf.get('summary', ['No description']) 423 | creator = conf.get('creator_id', 'N/A') 424 | 425 | # Calculate verification code (last 5 digits of creator ID) 426 | verification_code = str(creator)[-5:] if creator != 'N/A' else 'N/A' 427 | 428 | print(f"{index}. [{account_name}] {headline}") 429 | if creator != 'N/A': 430 | print(f" Creator ID: {creator}") 431 | print(f" Verification Code: [{verification_code}]") 432 | 433 | for line in summary_lines[:3]: 434 | if line: 435 | print(f" {line}") 436 | 437 | print() 438 | 439 | confirmation_map[index] = { 440 | 'account': account_name, 441 | 'conf': conf, 442 | 'data': info['data'] 443 | } 444 | index += 1 445 | 446 | print("=" * 60) 447 | print("SELECT ACTION:") 448 | print("=" * 60) 449 | print("[Number] - Select confirmation") 450 | print("[A] - Approve All") 451 | print("[D] - Deny All") 452 | print("[Q] - Quit") 453 | print("=" * 60) 454 | 455 | choice = input("\nYour choice: ").strip().lower() 456 | 457 | if choice == 'a': 458 | clear_console() 459 | print("\n" + "!" * 60) 460 | print("WARNING: You are about to APPROVE ALL confirmations!") 461 | print("This action cannot be undone.") 462 | print("!" * 60) 463 | confirm = input("\nPress 1 to CONFIRM, 2 to CANCEL: ").strip() 464 | 465 | if confirm == '1': 466 | clear_console() 467 | print("\nApproving all confirmations...\n") 468 | for idx, item in confirmation_map.items(): 469 | conf = item['conf'] 470 | conf_type = conf_types.get(conf.get('type', 0), 'Unknown') 471 | result = send_confirmation( 472 | item['data'], 473 | conf['id'], 474 | conf['nonce'], 475 | 'allow' 476 | ) 477 | status = "✓" if result else "✗" 478 | print(f"{status} [{item['account']}] {conf.get('headline', 'Unknown')} ({conf_type})") 479 | input("\nPress Enter to return...") 480 | else: 481 | clear_console() 482 | print("\nCancelled.") 483 | input("Press Enter to return...") 484 | elif choice == 'd': 485 | clear_console() 486 | print("\n" + "!" * 60) 487 | print("WARNING: You are about to DENY ALL confirmations!") 488 | print("This action cannot be undone.") 489 | print("!" * 60) 490 | confirm = input("\nPress 1 to CONFIRM, 2 to CANCEL: ").strip() 491 | 492 | if confirm == '1': 493 | clear_console() 494 | print("\nDenying all confirmations...\n") 495 | for idx, item in confirmation_map.items(): 496 | conf = item['conf'] 497 | conf_type = conf_types.get(conf.get('type', 0), 'Unknown') 498 | result = send_confirmation( 499 | item['data'], 500 | conf['id'], 501 | conf['nonce'], 502 | 'cancel' 503 | ) 504 | status = "✓" if result else "✗" 505 | print(f"{status} [{item['account']}] {conf.get('headline', 'Unknown')} ({conf_type})") 506 | input("\nPress Enter to return...") 507 | else: 508 | clear_console() 509 | print("\nCancelled.") 510 | input("Press Enter to return...") 511 | elif choice == 'q': 512 | return 513 | elif choice.isdigit(): 514 | idx = int(choice) 515 | if idx in confirmation_map: 516 | clear_console() 517 | item = confirmation_map[idx] 518 | conf = item['conf'] 519 | conf_type = conf_types.get(conf.get('type', 0), 'Unknown') 520 | headline = conf.get('headline', 'Unknown') 521 | 522 | print("\n" + "=" * 60) 523 | print(f"SELECTED: [{item['account']}] {headline}") 524 | print("=" * 60) 525 | print("\nACTION:") 526 | print("[1] - APPROVE") 527 | print("[2] - DENY") 528 | print("[Q] - Cancel") 529 | print("=" * 60) 530 | 531 | action = input("\nYour choice: ").strip().lower() 532 | 533 | if action == '1': 534 | print(f"\nApproving...") 535 | result = send_confirmation( 536 | item['data'], 537 | conf['id'], 538 | conf['nonce'], 539 | 'allow' 540 | ) 541 | if result: 542 | print(f"✓ Successfully approved!") 543 | else: 544 | print(f"✗ Failed to approve - please try again") 545 | input("\nPress Enter to return...") 546 | elif action == '2': 547 | print(f"\nDenying...") 548 | result = send_confirmation( 549 | item['data'], 550 | conf['id'], 551 | conf['nonce'], 552 | 'cancel' 553 | ) 554 | if result: 555 | print(f"✓ Successfully denied!") 556 | else: 557 | print(f"✗ Failed to deny - please try again") 558 | input("\nPress Enter to return...") 559 | else: 560 | clear_console() 561 | print("\nCancelled.") 562 | input("Press Enter to return...") 563 | else: 564 | clear_console() 565 | print("\nInvalid number") 566 | input("Press Enter to return...") 567 | else: 568 | clear_console() 569 | print("\nInvalid choice") 570 | input("Press Enter to return...") 571 | 572 | 573 | def get_time_remaining(): 574 | """Get seconds remaining until next TOTP code""" 575 | current_time = int(time.time() + getQueryTime()) 576 | return 30 - (current_time % 30) 577 | 578 | 579 | def draw_progress_bar(seconds_remaining, total_seconds=30, width=30): 580 | """Draw a clean progress bar""" 581 | filled = int(((total_seconds - seconds_remaining) / total_seconds) * width) 582 | bar = '█' * filled + '░' * (width - filled) 583 | return bar 584 | 585 | 586 | def run_code(): 587 | script_dir = os.path.dirname(os.path.abspath(__file__)) 588 | mafiles_dir = os.path.join(script_dir, 'maFiles') 589 | if not os.listdir(mafiles_dir): 590 | print("Directory is empty") 591 | return 592 | 593 | # Load all accounts 594 | accounts = [] 595 | with os.scandir(mafiles_dir) as files: 596 | for file in files: 597 | if file.is_file() and file.name.endswith('.maFile'): 598 | with open(file, 'r') as f: 599 | data = json.loads(f.read()) 600 | accounts.append({ 601 | 'name': data['account_name'], 602 | 'steamid': data['Session']['SteamID'], 603 | 'secret': data['shared_secret'] 604 | }) 605 | 606 | if not accounts: 607 | print("No accounts found") 608 | return 609 | 610 | # Initial sync with Steam 611 | print("\n[Syncing with Steam...]") 612 | getQueryTime() 613 | 614 | last_code_time = 0 615 | last_display_time = 0 616 | 617 | while True: 618 | current_time = time.time() 619 | time_remaining = get_time_remaining() 620 | current_code_time = (int(time.time() + getQueryTime()) // 30) 621 | 622 | # Regenerate codes if new cycle 623 | if current_code_time != last_code_time: 624 | for account in accounts: 625 | account['code'] = getGuardCode(account['secret']) 626 | last_code_time = current_code_time 627 | 628 | # Update display every second 629 | if current_time - last_display_time >= 1.0: 630 | clear_console() 631 | 632 | print("=" * 60) 633 | print("STEAM GUARD AUTHENTICATOR") 634 | print("=" * 60) 635 | print("\nPress 'Ctrl+C' to stop | Type '2' + Enter for confirmations\n") 636 | 637 | # Display each account 638 | for account in accounts: 639 | code = account.get('code', getGuardCode(account['secret'])) 640 | bar = draw_progress_bar(time_remaining) 641 | 642 | print(f"Username: {account['name']}") 643 | print(f"SteamId: {account['steamid']}") 644 | print(f"GuardCode: {code} [{bar}] {time_remaining}s") 645 | print() 646 | 647 | print("\n> ", end='', flush=True) 648 | 649 | last_display_time = current_time 650 | 651 | # Check for user input (non-blocking) 652 | if platform.system() != 'Windows': 653 | readable, _, _ = select.select([sys.stdin], [], [], 0.1) 654 | if readable: 655 | user_input = sys.stdin.readline().strip() 656 | # Only respond to '2' 657 | if user_input == '2': 658 | print("[Loading confirmations...]") 659 | verification_mode() 660 | last_display_time = 0 # Force immediate redraw 661 | else: 662 | time.sleep(0.1) 663 | 664 | 665 | def clear_console(): 666 | # Clear the console based on the operating system 667 | if platform.system() == 'Windows': 668 | os.system('cls') 669 | else: 670 | os.system('clear') 671 | 672 | 673 | symbols = '23456789BCDFGHJKMNPQRTVWXY' 674 | 675 | # Initial clear only 676 | clear_console() 677 | 678 | try: 679 | run_code() 680 | except KeyboardInterrupt: 681 | print("\n\nExiting...") 682 | pass 683 | --------------------------------------------------------------------------------