mirror of
https://github.com/FiloSottile/age.git
synced 2026-11-08 12:26:58 +00:00
Compare commits
6
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5d96bfa9a9 | ||
|
|
f04064a41b | ||
|
|
0fa220e4d7 | ||
|
|
6546df3bac | ||
|
|
15df6e2cf7 | ||
|
|
50b61862d6 |
No files matched your search
@@ -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
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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.
|
||||
|
||||
@@ -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
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user