//go:build linux package escrow import ( "context" "fmt" "io" "os" "os/exec" "strings" "syscall" "golang.org/x/sys/unix" ) // runWithPassphrase runs a TTY-requiring command (PBS `key change-passphrase`, F-A1 in the spike // findings) on a pty, feeding `passphrase` `reps` times (once per prompt) and DISCARDING all pty // output so the echoed passphrase can never leak (F-A2). Linux-only — the agent runs on Proxmox // hosts; a non-linux stub returns an error. func runWithPassphrase(ctx context.Context, passphrase string, reps int, name string, args ...string) error { master, err := os.OpenFile("/dev/ptmx", os.O_RDWR, 0) if err != nil { return fmt.Errorf("escrow: open ptmx: %w", err) } defer master.Close() if err := unix.IoctlSetPointerInt(int(master.Fd()), unix.TIOCSPTLCK, 0); err != nil { // unlock return fmt.Errorf("escrow: unlock pty: %w", err) } ptn, err := unix.IoctlGetInt(int(master.Fd()), unix.TIOCGPTN) if err != nil { return fmt.Errorf("escrow: pty number: %w", err) } slave, err := os.OpenFile(fmt.Sprintf("/dev/pts/%d", ptn), os.O_RDWR|syscall.O_NOCTTY, 0) if err != nil { return fmt.Errorf("escrow: open pts: %w", err) } defer slave.Close() cmd := exec.CommandContext(ctx, name, args...) cmd.Stdin, cmd.Stdout, cmd.Stderr = slave, slave, slave // New session + the slave (fd 0) becomes the controlling terminal, so the child's tty prompts // read from / write to the pty rather than failing "no tty". cmd.SysProcAttr = &syscall.SysProcAttr{Setsid: true, Setctty: true} if err := cmd.Start(); err != nil { return fmt.Errorf("escrow: start %s: %w", name, err) } // Feed the passphrase once (the tty line discipline buffers both lines for the sequential // New/Verify prompts), then DISCARD everything the pty emits — the passphrase is echoed back // on the master fd and must never reach a log. go func() { _, _ = master.WriteString(strings.Repeat(passphrase+"\n", reps)) }() go func() { _, _ = io.Copy(io.Discard, master) }() return cmd.Wait() }