eleven / messenger Public

Clone
git clone https://amadan.net/eleven/messenger

Plain git — no account needed to clone.

Download

Download this file

1package client
2
3import (
4 "bytes"
5 "encoding/json"
6 "fmt"
7 "net/http"
8 "net/url"
9 "strings"
10 "time"
11
12 "lchat/internal/e2ee"
13)
14
15// JoinResult says what kind of link Join claimed, beyond the identity itself.
16type JoinResult struct {
17 // AddDevice: the link carried an existing member's keypair — the session is
18 // that member's identity on a new device, not a new person.
19 AddDevice bool
20 // ThreadID is the pending DM a user invite drops the joiner into ("" for
21 // other link kinds) — a client can open it first.
22 ThreadID string
23}
24
25// Join is web/static/join.js run headlessly: turn any invite link into a local
26// identity. It handles all three link shapes — a fresh-person invite (generate
27// a keypair, register it), a member-minted user invite (additionally re-wrap
28// the pending DM's key to the new keypair), and an add-device link (adopt the
29// keypair the link carries; the session then IS that member). The secret T
30// never reaches the server: the invite id and claim secret derive from it, and
31// whatever the link's wrapped blob holds decrypts locally.
32func Join(base, fragment, name string) (*Session, JoinResult, error) {
33 base = strings.TrimRight(base, "/")
34 hc := &http.Client{Timeout: 30 * time.Second}
35
36 inv, err := e2ee.DeriveInvite(fragment)
37 if err != nil {
38 return nil, JoinResult{}, fmt.Errorf("client: bad invite secret: %w", err)
39 }
40
41 var info struct {
42 OK bool `json:"ok"`
43 Wrapped string `json:"wrapped"`
44 }
45 if err := getJSON(hc, base+"/api/key-invite/"+url.PathEscape(inv.ID), &info); err != nil {
46 return nil, JoinResult{}, err
47 }
48 if !info.OK {
49 return nil, JoinResult{}, fmt.Errorf("client: this invite was already used or has expired — ask for a fresh one")
50 }
51
52 // What the link carries decides who we become (join.js): a keypair means
53 // add-device; a thread key means a user invite with a pending DM; nothing
54 // means a plain fresh-person invite.
55 var payload struct {
56 Priv string `json:"priv"`
57 Pub string `json:"pub"`
58 ThreadKey string `json:"threadKey"`
59 }
60 if info.Wrapped != "" {
61 if err := e2ee.DecryptJSON(inv.WrapKey, info.Wrapped, &payload); err != nil {
62 return nil, JoinResult{}, fmt.Errorf("client: couldn't unlock this link (wrong or corrupt secret)")
63 }
64 }
65
66 pub, priv := payload.Pub, payload.Priv
67 wrappedSelf := ""
68 if priv == "" {
69 kp, err := e2ee.GenerateUserKeypair()
70 if err != nil {
71 return nil, JoinResult{}, err
72 }
73 pub, priv = kp.PublicKey, kp.PrivateKey
74 if payload.ThreadKey != "" {
75 // User invite: re-wrap the pending DM's key to our new keypair so the
76 // chat with the person who invited us opens readable.
77 if wrappedSelf, err = e2ee.WrapThreadKey(payload.ThreadKey, pub); err != nil {
78 return nil, JoinResult{}, err
79 }
80 }
81 }
82
83 var claim struct {
84 OK bool `json:"ok"`
85 Token string `json:"token"`
86 ThreadID string `json:"thread_id"`
87 }
88 err = postJSON(hc, base+"/api/key-invite/"+url.PathEscape(inv.ID)+"/claim",
89 map[string]string{"claim_secret": inv.ClaimSecret, "wrapped": wrappedSelf}, &claim)
90 if err != nil {
91 return nil, JoinResult{}, err
92 }
93 if !claim.OK || claim.Token == "" {
94 return nil, JoinResult{}, fmt.Errorf("client: couldn't claim the invite (it may have just been used)")
95 }
96
97 // Register (or, on add-device, harmlessly re-register) the public key.
98 err = postJSON(hc, base+"/api/pubkey", map[string]string{"token": claim.Token, "public_key": pub}, nil)
99 if err != nil {
100 return nil, JoinResult{}, err
101 }
102
103 sess := &Session{Base: base, Token: claim.Token, Name: name, UserPub: pub, UserPriv: priv}
104 return sess, JoinResult{AddDevice: payload.Priv != "", ThreadID: claim.ThreadID}, nil
105}
106
107func getJSON(hc *http.Client, url string, out any) error {
108 resp, err := hc.Get(url)
109 if err != nil {
110 return err
111 }
112 defer resp.Body.Close()
113 return json.NewDecoder(resp.Body).Decode(out)
114}
115
116func postJSON(hc *http.Client, url string, body, out any) error {
117 raw, err := json.Marshal(body)
118 if err != nil {
119 return err
120 }
121 resp, err := hc.Post(url, "application/json", bytes.NewReader(raw))
122 if err != nil {
123 return err
124 }
125 defer resp.Body.Close()
126 if out == nil {
127 if resp.StatusCode != http.StatusOK {
128 return fmt.Errorf("client: %s: HTTP %d", url, resp.StatusCode)
129 }
130 return nil
131 }
132 return json.NewDecoder(resp.Body).Decode(out)
133}
134