check / check (push) Waiting to run
The key index, --words, the --to recipients and the input file of age encrypt and age decrypt were checked only after the mnemonic had been obtained, so a run that could not succeed still prompted for it or ran the mnemonic command. options.Index now refuses an index above derive.MaxIndex, which covers every command group; the mnemonic command calls the new childmnemonic.CheckWords first; age encrypt reads its recipients with the new agekey.Recipients, and both age commands open their input, before the key is derived. The README says which checks come first. Judgement call: the -o path is still looked at after the mnemonic. Model: opus-5-5
201 lines
4.9 KiB
Go
201 lines
4.9 KiB
Go
// Package agekey turns derived bytes into an age identity and uses
|
|
// that identity to encrypt and decrypt.
|
|
package agekey
|
|
|
|
import (
|
|
"bufio"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"strings"
|
|
|
|
"filippo.io/age"
|
|
"filippo.io/age/armor"
|
|
"github.com/btcsuite/btcd/btcutil/bech32"
|
|
)
|
|
|
|
const (
|
|
// Application is the number this key type occupies in the
|
|
// derivation path. It spells AGE the way BIP-85 spells RSA, as
|
|
// the ASCII codes of the letters written out.
|
|
Application = 657169
|
|
|
|
// keySize is how long an X25519 secret key is.
|
|
keySize = 32
|
|
|
|
// humanPart is what age puts in front of a secret key when it
|
|
// writes one down.
|
|
humanPart = "age-secret-key-"
|
|
)
|
|
|
|
// ErrSize is returned when the derived bytes are not the length an
|
|
// X25519 secret key has to be.
|
|
var ErrSize = errors.New("an age identity needs 32 derived bytes")
|
|
|
|
// ErrNotRecipient is returned when the file was encrypted to someone
|
|
// else, so this key cannot open it.
|
|
var ErrNotRecipient = errors.New(
|
|
"this mnemonic and index are not a recipient of the file",
|
|
)
|
|
|
|
// Key is one age identity.
|
|
type Key struct {
|
|
identity *age.X25519Identity
|
|
}
|
|
|
|
// New makes the identity whose X25519 secret key is the derived bytes,
|
|
// clamped the way that curve requires.
|
|
func New(derived []byte) (*Key, error) {
|
|
if len(derived) != keySize {
|
|
return nil, fmt.Errorf("%w, got %d", ErrSize, len(derived))
|
|
}
|
|
|
|
scalar := make([]byte, keySize)
|
|
copy(scalar, derived)
|
|
clamp(scalar)
|
|
|
|
// age offers no way to make an identity out of bytes, so the
|
|
// scalar goes in the way age writes a secret key down.
|
|
written, err := bech32.EncodeFromBase256(humanPart, scalar)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("writing the secret key: %w", err)
|
|
}
|
|
|
|
identity, err := age.ParseX25519Identity(strings.ToUpper(written))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("reading the secret key back: %w", err)
|
|
}
|
|
|
|
return &Key{identity: identity}, nil
|
|
}
|
|
|
|
// clamp makes the scalar one the curve accepts: the lowest three bits
|
|
// off, the highest bit off and the one below it on, as RFC 7748 says.
|
|
func clamp(scalar []byte) {
|
|
const (
|
|
lowestThreeOff = 0b1111_1000
|
|
highestOff = 0b0111_1111
|
|
secondHighestOn = 0b0100_0000
|
|
)
|
|
|
|
scalar[0] &= lowestThreeOff
|
|
scalar[len(scalar)-1] &= highestOff
|
|
scalar[len(scalar)-1] |= secondHighestOn
|
|
}
|
|
|
|
// Recipient returns the public key, the age1... line.
|
|
func (k *Key) Recipient() string {
|
|
return k.identity.Recipient().String()
|
|
}
|
|
|
|
// Identity returns the secret key, the AGE-SECRET-KEY-1... line.
|
|
func (k *Key) Identity() string {
|
|
return k.identity.String()
|
|
}
|
|
|
|
// Encrypt copies src to dst, encrypted to this key and to every extra
|
|
// recipient given, so the same mnemonic can always read it back again.
|
|
// Armored output is the text form age also reads.
|
|
func (k *Key) Encrypt(
|
|
dst io.Writer, src io.Reader, to []age.Recipient, armored bool,
|
|
) error {
|
|
all := append([]age.Recipient{k.identity.Recipient()}, to...)
|
|
|
|
out := dst
|
|
|
|
var text io.WriteCloser
|
|
|
|
if armored {
|
|
text = armor.NewWriter(dst)
|
|
out = text
|
|
}
|
|
|
|
err := encrypt(out, src, all)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if text == nil {
|
|
return nil
|
|
}
|
|
|
|
err = text.Close()
|
|
if err != nil {
|
|
return fmt.Errorf("finishing the text form: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Recipients reads the recipients named on the command line, for
|
|
// Encrypt to add to the key's own.
|
|
func Recipients(names []string) ([]age.Recipient, error) {
|
|
list := make([]age.Recipient, 0, len(names))
|
|
|
|
for _, name := range names {
|
|
parsed, err := age.ParseX25519Recipient(name)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("reading recipient %q: %w", name, err)
|
|
}
|
|
|
|
list = append(list, parsed)
|
|
}
|
|
|
|
return list, nil
|
|
}
|
|
|
|
// encrypt writes src into dst for the recipients.
|
|
func encrypt(dst io.Writer, src io.Reader, to []age.Recipient) error {
|
|
sealed, err := age.Encrypt(dst, to...)
|
|
if err != nil {
|
|
return fmt.Errorf("starting the encryption: %w", err)
|
|
}
|
|
|
|
_, err = io.Copy(sealed, src)
|
|
if err != nil {
|
|
return fmt.Errorf("encrypting: %w", err)
|
|
}
|
|
|
|
err = sealed.Close()
|
|
if err != nil {
|
|
return fmt.Errorf("finishing the encryption: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Decrypt copies src to dst, decrypted with this key. The text form is
|
|
// recognised by the line it starts with, so it needs no flag.
|
|
func (k *Key) Decrypt(dst io.Writer, src io.Reader) error {
|
|
plain, err := age.Decrypt(unarmored(src), k.identity)
|
|
|
|
noMatch := &age.NoIdentityMatchError{}
|
|
if errors.As(err, &noMatch) {
|
|
return ErrNotRecipient
|
|
}
|
|
|
|
if err != nil {
|
|
return fmt.Errorf("decrypting: %w", err)
|
|
}
|
|
|
|
_, err = io.Copy(dst, plain)
|
|
if err != nil {
|
|
return fmt.Errorf("reading the decrypted file: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// unarmored strips the text form when the input begins with the line
|
|
// that starts one, and leaves binary input alone.
|
|
func unarmored(src io.Reader) io.Reader {
|
|
buffered := bufio.NewReader(src)
|
|
|
|
start, err := buffered.Peek(len(armor.Header))
|
|
if err == nil && string(start) == armor.Header {
|
|
return armor.NewReader(buffered)
|
|
}
|
|
|
|
return buffered
|
|
}
|