The age commands: pub, priv, encrypt and decrypt (closes #3)
All checks were successful
check / check (push) Successful in 3m57s
All checks were successful
check / check (push) Successful in 3m57s
The application number is 657169, so the path is m/83696968'/657169'/<n>'. The 32 derived bytes are clamped the way X25519 requires and go through bech32 into an age identity, which is the only route age offers from raw bytes to a key; these are the steps sneak/secret takes in its agehd package. The derived recipient is always first in the recipient list, so the mnemonic that encrypted a file can always read it back. Decrypting recognises the text form by the line it starts with, so it needs no flag. A file named with -o is created readable only by its owner, since a decrypted one is as secret as what went into it. Model: opus-5
This commit is contained in:
238
internal/cli/age/age.go
Normal file
238
internal/cli/age/age.go
Normal file
@@ -0,0 +1,238 @@
|
||||
// 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)
|
||||
}
|
||||
Reference in New Issue
Block a user