From 9b83d948f591d1ad045a55e62faa54f192f7a784 Mon Sep 17 00:00:00 2001 From: Filippo Valsorda Date: Sat, 27 Jun 2020 19:44:26 -0400 Subject: [PATCH] internal/age: surface format.Recipient as type Stanza --- cmd/age/encrypted_keys.go | 3 +-- internal/age/age.go | 20 ++++++++++++++------ internal/age/recipients_test.go | 5 +++-- internal/age/scrypt.go | 6 +++--- internal/age/x25519.go | 6 +++--- internal/agessh/agessh.go | 12 ++++++------ internal/agessh/agessh_test.go | 5 +++-- internal/agessh/encrypted_keys.go | 5 ++--- internal/armor/armor.go | 6 ++++-- internal/format/format.go | 12 +++++++----- 10 files changed, 46 insertions(+), 34 deletions(-) diff --git a/cmd/age/encrypted_keys.go b/cmd/age/encrypted_keys.go index e79bae6..61aa8d4 100644 --- a/cmd/age/encrypted_keys.go +++ b/cmd/age/encrypted_keys.go @@ -11,7 +11,6 @@ import ( "os" "filippo.io/age/internal/age" - "filippo.io/age/internal/format" "golang.org/x/crypto/ssh/terminal" ) @@ -25,7 +24,7 @@ func (i *LazyScryptIdentity) Type() string { return "scrypt" } -func (i *LazyScryptIdentity) Unwrap(block *format.Recipient) (fileKey []byte, err error) { +func (i *LazyScryptIdentity) Unwrap(block *age.Stanza) (fileKey []byte, err error) { pass, err := i.Passphrase() if err != nil { return nil, fmt.Errorf("could not read passphrase: %v", err) diff --git a/internal/age/age.go b/internal/age/age.go index d68283f..db49a7f 100644 --- a/internal/age/age.go +++ b/internal/age/age.go @@ -25,7 +25,7 @@ import ( // the identity, any other error might be considered fatal. type Identity interface { Type() string - Unwrap(block *format.Recipient) (fileKey []byte, err error) + Unwrap(block *Stanza) (fileKey []byte, err error) } // IdentityMatcher can be optionally implemented by an Identity that can @@ -37,7 +37,7 @@ type Identity interface { // other error might be considered fatal. type IdentityMatcher interface { Identity - Match(block *format.Recipient) error + Match(block *Stanza) error } var ErrIncorrectIdentity = errors.New("incorrect identity for recipient block") @@ -46,7 +46,15 @@ var ErrIncorrectIdentity = errors.New("incorrect identity for recipient block") // key to a recipient stanza. type Recipient interface { Type() string - Wrap(fileKey []byte) (*format.Recipient, error) + Wrap(fileKey []byte) (*Stanza, error) +} + +// A Stanza is a section of the age header that encapsulates the file key as +// encrypted to a specific recipient. +type Stanza struct { + Type string + Args []string + Body []byte } // Encrypt returns a WriteCloser. Writes to the returned value are encrypted and @@ -74,7 +82,7 @@ func Encrypt(dst io.Writer, recipients ...Recipient) (io.WriteCloser, error) { if err != nil { return nil, fmt.Errorf("failed to wrap key for recipient #%d: %v", i, err) } - hdr.Recipients = append(hdr.Recipients, block) + hdr.Recipients = append(hdr.Recipients, (*format.Stanza)(block)) } if mac, err := headerMAC(fileKey, hdr); err != nil { return nil, fmt.Errorf("failed to compute header MAC: %v", err) @@ -123,7 +131,7 @@ RecipientsLoop: } if i, ok := i.(IdentityMatcher); ok { - err := i.Match(r) + err := i.Match((*Stanza)(r)) if err != nil { if err == ErrIncorrectIdentity { continue @@ -132,7 +140,7 @@ RecipientsLoop: } } - fileKey, err = i.Unwrap(r) + fileKey, err = i.Unwrap((*Stanza)(r)) if err != nil { if err == ErrIncorrectIdentity { // TODO: we should collect these errors and return them as an diff --git a/internal/age/recipients_test.go b/internal/age/recipients_test.go index 799b418..f109582 100644 --- a/internal/age/recipients_test.go +++ b/internal/age/recipients_test.go @@ -12,6 +12,7 @@ import ( "testing" "filippo.io/age/internal/age" + "filippo.io/age/internal/format" "golang.org/x/crypto/curve25519" ) @@ -55,7 +56,7 @@ func TestX25519RoundTrip(t *testing.T) { t.Fatal(err) } b := &bytes.Buffer{} - block.Marshal(b) + (*format.Stanza)(block).Marshal(b) t.Logf("%s", b.Bytes()) out, err := i.Unwrap(block) @@ -94,7 +95,7 @@ func TestScryptRoundTrip(t *testing.T) { t.Fatal(err) } b := &bytes.Buffer{} - block.Marshal(b) + (*format.Stanza)(block).Marshal(b) t.Logf("%s", b.Bytes()) out, err := i.Unwrap(block) diff --git a/internal/age/scrypt.go b/internal/age/scrypt.go index 98159de..fd2625f 100644 --- a/internal/age/scrypt.go +++ b/internal/age/scrypt.go @@ -60,14 +60,14 @@ func (r *ScryptRecipient) SetWorkFactor(logN int) { r.workFactor = logN } -func (r *ScryptRecipient) Wrap(fileKey []byte) (*format.Recipient, error) { +func (r *ScryptRecipient) Wrap(fileKey []byte) (*Stanza, error) { salt := make([]byte, 16) if _, err := rand.Read(salt[:]); err != nil { return nil, err } logN := r.workFactor - l := &format.Recipient{ + l := &Stanza{ Type: "scrypt", Args: []string{format.EncodeToString(salt), strconv.Itoa(logN)}, } @@ -122,7 +122,7 @@ func (i *ScryptIdentity) SetMaxWorkFactor(logN int) { i.maxWorkFactor = logN } -func (i *ScryptIdentity) Unwrap(block *format.Recipient) ([]byte, error) { +func (i *ScryptIdentity) Unwrap(block *Stanza) ([]byte, error) { if block.Type != "scrypt" { return nil, ErrIncorrectIdentity } diff --git a/internal/age/x25519.go b/internal/age/x25519.go index 01b9d53..2907595 100644 --- a/internal/age/x25519.go +++ b/internal/age/x25519.go @@ -61,7 +61,7 @@ func ParseX25519Recipient(s string) (*X25519Recipient, error) { return r, nil } -func (r *X25519Recipient) Wrap(fileKey []byte) (*format.Recipient, error) { +func (r *X25519Recipient) Wrap(fileKey []byte) (*Stanza, error) { ephemeral := make([]byte, curve25519.ScalarSize) if _, err := rand.Read(ephemeral); err != nil { return nil, err @@ -76,7 +76,7 @@ func (r *X25519Recipient) Wrap(fileKey []byte) (*format.Recipient, error) { return nil, err } - l := &format.Recipient{ + l := &Stanza{ Type: "X25519", Args: []string{format.EncodeToString(ourPublicKey)}, } @@ -153,7 +153,7 @@ func ParseX25519Identity(s string) (*X25519Identity, error) { return r, nil } -func (i *X25519Identity) Unwrap(block *format.Recipient) ([]byte, error) { +func (i *X25519Identity) Unwrap(block *Stanza) ([]byte, error) { if block.Type != "X25519" { return nil, ErrIncorrectIdentity } diff --git a/internal/agessh/agessh.go b/internal/agessh/agessh.go index e3a51c9..9950c89 100644 --- a/internal/agessh/agessh.go +++ b/internal/agessh/agessh.go @@ -67,8 +67,8 @@ func NewRSARecipient(pk ssh.PublicKey) (*RSARecipient, error) { return r, nil } -func (r *RSARecipient) Wrap(fileKey []byte) (*format.Recipient, error) { - l := &format.Recipient{ +func (r *RSARecipient) Wrap(fileKey []byte) (*age.Stanza, error) { + l := &age.Stanza{ Type: "ssh-rsa", Args: []string{sshFingerprint(r.sshKey)}, } @@ -103,7 +103,7 @@ func NewRSAIdentity(key *rsa.PrivateKey) (*RSAIdentity, error) { return i, nil } -func (i *RSAIdentity) Unwrap(block *format.Recipient) ([]byte, error) { +func (i *RSAIdentity) Unwrap(block *age.Stanza) ([]byte, error) { if block.Type != "ssh-rsa" { return nil, age.ErrIncorrectIdentity } @@ -207,7 +207,7 @@ func ed25519PublicKeyToCurve25519(pk ed25519.PublicKey) []byte { const ed25519Label = "age-encryption.org/v1/ssh-ed25519" -func (r *Ed25519Recipient) Wrap(fileKey []byte) (*format.Recipient, error) { +func (r *Ed25519Recipient) Wrap(fileKey []byte) (*age.Stanza, error) { ephemeral := make([]byte, curve25519.ScalarSize) if _, err := rand.Read(ephemeral); err != nil { return nil, err @@ -229,7 +229,7 @@ func (r *Ed25519Recipient) Wrap(fileKey []byte) (*format.Recipient, error) { } sharedSecret, _ = curve25519.X25519(tweak, sharedSecret) - l := &format.Recipient{ + l := &age.Stanza{ Type: "ssh-ed25519", Args: []string{sshFingerprint(r.sshKey), format.EncodeToString(ourPublicKey[:])}, @@ -298,7 +298,7 @@ func ed25519PrivateKeyToCurve25519(pk ed25519.PrivateKey) []byte { return out[:curve25519.ScalarSize] } -func (i *Ed25519Identity) Unwrap(block *format.Recipient) ([]byte, error) { +func (i *Ed25519Identity) Unwrap(block *age.Stanza) ([]byte, error) { if block.Type != "ssh-ed25519" { return nil, age.ErrIncorrectIdentity } diff --git a/internal/agessh/agessh_test.go b/internal/agessh/agessh_test.go index cdb5f5c..d800d0e 100644 --- a/internal/agessh/agessh_test.go +++ b/internal/agessh/agessh_test.go @@ -14,6 +14,7 @@ import ( "testing" "filippo.io/age/internal/agessh" + "filippo.io/age/internal/format" "golang.org/x/crypto/ssh" ) @@ -49,7 +50,7 @@ func TestSSHRSARoundTrip(t *testing.T) { t.Fatal(err) } b := &bytes.Buffer{} - block.Marshal(b) + (*format.Stanza)(block).Marshal(b) t.Logf("%s", b.Bytes()) out, err := i.Unwrap(block) @@ -94,7 +95,7 @@ func TestSSHEd25519RoundTrip(t *testing.T) { t.Fatal(err) } b := &bytes.Buffer{} - block.Marshal(b) + (*format.Stanza)(block).Marshal(b) t.Logf("%s", b.Bytes()) out, err := i.Unwrap(block) diff --git a/internal/agessh/encrypted_keys.go b/internal/agessh/encrypted_keys.go index 2e0796f..68c07a9 100644 --- a/internal/agessh/encrypted_keys.go +++ b/internal/agessh/encrypted_keys.go @@ -12,7 +12,6 @@ import ( "fmt" "filippo.io/age/internal/age" - "filippo.io/age/internal/format" "golang.org/x/crypto/ssh" ) @@ -65,7 +64,7 @@ func (i *EncryptedSSHIdentity) Type() string { // Unwrap implements age.Identity. If the private key is still encrypted, it // will request the passphrase. The decrypted private key will be cached after // the first successful invocation. -func (i *EncryptedSSHIdentity) Unwrap(block *format.Recipient) (fileKey []byte, err error) { +func (i *EncryptedSSHIdentity) Unwrap(block *age.Stanza) (fileKey []byte, err error) { if i.decrypted != nil { return i.decrypted.Unwrap(block) } @@ -99,7 +98,7 @@ func (i *EncryptedSSHIdentity) Unwrap(block *format.Recipient) (fileKey []byte, // Match implements age.IdentityMatcher without decrypting the private key, to // ensure the passphrase is only obtained if necessary. -func (i *EncryptedSSHIdentity) Match(block *format.Recipient) error { +func (i *EncryptedSSHIdentity) Match(block *age.Stanza) error { if block.Type != i.Type() { return age.ErrIncorrectIdentity } diff --git a/internal/armor/armor.go b/internal/armor/armor.go index eebd244..7cc6615 100644 --- a/internal/armor/armor.go +++ b/internal/armor/armor.go @@ -21,8 +21,10 @@ import ( "filippo.io/age/internal/format" ) -const Header = "-----BEGIN AGE ENCRYPTED FILE-----" -const Footer = "-----END AGE ENCRYPTED FILE-----" +const ( + Header = "-----BEGIN AGE ENCRYPTED FILE-----" + Footer = "-----END AGE ENCRYPTED FILE-----" +) type armoredWriter struct { started, closed bool diff --git a/internal/format/format.go b/internal/format/format.go index cd4ed70..07eba83 100644 --- a/internal/format/format.go +++ b/internal/format/format.go @@ -18,11 +18,13 @@ import ( ) type Header struct { - Recipients []*Recipient + Recipients []*Stanza MAC []byte } -type Recipient struct { +// Stanza is assignable to age.Stanza, and if this package is made public, +// age.Stanza can be made a type alias of this type. +type Stanza struct { Type string Args []string Body []byte @@ -83,7 +85,7 @@ const intro = "age-encryption.org/v1\n" var recipientPrefix = []byte("->") var footerPrefix = []byte("---") -func (r *Recipient) Marshal(w io.Writer) error { +func (r *Stanza) Marshal(w io.Writer) error { if _, err := w.Write(recipientPrefix); err != nil { return err } @@ -155,7 +157,7 @@ func Parse(input io.Reader) (*Header, io.Reader, error) { return nil, nil, errorf("unexpected intro: %q", line) } - var r *Recipient + var r *Stanza for { line, err := rr.ReadBytes('\n') if err != nil { @@ -174,7 +176,7 @@ func Parse(input io.Reader) (*Header, io.Reader, error) { break } else if bytes.HasPrefix(line, recipientPrefix) { - r = &Recipient{} + r = &Stanza{} prefix, args := splitArgs(line) if prefix != string(recipientPrefix) || len(args) < 1 { return nil, nil, errorf("malformed recipient: %q", line)