diff --git a/cmd/age-keygen/keygen.go b/cmd/age-keygen/keygen.go index 80f2d69..6a08c77 100644 --- a/cmd/age-keygen/keygen.go +++ b/cmd/age-keygen/keygen.go @@ -38,8 +38,8 @@ func main() { if fi, err := out.Stat(); err == nil { if fi.Mode().IsRegular() && fi.Mode().Perm()&0004 != 0 { - fmt.Fprintf(os.Stderr, "Warning: writing to a world-readable file.\n") - fmt.Fprintf(os.Stderr, "Consider setting the umask to 066 and trying again.\n") + fmt.Fprintf(os.Stderr, "Warning: writing to a world-readable file.\n"+ + "Consider setting the umask to 066 and trying again.\n") } } diff --git a/cmd/age/encrypted_keys.go b/cmd/age/encrypted_keys.go index 7867045..293118f 100644 --- a/cmd/age/encrypted_keys.go +++ b/cmd/age/encrypted_keys.go @@ -84,7 +84,7 @@ func sshFingerprint(pk ssh.PublicKey) string { return format.EncodeToString(h[:4]) } -func (i *EncryptedSSHIdentity) Matches(block *format.Recipient) error { +func (i *EncryptedSSHIdentity) Match(block *format.Recipient) error { if block.Type != i.Type() { return age.ErrIncorrectIdentity } diff --git a/internal/age/age.go b/internal/age/age.go index 6ce04ec..d68283f 100644 --- a/internal/age/age.go +++ b/internal/age/age.go @@ -18,23 +18,42 @@ import ( "filippo.io/age/internal/stream" ) +// An Identity is a private key or other value that can decrypt an opaque file +// key from a recipient stanza. +// +// Unwrap must return ErrIncorrectIdentity for recipient blocks that don't match +// the identity, any other error might be considered fatal. type Identity interface { Type() string Unwrap(block *format.Recipient) (fileKey []byte, err error) } +// IdentityMatcher can be optionally implemented by an Identity that can +// communicate whether it can decrypt a recipient stanza without decrypting it. +// +// If an Identity implements IdentityMatcher, its Unwrap method will only be +// invoked on blocks for which Match returned nil. Match must return +// ErrIncorrectIdentity for recipient blocks that don't match the identity, any +// other error might be considered fatal. type IdentityMatcher interface { Identity - Matches(block *format.Recipient) error + Match(block *format.Recipient) error } var ErrIncorrectIdentity = errors.New("incorrect identity for recipient block") +// A Recipient is a public key or other value that can encrypt an opaque file +// key to a recipient stanza. type Recipient interface { Type() string Wrap(fileKey []byte) (*format.Recipient, error) } +// Encrypt returns a WriteCloser. Writes to the returned value are encrypted and +// written to dst as an age file. Every recipient will be able to decrypt the file. +// +// The caller must call Close on the returned value when done for the last chunk +// to be encrypted and flushed to dst. func Encrypt(dst io.Writer, recipients ...Recipient) (io.WriteCloser, error) { if len(recipients) == 0 { return nil, errors.New("no recipients specified") @@ -77,6 +96,8 @@ func Encrypt(dst io.Writer, recipients ...Recipient) (io.WriteCloser, error) { return stream.NewWriter(streamKey(fileKey, nonce), dst) } +// Decrypt returns a Reader reading the decrypted plaintext of the age file read +// from src. All identities will be tried until one successfully decrypts the file. func Decrypt(src io.Reader, identities ...Identity) (io.Reader, error) { if len(identities) == 0 { return nil, errors.New("no identities specified") @@ -102,7 +123,7 @@ RecipientsLoop: } if i, ok := i.(IdentityMatcher); ok { - err := i.Matches(r) + err := i.Match(r) if err != nil { if err == ErrIncorrectIdentity { continue diff --git a/internal/age/scrypt.go b/internal/age/scrypt.go index 566eeb9..98159de 100644 --- a/internal/age/scrypt.go +++ b/internal/age/scrypt.go @@ -19,6 +19,14 @@ import ( const scryptLabel = "age-encryption.org/v1/scrypt" +// ScryptRecipient is a password-based recipient. +// +// If a ScryptRecipient is used, it must be the only recipient for the file: it +// can't be mixed with other recipient types and can't be used multiple times +// for the same file. +// +// Its use is not recommended for automated systems, which should prefer +// X25519Recipient. type ScryptRecipient struct { password []byte workFactor int @@ -28,6 +36,7 @@ var _ Recipient = &ScryptRecipient{} func (*ScryptRecipient) Type() string { return "scrypt" } +// NewScryptRecipient returns a new ScryptRecipient with the provided password. func NewScryptRecipient(password string) (*ScryptRecipient, error) { if len(password) == 0 { return nil, errors.New("passphrase can't be empty") @@ -42,6 +51,8 @@ func NewScryptRecipient(password string) (*ScryptRecipient, error) { // SetWorkFactor sets the scrypt work factor to 2^logN. // It must be called before Wrap. +// +// If SetWorkFactor is not called, a reasonable default is used. func (r *ScryptRecipient) SetWorkFactor(logN int) { if logN > 30 || logN < 1 { panic("age: SetWorkFactor called with illegal value") @@ -76,6 +87,7 @@ func (r *ScryptRecipient) Wrap(fileKey []byte) (*format.Recipient, error) { return l, nil } +// ScryptIdentity is a password-based identity. type ScryptIdentity struct { password []byte maxWorkFactor int @@ -85,6 +97,7 @@ var _ Identity = &ScryptIdentity{} func (*ScryptIdentity) Type() string { return "scrypt" } +// NewScryptIdentity returns a new ScryptIdentity with the provided password. func NewScryptIdentity(password string) (*ScryptIdentity, error) { if len(password) == 0 { return nil, errors.New("passphrase can't be empty") @@ -96,8 +109,12 @@ func NewScryptIdentity(password string) (*ScryptIdentity, error) { return i, nil } -// SetWorkFactor sets the maximum accepted scrypt work factor to 2^logN. +// SetMaxWorkFactor sets the maximum accepted scrypt work factor to 2^logN. // It must be called before Unwrap. +// +// This caps the amount of work that Decrypt might have to do to process +// received files. If SetMaxWorkFactor is not called, a fairly high default is +// used, which might not be suitable for systems processing untrusted files. func (i *ScryptIdentity) SetMaxWorkFactor(logN int) { if logN > 30 || logN < 1 { panic("age: SetMaxWorkFactor called with illegal value") diff --git a/internal/age/x25519.go b/internal/age/x25519.go index e7c7cb4..01b9d53 100644 --- a/internal/age/x25519.go +++ b/internal/age/x25519.go @@ -23,6 +23,7 @@ import ( const x25519Label = "age-encryption.org/v1/X25519" +// X25519Recipient is the standard age public key, based on a Curve25519 point. type X25519Recipient struct { theirPublicKey []byte } @@ -31,6 +32,7 @@ var _ Recipient = &X25519Recipient{} func (*X25519Recipient) Type() string { return "X25519" } +// NewX25519Recipient returns a new X25519Recipient from a raw Curve25519 point. func NewX25519Recipient(publicKey []byte) (*X25519Recipient, error) { if len(publicKey) != curve25519.PointSize { return nil, errors.New("invalid X25519 public key") @@ -42,6 +44,8 @@ func NewX25519Recipient(publicKey []byte) (*X25519Recipient, error) { return r, nil } +// ParseX25519Recipient returns a new X25519Recipient from a Bech32 public key +// encoding with the "age1" prefix. func ParseX25519Recipient(s string) (*X25519Recipient, error) { t, k, err := bech32.Decode(s) if err != nil { @@ -95,11 +99,13 @@ func (r *X25519Recipient) Wrap(fileKey []byte) (*format.Recipient, error) { return l, nil } +// String returns the Bech32 public key encoding of r. func (r *X25519Recipient) String() string { s, _ := bech32.Encode("age", r.theirPublicKey) return s } +// X25519Identity is the standard age private key, based on a Curve25519 scalar. type X25519Identity struct { secretKey, ourPublicKey []byte } @@ -108,6 +114,7 @@ var _ Identity = &X25519Identity{} func (*X25519Identity) Type() string { return "X25519" } +// NewX25519Identity returns a new X25519Identity from a raw Curve25519 scalar. func NewX25519Identity(secretKey []byte) (*X25519Identity, error) { if len(secretKey) != curve25519.ScalarSize { return nil, errors.New("invalid X25519 secret key") @@ -120,6 +127,7 @@ func NewX25519Identity(secretKey []byte) (*X25519Identity, error) { return i, nil } +// GenerateX25519Identity generates a fresh X25519Identity. func GenerateX25519Identity() (*X25519Identity, error) { secretKey := make([]byte, curve25519.ScalarSize) if _, err := rand.Read(secretKey); err != nil { @@ -128,6 +136,8 @@ func GenerateX25519Identity() (*X25519Identity, error) { return NewX25519Identity(secretKey) } +// ParseX25519Identity returns a new X25519Recipient from a Bech32 private key +// encoding with the "AGE-SECRET-KEY-1" prefix. func ParseX25519Identity(s string) (*X25519Identity, error) { t, k, err := bech32.Decode(s) if err != nil { @@ -179,12 +189,14 @@ func (i *X25519Identity) Unwrap(block *format.Recipient) ([]byte, error) { return fileKey, nil } +// Recipient returns the public X25519Recipient value corresponding to i. func (i *X25519Identity) Recipient() *X25519Recipient { r := &X25519Recipient{} r.theirPublicKey = i.ourPublicKey return r } +// String returns the Bech32 private key encoding of i. func (i *X25519Identity) String() string { s, _ := bech32.Encode("AGE-SECRET-KEY-", i.secretKey) return strings.ToUpper(s) diff --git a/internal/stream/stream.go b/internal/stream/stream.go index 96a0660..0f855ea 100644 --- a/internal/stream/stream.go +++ b/internal/stream/stream.go @@ -106,7 +106,7 @@ func (r *Reader) readChunk() (last bool, err error) { out, err = r.a.Open(outBuf, r.nonce[:], in, nil) } if err != nil { - return false, err + return false, errors.New("failed to decrypt and authenticate payload chunk") } incNonce(&r.nonce)