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/authorkey/authorkey.go

237 lines
7.5 KiB

// Package authorkey handles the keys of the author signature of alg 1 (spec
// v0.11, §29.12): an Ed25519 seed of 32 bytes, written in Bech32 as
// DKAUTHOR-SECRET-KEY-1…, 79 characters in upper case, whose public key A is
// written dkauthor1…, 67 characters in lower case. A file of a secret key
// holds that line and, by default, is encrypted with age and a passphrase,
// scrypt with a work factor of WorkFactor.
//
// A signature of alg 1 proves that someone with the secret key signed, not
// who holds it: whoever opens a capsule knows a public key only through
// another channel (§29.12).
package authorkey
import (
"bufio"
"bytes"
"crypto/ed25519"
"errors"
"fmt"
"io"
"strings"
"filippo.io/age"
"g.activething.com/go/DateKeys/codec/bech32"
"g.activething.com/go/DateKeys/internal/ed25519strict"
)
const (
// PublicPrefix is the Bech32 prefix of a public key, in lower case.
PublicPrefix = "dkauthor"
// SecretPrefix is the Bech32 prefix of a secret key, in upper case.
SecretPrefix = "DKAUTHOR-SECRET-KEY-"
// PublicLength and SecretLength are the lengths of their Bech32 strings.
PublicLength = 67
SecretLength = 79
// WorkFactor is the scrypt work factor, logN, of an encrypted key file:
// 64 MiB, which a phone can afford, where age's 18 would take 256 MiB.
WorkFactor = 16
// maxFile bounds a key file.
maxFile = 64 << 10
)
// Key is a secret key of an author.
type Key struct {
priv ed25519.PrivateKey
}
// Generate returns a new key from the CSPRNG.
func Generate() (*Key, error) {
_, priv, err := ed25519.GenerateKey(nil)
if err != nil {
return nil, err
}
return &Key{priv}, nil
}
// NewFromSeed returns the key of a seed of 32 bytes.
func NewFromSeed(seed []byte) (*Key, error) {
if len(seed) != ed25519.SeedSize {
return nil, fmt.Errorf("authorkey: a seed has %d bytes, not %d", len(seed), ed25519.SeedSize)
}
return &Key{ed25519.NewKeyFromSeed(seed)}, nil
}
// Public returns the public key A, 32 bytes.
func (k *Key) Public() []byte { return bytes.Clone(k.priv[32:]) }
// Sign returns the Ed25519 signature of msg, 64 bytes.
func (k *Key) Sign(msg []byte) []byte { return ed25519.Sign(k.priv, msg) }
// Clear wipes the key; it cannot sign afterwards.
func (k *Key) Clear() { clear(k.priv) }
// Secret returns the secret key, DKAUTHOR-SECRET-KEY-1…. Only a key file
// should ever hold it.
func (k *Key) Secret() string {
s, err := bech32.Encode(SecretPrefix, k.priv.Seed())
if err != nil {
panic(err) // the prefix and the length are fixed
}
return s
}
// String hides the secret key, so that a %v in a log or in an error never
// prints it; Secret returns it.
func (k *Key) String() string { return SecretPrefix + "1… (hidden)" }
// GoString hides the secret key for %#v, as String does for %v.
func (k *Key) GoString() string { return k.String() }
// PublicString returns the public key pub as dkauthor1….
func PublicString(pub []byte) (string, error) {
if len(pub) != ed25519.PublicKeySize {
return "", fmt.Errorf("authorkey: a public key has %d bytes, not %d", ed25519.PublicKeySize, len(pub))
}
return bech32.Encode(PublicPrefix, pub)
}
// ParsePublic returns the public key of a string dkauthor1…: lower case, of
// PublicLength characters, with the right prefix and padding, and a key that
// the strict profile could accept: canonical, a point of the curve and not of
// small order.
func ParsePublic(s string) ([]byte, error) {
if len(s) != PublicLength {
return nil, fmt.Errorf("authorkey: a public key has %d characters, not %d", PublicLength, len(s))
}
if strings.ToLower(s) != s {
return nil, errors.New("authorkey: a public key is written in lower case")
}
hrp, data, err := bech32.Decode(s)
if err != nil {
return nil, fmt.Errorf("authorkey: %w", err)
}
if hrp != PublicPrefix || len(data) != ed25519.PublicKeySize {
return nil, fmt.Errorf("authorkey: %q is not a public key %s1…", s, PublicPrefix)
}
if !ed25519strict.Canonical(data) || !ed25519strict.OnCurve(data) || ed25519strict.SmallOrder(data) {
return nil, errors.New("authorkey: the public key is not canonical, not a point of the curve or of small order: no signature would verify")
}
return data, nil
}
// ParseSecret returns the key of a string DKAUTHOR-SECRET-KEY-1…: upper case,
// of SecretLength characters, with the right prefix and padding.
func ParseSecret(s string) (*Key, error) {
if len(s) != SecretLength {
return nil, fmt.Errorf("authorkey: a secret key has %d characters, not %d", SecretLength, len(s))
}
if strings.ToUpper(s) != s {
return nil, errors.New("authorkey: a secret key is written in upper case")
}
hrp, data, err := bech32.Decode(s)
if err != nil {
return nil, fmt.Errorf("authorkey: %w", err)
}
defer clear(data)
if hrp != SecretPrefix || len(data) != ed25519.SeedSize {
return nil, fmt.Errorf("authorkey: not a secret key %s1…", SecretPrefix)
}
return NewFromSeed(data)
}
// Marshal returns the file of a key without encryption: a comment with the
// public key and the line of the secret key.
func Marshal(k *Key) []byte {
pub, _ := PublicString(k.Public())
return []byte("# public key: " + pub + "\n" + k.Secret() + "\n")
}
// Encrypt writes the file of a key encrypted with age and passphrase,
// scrypt with a work factor of WorkFactor.
func Encrypt(w io.Writer, k *Key, passphrase string) error {
if passphrase == "" {
return errors.New("authorkey: an empty passphrase")
}
r, err := age.NewScryptRecipient(passphrase)
if err != nil {
return err
}
r.SetWorkFactor(WorkFactor)
plain := Marshal(k)
defer clear(plain)
aw, err := age.Encrypt(w, r)
if err != nil {
return err
}
if _, err := aw.Write(plain); err != nil {
return err
}
return aw.Close()
}
// Read returns the key of a file: encrypted with age and a passphrase, as
// Encrypt writes it, or plain, as Marshal does, with one line of a secret key
// and, besides it, only empty lines and comments that start with '#'. The
// passphrase is needed only for an encrypted file.
func Read(r io.Reader, passphrase string) (*Key, error) {
b, err := io.ReadAll(io.LimitReader(r, maxFile+1))
if err != nil {
return nil, err
}
defer clear(b)
if len(b) > maxFile {
return nil, fmt.Errorf("authorkey: a key file of more than %d bytes", maxFile)
}
if bytes.HasPrefix(b, []byte("age-encryption.org/v1\n")) {
if passphrase == "" {
return nil, errors.New("authorkey: the key file is encrypted: it needs its passphrase")
}
id, err := age.NewScryptIdentity(passphrase)
if err != nil {
return nil, err
}
// A work factor above ours would let a hostile file ask for gigabytes
// of memory, so it is refused. A lower one is a weaker file that the
// person made with another tool, and it opens (§29.12 fixes only the
// default).
id.SetMaxWorkFactor(WorkFactor)
ar, err := age.Decrypt(bytes.NewReader(b), id)
if err != nil {
return nil, fmt.Errorf("authorkey: %w", err)
}
plain, err := io.ReadAll(io.LimitReader(ar, maxFile+1))
if err != nil {
return nil, fmt.Errorf("authorkey: %w", err)
}
defer clear(plain)
return parseFile(plain)
}
return parseFile(b)
}
func parseFile(b []byte) (*Key, error) {
var key *Key
sc := bufio.NewScanner(bytes.NewReader(b))
for sc.Scan() {
line := strings.TrimSpace(sc.Text())
if line == "" || strings.HasPrefix(line, "#") {
continue
}
if key != nil {
return nil, errors.New("authorkey: a key file holds one secret key")
}
k, err := ParseSecret(line)
if err != nil {
return nil, err
}
key = k
}
if err := sc.Err(); err != nil {
return nil, err
}
if key == nil {
return nil, errors.New("authorkey: no secret key in the file")
}
return key, nil
}

Powered by TurnKey Linux.