package main import ( "encoding/hex" "fmt" "net/http" "sync" "time" cs "fn-registry/functions/cybersecurity" ) // session is one logged-in browser. In the wallet model each session carries the // user's OWN bus identity: the browser unlocks its locally-encrypted private key // and hands the full keypair to the gateway over TLS, and the gateway spins up a // dedicated bus client (a *gateway) that acts AS that user. The private key lives // only in this process's memory for the life of the session — it is never written // to disk and is dropped when the session ends. // // A session may instead point at the shared operator gateway (the legacy // passphrase login); `owned` distinguishes the two so logout only closes the bus // client it created. type session struct { gw *gateway owned bool // true => gw was built for this session and must be Closed on logout handle string issuedAt time.Time } // sessionStore is the gateway's set of live browser sessions, keyed by the opaque // cookie token. It is independent of any single bus identity. type sessionStore struct { mu sync.Mutex m map[string]*session } func newSessionStore() *sessionStore { return &sessionStore{m: map[string]*session{}} } func (st *sessionStore) put(token string, s *session) { st.mu.Lock() st.m[token] = s st.mu.Unlock() } func (st *sessionStore) get(token string) (*session, bool) { st.mu.Lock() defer st.mu.Unlock() s, ok := st.m[token] return s, ok } // drop removes a session and returns it so the caller can close an owned gateway. func (st *sessionStore) drop(token string) (*session, bool) { st.mu.Lock() defer st.mu.Unlock() s, ok := st.m[token] if ok { delete(st.m, token) } return s, ok } // closeAll closes every owned per-user gateway (used at shutdown). The shared // operator gateway is owned by main and closed separately. func (st *sessionStore) closeAll() { st.mu.Lock() defer st.mu.Unlock() for tok, s := range st.m { if s.owned && s.gw != nil { _ = s.gw.Close() } delete(st.m, tok) } } // identityFromHex builds a cs.Identity from the four hex halves the browser sends // on POST /api/session. It enforces the exact key sizes (sign_pub 32, sign_priv // 64, kex_pub 32, kex_priv 32) so a malformed body cannot produce a half-built // identity that fails opaquely deep in the bus client. func identityFromHex(signPub, signPriv, kexPub, kexPriv string) (cs.Identity, error) { sp, err := hex.DecodeString(signPub) if err != nil { return cs.Identity{}, fmt.Errorf("sign_pub: %w", err) } spriv, err := hex.DecodeString(signPriv) if err != nil { return cs.Identity{}, fmt.Errorf("sign_priv: %w", err) } kp, err := hex.DecodeString(kexPub) if err != nil { return cs.Identity{}, fmt.Errorf("kex_pub: %w", err) } kpriv, err := hex.DecodeString(kexPriv) if err != nil { return cs.Identity{}, fmt.Errorf("kex_priv: %w", err) } if len(sp) != 32 || len(spriv) != 64 || len(kp) != 32 || len(kpriv) != 32 { return cs.Identity{}, fmt.Errorf("wrong key sizes (sign_pub=%d sign_priv=%d kex_pub=%d kex_priv=%d; want 32/64/32/32)", len(sp), len(spriv), len(kp), len(kpriv)) } return cs.Identity{SignPub: sp, SignPriv: spriv, KexPub: kp, KexPriv: kpriv}, nil } // sessionReq is the POST /api/session body: the user's full wallet identity (hex) // plus a display handle. The private halves arrive only over TLS and are held in // memory for the session; they are never persisted server-side. type sessionReq struct { Handle string `json:"handle"` SignPub string `json:"sign_pub"` SignPriv string `json:"sign_priv"` KexPub string `json:"kex_pub"` KexPriv string `json:"kex_priv"` } // handleSession opens a per-user session. It builds the user's bus identity from // the posted keypair, connects a dedicated bus client as that user, and issues a // session cookie bound to it. This is the wallet-model replacement for the // operator passphrase login. func (s *server) handleSession(w http.ResponseWriter, r *http.Request) { var req sessionReq if !decode(w, r, &req) { return } id, err := identityFromHex(req.SignPub, req.SignPriv, req.KexPub, req.KexPriv) if err != nil { writeErr(w, http.StatusBadRequest, "bad identity: "+err.Error()) return } cfg := s.busTemplate cfg.Identity = id gw, err := newGateway(cfg) if err != nil { writeErr(w, http.StatusBadGateway, "connect bus as user: "+err.Error()) return } tok := newToken() s.sessions.put(tok, &session{gw: gw, owned: true, handle: req.Handle, issuedAt: time.Now()}) http.SetCookie(w, &http.Cookie{ Name: sessionCookie, Value: tok, Path: "/", HttpOnly: true, SameSite: http.SameSiteLaxMode, }) writeJSON(w, http.StatusOK, meResp{Endpoint: gw.endpoint, SignPub: req.SignPub, Handle: req.Handle}) }