Files
secret/internal/cli/root.go
T
sneak accfd5878f
check / check (push) Waiting to run
Write encrypt and decrypt output only once they succeed (closes #143)
`secret encrypt` and `secret decrypt` emptied the file named by
`--output` before reading any input, so a failure left it empty, and a
file given as both `--input` and `--output` was emptied before it was
read. They now write into a temporary file of mode 0600 next to it, or
next to the file it is a symbolic link to, and rename that over it only
on success, using the new `secret.WriteFileAtomicFunc`, which
`secret.WriteFileAtomic` now calls. Ctrl-C or SIGTERM removes the
temporary file. A device or pipe given as `--output`, such as
`/dev/null`, is written to directly.

Judgement call: the device and pipe case goes beyond the plan.
Judgement call: a link to a file that does not exist yet is replaced.

Model: opus-5-5
2026-10-14 18:28:40 +00:00

104 lines
2.9 KiB
Go

package cli
import (
"os"
"github.com/awnumar/memguard"
"github.com/spf13/cobra"
"golang.org/x/sys/unix"
"golang.org/x/term"
"sneak.berlin/go/secret/internal/secret"
)
// Entry runs the secret CLI and returns the process exit code. It wipes
// every memguard buffer before it returns, so the caller must do nothing
// but exit with the code.
func Entry() int {
// On SIGINT or SIGTERM memguard runs this function, wipes every buffer
// and exits with status 1. The passphrase prompt turns terminal echo
// off until the read finishes, so a signal there would leave echo off.
// Only a process in the terminal's foreground process group may reset
// it: one in the background that tries is stopped instead of exiting.
// The function also removes the temporary file that encrypt or decrypt
// is writing.
terminalState, terminalErr := term.GetState(unix.Stdin)
memguard.CatchSignal(func(os.Signal) {
foreground, err := unix.IoctlGetInt(unix.Stdin, unix.TIOCGPGRP)
if terminalErr == nil && err == nil && foreground == unix.Getpgrp() {
_ = term.Restore(unix.Stdin, terminalState)
}
name := temporaryOutput.Load()
if name != nil {
_ = os.Remove(*name)
}
}, os.Interrupt, unix.SIGTERM)
defer memguard.Purge()
err := newRootCmd().Execute()
if err != nil {
return 1
}
return 0
}
func newRootCmd() *cobra.Command {
secret.Debug("newRootCmd starting")
cmd := &cobra.Command{
Use: "secret",
Short: "A simple secrets manager",
Long: `A simple secrets manager to store and retrieve sensitive ` +
`information securely.`,
// Cobra prints the error a command returns; Entry does not.
SilenceErrors: false,
// Usage belongs only to a command called wrongly. Cobra has
// checked its arguments and flag values before this runs, but
// checks required flags (ValidateRequiredFlags) and flag groups
// (ValidateFlagGroups) only after it, so both are checked here
// to keep usage for them. An error after that comes from running
// the command, and usage would only bury it. A subcommand that
// sets its own PersistentPreRun replaces this one.
PersistentPreRunE: func(cmd *cobra.Command, _ []string) error {
err := cmd.ValidateRequiredFlags()
if err != nil {
return err
}
err = cmd.ValidateFlagGroups()
if err != nil {
return err
}
cmd.SilenceUsage = true
return nil
},
}
secret.Debug("Adding subcommands to root command")
// Add subcommands
cmd.AddCommand(NewInitCmd())
cmd.AddCommand(newGenerateCmd())
cmd.AddCommand(newVaultCmd())
cmd.AddCommand(newAddCmd())
cmd.AddCommand(newGetCmd())
cmd.AddCommand(newListCmd())
cmd.AddCommand(newRemoveCmd())
cmd.AddCommand(newMoveCmd())
cmd.AddCommand(newUnlockerCmd())
cmd.AddCommand(newImportCmd())
cmd.AddCommand(newEncryptCmd())
cmd.AddCommand(newDecryptCmd())
cmd.AddCommand(newVersionCmd())
cmd.AddCommand(newInfoCmd())
cmd.AddCommand(newCompletionCmd())
secret.Debug("newRootCmd completed")
return cmd
}