// Package age groups the commands that derive age identities and // encrypt and decrypt with them. package age import ( "fmt" "io" "os" "path/filepath" "git.eeqj.de/sneak/keyfunc/internal/agekey" "git.eeqj.de/sneak/keyfunc/internal/cli/options" "git.eeqj.de/sneak/keyfunc/internal/derive" "github.com/spf13/cobra" ) // Command returns the age command and everything under it. func Command() *cobra.Command { group := &cobra.Command{ Use: "age", Short: "derive age identities and encrypt and decrypt with them", } group.AddCommand(public(), private(), encrypt(), decrypt()) return group } // public returns the command that prints the recipient. func public() *cobra.Command { return &cobra.Command{ Use: "pub", Short: "print the recipient, the age1... public key", Args: cobra.NoArgs, RunE: func(cmd *cobra.Command, _ []string) error { key, err := derived(cmd) if err != nil { return err } return write(cmd, key.Recipient()) }, } } // private returns the command that prints the identity. func private() *cobra.Command { return &cobra.Command{ Use: "priv", Short: "print the identity, the AGE-SECRET-KEY-1... line", Args: cobra.NoArgs, RunE: func(cmd *cobra.Command, _ []string) error { key, err := derived(cmd) if err != nil { return err } return write(cmd, key.Identity()) }, } } // encrypt returns the command that encrypts a file or standard input. func encrypt() *cobra.Command { cmd := &cobra.Command{ Use: "encrypt [file]", Short: "encrypt to the derived recipient and any others given", Args: cobra.MaximumNArgs(1), RunE: runEncrypt, } cmd.Flags().StringArray( "to", nil, "another recipient to encrypt to, as well as the derived one", ) cmd.Flags().Bool( "armor", false, "write the text form instead of the binary one", ) addOutput(cmd) return cmd } // decrypt returns the command that decrypts a file or standard input. func decrypt() *cobra.Command { cmd := &cobra.Command{ Use: "decrypt [file]", Short: "decrypt with the derived identity", Args: cobra.MaximumNArgs(1), RunE: runDecrypt, } addOutput(cmd) return cmd } // runEncrypt encrypts to the derived recipient and any others given. func runEncrypt(cmd *cobra.Command, args []string) error { key, err := derived(cmd) if err != nil { return err } to, err := cmd.Flags().GetStringArray("to") if err != nil { return fmt.Errorf("reading the recipients: %w", err) } armored, err := cmd.Flags().GetBool("armor") if err != nil { return fmt.Errorf("reading the armor flag: %w", err) } return through(cmd, args, func(dst io.Writer, src io.Reader) error { return key.Encrypt(dst, src, to, armored) }) } // runDecrypt decrypts with the derived identity. func runDecrypt(cmd *cobra.Command, args []string) error { key, err := derived(cmd) if err != nil { return err } return through(cmd, args, key.Decrypt) } // through opens the input and the output the arguments ask for, hands // them to the work, and finishes the output afterwards either way. func through( cmd *cobra.Command, args []string, work func(io.Writer, io.Reader) error, ) error { src, closeSrc, err := input(cmd, args) if err != nil { return err } defer closeSrc() dst, done, err := output(cmd) if err != nil { return err } err = work(dst, src) return done(err) } // input returns what to read from: the named file, or the command's // own input when no file is named. The second result closes a file // that was opened and does nothing otherwise. func input(cmd *cobra.Command, args []string) (io.Reader, func(), error) { if len(args) == 0 { return cmd.InOrStdin(), func() {}, nil } file, err := os.Open(args[0]) if err != nil { return nil, nil, fmt.Errorf("opening %s: %w", args[0], err) } return file, func() { _ = file.Close() }, nil } // output returns what to write to: a new file beside the one --output // names, or the command's own output when it names none. The second // result finishes the write, and is given whatever the work returned: // the new file takes the named file's place only when the work // succeeded, so a file that is already there survives a run that // failed. func output(cmd *cobra.Command) (io.Writer, func(error) error, error) { name, err := cmd.Flags().GetString("output") if err != nil { return nil, nil, fmt.Errorf("reading the output file: %w", err) } if name == "" { return cmd.OutOrStdout(), func(failed error) error { return failed }, nil } // The file is made in the same directory so that putting it in // place is a rename and never a copy, and it is readable only by // its owner, which is the mode it keeps once renamed. file, err := os.CreateTemp(filepath.Dir(name), filepath.Base(name)+".") if err != nil { return nil, nil, fmt.Errorf("creating a file beside %s: %w", name, err) } return file, func(failed error) error { return finish(file, name, failed) }, nil } // finish closes the new file and puts it in the named file's place, or // throws it away when the work failed. It returns the error the caller // should report. func finish(file *os.File, name string, failed error) error { closeErr := file.Close() if failed != nil || closeErr != nil { _ = os.Remove(file.Name()) if failed != nil { return failed } return fmt.Errorf("finishing %s: %w", name, closeErr) } err := os.Rename(file.Name(), name) if err != nil { _ = os.Remove(file.Name()) return fmt.Errorf("putting %s in place: %w", name, err) } return nil } // addOutput gives a command its output file flag. func addOutput(cmd *cobra.Command) { cmd.Flags().StringP( "output", "o", "", "write to this file instead of standard output", ) } // write sends one line to wherever the command's output goes. func write(cmd *cobra.Command, line string) error { _, err := fmt.Fprintln(cmd.OutOrStdout(), line) if err != nil { return fmt.Errorf("writing the key: %w", err) } return nil } // derived returns the age key for this run. func derived(cmd *cobra.Command) (*agekey.Key, error) { index, err := options.Index(cmd) if err != nil { return nil, err } words, err := options.Mnemonic(cmd) if err != nil { return nil, err } material, err := derive.Bytes(words, agekey.Application, index) if err != nil { return nil, err } return agekey.New(material) }