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.
237 lines
7.5 KiB
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
|
|
}
|