From cbbaa6ba7aa6de56b9d6c50a8470c89df8f4ed3a Mon Sep 17 00:00:00 2001 From: Frederik Ring Date: Fri, 7 Feb 2025 13:43:41 +0100 Subject: [PATCH] Support passing standard ssh keys to age encryption (#530) * Support passing standard ssh keys to age encryption * Cover SSH keys in age test case --- cmd/backup/encrypt_archive.go | 16 +++++++++++++++- docs/reference/index.md | 4 ++-- go.mod | 1 + go.sum | 2 ++ test/age-publickey/run.sh | 6 +++++- 5 files changed, 25 insertions(+), 4 deletions(-) diff --git a/cmd/backup/encrypt_archive.go b/cmd/backup/encrypt_archive.go index e658a13..16aaa64 100644 --- a/cmd/backup/encrypt_archive.go +++ b/cmd/backup/encrypt_archive.go @@ -10,8 +10,10 @@ import ( "io" "os" "path" + "strings" "filippo.io/age" + "filippo.io/age/agessh" "github.com/ProtonMail/go-crypto/openpgp/armor" openpgp "github.com/ProtonMail/go-crypto/openpgp/v2" "github.com/offen/docker-volume-backup/internal/errwrap" @@ -73,7 +75,7 @@ func (s *script) getConfiguredAgeRecipients() ([]age.Recipient, error) { recipients := []age.Recipient{} if len(s.c.AgePublicKeys) > 0 { for _, pk := range s.c.AgePublicKeys { - pkr, err := age.ParseX25519Recipient(pk) + pkr, err := parseAgeRecipient(pk) if err != nil { return nil, errwrap.Wrap(err, "failed to parse age public key") } @@ -94,6 +96,18 @@ func (s *script) getConfiguredAgeRecipients() ([]age.Recipient, error) { return recipients, nil } +func parseAgeRecipient(arg string) (age.Recipient, error) { + // This logic is adapted from what the age CLI is doing + // stripping some special cases + switch { + case strings.HasPrefix(arg, "age1"): + return age.ParseX25519Recipient(arg) + case strings.HasPrefix(arg, "ssh-"): + return agessh.ParseRecipient(arg) + } + return nil, fmt.Errorf("unknown recipient type: %q", arg) +} + func (s *script) encryptWithAge(rec []age.Recipient) error { return s.doEncrypt("age", func(ciphertextWriter io.Writer) (io.WriteCloser, error) { return age.Encrypt(ciphertextWriter, rec...) diff --git a/docs/reference/index.md b/docs/reference/index.md index 3b6cce2..55ca7e0 100644 --- a/docs/reference/index.md +++ b/docs/reference/index.md @@ -358,8 +358,8 @@ You can populate below template according to your requirements and use it as you # AGE_PASSPHRASE="" # Backups can be encrypted asymmetrically using age in case publickeys are given. -# Multiple keys need to be provided as a comma separated list. Right now, this only -# support passing age keys, with no support for ssh keys. +# Multiple keys need to be provided as a comma separated list. Right now, this +# supports `age` and `ssh` keys # AGE_PUBLIC_KEYS="" diff --git a/go.mod b/go.mod index 2a7475b..f4bdd17 100644 --- a/go.mod +++ b/go.mod @@ -27,6 +27,7 @@ require ( ) require ( + filippo.io/edwards25519 v1.1.0 // indirect github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78 // indirect github.com/cloudflare/circl v1.3.7 // indirect github.com/containerd/log v0.1.0 // indirect diff --git a/go.sum b/go.sum index 2eaba20..e47103f 100644 --- a/go.sum +++ b/go.sum @@ -35,6 +35,8 @@ cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9 dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= filippo.io/age v1.2.1 h1:X0TZjehAZylOIj4DubWYU1vWQxv9bJpo+Uu2/LGhi1o= filippo.io/age v1.2.1/go.mod h1:JL9ew2lTN+Pyft4RiNGguFfOpewKwSHm5ayKD/A4004= +filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= +filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= github.com/Azure/azure-sdk-for-go/sdk/azcore v1.17.0 h1:g0EZJwz7xkXQiZAI5xi9f3WWFYBlX1CPTrR+NDToRkQ= github.com/Azure/azure-sdk-for-go/sdk/azcore v1.17.0/go.mod h1:XCW7KnZet0Opnr7HccfUw1PLc4CjHqpcaxW8DHklNkQ= github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.8.1 h1:1mvYtZfWQAnwNah/C+Z+Jb9rQH95LPE2vlmMuWAHJk8= diff --git a/test/age-publickey/run.sh b/test/age-publickey/run.sh index d52598b..8da16a5 100755 --- a/test/age-publickey/run.sh +++ b/test/age-publickey/run.sh @@ -13,7 +13,10 @@ PK_A="$(grep -E 'public key' <"$LOCAL_DIR/pk-a.txt" | cut -d: -f2 | xargs)" age-keygen >"$LOCAL_DIR/pk-b.txt" PK_B="$(grep -E 'public key' <"$LOCAL_DIR/pk-b.txt" | cut -d: -f2 | xargs)" -export BACKUP_AGE_PUBLIC_KEYS="$PK_A,$PK_B" +ssh-keygen -t ed25519 -m pem -f "$LOCAL_DIR/id_ed25519" -C "docker-volume-backup@local" +PK_C="$(cat $LOCAL_DIR/id_ed25519.pub)" + +export BACKUP_AGE_PUBLIC_KEYS="$PK_A,$PK_B,$PK_C" docker compose up -d --quiet-pull sleep 5 @@ -41,3 +44,4 @@ do_decrypt() { do_decrypt "$LOCAL_DIR/pk-a.txt" do_decrypt "$LOCAL_DIR/pk-b.txt" +do_decrypt "$LOCAL_DIR/id_ed25519"