├── 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 | [](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("