qr.go 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453
  1. package login
  2. import (
  3. "context"
  4. "crypto/rand"
  5. "crypto/rsa"
  6. "crypto/sha256"
  7. "crypto/x509"
  8. "encoding/base64"
  9. "encoding/json"
  10. "errors"
  11. "fmt"
  12. "io"
  13. "log/slog"
  14. stdhttp "net/http"
  15. "strings"
  16. "time"
  17. "github.com/ayn2op/discordo/internal/config"
  18. apphttp "github.com/ayn2op/discordo/internal/http"
  19. "github.com/ayn2op/discordo/internal/ui"
  20. "github.com/ayn2op/tview"
  21. "github.com/diamondburned/arikawa/v3/utils/httputil"
  22. "github.com/gdamore/tcell/v3"
  23. "github.com/gorilla/websocket"
  24. "github.com/skip2/go-qrcode"
  25. )
  26. const gatewayURL = "wss://remote-auth-gateway.discord.gg/?v=2"
  27. type qrLogin struct {
  28. *tview.TextView
  29. app *tview.Application
  30. cfg *config.Config
  31. done func(token string, err error)
  32. conn *websocket.Conn
  33. privKey *rsa.PrivateKey
  34. cancel context.CancelFunc
  35. fingerprint string
  36. }
  37. func newQRLogin(app *tview.Application, cfg *config.Config, done func(token string, err error)) *qrLogin {
  38. q := &qrLogin{
  39. TextView: tview.NewTextView(),
  40. app: app,
  41. cfg: cfg,
  42. done: done,
  43. }
  44. q.Box = ui.ConfigureBox(q.Box, &cfg.Theme)
  45. q.
  46. SetScrollable(true).
  47. SetWrap(false).
  48. SetTextAlign(tview.AlignmentCenter).
  49. SetChangedFunc(func() {
  50. q.app.QueueUpdateDraw(func() {})
  51. }).
  52. SetTitle("Login with QR")
  53. return q
  54. }
  55. func (q *qrLogin) HandleEvent(event tcell.Event) tview.Command {
  56. switch event := event.(type) {
  57. case *tview.KeyEvent:
  58. if event.Key() == tcell.KeyEsc {
  59. q.stop()
  60. if q.done != nil {
  61. q.done("", nil)
  62. }
  63. return tview.RedrawCommand{}
  64. }
  65. return q.TextView.HandleEvent(event)
  66. }
  67. return q.TextView.HandleEvent(event)
  68. }
  69. func (q *qrLogin) start() {
  70. ctx, cancel := context.WithCancel(context.Background())
  71. q.cancel = cancel
  72. go q.run(ctx)
  73. }
  74. func (q *qrLogin) stop() {
  75. if q.cancel != nil {
  76. q.cancel()
  77. }
  78. if q.conn != nil {
  79. q.conn.Close()
  80. }
  81. }
  82. func (q *qrLogin) setText(s string) {
  83. builder := tview.NewLineBuilder()
  84. builder.Write(s, tcell.StyleDefault)
  85. q.setLines(builder.Finish())
  86. }
  87. func (q *qrLogin) setLines(lines []tview.Line) {
  88. q.app.QueueUpdateDraw(func() {
  89. q.SetLines(q.centerLines(lines))
  90. })
  91. }
  92. func (q *qrLogin) centerLines(lines []tview.Line) []tview.Line {
  93. _, _, _, height := q.GetInnerRect()
  94. if height == 0 {
  95. height = 40
  96. }
  97. padding := (height - len(lines)) / 2
  98. if padding < 0 {
  99. padding = 0
  100. } else if padding < 1 && height > len(lines) {
  101. padding = 1
  102. }
  103. if padding == 0 {
  104. return lines
  105. }
  106. centered := make([]tview.Line, 0, padding+len(lines))
  107. centered = append(centered, make([]tview.Line, padding)...)
  108. centered = append(centered, lines...)
  109. return centered
  110. }
  111. func (q *qrLogin) writeJSON(data any) error {
  112. return q.conn.WriteJSON(data)
  113. }
  114. type raHello struct {
  115. TimeoutMs int `json:"timeout_ms"`
  116. HeartbeatInterval int `json:"heartbeat_interval"`
  117. }
  118. type raNonceProof struct {
  119. EncryptedNonce string `json:"encrypted_nonce"`
  120. }
  121. type raPendingInit struct {
  122. Fingerprint string `json:"fingerprint"`
  123. }
  124. type raPendingLogin struct {
  125. Ticket string `json:"ticket"`
  126. }
  127. type raPendingTicket struct {
  128. EncryptedUserPayload string `json:"encrypted_user_payload"`
  129. }
  130. func (q *qrLogin) run(ctx context.Context) {
  131. defer q.stop()
  132. q.setText("Preparing QR code...\n\nPress Esc to cancel")
  133. privKey, err := rsa.GenerateKey(rand.Reader, 2048)
  134. if err != nil {
  135. q.fail(err)
  136. return
  137. }
  138. q.privKey = privKey
  139. pubDER, err := x509.MarshalPKIXPublicKey(&privKey.PublicKey)
  140. if err != nil {
  141. q.fail(err)
  142. return
  143. }
  144. encodedPublicKey := base64.StdEncoding.EncodeToString(pubDER)
  145. q.setText("Connecting to Remote Auth Gateway...\n\nPress Esc to cancel")
  146. headers := apphttp.Headers()
  147. headers.Set("User-Agent", apphttp.BrowserUserAgent)
  148. dialer := websocket.Dialer{
  149. Proxy: stdhttp.ProxyFromEnvironment,
  150. HandshakeTimeout: 15 * time.Second,
  151. EnableCompression: true,
  152. }
  153. conn, resp, err := dialer.DialContext(ctx, gatewayURL, headers)
  154. if err != nil {
  155. var body []byte
  156. if resp != nil && resp.Body != nil {
  157. body, _ = io.ReadAll(resp.Body)
  158. }
  159. status := ""
  160. if resp != nil {
  161. status = resp.Status
  162. }
  163. q.fail(fmt.Errorf("websocket dial failed: %w, status=%s, body=%s", err, status, string(body)))
  164. return
  165. }
  166. q.conn = conn
  167. readCh := make(chan []byte, 1)
  168. readErr := make(chan error, 1)
  169. go func() {
  170. for {
  171. _, data, err := conn.ReadMessage()
  172. if err != nil {
  173. readErr <- err
  174. return
  175. }
  176. readCh <- data
  177. }
  178. }()
  179. for {
  180. select {
  181. case <-ctx.Done():
  182. return
  183. case err := <-readErr:
  184. if mapped := mapWSCloseError(err); mapped == nil {
  185. return
  186. } else {
  187. q.fail(mapped)
  188. }
  189. return
  190. case data := <-readCh:
  191. var opOnly struct {
  192. Op string `json:"op"`
  193. }
  194. if err := json.Unmarshal(data, &opOnly); err != nil {
  195. q.setText("Bad JSON: " + err.Error())
  196. q.fail(err)
  197. return
  198. }
  199. switch opOnly.Op {
  200. case "hello":
  201. var h raHello
  202. if err := json.Unmarshal(data, &h); err != nil {
  203. q.setText("Hello decode failed: " + err.Error())
  204. q.fail(err)
  205. return
  206. }
  207. if h.HeartbeatInterval > 0 {
  208. heartbeatTicker := time.NewTicker(time.Duration(h.HeartbeatInterval) * time.Millisecond)
  209. go func() {
  210. defer heartbeatTicker.Stop()
  211. for {
  212. select {
  213. case <-ctx.Done():
  214. return
  215. case <-heartbeatTicker.C:
  216. q.writeJSON(map[string]any{"op": "heartbeat"})
  217. }
  218. }
  219. }()
  220. }
  221. q.setText("Connected. Handshaking...\n\nPress Esc to cancel")
  222. if err := q.writeJSON(map[string]any{
  223. "op": "init",
  224. "encoded_public_key": encodedPublicKey,
  225. }); err != nil {
  226. q.setText("Init send failed: " + err.Error())
  227. q.fail(err)
  228. return
  229. }
  230. case "nonce_proof":
  231. var n raNonceProof
  232. if err := json.Unmarshal(data, &n); err != nil {
  233. q.setText("Nonce decode failed: " + err.Error())
  234. q.fail(err)
  235. return
  236. }
  237. enc, err := base64.StdEncoding.DecodeString(n.EncryptedNonce)
  238. if err != nil {
  239. q.setText("Nonce b64 decode failed: " + err.Error())
  240. q.fail(err)
  241. return
  242. }
  243. pt, err := rsa.DecryptOAEP(sha256.New(), rand.Reader, q.privKey, enc, nil)
  244. if err != nil {
  245. q.setText("Nonce decrypt failed: " + err.Error())
  246. q.fail(err)
  247. return
  248. }
  249. nonce := base64.RawURLEncoding.EncodeToString(pt)
  250. if err := q.writeJSON(map[string]any{"op": "nonce_proof", "nonce": nonce}); err != nil {
  251. q.setText("Nonce send failed: " + err.Error())
  252. q.fail(err)
  253. return
  254. }
  255. case "pending_remote_init":
  256. var p raPendingInit
  257. if err := json.Unmarshal(data, &p); err != nil {
  258. q.setText("Init decode failed: " + err.Error())
  259. q.fail(err)
  260. return
  261. }
  262. q.fingerprint = p.Fingerprint
  263. content := "https://discord.com/ra/" + p.Fingerprint
  264. qrLines, err := renderQR(content)
  265. if err != nil {
  266. q.setText("QR render failed: " + err.Error())
  267. q.fail(err)
  268. return
  269. }
  270. builder := tview.NewLineBuilder()
  271. builder.AppendLines(qrLines)
  272. builder.Write("\n\nScan with Discord mobile app\n\nPress Esc to cancel", tcell.StyleDefault)
  273. q.setLines(builder.Finish())
  274. case "heartbeat_ack":
  275. case "pending_ticket":
  276. var t raPendingTicket
  277. if err := json.Unmarshal(data, &t); err != nil {
  278. q.setText("Ticket decode failed: " + err.Error())
  279. q.fail(err)
  280. return
  281. }
  282. payload, err := base64.StdEncoding.DecodeString(t.EncryptedUserPayload)
  283. if err != nil {
  284. q.setText("Ticket payload b64 failed: " + err.Error())
  285. q.fail(err)
  286. return
  287. }
  288. pt, err := rsa.DecryptOAEP(sha256.New(), rand.Reader, q.privKey, payload, nil)
  289. if err != nil {
  290. q.setText("Ticket payload decrypt failed: " + err.Error())
  291. q.fail(err)
  292. return
  293. }
  294. parts := strings.SplitN(string(pt), ":", 4)
  295. var discriminator, username string
  296. if len(parts) == 4 {
  297. discriminator = parts[1]
  298. username = parts[3]
  299. }
  300. if discriminator == "" && username == "" {
  301. q.setText("Scan received.\n\nWaiting for approval on mobile...\n\nPress Esc to cancel")
  302. } else {
  303. q.setText("Logging in as " + username + "#" + discriminator + "\n\nConfirm on mobile...\n\nPress Esc to cancel")
  304. }
  305. case "pending_login":
  306. var p raPendingLogin
  307. if err := json.Unmarshal(data, &p); err != nil {
  308. q.setText("Login decode failed: " + err.Error())
  309. q.fail(err)
  310. return
  311. }
  312. q.setText("Authenticating...\n\nPlease wait")
  313. token, err := exchangeTicket(p.Ticket, q.fingerprint, q.privKey)
  314. if err != nil {
  315. q.setText("Ticket exchange failed: " + err.Error())
  316. q.fail(err)
  317. return
  318. }
  319. q.success(token)
  320. return
  321. case "cancel":
  322. q.setText("Login canceled on mobile")
  323. if q.done != nil {
  324. q.done("", nil)
  325. }
  326. return
  327. default:
  328. }
  329. }
  330. }
  331. }
  332. func renderQR(content string) ([]tview.Line, error) {
  333. code, err := qrcode.New(content, qrcode.Low)
  334. if err != nil {
  335. return nil, err
  336. }
  337. bitmap := code.Bitmap()
  338. builder := tview.NewLineBuilder()
  339. style := tcell.StyleDefault
  340. for y := 0; y < len(bitmap); y += 2 {
  341. for x := range bitmap[y] {
  342. top := bitmap[y][x]
  343. bottom := false
  344. if y+1 < len(bitmap) {
  345. bottom = bitmap[y+1][x]
  346. }
  347. if top && bottom {
  348. builder.Write("█", style)
  349. } else if top && !bottom {
  350. builder.Write("▀", style)
  351. } else if !top && bottom {
  352. builder.Write("▄", style)
  353. } else {
  354. builder.Write(" ", style)
  355. }
  356. }
  357. builder.NewLine()
  358. }
  359. return builder.Finish(), nil
  360. }
  361. func exchangeTicket(ticket string, fingerprint string, priv *rsa.PrivateKey) (string, error) {
  362. if ticket == "" {
  363. return "", errors.New("empty ticket")
  364. }
  365. headers := apphttp.Headers()
  366. if fingerprint != "" {
  367. headers.Set("X-Fingerprint", fingerprint)
  368. headers.Set("Referer", "https://discord.com/ra/"+fingerprint)
  369. }
  370. // Create an API client without a token.
  371. client := apphttp.NewClient("")
  372. client.OnRequest = append(client.OnRequest, httputil.WithHeaders(headers))
  373. token, err := client.ExchangeRemoteAuthTicket(ticket)
  374. if err != nil {
  375. return "", err
  376. }
  377. decodedToken, err := base64.StdEncoding.DecodeString(token)
  378. if err != nil {
  379. return "", err
  380. }
  381. decryptedToken, err := rsa.DecryptOAEP(sha256.New(), nil, priv, decodedToken, nil)
  382. if err != nil {
  383. return "", err
  384. }
  385. return string(decryptedToken), nil
  386. }
  387. func (q *qrLogin) success(token string) {
  388. if q.done != nil {
  389. q.done(token, nil)
  390. }
  391. }
  392. func (q *qrLogin) fail(err error) {
  393. slog.Error("qr login failed", "err", err)
  394. if q.done != nil {
  395. q.done("", err)
  396. }
  397. }
  398. func mapWSCloseError(err error) error {
  399. if err, ok := errors.AsType[*websocket.CloseError](err); ok {
  400. switch err.Code {
  401. case 1000:
  402. return errors.New("session closed")
  403. case 4000:
  404. return errors.New("remote auth: invalid version")
  405. case 4001:
  406. return errors.New("remote auth: decode error")
  407. case 4002:
  408. return errors.New("remote auth: handshake failure")
  409. case 4003:
  410. return errors.New("remote auth: session timed out")
  411. }
  412. }
  413. return err
  414. }