// 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 named, 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 []string, armored bool, ) error { all, err := recipients(k.identity.Recipient(), to) if err != nil { return err } 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 returns the key's own recipient followed by the ones // named on the command line, so what the key encrypts it can read. func recipients(mine age.Recipient, to []string) ([]age.Recipient, error) { list := []age.Recipient{mine} for _, name := range to { 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 }