package main import ( "encoding/base64" "encoding/json" "fmt" "os" "os/exec" cs "fn-registry/functions/cybersecurity" ) // identityJSON mirrors the on-disk / pass-stored identity format shared across // the unibus tooling: the four keypair halves, each std-base64. It is the SAME // shape the bus client persists (pkg/client identity file) and the operator's // `pass` entry unibus/operator-identity, so the web gateway loads the operator's // identity without a divergent serialization. Kept in lockstep with // unibus_admin/internal/admin/identity.go. type identityJSON struct { SignPub string `json:"sign_pub"` SignPriv string `json:"sign_priv"` KexPub string `json:"kex_pub"` KexPriv string `json:"kex_priv"` } // decodeIdentity turns the JSON identity bytes into a cs.Identity. The private // halves stay only in memory; this never writes them anywhere. func decodeIdentity(raw []byte) (cs.Identity, error) { var f identityJSON if err := json.Unmarshal(raw, &f); err != nil { return cs.Identity{}, fmt.Errorf("webgw: parse identity json: %w", err) } dec := base64.StdEncoding.DecodeString signPub, err := dec(f.SignPub) if err != nil { return cs.Identity{}, fmt.Errorf("webgw: decode sign_pub: %w", err) } signPriv, err := dec(f.SignPriv) if err != nil { return cs.Identity{}, fmt.Errorf("webgw: decode sign_priv: %w", err) } kexPub, err := dec(f.KexPub) if err != nil { return cs.Identity{}, fmt.Errorf("webgw: decode kex_pub: %w", err) } kexPriv, err := dec(f.KexPriv) if err != nil { return cs.Identity{}, fmt.Errorf("webgw: decode kex_priv: %w", err) } if len(signPub) != 32 || len(signPriv) != 64 || len(kexPub) != 32 || len(kexPriv) != 32 { return cs.Identity{}, fmt.Errorf("webgw: identity has wrong key sizes (sign_pub=%d sign_priv=%d kex_pub=%d kex_priv=%d)", len(signPub), len(signPriv), len(kexPub), len(kexPriv)) } return cs.Identity{SignPub: signPub, SignPriv: signPriv, KexPub: kexPub, KexPriv: kexPriv}, nil } // loadIdentityFromFile reads a 0600 identity JSON file (the same format the bus // client writes) and decodes it. Used on a deploy host where `pass` is not // available and the operator identity is delivered as a protected file. func loadIdentityFromFile(path string) (cs.Identity, error) { raw, err := os.ReadFile(path) if err != nil { return cs.Identity{}, fmt.Errorf("webgw: read identity file %q: %w", path, err) } return decodeIdentity(raw) } // loadIdentityFromPass shells out to `pass show ` and decodes the JSON // identity it returns. The secret is held only in memory; this process never // writes it to disk or argv. Used in local operator workflows where the GNU // password store holds unibus/operator-identity. func loadIdentityFromPass(entry string) (cs.Identity, error) { out, err := exec.Command("pass", "show", entry).Output() if err != nil { return cs.Identity{}, fmt.Errorf("webgw: pass show %q: %w", entry, err) } return decodeIdentity(out) } // loadPassValue returns the first line of a `pass show ` for non-identity // secrets (e.g. the unlock passphrase). Empty entry yields an empty string and // no error, so callers can treat "no pass entry configured" as "not set". func loadPassValue(entry string) (string, error) { if entry == "" { return "", nil } out, err := exec.Command("pass", "show", entry).Output() if err != nil { return "", fmt.Errorf("webgw: pass show %q: %w", entry, err) } s := string(out) for i := 0; i < len(s); i++ { if s[i] == '\n' || s[i] == '\r' { return s[:i], nil } } return s, nil }