package main import ( "errors" "fmt" "io" "os" "strings" "g.activething.com/go/DateKeys/authorkey" ) // maxPassFile bounds the file of a passphrase. const maxPassFile = 4 << 10 // readPass returns the passphrase in file, one line without its line ending, // or in the standard input when file is "-". The CLI takes no passphrase on // the command line, where the shell history keeps it, nor from the // environment, where other processes can read it. func readPass(cmd, file string, stdin io.Reader) (string, error) { var r io.Reader = stdin if file == "-" { // A terminal would echo the passphrase and keep it in the scrollback. if f, ok := stdin.(*os.File); ok { if info, err := f.Stat(); err == nil && info.Mode()&os.ModeCharDevice != 0 { return "", fmt.Errorf("%s: the passphrase would be read from a terminal and shown on screen: pipe it in, or use a file", cmd) } } } else { f, err := os.Open(file) if err != nil { return "", err } defer f.Close() r = f } b, err := io.ReadAll(io.LimitReader(r, maxPassFile+1)) defer clear(b) if err != nil { return "", err } if len(b) > maxPassFile { return "", fmt.Errorf("%s: the passphrase file is longer than %d bytes", cmd, maxPassFile) } s := strings.TrimSuffix(strings.TrimSuffix(string(b), "\n"), "\r") if s == "" { return "", fmt.Errorf("%s: the passphrase is empty", cmd) } return s, nil } // loadAuthorKey reads the key file path. An encrypted one needs passFile. func loadAuthorKey(cmd, path, passFile string, stdin io.Reader) (*authorkey.Key, error) { var pass string if passFile != "" { var err error if pass, err = readPass(cmd, passFile, stdin); err != nil { return nil, err } } f, err := os.Open(path) if err != nil { return nil, err } defer f.Close() k, err := authorkey.Read(f, pass) if err != nil { if pass == "" && strings.Contains(err.Error(), "passphrase") { return nil, fmt.Errorf("%s: %s is encrypted: give its passphrase with -pass-file FILE, or - for the standard input", cmd, path) } return nil, fmt.Errorf("%s: %s: %w", cmd, path, err) } return k, nil } // author runs "datekeys author keygen|public" (spec v0.11, ยง29.12). func author(args []string, stdout, stderr io.Writer, stdin io.Reader) error { if len(args) == 0 { return errUsage } switch args[0] { case "keygen": return authorKeygen(args[1:], stdout, stderr, stdin) case "public": return authorPublic(args[1:], stdout, stdin) } return errUsage } func authorKeygen(args []string, stdout, stderr io.Writer, stdin io.Reader) error { fs := newFlags("author keygen") out := fs.String("out", "", "new file for the secret key; never overwritten") passFile := fs.String("pass-file", "", "file with the passphrase that encrypts the key, or - for the standard input") plain := fs.Bool("plain", false, "write the key without encryption, as text anyone who reads the file can use") if err := parse(fs, args); err != nil { return err } switch { case *out == "": return errors.New("author keygen: -out is required") case *plain == (*passFile != ""): return errors.New("author keygen: give -pass-file, to encrypt the key, or -plain, to write it as text, and not both") } if err := checkNew(*out); err != nil { return err } var pass string if !*plain { var err error if pass, err = readPass("author keygen", *passFile, stdin); err != nil { return err } } k, err := authorkey.Generate() if err != nil { return err } defer k.Clear() err = writeAtomic(*out, func(w io.Writer) error { if *plain { b := authorkey.Marshal(k) defer clear(b) _, err := w.Write(b) return err } return authorkey.Encrypt(w, k, pass) }) if err != nil { return err } pub, err := authorkey.PublicString(k.Public()) if err != nil { return err } fmt.Fprintln(stdout, pub) fmt.Fprintf(stderr, "Secret key written to %s: keep it, and its passphrase, secret. The line above is the public key: give it to whoever must know your signature.\n", *out) return nil } func authorPublic(args []string, stdout io.Writer, stdin io.Reader) error { fs := newFlags("author public") key := fs.String("key", "", "file with the secret key") passFile := fs.String("pass-file", "", "file with the passphrase of an encrypted key, or - for the standard input") if err := parse(fs, args); err != nil { return err } if *key == "" { return errors.New("author public: -key is required") } k, err := loadAuthorKey("author public", *key, *passFile, stdin) if err != nil { return err } defer k.Clear() pub, err := authorkey.PublicString(k.Public()) if err != nil { return err } fmt.Fprintln(stdout, pub) return nil }