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 controller asks it to take a module's own secret, // such as a bot token, from the operator at their desk. // // **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 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}$`) // 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 asks, which the bus makes true — it // denies these subjects to every principal but the controller — and a caller's own words are refused, so // nobody puts a prompt of its choosing before the operator. func secretQuestion(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 } q.Prompt = names["secret"] + " for " + names["module"] q.Message = fmt.Sprintf("The mesh's controller asks for %s, the own secret of %s on %s.", names["secret"], names["module"], names["node"]) if askedBy != "" { q.Message += fmt.Sprintf(" It was asked for by %s.", askedBy) } q.Message += " What you type is not shown, and is sealed before it leaves this machine. Type it only if you " + "expect this ask." 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 }