mirror of
https://github.com/FiloSottile/age.git
synced 2025-12-23 13:35:14 +00:00
age: add ExtractHeader, DecryptHeader, and NewInjectedFileKeyIdentity
This commit is contained in:
84
age.go
84
age.go
@@ -46,6 +46,7 @@
|
|||||||
package age
|
package age
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"crypto/hmac"
|
"crypto/hmac"
|
||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
"errors"
|
"errors"
|
||||||
@@ -207,15 +208,29 @@ func (*NoIdentityMatchError) Error() string {
|
|||||||
// If no identity matches the encrypted file, the returned error will be of type
|
// If no identity matches the encrypted file, the returned error will be of type
|
||||||
// [NoIdentityMatchError].
|
// [NoIdentityMatchError].
|
||||||
func Decrypt(src io.Reader, identities ...Identity) (io.Reader, error) {
|
func Decrypt(src io.Reader, identities ...Identity) (io.Reader, error) {
|
||||||
if len(identities) == 0 {
|
|
||||||
return nil, errors.New("no identities specified")
|
|
||||||
}
|
|
||||||
|
|
||||||
hdr, payload, err := format.Parse(src)
|
hdr, payload, err := format.Parse(src)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to read header: %w", err)
|
return nil, fmt.Errorf("failed to read header: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fileKey, err := decryptHdr(hdr, identities...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
nonce := make([]byte, streamNonceSize)
|
||||||
|
if _, err := io.ReadFull(payload, nonce); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to read nonce: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return stream.NewReader(streamKey(fileKey, nonce), payload)
|
||||||
|
}
|
||||||
|
|
||||||
|
func decryptHdr(hdr *format.Header, identities ...Identity) ([]byte, error) {
|
||||||
|
if len(identities) == 0 {
|
||||||
|
return nil, errors.New("no identities specified")
|
||||||
|
}
|
||||||
|
|
||||||
stanzas := make([]*Stanza, 0, len(hdr.Recipients))
|
stanzas := make([]*Stanza, 0, len(hdr.Recipients))
|
||||||
for _, s := range hdr.Recipients {
|
for _, s := range hdr.Recipients {
|
||||||
stanzas = append(stanzas, (*Stanza)(s))
|
stanzas = append(stanzas, (*Stanza)(s))
|
||||||
@@ -223,6 +238,7 @@ func Decrypt(src io.Reader, identities ...Identity) (io.Reader, error) {
|
|||||||
errNoMatch := &NoIdentityMatchError{}
|
errNoMatch := &NoIdentityMatchError{}
|
||||||
var fileKey []byte
|
var fileKey []byte
|
||||||
for _, id := range identities {
|
for _, id := range identities {
|
||||||
|
var err error
|
||||||
fileKey, err = id.Unwrap(stanzas)
|
fileKey, err = id.Unwrap(stanzas)
|
||||||
if errors.Is(err, ErrIncorrectIdentity) {
|
if errors.Is(err, ErrIncorrectIdentity) {
|
||||||
errNoMatch.Errors = append(errNoMatch.Errors, err)
|
errNoMatch.Errors = append(errNoMatch.Errors, err)
|
||||||
@@ -244,12 +260,7 @@ func Decrypt(src io.Reader, identities ...Identity) (io.Reader, error) {
|
|||||||
return nil, errors.New("bad header MAC")
|
return nil, errors.New("bad header MAC")
|
||||||
}
|
}
|
||||||
|
|
||||||
nonce := make([]byte, streamNonceSize)
|
return fileKey, nil
|
||||||
if _, err := io.ReadFull(payload, nonce); err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to read nonce: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return stream.NewReader(streamKey(fileKey, nonce), payload)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// multiUnwrap is a helper that implements Identity.Unwrap in terms of a
|
// multiUnwrap is a helper that implements Identity.Unwrap in terms of a
|
||||||
@@ -270,3 +281,56 @@ func multiUnwrap(unwrap func(*Stanza) ([]byte, error), stanzas []*Stanza) ([]byt
|
|||||||
}
|
}
|
||||||
return nil, ErrIncorrectIdentity
|
return nil, ErrIncorrectIdentity
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ExtractHeader returns a detached header from the src file.
|
||||||
|
//
|
||||||
|
// The detached header can be decrypted with [DecryptHeader] (for example on a
|
||||||
|
// different system, without sharing the ciphertext) and then the file key can
|
||||||
|
// be used with [NewInjectedFileKeyIdentity].
|
||||||
|
//
|
||||||
|
// This is a low-level function that most users won't need.
|
||||||
|
func ExtractHeader(src io.Reader) ([]byte, error) {
|
||||||
|
hdr, _, err := format.Parse(src)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to read header: %w", err)
|
||||||
|
}
|
||||||
|
buf := &bytes.Buffer{}
|
||||||
|
if err := hdr.Marshal(buf); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to serialize header: %w", err)
|
||||||
|
}
|
||||||
|
return buf.Bytes(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DecryptHeader decrypts a detached header and returns a file key.
|
||||||
|
//
|
||||||
|
// The detached header can be produced by [ExtractHeader], and the
|
||||||
|
// returned file key can be used with [NewInjectedFileKeyIdentity].
|
||||||
|
//
|
||||||
|
// This is a low-level function that most users won't need.
|
||||||
|
// It is the caller's responsibility to keep track of what file the
|
||||||
|
// returned file key decrypts, and to ensure the file key is not used
|
||||||
|
// for any other purpose.
|
||||||
|
func DecryptHeader(header []byte, identities ...Identity) ([]byte, error) {
|
||||||
|
hdr, _, err := format.Parse(bytes.NewReader(header))
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to read header: %w", err)
|
||||||
|
}
|
||||||
|
return decryptHdr(hdr, identities...)
|
||||||
|
}
|
||||||
|
|
||||||
|
type injectedFileKeyIdentity struct {
|
||||||
|
fileKey []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewInjectedFileKeyIdentity returns an [Identity] that always produces
|
||||||
|
// a fixed file key, allowing the use of a file key obtained out-of-band,
|
||||||
|
// for example via [DecryptHeader].
|
||||||
|
//
|
||||||
|
// This is a low-level function that most users won't need.
|
||||||
|
func NewInjectedFileKeyIdentity(fileKey []byte) Identity {
|
||||||
|
return injectedFileKeyIdentity{fileKey}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i injectedFileKeyIdentity) Unwrap(stanzas []*Stanza) (fileKey []byte, err error) {
|
||||||
|
return i.fileKey, nil
|
||||||
|
}
|
||||||
|
|||||||
43
age_test.go
43
age_test.go
@@ -284,3 +284,46 @@ func TestLabels(t *testing.T) {
|
|||||||
t.Errorf("expected pqc+foo mixed with foo+pqc to work, got %v", err)
|
t.Errorf("expected pqc+foo mixed with foo+pqc to work, got %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestDetachedHeader(t *testing.T) {
|
||||||
|
i, err := age.GenerateX25519Identity()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
buf := &bytes.Buffer{}
|
||||||
|
w, err := age.Encrypt(buf, i.Recipient())
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if _, err := io.WriteString(w, helloWorld); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := w.Close(); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
encrypted := buf.Bytes()
|
||||||
|
|
||||||
|
header, err := age.ExtractHeader(bytes.NewReader(encrypted))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fileKey, err := age.DecryptHeader(header, i)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
identity := age.NewInjectedFileKeyIdentity(fileKey)
|
||||||
|
out, err := age.Decrypt(bytes.NewReader(encrypted), identity)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
outBytes, err := io.ReadAll(out)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if string(outBytes) != helloWorld {
|
||||||
|
t.Errorf("wrong data: %q, expected %q", outBytes, helloWorld)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user