Skip to content

Commit

Permalink
feat: support multiple public keys
Browse files Browse the repository at this point in the history
Breaking change: private keys can no longer be used as source public keys
  • Loading branch information
sloonz committed Jun 21, 2024
1 parent 756ed0d commit e025e7b
Show file tree
Hide file tree
Showing 8 changed files with 63 additions and 61 deletions.
4 changes: 2 additions & 2 deletions cmd/backup.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ var (
srcOpts := newOptionsBuilder(uback.EvalOptions(uback.SplitOptions(args[0]), presets)).
WithSource().
WithRetentionPolicies().
WithPublicKey().
WithRecipients().
WithStateFile().
FatalOnError()

Expand Down Expand Up @@ -114,7 +114,7 @@ var (

pr, pw := io.Pipe()
go func() {
cw, err := container.NewWriter(pw, srcOpts.PublicKey, srcOpts.SourceType, compressionLevel)
cw, err := container.NewWriter(pw, srcOpts.Recipients, srcOpts.SourceType, compressionLevel)
if err != nil {
pw.CloseWithError(err)
return
Expand Down
4 changes: 2 additions & 2 deletions cmd/container.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ var cmdContainerExtract = &cobra.Command{
logrus.Fatal(err)
}

sk, err := uback.LoadPrivateKey(cmdContainerExtractKeyFile, cmdContainerExtractKey)
sk, err := uback.LoadIdentities(cmdContainerExtractKeyFile, cmdContainerExtractKey)
if err != nil {
logrus.Fatal(err)
}
Expand Down Expand Up @@ -123,7 +123,7 @@ var cmdContainerCreate = &cobra.Command{
defer out.Close()
}

pk, err := uback.LoadPublicKey(cmdContainerCreateKeyFile, cmdContainerCreateKey)
pk, err := uback.LoadRecipients(cmdContainerCreateKeyFile, cmdContainerCreateKey)
if err != nil {
logrus.Fatal(err)
}
Expand Down
16 changes: 8 additions & 8 deletions cmd/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@ type optionsBuilder struct {
SourceType string
Destination uback.Destination
RetentionPolicies []uback.RetentionPolicy
PrivateKey age.Identity
PublicKey age.Recipient
Identities []age.Identity
Recipients []age.Recipient
Error error
}

Expand All @@ -42,23 +42,23 @@ func (o *optionsBuilder) WithDestination() *optionsBuilder {
return o
}

func (o *optionsBuilder) WithPublicKey() *optionsBuilder {
func (o *optionsBuilder) WithRecipients() *optionsBuilder {
if o.Error == nil {
if o.Options.String["NoEncryption"] != "" {
o.PublicKey = nil
o.Recipients = nil
} else {
o.PublicKey, o.Error = uback.LoadPublicKey(o.Options.String["KeyFile"], o.Options.String["Key"])
o.Recipients, o.Error = uback.LoadRecipients(o.Options.String["KeyFile"], o.Options.String["Key"])
}
}
return o
}

func (o *optionsBuilder) WithPrivateKey() *optionsBuilder {
func (o *optionsBuilder) WithIdentities() *optionsBuilder {
if o.Error == nil {
if o.Options.String["NoEncryption"] != "" {
o.PrivateKey = nil
o.Identities = nil
} else {
o.PrivateKey, o.Error = uback.LoadPrivateKey(o.Options.String["KeyFile"], o.Options.String["Key"])
o.Identities, o.Error = uback.LoadIdentities(o.Options.String["KeyFile"], o.Options.String["Key"])
}
}
return o
Expand Down
6 changes: 3 additions & 3 deletions cmd/restore.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import (
"github.com/spf13/cobra"
)

func restore(dst uback.Destination, b uback.Backup, sk age.Identity, targetDir string) error {
func restore(dst uback.Destination, b uback.Backup, sk []age.Identity, targetDir string) error {
logrus.Printf("restoring %v onto %v", b.Filename(), targetDir)

srcOpts, err := uback.EvalOptions(uback.SplitOptions(cmdRestoreSourceOptions), presets)
Expand Down Expand Up @@ -74,7 +74,7 @@ var (

dstOpts := newOptionsBuilder(uback.EvalOptions(uback.SplitOptions(args[0]), presets)).
WithDestination().
WithPrivateKey().
WithIdentities().
FatalOnError()

backups, err := uback.SortedListBackups(dstOpts.Destination)
Expand All @@ -99,7 +99,7 @@ var (
}

for i := len(fetchedBackups) - 1; i >= 0; i-- {
err = restore(dstOpts.Destination, fetchedBackups[i], dstOpts.PrivateKey, cmdRestoreTargetDir)
err = restore(dstOpts.Destination, fetchedBackups[i], dstOpts.Identities, cmdRestoreTargetDir)
if err != nil {
logrus.Fatal(err)
}
Expand Down
14 changes: 7 additions & 7 deletions container/container.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,13 +29,13 @@ type Writer struct {
zw *zstd.Encoder
}

func NewWriter(w io.Writer, pk age.Recipient, typ string, compressionLevel int) (*Writer, error) {
func NewWriter(w io.Writer, recipients []age.Recipient, typ string, compressionLevel int) (*Writer, error) {
var aw io.WriteCloser
var zw *zstd.Encoder

hdr := bytes.NewBuffer(nil)
hdr.WriteString(magic)
if pk == nil {
if len(recipients) == 0 {
hdr.WriteString(fmt.Sprintf("type=%s,compression=zstd,plain=1\n", typ))
} else {
hdr.WriteString(fmt.Sprintf("type=%s,compression=zstd\n", typ))
Expand All @@ -45,13 +45,13 @@ func NewWriter(w io.Writer, pk age.Recipient, typ string, compressionLevel int)
return nil, err
}

if pk == nil {
if len(recipients) == 0 {
zw, err = zstd.NewWriter(w, zstd.WithEncoderLevel(zstd.EncoderLevelFromZstd(compressionLevel)))
if err != nil {
return nil, err
}
} else {
aw, err = age.Encrypt(w, pk)
aw, err = age.Encrypt(w, recipients...)
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -138,10 +138,10 @@ func NewReader(r io.Reader) (*Reader, error) {
}

// Prepares the decryption process. This must be called before any Read() call
func (r *Reader) Unseal(sk age.Identity) error {
func (r *Reader) Unseal(identities []age.Identity) error {
var err error

if sk == nil {
if len(identities) == 0 {
if r.Options.String["Plain"] != "1" {
return errors.New("Encountered a encrypted backup, but a plaintext one was expected")
}
Expand All @@ -155,7 +155,7 @@ func (r *Reader) Unseal(sk age.Identity) error {
return errors.New("Encountered a plaintext backup, but secret key has been provided")
}

r.ar, err = age.Decrypt(r.br, sk)
r.ar, err = age.Decrypt(r.br, identities...)
if err != nil {
return err
}
Expand Down
10 changes: 5 additions & 5 deletions container/container_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ func TestReadWriter(t *testing.T) {
"It is one of the fastest ECC curves and is not covered by any known patents."

buf := bytes.NewBuffer(nil)
w, err := NewWriter(buf, pk, "test", 3)
w, err := NewWriter(buf, []age.Recipient{pk}, "test", 3)
if err != nil {
t.Errorf("cannot create writer: %v", err)
return
Expand Down Expand Up @@ -56,7 +56,7 @@ func TestReadWriter(t *testing.T) {
return
}

err = r.Unseal(sk)
err = r.Unseal([]age.Identity{sk})
if err != nil {
t.Errorf("cannot unseal reader: %v", err)
}
Expand Down Expand Up @@ -100,7 +100,7 @@ func TestAgeZstd(t *testing.T) {
return
}

err = r.Unseal(identities[0])
err = r.Unseal(identities)
if err != nil {
t.Error(err)
return
Expand Down Expand Up @@ -137,7 +137,7 @@ func TestTamperedHeader(t *testing.T) {
return
}

err = r.Unseal(identities[0])
err = r.Unseal(identities)
if !errors.Is(err, ErrInvalidHeaderHash) {
t.Errorf("expected ErrInvalidHeaderHash, got %v", err)
return
Expand All @@ -161,7 +161,7 @@ func TestTamperedPayload(t *testing.T) {
return
}

err = r.Unseal(identities[0])
err = r.Unseal(identities)
if err != nil {
t.Error(err)
return
Expand Down
38 changes: 4 additions & 34 deletions lib/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -200,7 +200,7 @@ func GetFullChain(backup Backup, index map[string]Backup) ([]Backup, bool) {
}

// Load a private key either from a file (if keyFile argument is provided), or from its content (key argument)
func LoadPrivateKey(keyFile, key string) (age.Identity, error) {
func LoadIdentities(keyFile, key string) ([]age.Identity, error) {
if keyFile != "" && key != "" {
return nil, fmt.Errorf("must provide one of key file or key, not both")
}
Expand All @@ -214,22 +214,12 @@ func LoadPrivateKey(keyFile, key string) (age.Identity, error) {
key = string(keyData)
}

identities, err := age.ParseIdentities(bytes.NewBufferString(key))
if err != nil {
return nil, err
}

identity, ok := identities[0].(*age.X25519Identity)
if !ok {
return nil, fmt.Errorf("only x25519 age keys are supported")
}

return identity, nil
return age.ParseIdentities(bytes.NewBufferString(key))
}

// Load a public key either from a file (if keyFile argument is provided), or from its content (key argument)
// If the file or the content represents a private key, derive the public key from it
func LoadPublicKey(keyFile, key string) (age.Recipient, error) {
func LoadRecipients(keyFile, key string) ([]age.Recipient, error) {
if keyFile != "" && key != "" {
return nil, fmt.Errorf("must provide one of key file or key, not both")
}
Expand All @@ -243,27 +233,7 @@ func LoadPublicKey(keyFile, key string) (age.Recipient, error) {
key = string(keyData)
}

recipients, err := age.ParseRecipients(bytes.NewBufferString(key))
if err != nil {
identities, err2 := age.ParseIdentities(bytes.NewBufferString(key))
if err2 != nil {
return nil, err
}

identity, ok := identities[0].(*age.X25519Identity)
if !ok {
return nil, err
}

return identity.Recipient(), nil
}

recipient, ok := recipients[0].(*age.X25519Recipient)
if !ok {
return nil, fmt.Errorf("only x25519 age keys are supported")
}

return recipient, nil
return age.ParseRecipients(bytes.NewBufferString(key))
}

func BuildCommand(command []string, additionalArgs ...string) *exec.Cmd {
Expand Down
32 changes: 32 additions & 0 deletions tests/multiple_recipients_tests.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
from .common import *

def fread(f):
with open(f) as fd:
return fd.read()

class SrcTarTests(unittest.TestCase, SrcBaseTests):
def test_tar_source(self):
with tempfile.TemporaryDirectory() as d:
source = f"type=tar,path={d}/source,key-file={d}/backup-all.pub,state-file={d}/state.json,snapshots-path={d}/snapshots,full-interval=weekly,@command=tar"
dest = f"id=test,type=fs,path={d}/backups,@retention-policy=daily=3"

ensure_dir(f"{d}/backups")
ensure_dir(f"{d}/restore")
ensure_dir(f"{d}/source")
subprocess.check_call([uback, "key", "gen", f"{d}/backup1.key", f"{d}/backup1.pub"])
subprocess.check_call([uback, "key", "gen", f"{d}/backup2.key", f"{d}/backup2.pub"])
with open(f"{d}/backup-all.pub", "w+") as fd:
fd.write(fread(f"{d}/backup1.pub") + fread(f"{d}/backup2.pub"))

with open(f"{d}/source/a", "w+") as fd: fd.write("av1")
b = subprocess.check_output([uback, "backup", source, dest]).strip().decode()
s = b.split("-")[0]
subprocess.check_call([uback, "restore", "-d", f"{d}/restore", f"{dest},key-file={d}/backup1.key"])
self.assertEqual(b"av1", read_file(f"{d}/restore/{s}/a"))
self.assertEqual(set(os.listdir(f"{d}/restore/{s}")), {"a"})
self._cleanup_restore(d)

subprocess.check_call([uback, "restore", "-d", f"{d}/restore", f"{dest},key-file={d}/backup2.key"])
self.assertEqual(b"av1", read_file(f"{d}/restore/{s}/a"))
self.assertEqual(set(os.listdir(f"{d}/restore/{s}")), {"a"})
self._cleanup_restore(d)

0 comments on commit e025e7b

Please sign in to comment.