Skip to content

Commit

Permalink
age: move the scrypt lone recipient check out of Decrypt
Browse files Browse the repository at this point in the history
The important one is the decryption side one, because when a user types
a password they expect it to both decrypt and authenticate the file.
Moved that one out of Decrypt and into ScryptIdentity, now that
Identities get all the stanzas. special_cases--

This also opens the door to other Identity implementations that do allow
multiple scrypt recipients, if someone really wants that. The CLI will
never allow it, but an explicit choice by an API consumer feels like
something we shouldn't interfere with.

Moreover, this also allows alternative Identity implementations that use
different recipient types to replicate the behavior if they have the
same authentication semantics.

The encryption side one is only a courtesy, to stop API users from
making files that won't decrypt. Unfortunately, that one needs to stay
as a special case in Encrypt, as the Recipient can't see around itself.
However, changed it to a type assertion, so custom recipients can
generate multiple scrypt recipient stanzas, if they really want.
  • Loading branch information
FiloSottile committed Jun 15, 2021
1 parent 1ddf01d commit 9d4b2ae
Show file tree
Hide file tree
Showing 15 changed files with 49 additions and 23 deletions.
2 changes: 0 additions & 2 deletions README.md
Expand Up @@ -105,9 +105,7 @@ $ age-keygen | age -p > key.age
Public key: age1yhm4gctwfmrpz87tdslm550wrx6m79y9f2hdzt0lndjnehwj0ukqrjpyx5
Enter passphrase (leave empty to autogenerate a secure one):
Using the autogenerated passphrase "hip-roast-boring-snake-mention-east-wasp-honey-input-actress".
$ age -r age1yhm4gctwfmrpz87tdslm550wrx6m79y9f2hdzt0lndjnehwj0ukqrjpyx5 secrets.txt > secrets.txt.age
$ age -d -i key.age secrets.txt.age > secrets.txt
Enter passphrase for identity file "key.age":
```
Expand Down
21 changes: 10 additions & 11 deletions age.go
Expand Up @@ -103,6 +103,16 @@ func Encrypt(dst io.Writer, recipients ...Recipient) (io.WriteCloser, error) {
return nil, errors.New("no recipients specified")
}

// As a best effort, prevent an API user from generating a file that the
// ScryptIdentity will refuse to decrypt. This check can't unfortunately be
// implemented as part of the Recipient interface, so it lives as a special
// case in Encrypt.
for _, r := range recipients {
if _, ok := r.(*ScryptRecipient); ok && len(recipients) != 1 {
return nil, errors.New("an ScryptRecipient must be the only one for the file")
}
}

fileKey := make([]byte, fileKeySize)
if _, err := rand.Read(fileKey); err != nil {
return nil, err
Expand All @@ -118,11 +128,6 @@ func Encrypt(dst io.Writer, recipients ...Recipient) (io.WriteCloser, error) {
hdr.Recipients = append(hdr.Recipients, (*format.Stanza)(s))
}
}
for _, s := range hdr.Recipients {
if s.Type == "scrypt" && len(hdr.Recipients) != 1 {
return nil, errors.New("an scrypt recipient must be the only one")
}
}
if mac, err := headerMAC(fileKey, hdr); err != nil {
return nil, fmt.Errorf("failed to compute header MAC: %v", err)
} else {
Expand Down Expand Up @@ -169,12 +174,6 @@ func Decrypt(src io.Reader, identities ...Identity) (io.Reader, error) {
return nil, fmt.Errorf("failed to read header: %v", err)
}

for _, r := range hdr.Recipients {
if r.Type == "scrypt" && len(hdr.Recipients) != 1 {
return nil, errors.New("an scrypt recipient must be the only one")
}
}

stanzas := make([]*Stanza, 0, len(hdr.Recipients))
for _, s := range hdr.Recipients {
stanzas = append(stanzas, (*Stanza)(s))
Expand Down
28 changes: 18 additions & 10 deletions cmd/age/age_test.go
Expand Up @@ -18,24 +18,30 @@ import (
)

func TestVectors(t *testing.T) {
defaultIDs, err := parseIdentitiesFile("testdata/default_key.txt")
var defaultIDs []age.Identity

password, err := ioutil.ReadFile("testdata/default_password.txt")
if err != nil {
t.Fatal(err)
}
password, err := ioutil.ReadFile("testdata/default_password.txt")
if err == nil {
p := strings.TrimSpace(string(password))
i, err := age.NewScryptIdentity(p)
if err != nil {
t.Fatal(err)
}
defaultIDs = append(defaultIDs, i)
p := strings.TrimSpace(string(password))
i, err := age.NewScryptIdentity(p)
if err != nil {
t.Fatal(err)
}
defaultIDs = append(defaultIDs, i)

ids, err := parseIdentitiesFile("testdata/default_key.txt")
if err != nil {
t.Fatal(err)
}
defaultIDs = append(defaultIDs, ids...)

files, _ := filepath.Glob("testdata/*.age")
for _, f := range files {
_, name := filepath.Split(f)
name = strings.TrimSuffix(name, ".age")
expectPass := strings.HasPrefix(name, "good_")
expectFailure := strings.HasPrefix(name, "fail_")
expectNoMatch := strings.HasPrefix(name, "nomatch_")
t.Run(name, func(t *testing.T) {
Expand Down Expand Up @@ -73,7 +79,7 @@ func TestVectors(t *testing.T) {
if e := new(age.NoIdentityMatchError); !errors.As(err, &e) {
t.Errorf("expected ErrIncorrectIdentity, got %v", err)
}
} else {
} else if expectPass {
if err != nil {
t.Fatal(err)
}
Expand All @@ -82,6 +88,8 @@ func TestVectors(t *testing.T) {
t.Fatal(err)
}
t.Logf("%s", out)
} else {
t.Fatal("invalid test vector")
}
})
}
Expand Down
5 changes: 5 additions & 0 deletions cmd/age/encrypted_keys.go
Expand Up @@ -24,6 +24,11 @@ type LazyScryptIdentity struct {
var _ age.Identity = &LazyScryptIdentity{}

func (i *LazyScryptIdentity) Unwrap(stanzas []*age.Stanza) (fileKey []byte, err error) {
for _, s := range stanzas {
if s.Type == "scrypt" && len(stanzas) != 1 {
return nil, errors.New("an scrypt recipient must be the only one")
}
}
if len(stanzas) != 1 || stanzas[0].Type != "scrypt" {
return nil, age.ErrIncorrectIdentity
}
Expand Down
5 changes: 5 additions & 0 deletions cmd/age/testdata/fail_bad_hmac.age
@@ -0,0 +1,5 @@
age-encryption.org/v1
-> X25519 i6JOY3uvMdBuEybYbTp3ECFsOPEY/A3lJY1l0Qv2NC4
cD7VpfIOchU6ZjAccEjlPCNSOdJvVkxZPSf+7XS1YhY
--- 1111111111111111111111111111111111111111111
�-\�P9��0�hń��Tt�|:٘�#&R�r� ��
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
6 changes: 6 additions & 0 deletions cmd/age/testdata/good_simple.age
@@ -0,0 +1,6 @@
age-encryption.org/v1
-> X25519 kx2RzHNfNuts0I131KwMCyYclZzKCGMzPUaMkH9J4z4
9qEzjtIF4NsLFnxv8EEtCwOQiXj5WHl+HWaDKNeAk+4
--- N+7l3M/ofCyzZVlPJ33CTHH8AddF0itK70QV+IIvXXA
�]� �+zAI�����Ǐ�L������
H�%ѥ�
5 changes: 5 additions & 0 deletions scrypt.go
Expand Up @@ -122,6 +122,11 @@ func (i *ScryptIdentity) SetMaxWorkFactor(logN int) {
}

func (i *ScryptIdentity) Unwrap(stanzas []*Stanza) ([]byte, error) {
for _, s := range stanzas {
if s.Type == "scrypt" && len(stanzas) != 1 {
return nil, errors.New("an scrypt recipient must be the only one")
}
}
return multiUnwrap(i.unwrap, stanzas)
}

Expand Down

0 comments on commit 9d4b2ae

Please sign in to comment.