From 0607eae847f1ae21205b5e2a919de6d5868f6395 Mon Sep 17 00:00:00 2001 From: Brian McGee Date: Wed, 2 Oct 2024 11:44:38 +0100 Subject: [PATCH] feat: add age plugin support Signed-off-by: Brian McGee Co-authored-by: Maximilian Bosch Signed-off-by: Brian McGee --- age/keysource.go | 74 ++++++++++++++++++++++++++++++++++++++++++------ go.mod | 2 ++ go.sum | 4 +-- 3 files changed, 70 insertions(+), 10 deletions(-) diff --git a/age/keysource.go b/age/keysource.go index 83bdbe0a6..a9051c926 100644 --- a/age/keysource.go +++ b/age/keysource.go @@ -1,8 +1,11 @@ package age import ( + "bufio" "bytes" "errors" + "filippo.io/age/plugin" + "filippo.io/age/tui" "fmt" "io" "os" @@ -60,7 +63,7 @@ type MasterKey struct { parsedIdentities []age.Identity // parsedRecipient contains a parsed age public key. // It is used to lazy-load the Recipient at-most once. - parsedRecipient *age.X25519Recipient + parsedRecipient age.Recipient } // MasterKeysFromRecipients takes a comma-separated list of Bech32-encoded @@ -126,6 +129,7 @@ func (i ParsedIdentities) ApplyToMasterKey(key *MasterKey) { // Encrypt takes a SOPS data key, encrypts it with the Recipient, and stores // the result in the EncryptedKey field. + func (key *MasterKey) Encrypt(dataKey []byte) error { if key.parsedRecipient == nil { parsedRecipient, err := parseRecipient(key.Recipient) @@ -284,7 +288,12 @@ func (key *MasterKey) loadIdentities() (ParsedIdentities, error) { var identities ParsedIdentities for n, r := range readers { - ids, err := age.ParseIdentities(r) + buf := new(strings.Builder) + _, err := io.Copy(buf, r) + if err != nil { + return nil, fmt.Errorf("failed to read '%s' age identities: %w", n, err) + } + ids, err := parseIdentities(buf.String()) if err != nil { return nil, fmt.Errorf("failed to parse '%s' age identities: %w", n, err) } @@ -295,12 +304,21 @@ func (key *MasterKey) loadIdentities() (ParsedIdentities, error) { // parseRecipient attempts to parse a string containing an encoded age public // key. -func parseRecipient(recipient string) (*age.X25519Recipient, error) { - parsedRecipient, err := age.ParseX25519Recipient(recipient) - if err != nil { - return nil, fmt.Errorf("failed to parse input as Bech32-encoded age public key: %w", err) +func parseRecipient(recipient string) (age.Recipient, error) { + switch { + case strings.HasPrefix(recipient, "age1") && strings.Count(recipient, "1") > 1: + parsedRecipient, err := plugin.NewRecipient(recipient, tui.PluginTerminalUI) + if err != nil { + return nil, fmt.Errorf("failed to parse input as age key from age plugin: %w", err) + } + return parsedRecipient, nil + default: + parsedRecipient, err := age.ParseX25519Recipient(recipient) + if err != nil { + return nil, fmt.Errorf("failed to parse input as Bech32-encoded age public key: %w", err) + } + return parsedRecipient, nil } - return parsedRecipient, nil } // parseIdentities attempts to parse the string set of encoded age identities. @@ -309,7 +327,7 @@ func parseRecipient(recipient string) (*age.X25519Recipient, error) { func parseIdentities(identity ...string) (ParsedIdentities, error) { var identities []age.Identity for _, i := range identity { - parsed, err := age.ParseIdentities(strings.NewReader(i)) + parsed, err := _parseIdentities(strings.NewReader(i)) if err != nil { return nil, err } @@ -317,3 +335,43 @@ func parseIdentities(identity ...string) (ParsedIdentities, error) { } return identities, nil } + +func parseIdentity(s string) (age.Identity, error) { + switch { + case strings.HasPrefix(s, "AGE-PLUGIN-"): + return plugin.NewIdentity(s, tui.PluginTerminalUI) + case strings.HasPrefix(s, "AGE-SECRET-KEY-1"): + return age.ParseX25519Identity(s) + default: + return nil, fmt.Errorf("unknown identity type") + } +} + +// parseIdentities is like age.ParseIdentities, but supports plugin identities. +func _parseIdentities(f io.Reader) (ParsedIdentities, error) { + const privateKeySizeLimit = 1 << 24 // 16 MiB + var ids []age.Identity + scanner := bufio.NewScanner(io.LimitReader(f, privateKeySizeLimit)) + var n int + for scanner.Scan() { + n++ + line := scanner.Text() + if strings.HasPrefix(line, "#") || line == "" { + continue + } + + i, err := parseIdentity(line) + if err != nil { + return nil, fmt.Errorf("error at line %d: %v", n, err) + } + ids = append(ids, i) + + } + if err := scanner.Err(); err != nil { + return nil, fmt.Errorf("failed to read secret keys file: %v", err) + } + if len(ids) == 0 { + return nil, fmt.Errorf("no secret keys found") + } + return ids, nil +} diff --git a/go.mod b/go.mod index 9bab12dd1..af659ef00 100644 --- a/go.mod +++ b/go.mod @@ -149,3 +149,5 @@ require ( google.golang.org/grpc/stats/opentelemetry v0.0.0-20240907200651-3ffb98b2c93a // indirect gopkg.in/yaml.v2 v2.4.0 // indirect ) + +replace filippo.io/age => github.com/brianmcgee/age v0.0.0-20241002093043-152b6edfe56a diff --git a/go.sum b/go.sum index 53a1f7a53..5ec9059b8 100644 --- a/go.sum +++ b/go.sum @@ -27,8 +27,6 @@ cloud.google.com/go/trace v1.11.1 h1:UNqdP+HYYtnm6lb91aNA5JQ0X14GnxkABGlfz2PzPew cloud.google.com/go/trace v1.11.1/go.mod h1:IQKNQuBzH72EGaXEodKlNJrWykGZxet2zgjtS60OtjA= dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s= dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= -filippo.io/age v1.2.0 h1:vRDp7pUMaAJzXNIWJVAZnEf/Dyi4Vu4wI8S1LBzufhE= -filippo.io/age v1.2.0/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.16.0 h1:JZg6HRh6W6U4OLl6lk7BZ7BLisIzM9dG1R50zUk9C/M= @@ -107,6 +105,8 @@ github.com/aws/smithy-go v1.22.0 h1:uunKnWlcoL3zO7q+gG2Pk53joueEOsnNB28QdMsmiMM= github.com/aws/smithy-go v1.22.0/go.mod h1:irrKGvNn1InZwb2d7fkIRNucdfwR8R+Ts3wxYa/cJHg= github.com/blang/semver v3.5.1+incompatible h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdnnjpJbkM4JQ= github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk= +github.com/brianmcgee/age v0.0.0-20241002093043-152b6edfe56a h1:c4M1FY6q29uftkFWCWWmEEdB/FpjHPPyT3FMWsiJ9VI= +github.com/brianmcgee/age v0.0.0-20241002093043-152b6edfe56a/go.mod h1:JL9ew2lTN+Pyft4RiNGguFfOpewKwSHm5ayKD/A4004= github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=