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.
181 lines
5.2 KiB
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
|
|
}
|