mirror of
https://github.com/FiloSottile/age.git
synced 2025-12-23 05:25:14 +00:00
age,plugin: add RecipientWithLabels
This commit is contained in:
59
age.go
59
age.go
@@ -13,7 +13,7 @@
|
|||||||
// age encrypted files are binary and not malleable. For encoding them as text,
|
// age encrypted files are binary and not malleable. For encoding them as text,
|
||||||
// use the filippo.io/age/armor package.
|
// use the filippo.io/age/armor package.
|
||||||
//
|
//
|
||||||
// Key management
|
// # Key management
|
||||||
//
|
//
|
||||||
// age does not have a global keyring. Instead, since age keys are small,
|
// age does not have a global keyring. Instead, since age keys are small,
|
||||||
// textual, and cheap, you are encouraged to generate dedicated keys for each
|
// textual, and cheap, you are encouraged to generate dedicated keys for each
|
||||||
@@ -34,7 +34,7 @@
|
|||||||
// infrastructure, you might want to consider implementing your own Recipient
|
// infrastructure, you might want to consider implementing your own Recipient
|
||||||
// and Identity.
|
// and Identity.
|
||||||
//
|
//
|
||||||
// Backwards compatibility
|
// # Backwards compatibility
|
||||||
//
|
//
|
||||||
// Files encrypted with a stable version (not alpha, beta, or release candidate)
|
// Files encrypted with a stable version (not alpha, beta, or release candidate)
|
||||||
// of age, or with any v1.0.0 beta or release candidate, will decrypt with any
|
// of age, or with any v1.0.0 beta or release candidate, will decrypt with any
|
||||||
@@ -51,6 +51,7 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
"sort"
|
||||||
|
|
||||||
"filippo.io/age/internal/format"
|
"filippo.io/age/internal/format"
|
||||||
"filippo.io/age/internal/stream"
|
"filippo.io/age/internal/stream"
|
||||||
@@ -84,6 +85,21 @@ type Recipient interface {
|
|||||||
Wrap(fileKey []byte) ([]*Stanza, error)
|
Wrap(fileKey []byte) ([]*Stanza, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RecipientWithLabels can be optionally implemented by a Recipient, in which
|
||||||
|
// case Encrypt will use WrapWithLabels instead of Wrap.
|
||||||
|
//
|
||||||
|
// Encrypt will succeed only if the labels returned by all the recipients
|
||||||
|
// (assuming the empty set for those that don't implement RecipientWithLabels)
|
||||||
|
// are the same.
|
||||||
|
//
|
||||||
|
// This can be used to ensure a recipient is only used with other recipients
|
||||||
|
// with equivalent properties (for example by setting a "postquantum" label) or
|
||||||
|
// to ensure a recipient is always used alone (by returning a random label, for
|
||||||
|
// example to preserve its authentication properties).
|
||||||
|
type RecipientWithLabels interface {
|
||||||
|
WrapWithLabels(fileKey []byte) (s []*Stanza, labels []string, err error)
|
||||||
|
}
|
||||||
|
|
||||||
// A Stanza is a section of the age header that encapsulates the file key as
|
// A Stanza is a section of the age header that encapsulates the file key as
|
||||||
// encrypted to a specific recipient.
|
// encrypted to a specific recipient.
|
||||||
//
|
//
|
||||||
@@ -111,27 +127,24 @@ func Encrypt(dst io.Writer, recipients ...Recipient) (io.WriteCloser, error) {
|
|||||||
return nil, errors.New("no recipients specified")
|
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)
|
fileKey := make([]byte, fileKeySize)
|
||||||
if _, err := rand.Read(fileKey); err != nil {
|
if _, err := rand.Read(fileKey); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
hdr := &format.Header{}
|
hdr := &format.Header{}
|
||||||
|
var labels []string
|
||||||
for i, r := range recipients {
|
for i, r := range recipients {
|
||||||
stanzas, err := r.Wrap(fileKey)
|
stanzas, l, err := wrapWithLabels(r, fileKey)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to wrap key for recipient #%d: %v", i, err)
|
return nil, fmt.Errorf("failed to wrap key for recipient #%d: %v", i, err)
|
||||||
}
|
}
|
||||||
|
sort.Strings(l)
|
||||||
|
if i == 0 {
|
||||||
|
labels = l
|
||||||
|
} else if !slicesEqual(labels, l) {
|
||||||
|
return nil, fmt.Errorf("incompatible recipients")
|
||||||
|
}
|
||||||
for _, s := range stanzas {
|
for _, s := range stanzas {
|
||||||
hdr.Recipients = append(hdr.Recipients, (*format.Stanza)(s))
|
hdr.Recipients = append(hdr.Recipients, (*format.Stanza)(s))
|
||||||
}
|
}
|
||||||
@@ -156,6 +169,26 @@ func Encrypt(dst io.Writer, recipients ...Recipient) (io.WriteCloser, error) {
|
|||||||
return stream.NewWriter(streamKey(fileKey, nonce), dst)
|
return stream.NewWriter(streamKey(fileKey, nonce), dst)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func wrapWithLabels(r Recipient, fileKey []byte) (s []*Stanza, labels []string, err error) {
|
||||||
|
if r, ok := r.(RecipientWithLabels); ok {
|
||||||
|
return r.WrapWithLabels(fileKey)
|
||||||
|
}
|
||||||
|
s, err = r.Wrap(fileKey)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func slicesEqual(s1, s2 []string) bool {
|
||||||
|
if len(s1) != len(s2) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for i := range s1 {
|
||||||
|
if s1[i] != s2[i] {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
// NoIdentityMatchError is returned by Decrypt when none of the supplied
|
// NoIdentityMatchError is returned by Decrypt when none of the supplied
|
||||||
// identities match the encrypted file.
|
// identities match the encrypted file.
|
||||||
type NoIdentityMatchError struct {
|
type NoIdentityMatchError struct {
|
||||||
|
|||||||
64
age_test.go
64
age_test.go
@@ -220,3 +220,67 @@ AGE-SECRET-KEY--1D6K0SGAX3NU66R4GYFZY0UQWCLM3UUSF3CXLW4KXZM342WQSJ82QKU59Q`},
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type testRecipient struct {
|
||||||
|
labels []string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (testRecipient) Wrap(fileKey []byte) ([]*age.Stanza, error) {
|
||||||
|
panic("expected WrapWithLabels instead")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t testRecipient) WrapWithLabels(fileKey []byte) (s []*age.Stanza, labels []string, err error) {
|
||||||
|
return []*age.Stanza{{Type: "test"}}, t.labels, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLabels(t *testing.T) {
|
||||||
|
scrypt, err := age.NewScryptRecipient("xxx")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
i, err := age.GenerateX25519Identity()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
x25519 := i.Recipient()
|
||||||
|
pqc := testRecipient{[]string{"postquantum"}}
|
||||||
|
pqcAndFoo := testRecipient{[]string{"postquantum", "foo"}}
|
||||||
|
fooAndPQC := testRecipient{[]string{"foo", "postquantum"}}
|
||||||
|
|
||||||
|
if _, err := age.Encrypt(io.Discard, scrypt, scrypt); err == nil {
|
||||||
|
t.Error("expected two scrypt recipients to fail")
|
||||||
|
}
|
||||||
|
if _, err := age.Encrypt(io.Discard, scrypt, x25519); err == nil {
|
||||||
|
t.Error("expected x25519 mixed with scrypt to fail")
|
||||||
|
}
|
||||||
|
if _, err := age.Encrypt(io.Discard, x25519, scrypt); err == nil {
|
||||||
|
t.Error("expected x25519 mixed with scrypt to fail")
|
||||||
|
}
|
||||||
|
if _, err := age.Encrypt(io.Discard, pqc, x25519); err == nil {
|
||||||
|
t.Error("expected x25519 mixed with pqc to fail")
|
||||||
|
}
|
||||||
|
if _, err := age.Encrypt(io.Discard, x25519, pqc); err == nil {
|
||||||
|
t.Error("expected x25519 mixed with pqc to fail")
|
||||||
|
}
|
||||||
|
if _, err := age.Encrypt(io.Discard, pqc, pqc); err != nil {
|
||||||
|
t.Errorf("expected two pqc to work, got %v", err)
|
||||||
|
}
|
||||||
|
if _, err := age.Encrypt(io.Discard, pqc); err != nil {
|
||||||
|
t.Errorf("expected one pqc to work, got %v", err)
|
||||||
|
}
|
||||||
|
if _, err := age.Encrypt(io.Discard, pqcAndFoo, pqc); err == nil {
|
||||||
|
t.Error("expected pqc+foo mixed with pqc to fail")
|
||||||
|
}
|
||||||
|
if _, err := age.Encrypt(io.Discard, pqc, pqcAndFoo); err == nil {
|
||||||
|
t.Error("expected pqc+foo mixed with pqc to fail")
|
||||||
|
}
|
||||||
|
if _, err := age.Encrypt(io.Discard, pqc, pqc, pqcAndFoo); err == nil {
|
||||||
|
t.Error("expected pqc+foo mixed with pqc to fail")
|
||||||
|
}
|
||||||
|
if _, err := age.Encrypt(io.Discard, pqcAndFoo, pqcAndFoo); err != nil {
|
||||||
|
t.Errorf("expected two pqc+foo to work, got %v", err)
|
||||||
|
}
|
||||||
|
if _, err := age.Encrypt(io.Discard, pqcAndFoo, fooAndPQC); err != nil {
|
||||||
|
t.Errorf("expected pqc+foo mixed with foo+pqc to work, got %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -41,6 +41,8 @@ func TestMain(m *testing.M) {
|
|||||||
scanner.Scan() // wrap-file-key
|
scanner.Scan() // wrap-file-key
|
||||||
scanner.Scan() // body
|
scanner.Scan() // body
|
||||||
fileKey := scanner.Text()
|
fileKey := scanner.Text()
|
||||||
|
scanner.Scan() // extension-labels
|
||||||
|
scanner.Scan() // body
|
||||||
scanner.Scan() // done
|
scanner.Scan() // done
|
||||||
scanner.Scan() // body
|
scanner.Scan() // body
|
||||||
os.Stdout.WriteString("-> recipient-stanza 0 test\n")
|
os.Stdout.WriteString("-> recipient-stanza 0 test\n")
|
||||||
|
|||||||
2
go.mod
2
go.mod
@@ -5,7 +5,7 @@ go 1.19
|
|||||||
require (
|
require (
|
||||||
filippo.io/edwards25519 v1.0.0
|
filippo.io/edwards25519 v1.0.0
|
||||||
golang.org/x/crypto v0.4.0
|
golang.org/x/crypto v0.4.0
|
||||||
golang.org/x/sys v0.3.0
|
golang.org/x/sys v0.11.0
|
||||||
golang.org/x/term v0.3.0
|
golang.org/x/term v0.3.0
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
4
go.sum
4
go.sum
@@ -8,7 +8,7 @@ github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e h1:aoZm08cpOy4WuID//EZDgc
|
|||||||
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
|
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
|
||||||
golang.org/x/crypto v0.4.0 h1:UVQgzMY87xqpKNgb+kDsll2Igd33HszWHFLmpaRMq/8=
|
golang.org/x/crypto v0.4.0 h1:UVQgzMY87xqpKNgb+kDsll2Igd33HszWHFLmpaRMq/8=
|
||||||
golang.org/x/crypto v0.4.0/go.mod h1:3quD/ATkf6oY+rnes5c3ExXTbLc8mueNue5/DoinL80=
|
golang.org/x/crypto v0.4.0/go.mod h1:3quD/ATkf6oY+rnes5c3ExXTbLc8mueNue5/DoinL80=
|
||||||
golang.org/x/sys v0.3.0 h1:w8ZOecv6NaNa/zC8944JTU3vz4u6Lagfk4RPQxv92NQ=
|
golang.org/x/sys v0.11.0 h1:eG7RXZHdqOJ1i+0lgLgCpSXAp6M3LYlAo6osgSi0xOM=
|
||||||
golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/term v0.3.0 h1:qoo4akIqOcDME5bhc/NgxUdovd6BSS2uMsVjB56q1xI=
|
golang.org/x/term v0.3.0 h1:qoo4akIqOcDME5bhc/NgxUdovd6BSS2uMsVjB56q1xI=
|
||||||
golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA=
|
golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA=
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"math/rand"
|
"math/rand"
|
||||||
"os"
|
"os"
|
||||||
|
"path/filepath"
|
||||||
"strconv"
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -33,6 +34,7 @@ type Recipient struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var _ age.Recipient = &Recipient{}
|
var _ age.Recipient = &Recipient{}
|
||||||
|
var _ age.RecipientWithLabels = &Recipient{}
|
||||||
|
|
||||||
func NewRecipient(s string, ui *ClientUI) (*Recipient, error) {
|
func NewRecipient(s string, ui *ClientUI) (*Recipient, error) {
|
||||||
name, _, err := ParseRecipient(s)
|
name, _, err := ParseRecipient(s)
|
||||||
@@ -52,6 +54,11 @@ func (r *Recipient) Name() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (r *Recipient) Wrap(fileKey []byte) (stanzas []*age.Stanza, err error) {
|
func (r *Recipient) Wrap(fileKey []byte) (stanzas []*age.Stanza, err error) {
|
||||||
|
stanzas, _, err = r.WrapWithLabels(fileKey)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Recipient) WrapWithLabels(fileKey []byte) (stanzas []*age.Stanza, labels []string, err error) {
|
||||||
defer func() {
|
defer func() {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
err = fmt.Errorf("%s plugin: %w", r.name, err)
|
err = fmt.Errorf("%s plugin: %w", r.name, err)
|
||||||
@@ -60,7 +67,7 @@ func (r *Recipient) Wrap(fileKey []byte) (stanzas []*age.Stanza, err error) {
|
|||||||
|
|
||||||
conn, err := openClientConnection(r.name, "recipient-v1")
|
conn, err := openClientConnection(r.name, "recipient-v1")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("couldn't start plugin: %v", err)
|
return nil, nil, fmt.Errorf("couldn't start plugin: %v", err)
|
||||||
}
|
}
|
||||||
defer conn.Close()
|
defer conn.Close()
|
||||||
|
|
||||||
@@ -70,16 +77,19 @@ func (r *Recipient) Wrap(fileKey []byte) (stanzas []*age.Stanza, err error) {
|
|||||||
addType = "add-identity"
|
addType = "add-identity"
|
||||||
}
|
}
|
||||||
if err := writeStanza(conn, addType, r.encoding); err != nil {
|
if err := writeStanza(conn, addType, r.encoding); err != nil {
|
||||||
return nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
if err := writeStanza(conn, fmt.Sprintf("grease-%x", rand.Int())); err != nil {
|
if err := writeStanza(conn, fmt.Sprintf("grease-%x", rand.Int())); err != nil {
|
||||||
return nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
if err := writeStanzaWithBody(conn, "wrap-file-key", fileKey); err != nil {
|
if err := writeStanzaWithBody(conn, "wrap-file-key", fileKey); err != nil {
|
||||||
return nil, err
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
if err := writeStanza(conn, "extension-labels"); err != nil {
|
||||||
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
if err := writeStanza(conn, "done"); err != nil {
|
if err := writeStanza(conn, "done"); err != nil {
|
||||||
return nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Phase 2: plugin responds with stanzas
|
// Phase 2: plugin responds with stanzas
|
||||||
@@ -88,21 +98,21 @@ ReadLoop:
|
|||||||
for {
|
for {
|
||||||
s, err := r.ui.readStanza(r.name, sr)
|
s, err := r.ui.readStanza(r.name, sr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
switch s.Type {
|
switch s.Type {
|
||||||
case "recipient-stanza":
|
case "recipient-stanza":
|
||||||
if len(s.Args) < 2 {
|
if len(s.Args) < 2 {
|
||||||
return nil, fmt.Errorf("malformed recipient stanza: unexpected argument count")
|
return nil, nil, fmt.Errorf("malformed recipient stanza: unexpected argument count")
|
||||||
}
|
}
|
||||||
n, err := strconv.Atoi(s.Args[0])
|
n, err := strconv.Atoi(s.Args[0])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("malformed recipient stanza: invalid index")
|
return nil, nil, fmt.Errorf("malformed recipient stanza: invalid index")
|
||||||
}
|
}
|
||||||
// We only send a single file key, so the index must be 0.
|
// We only send a single file key, so the index must be 0.
|
||||||
if n != 0 {
|
if n != 0 {
|
||||||
return nil, fmt.Errorf("malformed recipient stanza: unexpected index")
|
return nil, nil, fmt.Errorf("malformed recipient stanza: unexpected index")
|
||||||
}
|
}
|
||||||
|
|
||||||
stanzas = append(stanzas, &age.Stanza{
|
stanzas = append(stanzas, &age.Stanza{
|
||||||
@@ -112,32 +122,41 @@ ReadLoop:
|
|||||||
})
|
})
|
||||||
|
|
||||||
if err := writeStanza(conn, "ok"); err != nil {
|
if err := writeStanza(conn, "ok"); err != nil {
|
||||||
return nil, err
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
case "labels":
|
||||||
|
if labels != nil {
|
||||||
|
return nil, nil, fmt.Errorf("repeated labels stanza")
|
||||||
|
}
|
||||||
|
labels = s.Args
|
||||||
|
|
||||||
|
if err := writeStanza(conn, "ok"); err != nil {
|
||||||
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
case "error":
|
case "error":
|
||||||
if err := writeStanza(conn, "ok"); err != nil {
|
if err := writeStanza(conn, "ok"); err != nil {
|
||||||
return nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil, fmt.Errorf("%s", s.Body)
|
return nil, nil, fmt.Errorf("%s", s.Body)
|
||||||
case "done":
|
case "done":
|
||||||
break ReadLoop
|
break ReadLoop
|
||||||
default:
|
default:
|
||||||
if ok, err := r.ui.handle(r.name, conn, s); err != nil {
|
if ok, err := r.ui.handle(r.name, conn, s); err != nil {
|
||||||
return nil, err
|
return nil, nil, err
|
||||||
} else if !ok {
|
} else if !ok {
|
||||||
if err := writeStanza(conn, "unsupported"); err != nil {
|
if err := writeStanza(conn, "unsupported"); err != nil {
|
||||||
return nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(stanzas) == 0 {
|
if len(stanzas) == 0 {
|
||||||
return nil, fmt.Errorf("received zero recipient stanzas")
|
return nil, nil, fmt.Errorf("received zero recipient stanzas")
|
||||||
}
|
}
|
||||||
|
|
||||||
return stanzas, nil
|
return stanzas, labels, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type Identity struct {
|
type Identity struct {
|
||||||
@@ -367,8 +386,14 @@ type clientConnection struct {
|
|||||||
close func()
|
close func()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var testOnlyPluginPath string
|
||||||
|
|
||||||
func openClientConnection(name, protocol string) (*clientConnection, error) {
|
func openClientConnection(name, protocol string) (*clientConnection, error) {
|
||||||
cmd := exec.Command("age-plugin-"+name, "--age-plugin="+protocol)
|
path := "age-plugin-" + name
|
||||||
|
if testOnlyPluginPath != "" {
|
||||||
|
path = filepath.Join(testOnlyPluginPath, path)
|
||||||
|
}
|
||||||
|
cmd := exec.Command(path, "--age-plugin="+protocol)
|
||||||
|
|
||||||
stdout, err := cmd.StdoutPipe()
|
stdout, err := cmd.StdoutPipe()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
129
plugin/client_test.go
Normal file
129
plugin/client_test.go
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
// Copyright 2023 The age Authors
|
||||||
|
//
|
||||||
|
// 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 plugin
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"filippo.io/age"
|
||||||
|
"filippo.io/age/internal/bech32"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestMain(m *testing.M) {
|
||||||
|
switch filepath.Base(os.Args[0]) {
|
||||||
|
// TODO: deduplicate from cmd/age TestMain.
|
||||||
|
case "age-plugin-test":
|
||||||
|
switch os.Args[1] {
|
||||||
|
case "--age-plugin=recipient-v1":
|
||||||
|
scanner := bufio.NewScanner(os.Stdin)
|
||||||
|
scanner.Scan() // add-recipient
|
||||||
|
scanner.Scan() // body
|
||||||
|
scanner.Scan() // grease
|
||||||
|
scanner.Scan() // body
|
||||||
|
scanner.Scan() // wrap-file-key
|
||||||
|
scanner.Scan() // body
|
||||||
|
fileKey := scanner.Text()
|
||||||
|
scanner.Scan() // extension-labels
|
||||||
|
scanner.Scan() // body
|
||||||
|
scanner.Scan() // done
|
||||||
|
scanner.Scan() // body
|
||||||
|
os.Stdout.WriteString("-> recipient-stanza 0 test\n")
|
||||||
|
os.Stdout.WriteString(fileKey + "\n")
|
||||||
|
scanner.Scan() // ok
|
||||||
|
scanner.Scan() // body
|
||||||
|
os.Stdout.WriteString("-> done\n\n")
|
||||||
|
os.Exit(0)
|
||||||
|
default:
|
||||||
|
panic(os.Args[1])
|
||||||
|
}
|
||||||
|
case "age-plugin-testpqc":
|
||||||
|
switch os.Args[1] {
|
||||||
|
case "--age-plugin=recipient-v1":
|
||||||
|
scanner := bufio.NewScanner(os.Stdin)
|
||||||
|
scanner.Scan() // add-recipient
|
||||||
|
scanner.Scan() // body
|
||||||
|
scanner.Scan() // grease
|
||||||
|
scanner.Scan() // body
|
||||||
|
scanner.Scan() // wrap-file-key
|
||||||
|
scanner.Scan() // body
|
||||||
|
fileKey := scanner.Text()
|
||||||
|
scanner.Scan() // extension-labels
|
||||||
|
scanner.Scan() // body
|
||||||
|
scanner.Scan() // done
|
||||||
|
scanner.Scan() // body
|
||||||
|
os.Stdout.WriteString("-> recipient-stanza 0 test\n")
|
||||||
|
os.Stdout.WriteString(fileKey + "\n")
|
||||||
|
scanner.Scan() // ok
|
||||||
|
scanner.Scan() // body
|
||||||
|
os.Stdout.WriteString("-> labels postquantum\n\n")
|
||||||
|
scanner.Scan() // ok
|
||||||
|
scanner.Scan() // body
|
||||||
|
os.Stdout.WriteString("-> done\n\n")
|
||||||
|
os.Exit(0)
|
||||||
|
default:
|
||||||
|
panic(os.Args[1])
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
os.Exit(m.Run())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLabels(t *testing.T) {
|
||||||
|
temp := t.TempDir()
|
||||||
|
testOnlyPluginPath = temp
|
||||||
|
t.Cleanup(func() { testOnlyPluginPath = "" })
|
||||||
|
ex, err := os.Executable()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := os.Link(ex, filepath.Join(temp, "age-plugin-test")); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := os.Chmod(filepath.Join(temp, "age-plugin-test"), 0755); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := os.Link(ex, filepath.Join(temp, "age-plugin-testpqc")); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := os.Chmod(filepath.Join(temp, "age-plugin-testpqc"), 0755); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
name, err := bech32.Encode("age1test", nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
testPlugin, err := NewRecipient(name, &ClientUI{})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
namePQC, err := bech32.Encode("age1testpqc", nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
testPluginPQC, err := NewRecipient(namePQC, &ClientUI{})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := age.Encrypt(io.Discard, testPluginPQC); err != nil {
|
||||||
|
t.Errorf("expected one pqc to work, got %v", err)
|
||||||
|
}
|
||||||
|
if _, err := age.Encrypt(io.Discard, testPluginPQC, testPluginPQC); err != nil {
|
||||||
|
t.Errorf("expected two pqc to work, got %v", err)
|
||||||
|
}
|
||||||
|
if _, err := age.Encrypt(io.Discard, testPluginPQC, testPlugin); err == nil {
|
||||||
|
t.Errorf("expected one pqc and one normal to fail")
|
||||||
|
}
|
||||||
|
if _, err := age.Encrypt(io.Discard, testPlugin, testPluginPQC); err == nil {
|
||||||
|
t.Errorf("expected one pqc and one normal to fail")
|
||||||
|
}
|
||||||
|
}
|
||||||
24
scrypt.go
24
scrypt.go
@@ -6,6 +6,7 @@ package age
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
|
"encoding/hex"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"regexp"
|
"regexp"
|
||||||
@@ -87,6 +88,29 @@ func (r *ScryptRecipient) Wrap(fileKey []byte) ([]*Stanza, error) {
|
|||||||
return []*Stanza{l}, nil
|
return []*Stanza{l}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// WrapWithLabels implements [age.RecipientWithLabels], returning a random
|
||||||
|
// label. This ensures a ScryptRecipient can't be mixed with other recipients
|
||||||
|
// (including other ScryptRecipients).
|
||||||
|
//
|
||||||
|
// Users reasonably expect files encrypted to a passphrase to be [authenticated]
|
||||||
|
// by that passphrase, i.e. for it to be impossible to produce a file that
|
||||||
|
// decrypts successfully with a passphrase without knowing it. If a file is
|
||||||
|
// encrypted to other recipients, those parties can produce different files that
|
||||||
|
// would break that expectation.
|
||||||
|
//
|
||||||
|
// [authenticated]: https://words.filippo.io/dispatches/age-authentication/
|
||||||
|
func (r *ScryptRecipient) WrapWithLabels(fileKey []byte) (stanzas []*Stanza, labels []string, err error) {
|
||||||
|
stanzas, err = r.Wrap(fileKey)
|
||||||
|
|
||||||
|
random := make([]byte, 16)
|
||||||
|
if _, err := rand.Read(random); err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
labels = []string{hex.EncodeToString(random)}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// ScryptIdentity is a password-based identity.
|
// ScryptIdentity is a password-based identity.
|
||||||
type ScryptIdentity struct {
|
type ScryptIdentity struct {
|
||||||
password []byte
|
password []byte
|
||||||
|
|||||||
Reference in New Issue
Block a user