package main import ( "crypto/rand" "encoding/base64" "errors" "fmt" "regexp" "strings" "time" "golang.org/x/crypto/nacl/box" ) // node-launcher's verb `secret` (novox/hq ADR 0259 §10): a prompt that does not show what is typed, whose // answer is sealed to the key the asker gives before it leaves this process — never in the clear on the // bus, in the runtime's log or in any call's record. The vault asks it, in its own name, to take a module's own // secret, such as a bot token, from the operator at their desk; the controller asks it for a module still filled // the old way, until R1 (issue 682). // // **What it does not do**: on an X11 desk any program of the operator's account can read the keys as they // are typed, and a program that asks this verb with a key of its own is answered with what the operator // typed into a prompt they did not ask for. The bus lets only the controller and the vault ask this verb, and the prompt, // written here from names, says so and for what, so the operator types only into one they started with give // (ADR 0234's accepted residual risk is what remains: a program drawing a window of its own). // SecretQuestion is what node-launcher.secret asks. type SecretQuestion struct { Prompt string Message string SealTo [32]byte Timeout time.Duration } // SecretAnswer is what node-launcher.secret answers: the value sealed, or why there is none. Never the value. type SecretAnswer struct { Sealed string `json:"sealed,omitempty"` Cancelled bool `json:"cancelled"` TimedOut bool `json:"timed_out,omitempty"` } // aName is a module's, a secret's or a machine's name as the mesh writes them: nothing else reaches the prompt. var aName = regexp.MustCompile(`^[a-z0-9][a-z0-9-]{0,62}$`) // aCaller is who asked, as the controller names the bus's caller (novox/hq ADR 0277): a principal such as // `g14/claude-code`, or an account at a shell, in a name's characters and spaces, at most 80. Nothing else — no line // break, no markup, no punctuation a sentence is written with — so the prompt's words stay this holder's. var aCaller = regexp.MustCompile(`^[A-Za-z0-9][A-Za-z0-9._/@ -]{0,79}$`) // theVault says the caller the runtime proved is the vault's own account (`.mesh-vault`, issue 723): the one // account beside the controller the bus lets ask this verb, and the only one that asks it in its own name. func theVault(caller string) bool { return strings.HasSuffix(caller, ".mesh-vault") } // escapeMarkup is `what` shown as text, never markup (novox/hq issue 789): the window's message is read with // pango markup on, so a catalogue's words reach it with the characters pango gives meaning to turned to their // entities first — never interpreted, whatever a module's manifest holds. var escapeMarkupReplacer = strings.NewReplacer("&", "&", "<", "<", ">", ">") func escapeMarkup(s string) string { return escapeMarkupReplacer.Replace(s) } // secretQuestion reads the call. The prompt is this holder's words, written from the module, the secret and the // machine (the review of 2026-10-09, M4): it says the mesh's controller or the mesh's vault asks, which the bus // makes true — it denies these subjects to every principal but those two, and lets the vault alone call them in // its name — and a caller's own words are refused, so nobody puts a prompt of its choosing before the operator. func secretQuestion(caller string, args map[string]any) (SecretQuestion, error) { var q SecretQuestion for _, free := range []string{"prompt", "message"} { if _, there := args[free]; there { return q, fmt.Errorf("%s is not taken: the prompt is written here from module, secret and node", free) } } names := map[string]string{} for _, key := range []string{"module", "secret", "node"} { v, err := text(args, key, true) if err != nil { return q, err } if !aName.MatchString(v) { return q, fmt.Errorf("%s %q is not a name the mesh writes", key, v) } names[key] = v } // Who asked the controller, when it says (novox/hq ADR 0277): the controller's word on the bus's caller, held to a // name's characters here, so the operator knows whose ask they are typing into — and refuses one they did not start. askedBy := "" if raw, there := args["asked_by"]; there { v, err := text(args, "asked_by", true) if err != nil { return q, err } if !aCaller.MatchString(v) || strings.Contains(v, " ") { return q, fmt.Errorf("asked_by %q is not a caller's name as the mesh writes it", raw) } askedBy = v } // What the secret is for, in the catalogue's own words (novox/hq issue 789): the controller's own line, read // from the secret's declared `prompt` and never a word the caller chose. One line, so it is shown under the // module and secret names as its own; its characters are never markup, whatever the catalogue holds. what := "" if raw, there := args["what"]; there { v, err := text(args, "what", true) if err != nil { return q, err } if strings.ContainsAny(v, "\n\r") { return q, fmt.Errorf("what %q holds more than one line", raw) } what = v } q.Prompt = names["secret"] + " for " + names["module"] asker := "controller" if theVault(caller) { asker = "vault" } q.Message = fmt.Sprintf("The mesh's %s asks for %s, the own secret of %s on %s.", asker, names["secret"], names["module"], names["node"]) if what != "" { q.Message += "\nFor: " + escapeMarkup(what) } tail := "" if askedBy != "" { tail += fmt.Sprintf(" It was asked for by %s.", askedBy) } tail += " What you type is not shown, and is sealed before it leaves this machine. Type it only if you " + "expect this ask." if what != "" { q.Message += "\n" + strings.TrimSpace(tail) } else { q.Message += tail } key, err := text(args, "seal_to", true) if err != nil { return q, err } raw, err := base64.StdEncoding.DecodeString(strings.TrimSpace(key)) if err != nil || len(raw) != 32 { return q, errors.New("seal_to is not a sealing key: 32 bytes, base64") } copy(q.SealTo[:], raw) q.Timeout, err = seconds(args, "timeout_seconds", menuMost, menuMost) return q, err } // Secret asks the operator in a prompt that shows nothing of what is typed, and seals the answer. func Secret(q SecretQuestion) (SecretAnswer, error) { s, err := findSession() if err != nil { return SecretAnswer{}, err } args := []string{"-dmenu", "-password", "-p", q.Prompt, "-theme-str", "listview { enabled: false; }"} if q.Message != "" { args = append(args, "-mesg", q.Message) } r, err := s.run(q.Timeout, "", "rofi", args...) if errors.Is(err, ErrTimedOut) { return SecretAnswer{Cancelled: true, TimedOut: true}, nil } if err != nil { return SecretAnswer{}, err } switch r.Code { case 0: case 1: return SecretAnswer{Cancelled: true}, nil default: // rofi's own words, never what was typed: that is on standard output, which is not said. return SecretAnswer{}, fmt.Errorf("rofi exited %d: %s", r.Code, strings.TrimSpace(r.Stderr)) } value := []byte(strings.TrimRight(r.Stdout, "\r\n")) r.Stdout = "" if len(value) == 0 { return SecretAnswer{Cancelled: true}, nil } sealed, err := box.SealAnonymous(nil, value, &q.SealTo, rand.Reader) for i := range value { value[i] = 0 } if err != nil { return SecretAnswer{}, errors.New("what was typed could not be sealed; nothing is answered") } return SecretAnswer{Sealed: base64.StdEncoding.EncodeToString(sealed)}, nil }