Author SHA1 Message Date
Filippo Valsorda 5d96bfa9a9 age: make Identity and Recipient work on multiple stanzas
This is a breaking change, but like the other changes to these
interfaces it should not matter to consumers of the API that don't
implement custom Recipients or Identities, which is all of them so far,
as far as I can tell.

It became clear working on plugins that we might want Recipient to
return multiple recipient stanzas, for example if the plugin recipient
is an alias or a group. The Identity side is less important, but it
might help avoid round-trips and it makes sense to keep things
symmetric.
2021-02-08 19:55:28 +01:00
Filippo Valsorda f04064a41b age: add NoIdentityMatchError
Closes #147
2021-02-08 19:55:28 +01:00
Filippo Valsorda 0fa220e4d7 age: remove IdentityMatcher
It was completely useless: the same checks in Match could be implemented
in Unwrap, returning an early ErrIncorrectIdentity.

Not sure why I added it. It felt clever at the time.
2021-02-08 19:55:28 +01:00
Filippo Valsorda 6546df3bac age: remove Type method from Recipient and Identity interfaces
The Type() method was a mistake, as proven by the fact that I can remove
it without losing any functionality. It gives special meaning to the
"0th argument" of recipient stanzas, when actually it should be left up
to Recipient implementations to make their own stanzas recognizable to
their Identity counterparts.

More importantly, there are totally reasonable Identity (and probably
Recipient) implementations that don't know their own stanza type in
advance. For example, a proxy plugin.

Concretely, it was only used to special-case "scrypt" recipients, and to
skip invoking Unwrap. The former can be done based on the returned
recipient stanza, and the latter is best avoided entirely: the Identity
should start by looking at the stanza and returning ErrIncorrectIdentity
if it's of the wrong type.

This is a breaking API change. However, we are still in beta, and none
of the public downstreams look like they would be affected, as they only
use Recipient and Identity implementations from this package, they only
use them with the interfaces defined in this package, and they don't
directly use the Type() method.
2021-02-08 19:55:28 +01:00
Filippo Valsorda 15df6e2cf7 internal/format: require the last line of stanzas to be short
We are going to reuse the stanza format for IPC in the plugin protocol,
but in that context we need stanzas to be self-closing. Currently they
almost are, but if the body is 0 modulo 48, there is no way to know if
the stanza is over after the last line.

Now, all stanzas have to end with a short line, even if empty.

No ciphertexts generated by age in the past are affected, but 3% of the
ciphertexts generated by rage will now stop working. They are still
supported by rage going forward. If it turns out to be a common issue,
we can add an exception.
2021-02-08 19:55:28 +01:00
James Brown 50b61862d6 HomebrewFormula: update to v1.0.0-beta6 (#180) 2021-02-08 19:55:28 +01:00
13 changed files with 253 additions and 201 deletions

No files matched your search

+2 -2
View File
@@ -7,8 +7,8 @@
class Age < Formula
desc "Simple, modern, secure file encryption"
homepage "https://filippo.io/age"
url "https://github.com/FiloSottile/age/archive/v1.0.0-beta2.zip"
sha256 "b7417e94c32c7e9356e441815f814073009c4a6455da96bde1536fae8cb0edbf"
url "https://github.com/FiloSottile/age/archive/v1.0.0-beta6.zip"
sha256 "6ffa23aee0f03c3e00707915e4300591847a2b0c5157ca7a696eb39bfeb7359c"
depends_on "go" => :build
+81 -57
View File
@@ -48,39 +48,40 @@ 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.
// An Identity is passed to Decrypt to unwrap an opaque file key from a
// recipient stanza. It can be for example a secret key like X25519Identity, a
// plugin, or a custom implementation.
//
// Unwrap must return ErrIncorrectIdentity for recipient blocks that don't match
// the identity, any other error might be considered fatal.
// Unwrap must return an error wrapping ErrIncorrectIdentity if none of the
// recipient stanzas match the identity, any other error will be considered
// fatal.
//
// Most age API users won't need to interact with this directly, and should
// instead pass Recipient implementations to Encrypt and Identity
// implementations to Decrypt.
type Identity interface {
Type() string
Unwrap(block *Stanza) (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
Match(block *Stanza) error
Unwrap(stanzas []*Stanza) (fileKey []byte, err 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.
// A Recipient is passed to Encrypt to wrap an opaque file key to one or more
// recipient stanza(s). It can be for example a public key like X25519Recipient,
// a plugin, or a custom implementation.
//
// Most age API users won't need to interact with this directly, and should
// instead pass Recipient implementations to Encrypt and Identity
// implementations to Decrypt.
type Recipient interface {
Type() string
Wrap(fileKey []byte) (*Stanza, 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.
//
// Most age API users won't need to interact with this directly, and should
// instead pass Recipient implementations to Encrypt and Identity
// implementations to Decrypt.
type Stanza struct {
Type string
Args []string
@@ -109,15 +110,18 @@ func Encrypt(dst io.Writer, recipients ...Recipient) (io.WriteCloser, error) {
hdr := &format.Header{}
for i, r := range recipients {
if r.Type() == "scrypt" && len(recipients) != 1 {
return nil, errors.New("an scrypt recipient must be the only one")
}
block, err := r.Wrap(fileKey)
stanzas, err := r.Wrap(fileKey)
if err != nil {
return nil, fmt.Errorf("failed to wrap key for recipient #%d: %v", i, err)
}
hdr.Recipients = append(hdr.Recipients, (*format.Stanza)(block))
for _, s := range stanzas {
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)
@@ -139,6 +143,18 @@ func Encrypt(dst io.Writer, recipients ...Recipient) (io.WriteCloser, error) {
return stream.NewWriter(streamKey(fileKey, nonce), dst)
}
// NoIdentityMatchError is returned by Decrypt when none of the supplied
// identities match the encrypted file.
type NoIdentityMatchError struct {
// Errors is a slice of all the errors returned to Decrypt by the Unwrap
// calls it made. They all wrap ErrIncorrectIdentity.
Errors []error
}
func (*NoIdentityMatchError) Error() string {
return "no identity matched any of the recipients"
}
// Decrypt decrypts a file encrypted to one or more identities.
//
// It returns a Reader reading the decrypted plaintext of the age file read
@@ -156,43 +172,32 @@ func Decrypt(src io.Reader, identities ...Identity) (io.Reader, error) {
return nil, errors.New("too many recipients")
}
var fileKey []byte
RecipientsLoop:
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")
}
for _, i := range identities {
if i.Type() != r.Type {
continue
}
}
if i, ok := i.(IdentityMatcher); ok {
err := i.Match((*Stanza)(r))
if err != nil {
if err == ErrIncorrectIdentity {
continue
}
return nil, err
}
}
fileKey, err = i.Unwrap((*Stanza)(r))
if err != nil {
if err == ErrIncorrectIdentity {
// TODO: we should collect these errors and return them as an
// []error type with an Error method. That will require turning
// ErrIncorrectIdentity into an interface or wrapper error.
continue
}
return nil, err
}
break RecipientsLoop
stanzas := make([]*Stanza, 0, len(hdr.Recipients))
for _, s := range hdr.Recipients {
stanzas = append(stanzas, (*Stanza)(s))
}
errNoMatch := &NoIdentityMatchError{}
var fileKey []byte
for _, id := range identities {
fileKey, err = id.Unwrap(stanzas)
if errors.Is(err, ErrIncorrectIdentity) {
errNoMatch.Errors = append(errNoMatch.Errors, err)
continue
}
if err != nil {
return nil, err
}
break
}
if fileKey == nil {
return nil, errors.New("no identity matched a recipient")
return nil, errNoMatch
}
if mac, err := headerMAC(fileKey, hdr); err != nil {
@@ -208,3 +213,22 @@ RecipientsLoop:
return stream.NewReader(streamKey(fileKey, nonce), payload)
}
// multiUnwrap is a helper that implements Identity.Unwrap in terms of a
// function that unwraps a single recipient stanza.
func multiUnwrap(unwrap func(*Stanza) ([]byte, error), stanzas []*Stanza) ([]byte, error) {
for _, s := range stanzas {
fileKey, err := unwrap(s)
if errors.Is(err, ErrIncorrectIdentity) {
// If we ever start returning something interesting wrapping
// ErrIncorrectIdentity, we should let it make its way up through
// Decrypt into NoIdentityMatchError.Errors.
continue
}
if err != nil {
return nil, err
}
return fileKey, nil
}
return nil, ErrIncorrectIdentity
}
+34 -14
View File
@@ -48,8 +48,6 @@ type RSARecipient struct {
var _ age.Recipient = &RSARecipient{}
func (*RSARecipient) Type() string { return "ssh-rsa" }
func NewRSARecipient(pk ssh.PublicKey) (*RSARecipient, error) {
if pk.Type() != "ssh-rsa" {
return nil, errors.New("SSH public key is not an RSA key")
@@ -70,7 +68,7 @@ func NewRSARecipient(pk ssh.PublicKey) (*RSARecipient, error) {
return r, nil
}
func (r *RSARecipient) Wrap(fileKey []byte) (*age.Stanza, error) {
func (r *RSARecipient) Wrap(fileKey []byte) ([]*age.Stanza, error) {
l := &age.Stanza{
Type: "ssh-rsa",
Args: []string{sshFingerprint(r.sshKey)},
@@ -83,7 +81,7 @@ func (r *RSARecipient) Wrap(fileKey []byte) (*age.Stanza, error) {
}
l.Body = wrappedKey
return l, nil
return []*age.Stanza{l}, nil
}
type RSAIdentity struct {
@@ -93,8 +91,6 @@ type RSAIdentity struct {
var _ age.Identity = &RSAIdentity{}
func (*RSAIdentity) Type() string { return "ssh-rsa" }
func NewRSAIdentity(key *rsa.PrivateKey) (*RSAIdentity, error) {
s, err := ssh.NewSignerFromKey(key)
if err != nil {
@@ -106,7 +102,11 @@ func NewRSAIdentity(key *rsa.PrivateKey) (*RSAIdentity, error) {
return i, nil
}
func (i *RSAIdentity) Unwrap(block *age.Stanza) ([]byte, error) {
func (i *RSAIdentity) Unwrap(stanzas []*age.Stanza) ([]byte, error) {
return multiUnwrap(i.unwrap, stanzas)
}
func (i *RSAIdentity) unwrap(block *age.Stanza) ([]byte, error) {
if block.Type != "ssh-rsa" {
return nil, age.ErrIncorrectIdentity
}
@@ -133,8 +133,6 @@ type Ed25519Recipient struct {
var _ age.Recipient = &Ed25519Recipient{}
func (*Ed25519Recipient) Type() string { return "ssh-ed25519" }
func NewEd25519Recipient(pk ssh.PublicKey) (*Ed25519Recipient, error) {
if pk.Type() != "ssh-ed25519" {
return nil, errors.New("SSH public key is not an Ed25519 key")
@@ -193,7 +191,7 @@ func ed25519PublicKeyToCurve25519(pk ed25519.PublicKey) ([]byte, error) {
const ed25519Label = "age-encryption.org/v1/ssh-ed25519"
func (r *Ed25519Recipient) Wrap(fileKey []byte) (*age.Stanza, 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
@@ -236,7 +234,7 @@ func (r *Ed25519Recipient) Wrap(fileKey []byte) (*age.Stanza, error) {
}
l.Body = wrappedKey
return l, nil
return []*age.Stanza{l}, nil
}
type Ed25519Identity struct {
@@ -246,8 +244,6 @@ type Ed25519Identity struct {
var _ age.Identity = &Ed25519Identity{}
func (*Ed25519Identity) Type() string { return "ssh-ed25519" }
func NewEd25519Identity(key ed25519.PrivateKey) (*Ed25519Identity, error) {
s, err := ssh.NewSignerFromKey(key)
if err != nil {
@@ -284,7 +280,11 @@ func ed25519PrivateKeyToCurve25519(pk ed25519.PrivateKey) []byte {
return out[:curve25519.ScalarSize]
}
func (i *Ed25519Identity) Unwrap(block *age.Stanza) ([]byte, error) {
func (i *Ed25519Identity) Unwrap(stanzas []*age.Stanza) ([]byte, error) {
return multiUnwrap(i.unwrap, stanzas)
}
func (i *Ed25519Identity) unwrap(block *age.Stanza) ([]byte, error) {
if block.Type != "ssh-ed25519" {
return nil, age.ErrIncorrectIdentity
}
@@ -331,6 +331,26 @@ func (i *Ed25519Identity) Unwrap(block *age.Stanza) ([]byte, error) {
return fileKey, nil
}
// multiUnwrap is copied from package age. It's a helper that implements
// Identity.Unwrap in terms of a function that unwraps a single recipient
// stanza.
func multiUnwrap(unwrap func(*age.Stanza) ([]byte, error), stanzas []*age.Stanza) ([]byte, error) {
for _, s := range stanzas {
fileKey, err := unwrap(s)
if errors.Is(err, age.ErrIncorrectIdentity) {
// If we ever start returning something interesting wrapping
// ErrIncorrectIdentity, we should let it make its way up through
// Decrypt into NoIdentityMatchError.Errors.
continue
}
if err != nil {
return nil, err
}
return fileKey, nil
}
return nil, age.ErrIncorrectIdentity
}
// aeadEncrypt and aeadDecrypt are copied from package age.
//
// They don't limit the file key size because multi-key attacks are irrelevant
+4 -19
View File
@@ -14,7 +14,6 @@ import (
"testing"
"filippo.io/age/agessh"
"filippo.io/age/internal/format"
"golang.org/x/crypto/ssh"
)
@@ -37,23 +36,16 @@ func TestSSHRSARoundTrip(t *testing.T) {
t.Fatal(err)
}
if r.Type() != i.Type() || r.Type() != "ssh-rsa" {
t.Errorf("invalid Type values: %v, %v", r.Type(), i.Type())
}
fileKey := make([]byte, 16)
if _, err := rand.Read(fileKey); err != nil {
t.Fatal(err)
}
block, err := r.Wrap(fileKey)
stanzas, err := r.Wrap(fileKey)
if err != nil {
t.Fatal(err)
}
b := &bytes.Buffer{}
(*format.Stanza)(block).Marshal(b)
t.Logf("%s", b.Bytes())
out, err := i.Unwrap(block)
out, err := i.Unwrap(stanzas)
if err != nil {
t.Fatal(err)
}
@@ -82,23 +74,16 @@ func TestSSHEd25519RoundTrip(t *testing.T) {
t.Fatal(err)
}
if r.Type() != i.Type() || r.Type() != "ssh-ed25519" {
t.Errorf("invalid Type values: %v, %v", r.Type(), i.Type())
}
fileKey := make([]byte, 16)
if _, err := rand.Read(fileKey); err != nil {
t.Fatal(err)
}
block, err := r.Wrap(fileKey)
stanzas, err := r.Wrap(fileKey)
if err != nil {
t.Fatal(err)
}
b := &bytes.Buffer{}
(*format.Stanza)(block).Marshal(b)
t.Logf("%s", b.Bytes())
out, err := i.Unwrap(block)
out, err := i.Unwrap(stanzas)
if err != nil {
t.Fatal(err)
}
+40 -39
View File
@@ -15,14 +15,13 @@ import (
"golang.org/x/crypto/ssh"
)
// EncryptedSSHIdentity is an age.IdentityMatcher implementation based on a
// passphrase encrypted SSH private key.
// EncryptedSSHIdentity is an age.Identity implementation based on a passphrase
// encrypted SSH private key.
//
// It provides public key based matching and deferred decryption so the
// passphrase is only requested if necessary. If the application knows it will
// unconditionally have to decrypt the private key, it would be simpler to use
// ssh.ParseRawPrivateKeyWithPassphrase directly and pass the result to
// NewEd25519Identity or NewRSAIdentity.
// It requests the passphrase only if the public key matches a recipient stanza.
// If the application knows it will always have to decrypt the private key, it
// would be simpler to use ssh.ParseRawPrivateKeyWithPassphrase directly and
// pass the result to NewEd25519Identity or NewRSAIdentity.
type EncryptedSSHIdentity struct {
pubKey ssh.PublicKey
pemBytes []byte
@@ -35,7 +34,7 @@ type EncryptedSSHIdentity struct {
//
// pubKey must be the public key associated with the encrypted private key, and
// it must have type "ssh-ed25519" or "ssh-rsa". For OpenSSH encrypted files it
// can be extracted from an ssh.PassphraseMissingError, otherwise in can often
// can be extracted from an ssh.PassphraseMissingError, otherwise it can often
// be found in ".pub" files.
//
// pemBytes must be a valid input to ssh.ParseRawPrivateKeyWithPassphrase.
@@ -54,19 +53,32 @@ func NewEncryptedSSHIdentity(pubKey ssh.PublicKey, pemBytes []byte, passphrase f
}, nil
}
var _ age.IdentityMatcher = &EncryptedSSHIdentity{}
var _ age.Identity = &EncryptedSSHIdentity{}
// Type returns the type of the underlying private key, "ssh-ed25519" or "ssh-rsa".
func (i *EncryptedSSHIdentity) Type() string {
return i.pubKey.Type()
}
// 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 *age.Stanza) (fileKey []byte, err error) {
// Unwrap implements age.Identity. If the private key is still encrypted, and
// any of the stanzas match the public key, it will request the passphrase. The
// decrypted private key will be cached after the first successful invocation.
func (i *EncryptedSSHIdentity) Unwrap(stanzas []*age.Stanza) (fileKey []byte, err error) {
if i.decrypted != nil {
return i.decrypted.Unwrap(block)
return i.decrypted.Unwrap(stanzas)
}
var match bool
for _, s := range stanzas {
if s.Type != i.pubKey.Type() {
continue
}
if len(s.Args) < 1 {
return nil, fmt.Errorf("invalid %v recipient block", i.pubKey.Type())
}
if s.Args[0] != sshFingerprint(i.pubKey) {
continue
}
match = true
break
}
if !match {
return nil, age.ErrIncorrectIdentity
}
passphrase, err := i.passphrase()
@@ -81,33 +93,22 @@ func (i *EncryptedSSHIdentity) Unwrap(block *age.Stanza) (fileKey []byte, err er
switch k := k.(type) {
case *ed25519.PrivateKey:
i.decrypted, err = NewEd25519Identity(*k)
// TODO: here and below, better check that the two public keys match,
// rather than just the type.
if i.pubKey.Type() != ssh.KeyAlgoED25519 {
return nil, fmt.Errorf("mismatched private (%s) and public (%s) SSH key types", ssh.KeyAlgoED25519, i.pubKey.Type())
}
case *rsa.PrivateKey:
i.decrypted, err = NewRSAIdentity(k)
if i.pubKey.Type() != ssh.KeyAlgoRSA {
return nil, fmt.Errorf("mismatched private (%s) and public (%s) SSH key types", ssh.KeyAlgoRSA, i.pubKey.Type())
}
default:
return nil, fmt.Errorf("unexpected SSH key type: %T", k)
}
if err != nil {
return nil, fmt.Errorf("invalid SSH key: %v", err)
}
if i.decrypted.Type() != i.pubKey.Type() {
return nil, fmt.Errorf("mismatched SSH key type: got %q, expected %q", i.decrypted.Type(), i.pubKey.Type())
}
return i.decrypted.Unwrap(block)
}
// Match implements age.IdentityMatcher without decrypting the private key, to
// ensure the passphrase is only obtained if necessary.
func (i *EncryptedSSHIdentity) Match(block *age.Stanza) error {
if block.Type != i.Type() {
return age.ErrIncorrectIdentity
}
if len(block.Args) < 1 {
return fmt.Errorf("invalid %v recipient block", i.Type())
}
if block.Args[0] != sshFingerprint(i.pubKey) {
return age.ErrIncorrectIdentity
}
return nil
return i.decrypted.Unwrap(stanzas)
}
+12 -11
View File
@@ -7,6 +7,7 @@
package main
import (
"errors"
"fmt"
"os"
@@ -20,11 +21,10 @@ type LazyScryptIdentity struct {
var _ age.Identity = &LazyScryptIdentity{}
func (i *LazyScryptIdentity) Type() string {
return "scrypt"
}
func (i *LazyScryptIdentity) Unwrap(block *age.Stanza) (fileKey []byte, err error) {
func (i *LazyScryptIdentity) Unwrap(stanzas []*age.Stanza) (fileKey []byte, err error) {
if len(stanzas) != 1 || stanzas[0].Type != "scrypt" {
return nil, age.ErrIncorrectIdentity
}
pass, err := i.Passphrase()
if err != nil {
return nil, fmt.Errorf("could not read passphrase: %v", err)
@@ -33,12 +33,13 @@ func (i *LazyScryptIdentity) Unwrap(block *age.Stanza) (fileKey []byte, err erro
if err != nil {
return nil, err
}
fileKey, err = ii.Unwrap(block)
if err == age.ErrIncorrectIdentity {
// The API will just ignore the identity if the passphrase is wrong, and
// move on, eventually returning "no identity matched a recipient".
// Since we only supply one identity from the CLI, make it a fatal
// error with a better message.
fileKey, err = ii.Unwrap(stanzas)
if errors.Is(err, age.ErrIncorrectIdentity) {
// ScryptIdentity returns ErrIncorrectIdentity for an incorrect
// passphrase, which would lead Decrypt to returning "no identity
// matched any recipient". That makes sense in the API, where there
// might be multiple configured ScryptIdentity. Since in cmd/age there
// can be only one, return a better error message.
return nil, fmt.Errorf("incorrect passphrase")
}
return fileKey, err
+6 -5
View File
@@ -1,6 +1,7 @@
age-encryption.org/v1
-> ssh-ed25519 o1Hudg SZISkI5Qn8YgUBmTKG/Zp/QpFjXWvAivzvB+hOcN5W8
dYfwGWYvCwpSU5EXIC1XqfXdsBvCi3kMypdqCVShrpk
-> joint-oil-hw
--- gC/27VAgqOEzAQMKHvBjih7sJ1oDKht+HNdguTIbjt8
fëtAeµÖ¨&8{Ëéðνcat—íΘœ¯šË·}«=šC†Ÿu
-> X25519 alRneDshIh43nwyD5+fhuTD5TReSn88f2us4hzZPyzU
pGduNK5MUhnuzMxW0qbZnC2k7mRzz69bbJpKQrRc7uc
-> A7)h-grease !,_
--- 5bA0uXjBxI6wuI5SseCRgD5/G8LkSVISRe/hnrQMb9s
¦Ã 1‘·¬Žä6_RÙäÚ…›…U<ï1ús®ÿ?`Ö+–’$§HÖW‚v?w8ZW
+1 -7
View File
@@ -1,7 +1 @@
-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
QyNTUxOQAAACAwKgrb/LkvtI887QylSoUh5xUlKr1fb37euR6et5jHowAAAJgxqUx+MalM
fgAAAAtzc2gtZWQyNTUxOQAAACAwKgrb/LkvtI887QylSoUh5xUlKr1fb37euR6et5jHow
AAAEC7gKj74YIwaM1BT2tnODjfeZJvo8lcazvL6Uljv3+nIDAqCtv8uS+0jzztDKVKhSHn
FSUqvV9vft65Hp63mMejAAAADnJ1bm5lckBmdi1hejMyAQIDBAUGBw==
-----END OPENSSH PRIVATE KEY-----
AGE-SECRET-KEY-1TRYTV7PQS5XPUYSTAQZCD7DQCWC7Q77YJD7UVFJRMW4J82Q6930QS70MRX
+12 -12
View File
@@ -47,7 +47,8 @@ const BytesPerLine = ColumnsPerLine / 4 * 3
// NewlineWriter returns a Writer that writes to dst, inserting an LF character
// every ColumnsPerLine bytes. It does not insert a newline neither at the
// beginning nor at the end of the stream.
// beginning nor at the end of the stream, but it ensures the last line is
// shorter than ColumnsPerLine, which means it might be empty.
func NewlineWriter(dst io.Writer) io.Writer {
return &newlineWriter{dst: dst}
}
@@ -63,17 +64,16 @@ func (w *newlineWriter) Write(p []byte) (int, error) {
panic("age: internal error: non-empty newlineWriter.buf")
}
for len(p) > 0 {
remainingInLine := ColumnsPerLine - (w.written % ColumnsPerLine)
if remainingInLine == ColumnsPerLine && w.written != 0 {
w.buf.Write([]byte("\n"))
}
toWrite := remainingInLine
toWrite := ColumnsPerLine - (w.written % ColumnsPerLine)
if toWrite > len(p) {
toWrite = len(p)
}
n, _ := w.buf.Write(p[:toWrite])
w.written += n
p = p[n:]
if w.written%ColumnsPerLine == 0 {
w.buf.Write([]byte("\n"))
}
}
if _, err := w.buf.WriteTo(w.dst); err != nil {
// We always return n = 0 on error because it's hard to work back to the
@@ -101,9 +101,6 @@ func (r *Stanza) Marshal(w io.Writer) error {
if _, err := io.WriteString(w, "\n"); err != nil {
return err
}
if len(r.Body) == 0 {
return nil
}
ww := base64.NewEncoder(b64, NewlineWriter(w))
if _, err := ww.Write(r.Body); err != nil {
return err
@@ -169,6 +166,9 @@ func Parse(input io.Reader) (*Header, io.Reader, error) {
}
if bytes.HasPrefix(line, footerPrefix) {
if r != nil {
return nil, nil, errorf("malformed body line %q: reached footer without previous stanza being closed\nNote: this might be a file encrypted with an old beta version of rage. Use rage to decrypt it.", line)
}
prefix, args := splitArgs(line)
if prefix != string(footerPrefix) || len(args) != 1 {
return nil, nil, errorf("malformed closing line: %q", line)
@@ -180,6 +180,9 @@ func Parse(input io.Reader) (*Header, io.Reader, error) {
break
} else if bytes.HasPrefix(line, recipientPrefix) {
if r != nil {
return nil, nil, errorf("malformed body line %q: new stanza started without previous stanza being closed\nNote: this might be a file encrypted with an old beta version of rage. Use rage to decrypt it.", line)
}
r = &Stanza{}
prefix, args := splitArgs(line)
if prefix != string(recipientPrefix) || len(args) < 1 {
@@ -202,9 +205,6 @@ func Parse(input io.Reader) (*Header, io.Reader, error) {
if len(b) > BytesPerLine {
return nil, nil, errorf("malformed body line %q: too long", line)
}
if len(b) == 0 {
return nil, nil, errorf("malformed body line %q: line is empty", line)
}
r.Body = append(r.Body, b...)
if len(b) < BytesPerLine {
// Only the last line of a body can be short.
+41
View File
@@ -0,0 +1,41 @@
// Copyright 2021 Google LLC
//
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file or at
// https://developers.google.com/open-source/licenses/bsd
package format_test
import (
"bytes"
"testing"
"filippo.io/age/internal/format"
)
func TestStanzaMarshal(t *testing.T) {
s := &format.Stanza{
Type: "test",
Args: []string{"1", "2", "3"},
Body: nil, // empty
}
buf := &bytes.Buffer{}
s.Marshal(buf)
if exp := "-> test 1 2 3\n\n"; buf.String() != exp {
t.Errorf("wrong empty stanza encoding: expected %q, got %q", exp, buf.String())
}
buf.Reset()
s.Body = []byte("AAA")
s.Marshal(buf)
if exp := "-> test 1 2 3\nQUFB\n"; buf.String() != exp {
t.Errorf("wrong normal stanza encoding: expected %q, got %q", exp, buf.String())
}
buf.Reset()
s.Body = bytes.Repeat([]byte("A"), format.BytesPerLine)
s.Marshal(buf)
if exp := "-> test 1 2 3\nQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFB\n\n"; buf.String() != exp {
t.Errorf("wrong 64 columns stanza encoding: expected %q, got %q", exp, buf.String())
}
}
+4 -19
View File
@@ -12,7 +12,6 @@ import (
"testing"
"filippo.io/age"
"filippo.io/age/internal/format"
)
func TestX25519RoundTrip(t *testing.T) {
@@ -22,10 +21,6 @@ func TestX25519RoundTrip(t *testing.T) {
}
r := i.Recipient()
if r.Type() != i.Type() || r.Type() != "X25519" {
t.Errorf("invalid Type values: %v, %v", r.Type(), i.Type())
}
if r1, err := age.ParseX25519Recipient(r.String()); err != nil {
t.Fatal(err)
} else if r1.String() != r.String() {
@@ -41,15 +36,12 @@ func TestX25519RoundTrip(t *testing.T) {
if _, err := rand.Read(fileKey); err != nil {
t.Fatal(err)
}
block, err := r.Wrap(fileKey)
stanzas, err := r.Wrap(fileKey)
if err != nil {
t.Fatal(err)
}
b := &bytes.Buffer{}
(*format.Stanza)(block).Marshal(b)
t.Logf("%s", b.Bytes())
out, err := i.Unwrap(block)
out, err := i.Unwrap(stanzas)
if err != nil {
t.Fatal(err)
}
@@ -72,23 +64,16 @@ func TestScryptRoundTrip(t *testing.T) {
t.Fatal(err)
}
if r.Type() != i.Type() || r.Type() != "scrypt" {
t.Errorf("invalid Type values: %v, %v", r.Type(), i.Type())
}
fileKey := make([]byte, 16)
if _, err := rand.Read(fileKey); err != nil {
t.Fatal(err)
}
block, err := r.Wrap(fileKey)
stanzas, err := r.Wrap(fileKey)
if err != nil {
t.Fatal(err)
}
b := &bytes.Buffer{}
(*format.Stanza)(block).Marshal(b)
t.Logf("%s", b.Bytes())
out, err := i.Unwrap(block)
out, err := i.Unwrap(stanzas)
if err != nil {
t.Fatal(err)
}
+9 -9
View File
@@ -35,8 +35,6 @@ type ScryptRecipient struct {
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 {
@@ -63,7 +61,7 @@ func (r *ScryptRecipient) SetWorkFactor(logN int) {
const scryptSaltSize = 16
func (r *ScryptRecipient) Wrap(fileKey []byte) (*Stanza, error) {
func (r *ScryptRecipient) Wrap(fileKey []byte) ([]*Stanza, error) {
salt := make([]byte, scryptSaltSize)
if _, err := rand.Read(salt[:]); err != nil {
return nil, err
@@ -87,7 +85,7 @@ func (r *ScryptRecipient) Wrap(fileKey []byte) (*Stanza, error) {
}
l.Body = wrappedKey
return l, nil
return []*Stanza{l}, nil
}
// ScryptIdentity is a password-based identity.
@@ -98,8 +96,6 @@ type ScryptIdentity struct {
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 {
@@ -125,7 +121,11 @@ func (i *ScryptIdentity) SetMaxWorkFactor(logN int) {
i.maxWorkFactor = logN
}
func (i *ScryptIdentity) Unwrap(block *Stanza) ([]byte, error) {
func (i *ScryptIdentity) Unwrap(stanzas []*Stanza) ([]byte, error) {
return multiUnwrap(i.unwrap, stanzas)
}
func (i *ScryptIdentity) unwrap(block *Stanza) ([]byte, error) {
if block.Type != "scrypt" {
return nil, ErrIncorrectIdentity
}
@@ -159,9 +159,9 @@ func (i *ScryptIdentity) Unwrap(block *Stanza) ([]byte, error) {
// This AEAD is not robust, so an attacker could craft a message that
// decrypts under two different keys (meaning two different passphrases) and
// then use an error side-channel in an online decryption oracle to learn if
// either key is correct. This is deemed acceptable because the usa case (an
// either key is correct. This is deemed acceptable because the use case (an
// online decryption oracle) is not recommended, and the security loss is
// only one bit. This also does not bypass any scrypt work, but that work
// only one bit. This also does not bypass any scrypt work, although that work
// can be precomputed in an online oracle scenario.
fileKey, err := aeadDecrypt(k, fileKeySize, block.Body)
if err != nil {
+7 -7
View File
@@ -34,8 +34,6 @@ type X25519Recipient struct {
var _ Recipient = &X25519Recipient{}
func (*X25519Recipient) Type() string { return "X25519" }
// newX25519RecipientFromPoint returns a new X25519Recipient from a raw Curve25519 point.
func newX25519RecipientFromPoint(publicKey []byte) (*X25519Recipient, error) {
if len(publicKey) != curve25519.PointSize {
@@ -65,7 +63,7 @@ func ParseX25519Recipient(s string) (*X25519Recipient, error) {
return r, nil
}
func (r *X25519Recipient) Wrap(fileKey []byte) (*Stanza, error) {
func (r *X25519Recipient) Wrap(fileKey []byte) ([]*Stanza, error) {
ephemeral := make([]byte, curve25519.ScalarSize)
if _, err := rand.Read(ephemeral); err != nil {
return nil, err
@@ -100,7 +98,7 @@ func (r *X25519Recipient) Wrap(fileKey []byte) (*Stanza, error) {
}
l.Body = wrappedKey
return l, nil
return []*Stanza{l}, nil
}
// String returns the Bech32 public key encoding of r.
@@ -117,8 +115,6 @@ type X25519Identity struct {
var _ Identity = &X25519Identity{}
func (*X25519Identity) Type() string { return "X25519" }
// newX25519IdentityFromScalar returns a new X25519Identity from a raw Curve25519 scalar.
func newX25519IdentityFromScalar(secretKey []byte) (*X25519Identity, error) {
if len(secretKey) != curve25519.ScalarSize {
@@ -158,7 +154,11 @@ func ParseX25519Identity(s string) (*X25519Identity, error) {
return r, nil
}
func (i *X25519Identity) Unwrap(block *Stanza) ([]byte, error) {
func (i *X25519Identity) Unwrap(stanzas []*Stanza) ([]byte, error) {
return multiUnwrap(i.unwrap, stanzas)
}
func (i *X25519Identity) unwrap(block *Stanza) ([]byte, error) {
if block.Type != "X25519" {
return nil, ErrIncorrectIdentity
}