/
xbox.go
358 lines (326 loc) · 13 KB
/
xbox.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
package auth
import (
"bytes"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/sha256"
"encoding/base64"
"encoding/binary"
"encoding/json"
"fmt"
"github.com/google/uuid"
"net/http"
"time"
)
// xblUserAuthURL is the first URL that a POST request is made to, in order to obtain the XBOX Live user token.
const xblUserAuthURL = `https://user.auth.xboxlive.com/user/authenticate`
// xblDeviceAuthURL is the second URL that a POST request is made to, in order to authenticate a device.
const xblDeviceAuthURL = `https://device.auth.xboxlive.com/device/authenticate`
// xblTitleAuthURL is the third URL that a POST request is made to, in order to authenticate the title.
const xblTitleAuthURL = `https://title.auth.xboxlive.com/title/authenticate`
// xblAuthorizeURL is the last URL that a POST request is made to, in order to obtain the XSTS token, which
// is a combination of all previous tokens.
const xblAuthorizeURL = `https://xsts.auth.xboxlive.com/xsts/authorize`
// UserToken is the token obtained by requesting a user token by posting to xblUserAuthURL. Its Token field
// must be used in a request to the XSTS token.
type UserToken struct {
IssueInstant string
NotAfter string
Token string
DisplayClaims struct {
XUI []struct {
UHS string `json:"uhs"`
} `json:"xui"`
}
}
// DeviceToken is the token obtained by requesting a device token by posting to xblDeviceAuthURL. Its Token
// field may be used in a request to obtain the XSTS token.
type DeviceToken struct {
IssueInstant string
NotAfter string
Token string
DisplayClaims struct {
XDI struct {
DID string `json:"did"`
} `json:"xdi"`
}
}
// TitleToken is the token obtained by requesting a title token by posting to xblTitleAuthURL. Its Token field
// may be used in a request to obtain the XSTS token.
type TitleToken struct {
IssueInstant string
NotAfter string
Token string
DisplayClaims struct {
XTI struct {
TID string `json:"tid"`
} `json:"xti"`
}
}
// XSTSToken is the token obtained by requesting an XSTS token from xblAuthorizeURL. It may be obtained using
// any of the tokens above, and is required for authenticating with Minecraft. Its Token and UserHash field
// in particular are used.
type XSTSToken struct {
IssueInstant string
NotAfter string
Token string
DisplayClaims struct {
XUI []struct {
AgeGroup string `json:"agg"`
GamerTag string `json:"gtg"`
Privileges string `json:"prv"`
XUID string `json:"xid"`
UserHash string `json:"uhs"`
} `json:"xui"`
}
}
// RequestXSTSToken requests an XSTS token using the passed Live token pair. The token pair must be valid
// when passed in. RequestXSTSToken will not attempt to refresh the token pair if it not valid.
// RequestXSTSToken obtains the XSTS token by using the UserToken, DeviceToken and TitleToken. It appears only
// one of these tokens is actually required to produce an XSTS token valid to authenticate with Minecraft.
func RequestXSTSToken(liveToken *TokenPair) (*XSTSToken, error) {
if !liveToken.Valid() {
return nil, fmt.Errorf("live token is no longer valid")
}
c := &http.Client{}
defer c.CloseIdleConnections()
// We first generate an ECDSA private key which will be used to provide a 'ProofKey' to each of the
// requests, and to sign these requests.
key, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
// All following requests here use the same ECDSA private key. This is required, and failing to do so
// means that the signature of the second request will be refused.
userToken, err := userToken(c, liveToken.access, key)
if err != nil {
return nil, err
}
deviceToken, err := deviceToken(c, key)
if err != nil {
return nil, err
}
titleToken, err := titleToken(c, liveToken.access, deviceToken.Token, key)
if err != nil {
return nil, err
}
return xstsToken(c, userToken.Token, deviceToken.Token, titleToken.Token, key)
}
// userToken sends a POST request to xblUserAuthURL using the Live access token passed, and the ECDSA private
// key to sign the request. Signing the request is not actually mandatory, but we do so anyway just to be
// sure.
func userToken(c *http.Client, accessToken string, key *ecdsa.PrivateKey) (token *UserToken, err error) {
data, _ := json.Marshal(map[string]interface{}{
"RelyingParty": "http://auth.xboxlive.com",
"TokenType": "JWT",
"Properties": map[string]interface{}{
"AuthMethod": "RPS",
"SiteName": "user.auth.xboxlive.com",
"RpsTicket": "t=" + accessToken,
// Note that the ProofKey field here does not need to be present. Omitting this field will still
// return a valid user token.
"ProofKey": map[string]interface{}{
"crv": "P-256",
"alg": "ES256",
"use": "sig",
"kty": "EC",
"x": base64.RawURLEncoding.EncodeToString(key.PublicKey.X.Bytes()),
"y": base64.RawURLEncoding.EncodeToString(key.PublicKey.Y.Bytes()),
},
},
})
request, _ := http.NewRequest("POST", xblUserAuthURL, bytes.NewReader(data))
request.Header.Set("Content-Type", "application/json")
request.Header.Set("x-xbl-contract-version", "1")
// Signing the user token request is actually not mandatory. It may be omitted altogether, including the
// ProofKey field in the Properties of the request.
sign(request, data, key)
resp, err := c.Do(request)
if err != nil {
return nil, fmt.Errorf("POST %v: %v", xblUserAuthURL, err)
}
defer func() {
_ = resp.Body.Close()
}()
if resp.StatusCode != 200 {
return nil, fmt.Errorf("POST %v: %v", xblUserAuthURL, resp.Status)
}
token = &UserToken{}
return token, json.NewDecoder(resp.Body).Decode(token)
}
// deviceToken sends a POST request to xblDeviceAuthURL using the ECDSA private key passed to sign the
// request. Note that the device token is not mandatory to obtain a valid XSTS token.
func deviceToken(c *http.Client, key *ecdsa.PrivateKey) (token *DeviceToken, err error) {
data, _ := json.Marshal(map[string]interface{}{
"RelyingParty": "http://auth.xboxlive.com",
"TokenType": "JWT",
"Properties": map[string]interface{}{
"DeviceType": "Nintendo",
// These may simply be random UUIDs.
"Id": uuid.Must(uuid.NewRandom()).String(),
"SerialNumber": uuid.Must(uuid.NewRandom()).String(),
"Version": "0.0.0.0",
// Note the different AuthMethod here. Other requests typically have the RPS AuthMethod, but this
// uses ProofOfPossession.
"AuthMethod": "ProofOfPossession",
"ProofKey": map[string]interface{}{
"crv": "P-256",
"alg": "ES256",
"use": "sig",
"kty": "EC",
"x": base64.RawURLEncoding.EncodeToString(key.PublicKey.X.Bytes()),
"y": base64.RawURLEncoding.EncodeToString(key.PublicKey.Y.Bytes()),
},
},
})
request, _ := http.NewRequest("POST", xblDeviceAuthURL, bytes.NewReader(data))
request.Header.Set("Content-Type", "application/json")
request.Header.Set("x-xbl-contract-version", "1")
sign(request, data, key)
resp, err := c.Do(request)
if err != nil {
return nil, fmt.Errorf("POST %v: %v", xblDeviceAuthURL, err)
}
defer func() {
_ = resp.Body.Close()
}()
if resp.StatusCode != 200 {
return nil, fmt.Errorf("POST %v: %v", xblDeviceAuthURL, resp.Status)
}
token = &DeviceToken{}
return token, json.NewDecoder(resp.Body).Decode(token)
}
// titleToken sends a POST request to xblTitleAuthURL using the device and Live access token passed. The
// request is signed using the ECDSA private key passed.
func titleToken(c *http.Client, accessToken, deviceToken string, key *ecdsa.PrivateKey) (token *TitleToken, err error) {
data, _ := json.Marshal(map[string]interface{}{
"RelyingParty": "http://auth.xboxlive.com",
"TokenType": "JWT",
"Properties": map[string]interface{}{
"AuthMethod": "RPS",
"DeviceToken": deviceToken,
"SiteName": "user.auth.xboxlive.com",
"RpsTicket": "t=" + accessToken,
"ProofKey": map[string]interface{}{
"crv": "P-256",
"alg": "ES256",
"use": "sig",
"kty": "EC",
"x": base64.RawURLEncoding.EncodeToString(key.PublicKey.X.Bytes()),
"y": base64.RawURLEncoding.EncodeToString(key.PublicKey.Y.Bytes()),
},
},
})
request, _ := http.NewRequest("POST", xblTitleAuthURL, bytes.NewReader(data))
request.Header.Set("Content-Type", "application/json")
request.Header.Set("x-xbl-contract-version", "1")
sign(request, data, key)
resp, err := c.Do(request)
if err != nil {
return nil, fmt.Errorf("POST %v: %v", xblTitleAuthURL, err)
}
defer func() {
_ = resp.Body.Close()
}()
if resp.StatusCode != 200 {
return nil, fmt.Errorf("POST %v: %v", xblTitleAuthURL, resp.Status)
}
token = &TitleToken{}
return token, json.NewDecoder(resp.Body).Decode(token)
}
// xstsTokenWithoutDeviceAndTitle sends a POST request to the xblAuthorizeURL using the user token passed. It
// fetches it without requiring the device and title token.
func xstsTokenWithoutDeviceAndTitle(c *http.Client, userToken string) (token *XSTSToken, err error) {
data, _ := json.Marshal(map[string]interface{}{
// RelyingParty MUST be this URL to produce an XSTS token which may be used for Minecraft
// authentication.
"RelyingParty": "https://multiplayer.minecraft.net/",
"TokenType": "JWT",
"Properties": map[string]interface{}{
"UserTokens": []string{userToken},
"SandboxId": "RETAIL",
},
})
request, _ := http.NewRequest("POST", xblAuthorizeURL, bytes.NewReader(data))
request.Header.Set("Content-Type", "application/json; charset=UTF-8")
request.Header.Set("x-xbl-contract-version", "1")
resp, err := c.Do(request)
if err != nil {
return nil, fmt.Errorf("POST %v: %v", xblAuthorizeURL, err)
}
defer func() {
_ = resp.Body.Close()
}()
token = &XSTSToken{}
return token, json.NewDecoder(resp.Body).Decode(token)
}
// xstsToken sends a POST request to xblAuthorizeURL using the user, device and title token passed, and the
// ECDSA private key to sign the request. The device token, title token and signature are not mandatory to
// produce a valid XSTS token, but we require them here just in case.
func xstsToken(c *http.Client, userToken, deviceToken, titleToken string, key *ecdsa.PrivateKey) (token *XSTSToken, err error) {
data, _ := json.Marshal(map[string]interface{}{
// RelyingParty MUST be this URL to produce an XSTS token which may be used for Minecraft
// authentication.
"RelyingParty": "https://multiplayer.minecraft.net/",
"TokenType": "JWT",
"Properties": map[string]interface{}{
// DeviceToken is not required for Minecraft auth. The key may simply not be present.
"DeviceToken": deviceToken,
// TitleToken is also not required for Minecraft auth. The key may simply not be present.
"TitleToken": titleToken,
"UserTokens": []string{userToken},
"SandboxId": "RETAIL",
},
})
request, _ := http.NewRequest("POST", xblAuthorizeURL, bytes.NewReader(data))
request.Header.Set("Content-Type", "application/json; charset=UTF-8")
request.Header.Set("x-xbl-contract-version", "1")
// Signing the XSTS token request is not necessary. The header may simply not be present.
sign(request, data, key)
resp, err := c.Do(request)
if err != nil {
return nil, fmt.Errorf("POST %v: %v", xblAuthorizeURL, err)
}
defer func() {
_ = resp.Body.Close()
}()
token = &XSTSToken{}
return token, json.NewDecoder(resp.Body).Decode(token)
}
// sign signs the request passed containing the body passed. It signs the request using the ECDSA private key
// passed. If the request has a 'ProofKey' field in the Properties field, that key must be passed here.
func sign(request *http.Request, body []byte, key *ecdsa.PrivateKey) {
currentTime := windowsTimestamp()
hash := sha256.New()
// Signature policy version (0, 0, 0, 1) + 0 byte.
buf := bytes.NewBuffer([]byte{0, 0, 0, 1, 0})
// Timestamp + 0 byte.
_ = binary.Write(buf, binary.BigEndian, currentTime)
buf.Write([]byte{0})
hash.Write(buf.Bytes())
// HTTP method, generally POST + 0 byte.
hash.Write([]byte("POST"))
hash.Write([]byte{0})
// Request uri path + raw query + 0 byte.
hash.Write([]byte(request.URL.Path + request.URL.RawQuery))
hash.Write([]byte{0})
// Authorization header if present, otherwise an empty string + 0 byte.
hash.Write([]byte(request.Header.Get("Authorization")))
hash.Write([]byte{0})
// Body data (only up to a certain limit, but this limit is practically never reached) + 0 byte.
hash.Write(body)
hash.Write([]byte{0})
// Sign the checksum produced, and combine the 'r' and 's' into a single signature.
r, s, _ := ecdsa.Sign(rand.Reader, key, hash.Sum(nil))
signature := append(r.Bytes(), s.Bytes()...)
// The signature begins with 12 bytes, the first being the signature policy version (0, 0, 0, 1) again,
// and the other 8 the timestamp again.
buf = bytes.NewBuffer([]byte{0, 0, 0, 1})
_ = binary.Write(buf, binary.BigEndian, currentTime)
// Append the signature to the other 12 bytes, and encode the signature with standard base64 encoding.
sig := append(buf.Bytes(), signature...)
request.Header.Set("Signature", base64.StdEncoding.EncodeToString(sig))
}
// windowsTimestamp returns a Windows specific timestamp. It has a certain offset from Unix time which must be
// accounted for.
func windowsTimestamp() int64 {
return (time.Now().Unix() + 11644473600) * 10000000
}