You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
DateKeys/cmd/datekeys/author.go

181 lines
5.2 KiB

package main
import (
"errors"
"fmt"
"io"
"os"
"strings"
"g.activething.com/go/DateKeys/authorkey"
"g.activething.com/go/DateKeys/capsule"
)
// maxPassFile bounds the file of a passphrase.
const maxPassFile = 4 << 10
// announced is an author key that says, before it signs, which key signs
// and the code of AUTHOR_MESSAGE, as spec §62.1 rule 20 asks of a writer
// before each signature: whoever checks the capsule later compares the code.
type announced struct {
capsule.AuthorKey
w io.Writer
}
func (a announced) Sign(message []byte) []byte {
pub, _ := authorkey.PublicString(a.Public())
fmt.Fprintf(a.w, "Signing with the author key %s\n AUTHOR_MESSAGE code %s\n", pub, capsule.AuthorCode(message))
return a.AuthorKey.Sign(message)
}
// 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)
secret := "keep it, and its passphrase, secret"
if *plain {
secret = "it is not encrypted: keep the file secret"
}
fmt.Fprintf(stderr, "Secret key written to %s: %s. The line above is the public key: give it to whoever must know your signature.\n", *out, secret)
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
}

Powered by TurnKey Linux.