The passphrase of a key comes from a file, or from the standard input with "-", never from the command line or the environment, so the CLI needs no terminal library. decrypt -expect-author shows the signature as always and then fails, with the files already written, unless it is F3 with that key. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>v0.11
parent
c3175a150a
commit
d1441deb83
@ -0,0 +1,152 @@
|
||||
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 != "-" {
|
||||
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 {
|
||||
_, err := w.Write(authorkey.Marshal(k))
|
||||
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
|
||||
}
|
||||
@ -0,0 +1,97 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"g.activething.com/go/DateKeys/profile"
|
||||
)
|
||||
|
||||
// Spec v0.11, §29.9, §29.12: author keygen, encrypt -sign and decrypt
|
||||
// -expect-author, with the passphrase in a file and in the standard input.
|
||||
func TestAuthorSignRoundTrip(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
in := filepath.Join(dir, "carta.txt")
|
||||
os.WriteFile(in, []byte("firmada"), 0o600)
|
||||
pass := filepath.Join(dir, "pass.txt")
|
||||
os.WriteFile(pass, []byte("una contraseña larga\r\n"), 0o600)
|
||||
p := profile.Quicknet()
|
||||
unlock := time.Unix(p.GenesisTime+999*3, 0).UTC() // round 1000
|
||||
genesis := time.Unix(p.GenesisTime, 0)
|
||||
|
||||
keyFile := filepath.Join(dir, "autor.key")
|
||||
pub, stderr, err := cli(t, genesis, "author", "keygen", "-out", keyFile, "-pass-file", pass)
|
||||
if err != nil || !strings.HasPrefix(pub, "dkauthor1") || !strings.Contains(stderr, "keep it") {
|
||||
t.Fatalf("keygen: %q %v %s", pub, err, stderr)
|
||||
}
|
||||
pub = strings.TrimSpace(pub)
|
||||
if b, _ := os.ReadFile(keyFile); !strings.HasPrefix(string(b), "age-encryption.org/v1") {
|
||||
t.Error("the key file is not encrypted")
|
||||
}
|
||||
if _, _, err := cli(t, genesis, "author", "keygen", "-out", keyFile, "-pass-file", pass); err == nil {
|
||||
t.Error("overwrote a key")
|
||||
}
|
||||
if _, _, err := cli(t, genesis, "author", "keygen", "-out", filepath.Join(dir, "x.key")); err == nil {
|
||||
t.Error("wrote a key without a passphrase and without -plain")
|
||||
}
|
||||
if got, _, err := cli(t, genesis, "author", "public", "-key", keyFile, "-pass-file", pass); err != nil || strings.TrimSpace(got) != pub {
|
||||
t.Errorf("public: %q %v", got, err)
|
||||
}
|
||||
if _, _, err := cli(t, genesis, "author", "public", "-key", keyFile); err == nil || !strings.Contains(err.Error(), "-pass-file") {
|
||||
t.Errorf("public of an encrypted key without its passphrase: %v", err)
|
||||
}
|
||||
other := filepath.Join(dir, "otra.key")
|
||||
otherPub, _, err := cli(t, genesis, "author", "keygen", "-out", other, "-plain")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
otherPub = strings.TrimSpace(otherPub)
|
||||
|
||||
dkc := filepath.Join(dir, "c.dkc")
|
||||
if _, stderr, err := cli(t, genesis, "encrypt", "-at", unlock.Format(time.RFC3339), "-in", in, "-out", dkc, "-sign", keyFile, "-sign-pass-file", pass); err != nil {
|
||||
t.Fatalf("%v\n%s", err, stderr)
|
||||
}
|
||||
// The passphrase from the standard input.
|
||||
stdin = strings.NewReader("una contraseña larga\n")
|
||||
t.Cleanup(func() { stdin = os.Stdin })
|
||||
dkc2 := filepath.Join(dir, "c2.dkc")
|
||||
if _, stderr, err := cli(t, genesis, "encrypt", "-at", unlock.Format(time.RFC3339), "-in", in, "-out", dkc2, "-sign", keyFile, "-sign-pass-file", "-", "-large-area"); err != nil {
|
||||
t.Fatalf("%v\n%s", err, stderr)
|
||||
}
|
||||
|
||||
for i, tc := range []struct {
|
||||
file, expect, want string
|
||||
fails bool
|
||||
}{
|
||||
{dkc, "", "Firmado con la clave " + pub, false},
|
||||
{dkc, pub, "Firmado con la clave que guardaste como -expect-author.", false},
|
||||
{dkc2, pub, "Firmado con la clave que guardaste como -expect-author.", false},
|
||||
{dkc, otherPub, "Firmado con la clave " + pub, true},
|
||||
} {
|
||||
args := []string{"decrypt", "-in", tc.file, "-out", filepath.Join(dir, "out"+string(rune('a'+i))), "-relay", relay(t)}
|
||||
if tc.expect != "" {
|
||||
args = append(args, "-expect-author", tc.expect)
|
||||
}
|
||||
stdout, _, err := cli(t, later, args...)
|
||||
if (err != nil) != tc.fails || !strings.Contains(stdout, tc.want) {
|
||||
t.Errorf("case %d: %v\n%s", i, err, stdout)
|
||||
}
|
||||
if tc.fails && (err == nil || !strings.Contains(err.Error(), "not signed with the expected key")) {
|
||||
t.Errorf("case %d: %v", i, err)
|
||||
}
|
||||
}
|
||||
// An unsigned capsule does not meet -expect-author.
|
||||
plain := filepath.Join(dir, "plain.dkc")
|
||||
if _, _, err := cli(t, genesis, "encrypt", "-at", unlock.Format(time.RFC3339), "-in", in, "-out", plain); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, _, err := cli(t, later, "decrypt", "-in", plain, "-out", filepath.Join(dir, "outz"), "-relay", relay(t), "-expect-author", pub); err == nil {
|
||||
t.Error("an unsigned capsule met -expect-author")
|
||||
}
|
||||
if _, _, err := cli(t, later, "decrypt", "-in", plain, "-out", filepath.Join(dir, "outy"), "-relay", relay(t), "-expect-author", "dkauthor1x"); err == nil {
|
||||
t.Error("a malformed -expect-author was accepted")
|
||||
}
|
||||
}
|
||||
Loading…
Reference in new issue