├── confirmation.go ├── README.md ├── constants.go ├── unistr.go ├── LICENSE ├── time_aligner.go ├── session_data.go ├── example └── example.go ├── steam_web.go ├── authenticator_linker.go ├── user_login.go └── steam_guard_account.go /confirmation.go: -------------------------------------------------------------------------------- 1 | package mobileauth 2 | 3 | type Confirmation struct { 4 | ConfirmationID string 5 | ConfirmationKey string 6 | ConfirmationDescription string 7 | } 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![GoDoc](http://godoc.org/github.com/YellowOrWhite/go-steam-mobileauth?status.png)](http://godoc.org/github.com/YellowOrWhite/go-steam-mobileauth) 2 | 3 | A Go library that provides Steam Mobile Authenticator functionality 4 | 5 | This is a Go port of C# [SteamAuth library](https://github.com/geel9/SteamAuth) 6 | -------------------------------------------------------------------------------- /constants.go: -------------------------------------------------------------------------------- 1 | package mobileauth 2 | 3 | import ( 4 | "net/url" 5 | ) 6 | 7 | // Cookie path 8 | var cookiePath, _ = url.Parse("https://steamcommunity.com/") 9 | 10 | const UrlSteamApiBase string = "https://api.steampowered.com" 11 | const UrlCommunityBase string = "https://steamcommunity.com" 12 | const UrlConfirmationService string = UrlCommunityBase + "/mobileconf" 13 | const UrlMobileAuthService string = UrlSteamApiBase + "/IMobileAuthService" 14 | const UrlTwoFactorService string = UrlSteamApiBase + "/ITwoFactorService" 15 | -------------------------------------------------------------------------------- /unistr.go: -------------------------------------------------------------------------------- 1 | package mobileauth 2 | 3 | import ( 4 | "encoding/json" 5 | ) 6 | 7 | // Steam JSON has fields which can be either string in normal operation 8 | // or number when some error occurs. 9 | // UniStr unmarshals from string and silently ignores other types 10 | type uniStr string 11 | 12 | func (s *uniStr) UnmarshalJSON(data []byte) error { 13 | // non string 14 | if len(data) < 2 || data[0] != '"' || data[len(data)-1] != '"' { 15 | *s = uniStr("") 16 | return nil 17 | } 18 | // string 19 | var aux string 20 | if err := json.Unmarshal(data, &aux); err != nil { 21 | return err 22 | } 23 | *s = uniStr(aux) 24 | return nil 25 | } 26 | 27 | func (s uniStr) String() string { 28 | return string(s) 29 | } 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 YellowOrWhite 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /time_aligner.go: -------------------------------------------------------------------------------- 1 | package mobileauth 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "errors" 7 | "io/ioutil" 8 | "net/http" 9 | "time" 10 | ) 11 | 12 | var _isTimeAligned bool 13 | var _steamTimeDifference int64 14 | 15 | func GetSteamTime() int64 { 16 | if !_isTimeAligned { 17 | AlignTime() 18 | } 19 | return time.Now().Unix() + _steamTimeDifference 20 | } 21 | 22 | // Aligns system time with the Steam server time. Not super advanced; 23 | // probably not taking some things into account that it should. 24 | // Necessary to generate up-to-date codes. In general, this will have an error 25 | // of less than a second, assuming Steam is operational. 26 | func AlignTime() error { 27 | now := time.Now().Unix() 28 | client := new(http.Client) 29 | resp, err := client.Post(UrlTwoFactorService+"/QueryTime/v0001", "application/x-www-form-urlencoded", bytes.NewBuffer([]byte("steamid=0"))) 30 | if err != nil { 31 | return err 32 | } 33 | defer resp.Body.Close() 34 | respBody, err := ioutil.ReadAll(resp.Body) 35 | if err != nil { 36 | return err 37 | } 38 | r := timeQueryResponse{} 39 | if err = json.Unmarshal(respBody, &r); err != nil { 40 | return err 41 | } 42 | if r.Response == nil { 43 | return errors.New("steam returned empty time query response") 44 | } 45 | _steamTimeDifference = r.Response.ServerTime - now 46 | _isTimeAligned = true 47 | return nil 48 | } 49 | 50 | type timeQueryResponse struct { 51 | Response *timeQueryResult `json:"response"` 52 | } 53 | 54 | type timeQueryResult struct { 55 | ServerTime int64 `json:"server_time,string"` 56 | } 57 | -------------------------------------------------------------------------------- /session_data.go: -------------------------------------------------------------------------------- 1 | package mobileauth 2 | 3 | import ( 4 | "net/http" 5 | "net/http/cookiejar" 6 | "strconv" 7 | ) 8 | 9 | type SessionData struct { 10 | SessionID string 11 | SteamLogin string 12 | SteamLoginSecure string 13 | WebCookie string 14 | OAuthToken string 15 | SteamID uint64 16 | } 17 | 18 | func (sd *SessionData) AddCookies(cookies *cookiejar.Jar) { 19 | cookies.SetCookies(cookiePath, []*http.Cookie{ 20 | &http.Cookie{ 21 | Name: "mobileClientVersion", 22 | Value: "0 (2.1.3)", 23 | Path: "/", 24 | Domain: ".steamcommunity.com", 25 | }, 26 | &http.Cookie{ 27 | Name: "mobileClient", 28 | Value: "android", 29 | Path: "/", 30 | Domain: ".steamcommunity.com", 31 | }, 32 | &http.Cookie{ 33 | Name: "Steam_Language", 34 | Value: "english", 35 | Path: "/", 36 | Domain: ".steamcommunity.com", 37 | }, 38 | &http.Cookie{ 39 | Name: "steamid", 40 | Value: strconv.FormatUint(sd.SteamID, 10), 41 | Path: "/", 42 | Domain: ".steamcommunity.com", 43 | }, 44 | &http.Cookie{ 45 | Name: "steamLogin", 46 | Value: sd.SteamLogin, 47 | Path: "/", 48 | Domain: ".steamcommunity.com", 49 | HttpOnly: true, 50 | }, 51 | &http.Cookie{ 52 | Name: "steamLoginSecure", 53 | Value: sd.SteamLoginSecure, 54 | Path: "/", 55 | Domain: ".steamcommunity.com", 56 | Secure: true, 57 | HttpOnly: true, 58 | }, 59 | &http.Cookie{ 60 | Name: "dob", 61 | Value: "", 62 | Path: "/", 63 | Domain: ".steamcommunity.com", 64 | }, 65 | &http.Cookie{ 66 | Name: "sessionid", 67 | Value: sd.SessionID, 68 | Path: "/", 69 | Domain: ".steamcommunity.com", 70 | }, 71 | }) 72 | } 73 | -------------------------------------------------------------------------------- /example/example.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "github.com/YellowOrWhite/go-steam-mobileauth" 7 | "io/ioutil" 8 | "strconv" 9 | ) 10 | 11 | func main() { 12 | // This basic loop will log into user accounts you specify, enable the mobile authenticator, and save a maFile (mobile authenticator file) 13 | for { 14 | fmt.Print("Enter username: ") 15 | var username string 16 | fmt.Scanln(&username) 17 | fmt.Print("Enter password: ") 18 | var password string 19 | fmt.Scanln(&password) 20 | ul := mobileauth.NewUserLogin(username, password) 21 | for { 22 | if err := ul.DoLogin(); err != nil { 23 | switch err { 24 | case mobileauth.ErrNeedEmail: 25 | fmt.Print("Please enter your email code: ") 26 | var code string 27 | fmt.Scanln(&code) 28 | ul.EmailCode = code 29 | case mobileauth.ErrNeedCaptcha: 30 | fmt.Println("https://steamcommunity.com/public/captcha.php?gid=" + ul.CaptchaGID) 31 | fmt.Println("Please follow link to get captcha text.") 32 | fmt.Print("Please enter captcha text: ") 33 | var captchaText string 34 | fmt.Scanln(&captchaText) 35 | ul.CaptchaText = captchaText 36 | case mobileauth.ErrNeed2FA: 37 | fmt.Print("Please enter your mobile authenticator code: ") 38 | var code string 39 | fmt.Scanln(&code) 40 | ul.TwoFactorCode = code 41 | default: 42 | fmt.Printf("Failed to login: %v\n", err) 43 | return 44 | } 45 | } else { 46 | break 47 | } 48 | } 49 | 50 | linker := mobileauth.NewAuthenticatorLinker(ul.Session) 51 | linker.PhoneNumber = "" // Set this to add a new phone number to the account. 52 | if err := linker.AddAuthenticator(); err != nil { 53 | fmt.Printf("Failed to add authenticator: %v\n", err) 54 | continue 55 | } 56 | 57 | fileContent, err := json.Marshal(linker.LinkedAccount) 58 | if err != nil { 59 | panic("Failed to marshal LinkedAccount. For security, authenticator will not be finalized.") 60 | } 61 | fileName := strconv.FormatUint(linker.LinkedAccount.Session.SteamID, 10) + ".maFile" 62 | // write file 63 | err = ioutil.WriteFile(fileName, fileContent, 0644) 64 | if err != nil { 65 | panic("Failed to save maFile. For security, authenticator will not be finalized.") 66 | } 67 | 68 | fmt.Print("Please enter SMS code: ") 69 | var smsCode string 70 | fmt.Scanln(&smsCode) 71 | if err = linker.FinalizeAddAuthenticator(smsCode); err != nil { 72 | fmt.Printf("Failed to finalize authenticator: %v\n", err) 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /steam_web.go: -------------------------------------------------------------------------------- 1 | package mobileauth 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "io/ioutil" 7 | "net/http" 8 | "net/http/cookiejar" 9 | "net/url" 10 | "strings" 11 | ) 12 | 13 | // Perform a mobile login request 14 | // Method must be GET or POST 15 | // Returns response body 16 | func MobileLoginRequest(queryUrl, method string, params *url.Values, cookies *cookiejar.Jar, headers *map[string]string) ([]byte, error) { 17 | referer := UrlCommunityBase + "/mobilelogin?oauth_client_id=DE45CD61&oauth_scope=read_profile%20write_profile%20read_client%20write_client" 18 | return WebRequest(queryUrl, method, params, cookies, headers, &referer) 19 | } 20 | 21 | func WebRequest(queryUrl, method string, params *url.Values, cookies *cookiejar.Jar, headers *map[string]string, referer *string) ([]byte, error) { 22 | if referer == nil { 23 | aux := UrlCommunityBase 24 | referer = &aux 25 | } 26 | 27 | client := new(http.Client) 28 | 29 | // Create request 30 | var req *http.Request 31 | var err error 32 | switch method { 33 | case "GET": 34 | if params != nil { 35 | if strings.Contains(queryUrl, "?") { 36 | queryUrl = queryUrl + "&" 37 | } else { 38 | queryUrl = queryUrl + "?" 39 | } 40 | queryUrl = queryUrl + params.Encode() 41 | } 42 | req, err = http.NewRequest(method, queryUrl, nil) 43 | if err != nil { 44 | panic("failed to create http request") 45 | } 46 | case "POST": 47 | if params == nil { 48 | params = &url.Values{} 49 | } 50 | req, err = http.NewRequest(method, queryUrl, bytes.NewBufferString(params.Encode())) 51 | if err != nil { 52 | panic("failed to create http request") 53 | } 54 | req.Header.Set("Content-Type", "application/x-www-form-urlencoded; charset=UTF-8") 55 | default: 56 | panic("Only POST and GET requests supported") 57 | } 58 | 59 | // Set request header params 60 | req.Header.Set("Accept", "text/javascript, text/html, application/xml, text/xml, */*") 61 | req.Header.Set("User-Agent", "Mozilla/5.0 (Linux; U; Android 4.1.1; en-us; Google Nexus 4 - 4.1.1 - API 16 - 768x1280 Build/JRO03S) AppleWebKit/534.30 (KHTML, like Gecko) Version/4.0 Mobile Safari/534.30") 62 | req.Header.Set("Referer", *referer) 63 | 64 | if headers != nil { 65 | for key, val := range *headers { 66 | req.Header.Add(key, val) 67 | } 68 | } 69 | 70 | // Set cookies 71 | if cookies != nil { 72 | client.Jar = cookies 73 | } 74 | 75 | // Make request 76 | resp, err := client.Do(req) 77 | if err != nil { 78 | return nil, err 79 | } 80 | if resp.StatusCode != 200 { 81 | return nil, fmt.Errorf("request failed with status code: %v", resp.StatusCode) 82 | } 83 | defer resp.Body.Close() 84 | 85 | return ioutil.ReadAll(resp.Body) 86 | } 87 | -------------------------------------------------------------------------------- /authenticator_linker.go: -------------------------------------------------------------------------------- 1 | package mobileauth 2 | 3 | import ( 4 | "crypto/rand" 5 | "crypto/sha1" 6 | "encoding/hex" 7 | "encoding/json" 8 | "errors" 9 | "fmt" 10 | "net/http/cookiejar" 11 | "net/url" 12 | "strconv" 13 | ) 14 | 15 | // ErrMustProvidePhoneNumber is returned by AddAuthenticator 16 | // when no phone set on both authentication linker and account 17 | var ErrMustProvidePhoneNumber = errors.New("no phone number on the account") 18 | 19 | // ErrMustRemovePhoneNumber is returned by AddAuthenticator 20 | // when phone is set on both authentication linker and account 21 | var ErrMustRemovePhoneNumber = errors.New("a phone number is already on the account") 22 | 23 | // ErrAuthenticatorPresent is returned by AddAuthenticator 24 | var ErrAuthenticatorPresent = errors.New("authenticator already present") 25 | 26 | // ErrBadSMSCode is returned by FinalizeAddAuthenticator 27 | // when steam rejects supplied SMS code 28 | var ErrBadSMSCode = errors.New("bad sms code") 29 | 30 | // ErrUnableToGenerateCorrectCodes is returned by FinalizeAddAuthenticator 31 | var ErrUnableToGenerateCorrectCodes = errors.New("unable to generate correct codes") 32 | 33 | // Handles the linking process for a new mobile authenticator 34 | type AuthenticatorLinker struct { 35 | // Set to register a new phone number when linking. 36 | // If a phone number is not set on the account, this must be set. 37 | // If a phone number is set on the account, this must be null. 38 | PhoneNumber string 39 | // Randomly-generated device ID. Should only be generated once per linker. 40 | DeviceID string 41 | // After the initial link step, if successful, this will be 42 | // the SteamGuard data for the account. PLEASE save this somewhere 43 | // after generating it; it's vital data. 44 | LinkedAccount *SteamGuardAccount 45 | // True if the authenticator has been fully finalized. 46 | finalized bool 47 | 48 | _session *SessionData 49 | _cookies *cookiejar.Jar 50 | } 51 | 52 | func NewAuthenticatorLinker(session *SessionData) *AuthenticatorLinker { 53 | cookies, _ := cookiejar.New(nil) 54 | session.AddCookies(cookies) 55 | 56 | return &AuthenticatorLinker{ 57 | DeviceID: generateDeviceID(), 58 | _session: session, 59 | _cookies: cookies, 60 | } 61 | } 62 | 63 | func (al *AuthenticatorLinker) AddAuthenticator() error { 64 | hasPhone, _ := al._hasPhoneAttached() 65 | if hasPhone && al.PhoneNumber != "" { 66 | return ErrMustRemovePhoneNumber 67 | } 68 | if !hasPhone && al.PhoneNumber == "" { 69 | return ErrMustProvidePhoneNumber 70 | } 71 | 72 | if !hasPhone { 73 | if err := al._addPhoneNumber(); err != nil { 74 | return fmt.Errorf("failed to add phone number: %v", err) 75 | } 76 | } 77 | 78 | postData := url.Values{} 79 | postData.Set("access_token", al._session.OAuthToken) 80 | postData.Set("steamid", strconv.FormatUint(al._session.SteamID, 10)) 81 | postData.Set("authenticator_type", "1") 82 | postData.Set("device_identifier", al.DeviceID) 83 | postData.Set("sms_phone_id", "1") 84 | 85 | respBody, err := MobileLoginRequest(UrlSteamApiBase+"/ITwoFactorService/AddAuthenticator/v0001", "POST", &postData, nil, nil) 86 | if err != nil { 87 | return err 88 | } 89 | 90 | // Unmarshal response 91 | r := addAuthenticatorResponse{} 92 | if err := json.Unmarshal(respBody, &r); err != nil { 93 | return err 94 | } 95 | if r.Response == nil { 96 | return errors.New("steam returned empty add authenticator response") 97 | } 98 | if r.Response.Status == 29 { 99 | return ErrAuthenticatorPresent 100 | } 101 | if r.Response.Status != 1 { 102 | return fmt.Errorf("authenticator response status: %v\n", r.Response.Status) 103 | } 104 | 105 | al.LinkedAccount = r.Response 106 | al.LinkedAccount.Session = al._session 107 | al.LinkedAccount.DeviceID = al.DeviceID 108 | 109 | return nil 110 | } 111 | 112 | func (al *AuthenticatorLinker) FinalizeAddAuthenticator(smsCode string) error { 113 | var isSmsCodeGood bool 114 | postData := url.Values{} 115 | postData.Set("steamid", strconv.FormatUint(al._session.SteamID, 10)) 116 | postData.Set("access_token", al._session.OAuthToken) 117 | postData.Set("activation_code", smsCode) 118 | postData.Set("authenticator_code", "") 119 | retryCount := 30 120 | for tries := 0; tries <= retryCount; tries++ { 121 | var steamGuardCode string 122 | if tries != 0 { 123 | var err error 124 | steamGuardCode, err = al.LinkedAccount.GenerateSteamGuardCode() 125 | if err != nil { 126 | return fmt.Errorf("failed to generate steam guard code: %v", err) 127 | } 128 | } 129 | postData.Set("authenticator_code", steamGuardCode) 130 | postData.Set("authenticator_time", strconv.FormatInt(GetSteamTime(), 10)) 131 | 132 | if isSmsCodeGood { 133 | postData.Set("activation_code", "") 134 | } 135 | 136 | respBody, err := MobileLoginRequest(UrlSteamApiBase+"/ITwoFactorService/FinalizeAddAuthenticator/v0001", "POST", &postData, nil, nil) 137 | if err != nil { 138 | return err 139 | } 140 | 141 | r := finalizeAuthenticatorResponse{} 142 | if err := json.Unmarshal(respBody, &r); err != nil { 143 | return err 144 | } 145 | 146 | if r.Response == nil { 147 | return errors.New("steam returned empty finalize authenticator response") 148 | } 149 | 150 | if r.Response.Status == 89 { 151 | return ErrBadSMSCode 152 | } 153 | 154 | if r.Response.Status == 88 { 155 | if tries >= retryCount { 156 | return ErrUnableToGenerateCorrectCodes 157 | } 158 | } 159 | 160 | if !r.Response.Success { 161 | return errors.New("steam returned success false") 162 | } 163 | 164 | if r.Response.WantMore { 165 | isSmsCodeGood = true 166 | continue 167 | } 168 | 169 | al.LinkedAccount.FullyEnrolled = true 170 | return nil 171 | } 172 | 173 | return fmt.Errorf("failed to finalize authenticator in %v tries\n", retryCount) 174 | } 175 | 176 | func (al *AuthenticatorLinker) _addPhoneNumber() error { 177 | postData := url.Values{} 178 | postData.Set("op", "add_phone_number") 179 | postData.Set("arg", al.PhoneNumber) 180 | postData.Set("sessionid", al._session.SessionID) 181 | 182 | respBody, err := WebRequest(UrlCommunityBase+"/steamguard/phoneajax", "POST", &postData, al._cookies, nil, nil) 183 | if err != nil { 184 | return err 185 | } 186 | 187 | r := addPhoneResponse{} 188 | if err := json.Unmarshal(respBody, &r); err != nil { 189 | return err 190 | } 191 | if r.Success != true { 192 | return errors.New("steam returned success false") 193 | } 194 | return nil 195 | } 196 | 197 | func (al *AuthenticatorLinker) _hasPhoneAttached() (bool, error) { 198 | postData := url.Values{} 199 | postData.Set("op", "has_phone") 200 | postData.Set("arg", "null") 201 | postData.Set("sessionid", al._session.SessionID) 202 | 203 | respBody, err := MobileLoginRequest(UrlCommunityBase+"/steamguard/phoneajax", "POST", &postData, al._cookies, nil) 204 | if err != nil { 205 | return false, err 206 | } 207 | 208 | r := hasPhoneResponse{} 209 | if err := json.Unmarshal(respBody, &r); err != nil { 210 | return false, err 211 | } 212 | 213 | return r.HasPhone, nil 214 | } 215 | 216 | type addAuthenticatorResponse struct { 217 | Response *SteamGuardAccount 218 | } 219 | 220 | type finalizeAuthenticatorResponse struct { 221 | Response *finalizeAuthenticatorResult 222 | } 223 | 224 | type finalizeAuthenticatorResult struct { 225 | Status int32 226 | ServerTime int64 `json:"server_time,string"` 227 | WantMore bool `json:"want_more"` 228 | Success bool 229 | } 230 | 231 | type hasPhoneResponse struct { 232 | HasPhone bool `json:"has_phone"` 233 | } 234 | 235 | type addPhoneResponse struct { 236 | Success bool 237 | } 238 | 239 | func generateDeviceID() string { 240 | // Generate 8 random bytes 241 | b := make([]byte, 8) 242 | if _, err := rand.Read(b); err != nil { 243 | panic("Failed to read from source of random bytes") 244 | } 245 | // Generate sha1 hash 246 | hasher := sha1.New() 247 | hasher.Write(b) 248 | deviceId := make([]byte, 40) 249 | hex.Encode(deviceId, hasher.Sum(nil)) 250 | deviceId = deviceId[:32] 251 | // Insert "-" at 8,12,16,20 positions 252 | for i, pos := range []int{8, 12, 16, 20} { 253 | deviceId = append(deviceId[0:pos+i], 254 | append([]byte{'-'}, deviceId[pos+i:]...)...) 255 | } 256 | return "android:" + string(deviceId) 257 | } 258 | -------------------------------------------------------------------------------- /user_login.go: -------------------------------------------------------------------------------- 1 | package mobileauth 2 | 3 | import ( 4 | "crypto/rand" 5 | "crypto/rsa" 6 | "encoding/base64" 7 | "encoding/hex" 8 | "encoding/json" 9 | "errors" 10 | "fmt" 11 | "math/big" 12 | "net/http" 13 | "net/http/cookiejar" 14 | "net/url" 15 | "strconv" 16 | "strings" 17 | "time" 18 | ) 19 | 20 | // ErrBadRSA is returned by DoLogin 21 | // when steam returns success false to rsa public key request 22 | var ErrBadRSA = errors.New("bad RSA") 23 | 24 | // ErrBadCredentials is returned by DoLogin 25 | // when steam fails to authenticate us 26 | var ErrBadCredentials = errors.New("bad credentials") 27 | 28 | // ErrNeedCaptcha is returned by DoLogin 29 | // when captcha code is required 30 | var ErrNeedCaptcha = errors.New("need captcha") 31 | 32 | // ErrNeed2FA is returned by DoLogin 33 | // when mobile code is required 34 | var ErrNeed2FA = errors.New("need 2FA") 35 | 36 | // ErrNeedEmail is returned by DoLogin 37 | // when email code is required 38 | var ErrNeedEmail = errors.New("need email") 39 | 40 | // ErrNeedTooManyFailedLogins is returned by DoLogin 41 | var ErrTooManyFailedLogins = errors.New("two many failed logins") 42 | 43 | // Handles logging the user into the mobile Steam website. 44 | // Necessary to generate OAuth token and session cookies. 45 | type UserLogin struct { 46 | Username string 47 | Password string 48 | SteamID uint64 49 | 50 | RequiresCaptcha bool 51 | CaptchaGID string 52 | CaptchaText string 53 | 54 | RequiresEmail bool 55 | EmailDomain string 56 | EmailCode string 57 | 58 | Requires2FA bool 59 | TwoFactorCode string 60 | 61 | Session *SessionData 62 | LoggedIn bool 63 | 64 | _cookies *cookiejar.Jar 65 | } 66 | 67 | func NewUserLogin(username, password string) *UserLogin { 68 | cookies, _ := cookiejar.New(nil) 69 | return &UserLogin{ 70 | Username: username, 71 | Password: password, 72 | _cookies: cookies, 73 | } 74 | } 75 | 76 | func (ul *UserLogin) DoLogin() error { 77 | cookies := ul._cookies 78 | if len(cookies.Cookies(cookiePath)) == 0 { 79 | //Generate a SessionID 80 | cookies.SetCookies(cookiePath, []*http.Cookie{ 81 | &http.Cookie{ 82 | Name: "mobileClientVersion", 83 | Value: "0 (2.1.3)", 84 | Path: "/", 85 | Domain: ".steamcommunity.com", 86 | }, 87 | &http.Cookie{ 88 | Name: "mobileClient", 89 | Value: "android", 90 | Path: "/", 91 | Domain: ".steamcommunity.com", 92 | }, 93 | &http.Cookie{ 94 | Name: "Steam_Language", 95 | Value: "english", 96 | Path: "/", 97 | Domain: ".steamcommunity.com", 98 | }, 99 | }) 100 | headers := make(map[string]string) 101 | headers["X-Requested-With"] = "com.valvesoftware.android.steam.community" 102 | 103 | _, err := MobileLoginRequest(UrlCommunityBase+"/login?oauth_client_id=DE45CD61&oauth_scope=read_profile%20write_profile%20read_client%20write_client", "GET", nil, cookies, &headers) 104 | if err != nil { 105 | return err 106 | } 107 | } 108 | 109 | postData := url.Values{} 110 | postData.Set("username", ul.Username) 111 | respBody, err := MobileLoginRequest(UrlCommunityBase+"/login/getrsakey", "POST", &postData, cookies, nil) 112 | if err != nil { 113 | return err 114 | } 115 | r := rsaResponse{} 116 | if err = json.Unmarshal(respBody, &r); err != nil { 117 | return fmt.Errorf("failed to unmarshal rsa response: %v", err) 118 | } 119 | if !r.Success { 120 | return ErrBadRSA 121 | } 122 | 123 | // Rsa modulus 124 | modulusBytes, err := hex.DecodeString(r.Modulus) 125 | if err != nil { 126 | return err 127 | } 128 | modulus := big.NewInt(0) 129 | modulus.SetBytes(modulusBytes) 130 | // Rsa exponent 131 | exponentBytes, err := hex.DecodeString(r.Exponent) 132 | if err != nil { 133 | return err 134 | } 135 | exponent := big.NewInt(0) 136 | exponent.SetBytes(exponentBytes) 137 | // Generate encrypted password 138 | publicKey := rsa.PublicKey{ 139 | N: modulus, 140 | E: int(exponent.Int64()), 141 | } 142 | passwordBytes := []byte(ul.Password) 143 | encryptedPasswordBytes, err := rsa.EncryptPKCS1v15(rand.Reader, &publicKey, passwordBytes) 144 | if err != nil { 145 | return err 146 | } 147 | encryptedPassword := base64.StdEncoding.EncodeToString(encryptedPasswordBytes) 148 | 149 | // Create request params 150 | postData = url.Values{} 151 | postData.Set("username", ul.Username) 152 | postData.Set("password", encryptedPassword) 153 | postData.Set("twofactorcode", ul.TwoFactorCode) 154 | 155 | // TODO: is this parameters not required??? 156 | if ul.RequiresCaptcha { 157 | postData.Set("captchagid", ul.CaptchaGID) 158 | postData.Set("captcha_text", ul.CaptchaText) 159 | } 160 | 161 | if ul.Requires2FA || ul.RequiresEmail { 162 | postData.Set("emailsteamid", strconv.FormatUint(ul.SteamID, 10)) 163 | } 164 | if ul.RequiresEmail { 165 | postData.Set("emailauth", ul.EmailCode) 166 | } 167 | postData.Set("rsatimestamp", r.Timestamp) 168 | postData.Set("remember_login", "false") 169 | postData.Set("oauth_client_id", "DE45CD61") 170 | postData.Set("oauth_scope", "read_profile write_profile read_client write_client") 171 | postData.Set("loginfriendlyname", "#login_emailauth_friendlyname_mobile") 172 | postData.Set("donotcache", strconv.FormatInt(time.Now().Unix(), 10)) 173 | 174 | // Make request 175 | respBody, err = MobileLoginRequest(UrlCommunityBase+"/login/dologin", "POST", &postData, cookies, nil) 176 | if err != nil { 177 | return err 178 | } 179 | 180 | // Process response 181 | r2 := loginResponse{} 182 | if err = json.Unmarshal(respBody, &r2); err != nil { 183 | return err 184 | } 185 | if r2.Message != "" && strings.Contains(r2.Message, "Incorrect login") { 186 | return ErrBadCredentials 187 | } 188 | if r2.CaptchaNeeded { 189 | ul.RequiresCaptcha = true 190 | ul.CaptchaGID = r2.CaptchaGID.String() 191 | return ErrNeedCaptcha 192 | } else { 193 | ul.RequiresCaptcha = false 194 | } 195 | if r2.EmailAuthNeeded { 196 | ul.RequiresEmail = true 197 | ul.SteamID = r2.EmailSteamID 198 | return ErrNeedEmail 199 | } else { 200 | ul.RequiresEmail = false 201 | } 202 | if r2.TwoFactorNeeded && !r2.Success { 203 | ul.Requires2FA = true 204 | return ErrNeed2FA 205 | } else { 206 | ul.Requires2FA = false 207 | } 208 | if r2.Message != "" && strings.Contains(r2.Message, "too many login failures") { 209 | return ErrTooManyFailedLogins 210 | } 211 | if !r2.LoginComplete { 212 | return ErrBadCredentials 213 | } 214 | if r2.OAuth == nil || r2.OAuth.OAuthToken == "" { 215 | return errors.New("steam does not return oauth data") 216 | } 217 | 218 | // Get sessionid from cookies 219 | var sessionID string 220 | for _, cookie := range cookies.Cookies(cookiePath) { 221 | if cookie.Name == "sessionid" { 222 | sessionID = cookie.Value 223 | } 224 | } 225 | 226 | // Set session data 227 | stringSteamID := strconv.FormatUint(r2.OAuth.SteamID, 10) 228 | session := SessionData{ 229 | OAuthToken: r2.OAuth.OAuthToken, 230 | SteamID: r2.OAuth.SteamID, 231 | SteamLogin: stringSteamID + "%7C%7C" + r2.OAuth.SteamLogin, 232 | SteamLoginSecure: stringSteamID + "%7C%7C" + r2.OAuth.SteamLoginSecure, 233 | WebCookie: r2.OAuth.Webcookie, 234 | SessionID: sessionID, 235 | } 236 | ul.Session = &session 237 | ul.LoggedIn = true 238 | 239 | return nil 240 | } 241 | 242 | type loginResponse struct { 243 | Success bool `json:"success"` 244 | LoginComplete bool `json:"login_complete"` 245 | OAuth *oAuthResult `json:"oauth"` 246 | CaptchaNeeded bool `json:"captcha_needed"` 247 | CaptchaGID uniStr `json:"captcha_gid"` 248 | EmailSteamID uint64 `json:"emailsteamid,string"` 249 | EmailAuthNeeded bool `json:"emailauth_needed"` 250 | TwoFactorNeeded bool `json:"requires_twofactor"` 251 | Message string `json"message"` 252 | } 253 | 254 | type oAuthResult struct { 255 | SteamID uint64 `json:"steamid,string"` 256 | OAuthToken string `json:"oauth_token"` 257 | SteamLogin string `json:"wgtoken"` 258 | SteamLoginSecure string `json:"wgtoken_secure"` 259 | Webcookie string `json:"webcookie"` 260 | } 261 | 262 | func (o *oAuthResult) UnmarshalJSON(data []byte) error { 263 | // no oauth data 264 | if len(data) < 4 { 265 | return nil 266 | } 267 | // unquote 268 | unquotedData, err := strconv.Unquote(string(data)) 269 | if err != nil { 270 | return errors.New("failed to unquote oauth data") 271 | } 272 | // unmarshal 273 | type Alias oAuthResult 274 | aux := (*Alias)(o) 275 | if err = json.Unmarshal([]byte(unquotedData), &aux); err != nil { 276 | return err 277 | } 278 | return nil 279 | } 280 | 281 | type rsaResponse struct { 282 | Success bool `json:"success"` 283 | Exponent string `json:"publickey_exp"` 284 | Modulus string `json:"publickey_mod"` 285 | Timestamp string `json:"timestamp"` 286 | SteamID uint64 `json:"steamid"` 287 | } 288 | -------------------------------------------------------------------------------- /steam_guard_account.go: -------------------------------------------------------------------------------- 1 | package mobileauth 2 | 3 | import ( 4 | "crypto/hmac" 5 | "crypto/sha1" 6 | "encoding/base64" 7 | "encoding/binary" 8 | "encoding/json" 9 | "errors" 10 | "fmt" 11 | "net/http/cookiejar" 12 | "net/url" 13 | "regexp" 14 | "strconv" 15 | "strings" 16 | ) 17 | 18 | var confIDRegex = regexp.MustCompile("data-confid=\"(\\d+)\"") 19 | var confKeyRegex = regexp.MustCompile("data-key=\"(\\d+)\"") 20 | var confDescRegex = regexp.MustCompile("
((Confirm|Trade with|Sell -) .+)
") 21 | 22 | type SteamGuardAccount struct { 23 | SharedSecret string `json:"shared_secret"` 24 | SerialNumber string `json:"serial_number"` 25 | RevocationCode string `json:"revocation_code"` 26 | URI string `json:"uri"` 27 | ServerTime int64 `json:"server_time,string"` 28 | AccountName string `json:"account_name"` 29 | TokenGID string `json:"token_gid"` 30 | IdentitySecret string `json:"identity_secret"` 31 | Secret1 string `json:"secret_1"` 32 | Status int32 `json:"status"` 33 | DeviceID string `json:"device_id"` 34 | // Set to true if the authenticator has actually been applied to the account. 35 | FullyEnrolled bool `json:"fully_enrolled"` 36 | Session *SessionData `json:"session"` 37 | } 38 | 39 | func (a *SteamGuardAccount) DeactivateAuthenticator() error { 40 | postData := url.Values{} 41 | postData.Set("steamid", strconv.FormatUint(a.Session.SteamID, 10)) 42 | postData.Set("steamguard_scheme", "2") 43 | postData.Set("revocation_code", a.RevocationCode) 44 | postData.Set("access_token", a.Session.OAuthToken) 45 | 46 | respBody, err := MobileLoginRequest(UrlTwoFactorService+"/RemoveAuthenticator/v0001", "POST", &postData, nil, nil) 47 | if err != nil { 48 | return err 49 | } 50 | r := removeAuthenticatorResponse{} 51 | if err = json.Unmarshal(respBody, &r); err != nil { 52 | return err 53 | } 54 | if r.Response == nil { 55 | return errors.New("steam returned empty remove authenticator response") 56 | } 57 | if !r.Response.Success { 58 | return errors.New("steam returned success false") 59 | } 60 | 61 | return nil 62 | } 63 | 64 | func (a *SteamGuardAccount) GenerateSteamGuardCode() (string, error) { 65 | return a.GenerateSteamGuardCodeForTime(GetSteamTime()) 66 | } 67 | 68 | func (a *SteamGuardAccount) GenerateSteamGuardCodeForTime(t int64) (string, error) { 69 | if a.SharedSecret == "" { 70 | return "", errors.New("shared secret not set") 71 | } 72 | 73 | // Shared secret is our key 74 | sharedSecretBytes, err := base64.StdEncoding.DecodeString(a.SharedSecret) 75 | if err != nil { 76 | return "", err 77 | } 78 | 79 | // Time for code 80 | t = t / 30 // TODO: why we are doing this??? 81 | timeBytes := make([]byte, 8) 82 | binary.BigEndian.PutUint64(timeBytes, uint64(t)) 83 | 84 | // Generate hmac 85 | hmacGenerator := hmac.New(sha1.New, sharedSecretBytes) 86 | hmacGenerator.Write(timeBytes) 87 | mac := hmacGenerator.Sum(nil) 88 | 89 | // the last 4 bits of the mac say where the code starts 90 | // (e.g. if last 4 bit are 1100, we start at byte 12) 91 | start := int(mac[19] & 0x0f) 92 | 93 | // extract code - 4 bytes 94 | codeBytes := make([]byte, 4) 95 | copy(codeBytes, mac[start:]) 96 | fullCode := binary.BigEndian.Uint32(codeBytes) 97 | fullCode = fullCode & 0x7fffffff 98 | 99 | // character set for authenticator code 100 | chars := []byte{50, 51, 52, 53, 54, 55, 56, 57, 66, 67, 68, 70, 71, 72, 74, 75, 77, 78, 80, 81, 82, 84, 86, 87, 88, 89} 101 | 102 | // build the alphanumeric code 103 | var textCodeBytes []byte 104 | for i := 0; i < 5; i++ { 105 | textCodeBytes = append(textCodeBytes, chars[fullCode%uint32(len(chars))]) 106 | fullCode = fullCode / uint32(len(chars)) 107 | } 108 | 109 | return string(textCodeBytes[:]), nil 110 | } 111 | 112 | func (a *SteamGuardAccount) FetchConfirmations() ([]*Confirmation, error) { 113 | queryParams, err := a.GenerateConfirmationQueryParams("conf") 114 | if err != nil { 115 | return nil, fmt.Errorf("failed to generated confirmation query params: %v", err) 116 | } 117 | 118 | cookies, _ := cookiejar.New(nil) 119 | a.Session.AddCookies(cookies) 120 | 121 | respBody, err := WebRequest(UrlConfirmationService+"/conf", "GET", &queryParams, cookies, nil, nil) 122 | if err != nil { 123 | return nil, err 124 | } 125 | 126 | respString := string(respBody) 127 | 128 | // Nothing to confirm 129 | if strings.Contains(respString, "
Nothing to confirm
") { 130 | return nil, nil 131 | } 132 | if strings.Contains(respString, "
There was a problem loading the confirmations page. Please try your request again later.
") { 133 | return nil, errors.New("steam failed to return confirmations page, try again later") 134 | } 135 | 136 | // Try to parse response 137 | confIDs := confIDRegex.FindAllStringSubmatch(respString, -1) 138 | confKeys := confKeyRegex.FindAllStringSubmatch(respString, -1) 139 | confDescs := confDescRegex.FindAllStringSubmatch(respString, -1) 140 | 141 | if confIDs == nil || confKeys == nil || confDescs == nil { 142 | return nil, errors.New("failed to parse response") 143 | } 144 | 145 | if len(confIDs) != len(confKeys) || len(confIDs) != len(confDescs) { 146 | return nil, errors.New("unexpected response format: number of ids, keys and descriptions are not the same") 147 | } 148 | 149 | // Create confirmations slice 150 | var confirmations []*Confirmation 151 | for index, _ := range confIDs { 152 | cn := &Confirmation{ 153 | ConfirmationID: confIDs[index][1], 154 | ConfirmationKey: confKeys[index][1], 155 | ConfirmationDescription: confDescs[index][1], 156 | } 157 | confirmations = append(confirmations, cn) 158 | } 159 | 160 | return confirmations, nil 161 | } 162 | 163 | func (a *SteamGuardAccount) AcceptConfirmation(cn *Confirmation) error { 164 | return a._sendConfirmationAjax(cn, "allow") 165 | } 166 | 167 | func (a *SteamGuardAccount) DenyConfirmation(cn *Confirmation) error { 168 | return a._sendConfirmationAjax(cn, "cancel") 169 | } 170 | 171 | // Refreshes the Steam session. Necessary to perform confirmations if your session has expired or changed. 172 | func (a *SteamGuardAccount) RefreshSession() error { 173 | postData := url.Values{} 174 | postData.Set("access_token", a.Session.OAuthToken) 175 | 176 | respBody, err := WebRequest(UrlMobileAuthService+"/GetWGToken/v0001", "POST", &postData, nil, nil, nil) 177 | if err != nil { 178 | return err 179 | } 180 | 181 | r := refreshSessionDataResponse{} 182 | if err = json.Unmarshal(respBody, &r); err != nil { 183 | return err 184 | } 185 | if r.Response == nil { 186 | return errors.New("steam returned empty refresh session data response") 187 | } 188 | if r.Response.Token == "" { 189 | return errors.New("steam returned empty token") 190 | } 191 | 192 | stringSteamID := strconv.FormatUint(a.Session.SteamID, 10) 193 | token := stringSteamID + "%7C%7C" + r.Response.Token 194 | tokenSecure := stringSteamID + "%7C%7C" + r.Response.TokenSecure 195 | 196 | a.Session.SteamLogin = token 197 | a.Session.SteamLoginSecure = tokenSecure 198 | 199 | return nil 200 | } 201 | 202 | func (a *SteamGuardAccount) _sendConfirmationAjax(cn *Confirmation, op string) error { 203 | queryParams, err := a.GenerateConfirmationQueryParams(op) 204 | if err != nil { 205 | return fmt.Errorf("failed to generated confirmation query params: %v", err) 206 | } 207 | queryParams.Set("op", op) 208 | queryParams.Set("cid", cn.ConfirmationID) 209 | queryParams.Set("ck", cn.ConfirmationKey) 210 | 211 | cookies, _ := cookiejar.New(nil) 212 | a.Session.AddCookies(cookies) 213 | 214 | // TODO: do we need this??? 215 | //referer := urlConfirmationService + "/conf" 216 | 217 | respBody, err := WebRequest(UrlConfirmationService+"/ajaxop", "GET", &queryParams, cookies, nil, nil) 218 | if err != nil { 219 | return err 220 | } 221 | 222 | r := sendConfirmationResponse{} 223 | if err = json.Unmarshal(respBody, &r); err != nil { 224 | return err 225 | } 226 | if !r.Success { 227 | return errors.New("steam returned success false") 228 | } 229 | return nil 230 | } 231 | 232 | func (a *SteamGuardAccount) GenerateConfirmationQueryParams(tag string) (url.Values, error) { 233 | if a.DeviceID == "" { 234 | return nil, errors.New("Device ID is empty") 235 | } 236 | t := GetSteamTime() 237 | queryParams := url.Values{} 238 | queryParams.Set("p", a.DeviceID) 239 | queryParams.Set("a", strconv.FormatUint(a.Session.SteamID, 10)) 240 | queryParams.Set("k", a._generateConfirmationHashForTime(t, tag)) 241 | queryParams.Set("t", strconv.FormatInt(t, 10)) 242 | queryParams.Set("m", "android") 243 | queryParams.Set("tag", tag) 244 | return queryParams, nil 245 | } 246 | 247 | func (a *SteamGuardAccount) _generateConfirmationHashForTime(t int64, tag string) string { 248 | identitySecretBytes, err := base64.StdEncoding.DecodeString(a.IdentitySecret) 249 | if err != nil { 250 | // TODO: maybe we shall panic or return error up the chain 251 | return "" 252 | } 253 | 254 | data := make([]byte, 8) 255 | binary.BigEndian.PutUint64(data, uint64(t)) 256 | tagBytes := []byte(tag) 257 | if len(tagBytes) > 32 { 258 | // maximum tag length is 32 bytes 259 | tagBytes = tagBytes[:32] 260 | } 261 | data = append(data, tagBytes...) 262 | 263 | // Generate hmac 264 | hmacGenerator := hmac.New(sha1.New, identitySecretBytes) 265 | hmacGenerator.Write(data) 266 | mac := hmacGenerator.Sum(nil) 267 | 268 | return base64.StdEncoding.EncodeToString(mac) 269 | } 270 | 271 | type refreshSessionDataResponse struct { 272 | Response *refreshSessionDataResult `json:"response"` 273 | } 274 | 275 | type refreshSessionDataResult struct { 276 | Token string `json:"token"` 277 | TokenSecure string `json:"token_secure"` 278 | } 279 | 280 | type removeAuthenticatorResponse struct { 281 | Response *removeAuthenticatorResult `json:"response"` 282 | } 283 | 284 | type removeAuthenticatorResult struct { 285 | Success bool `json:"success"` 286 | } 287 | 288 | type sendConfirmationResponse struct { 289 | Success bool `json:"success"` 290 | } 291 | --------------------------------------------------------------------------------