// Package age groups the commands that derive age identities and // encrypt and decrypt with them. package age import ( "fmt" "io" "os" "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" ) // ownerOnly is the mode a file the tool creates gets, since a // decrypted file is as secret as what went into it. const ownerOnly = 0o600 // 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 closes an output file 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, closeDst, err := output(cmd) if err != nil { return err } err = work(dst, src) if err != nil { _ = closeDst() return err } return closeDst() } // 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: the file --output names, or the // command's own output. The second result closes a file that was // opened, so a failure to finish writing is not lost. func output(cmd *cobra.Command) (io.Writer, func() 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() error { return nil }, nil } //nolint:gosec // writing the file the user named is the point file, err := os.OpenFile( name, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, ownerOnly, ) if err != nil { return nil, nil, fmt.Errorf("creating %s: %w", name, err) } return file, file.Close, 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) }