From 776dffcf12a1514db223ae0a810519c4e19b372c Mon Sep 17 00:00:00 2001 From: Nicola Murino <nicola.murino@gmail.com> Date: Tue, 13 Jul 2021 21:17:21 +0200 Subject: [PATCH] kms: improve modularity --- go.mod | 6 +- go.sum | 12 +- httpd/httpd.go | 2 +- kms/aws.go | 51 -------- kms/aws/aws.go | 67 +++++++++++ kms/aws/aws_disabled.go | 11 ++ kms/aws_disabled.go | 17 --- kms/basesecret.go | 22 ++-- kms/builtin.go | 29 +++-- kms/disabled.go | 33 ----- kms/gcp.go | 51 -------- kms/gcp/gcp.go | 67 +++++++++++ kms/gcp/gcp_disabled.go | 11 ++ kms/gcp_disabled.go | 17 --- kms/{basegocloud.go => gocloud/gocloud.go} | 43 ++++--- kms/kms.go | 133 ++++++++++----------- kms/local.go | 30 +++-- kms/vault.go | 51 -------- kms/vault/vault.go | 67 +++++++++++ kms/vault/vault_disabled.go | 11 ++ kms/vault_disabled.go | 17 --- main.go | 3 + 22 files changed, 394 insertions(+), 357 deletions(-) delete mode 100644 kms/aws.go create mode 100644 kms/aws/aws.go create mode 100644 kms/aws/aws_disabled.go delete mode 100644 kms/aws_disabled.go delete mode 100644 kms/disabled.go delete mode 100644 kms/gcp.go create mode 100644 kms/gcp/gcp.go create mode 100644 kms/gcp/gcp_disabled.go delete mode 100644 kms/gcp_disabled.go rename kms/{basegocloud.go => gocloud/gocloud.go} (65%) delete mode 100644 kms/vault.go create mode 100644 kms/vault/vault.go create mode 100644 kms/vault/vault_disabled.go delete mode 100644 kms/vault_disabled.go diff --git a/go.mod b/go.mod index 27805311..722049c7 100644 --- a/go.mod +++ b/go.mod @@ -8,7 +8,7 @@ require ( github.com/GehirnInc/crypt v0.0.0-20200316065508-bb7000b8a962 github.com/StackExchange/wmi v1.2.0 // indirect github.com/alexedwards/argon2id v0.0.0-20210511081203-7d35d68092b8 - github.com/aws/aws-sdk-go v1.39.4 + github.com/aws/aws-sdk-go v1.39.5 github.com/cockroachdb/cockroach-go/v2 v2.1.1 github.com/eikenb/pipeat v0.0.0-20210603033007-44fc3ffce52b github.com/fatih/color v1.12.0 // indirect @@ -30,7 +30,7 @@ require ( github.com/klauspost/compress v1.13.1 github.com/klauspost/cpuid/v2 v2.0.8 // indirect github.com/lestrrat-go/backoff/v2 v2.0.8 // indirect - github.com/lestrrat-go/jwx v1.2.1 + github.com/lestrrat-go/jwx v1.2.2 github.com/lib/pq v1.10.2 github.com/mattn/go-isatty v0.0.13 // indirect github.com/mattn/go-sqlite3 v1.14.7 @@ -63,7 +63,7 @@ require ( golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c golang.org/x/time v0.0.0-20210611083556-38a9dc6acbc6 google.golang.org/api v0.50.0 - google.golang.org/genproto v0.0.0-20210708141623-e76da96a951f // indirect + google.golang.org/genproto v0.0.0-20210713002101-d411969a0d9a // indirect google.golang.org/grpc v1.39.0 google.golang.org/protobuf v1.27.1 gopkg.in/natefinch/lumberjack.v2 v2.0.0 diff --git a/go.sum b/go.sum index ba6d6291..2971e4cc 100644 --- a/go.sum +++ b/go.sum @@ -131,8 +131,8 @@ github.com/aws/aws-sdk-go v1.25.37/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpi github.com/aws/aws-sdk-go v1.27.0/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= github.com/aws/aws-sdk-go v1.30.27/go.mod h1:5zCpMtNQVjRREroY7sYe8lOMRSxkhG6MZveU8YkpAk0= github.com/aws/aws-sdk-go v1.38.35/go.mod h1:hcU610XS61/+aQV88ixoOzUoG7v3b31pl2zKMmprdro= -github.com/aws/aws-sdk-go v1.39.4 h1:nXBChUaG5cinrl3yg4/rUyssOOLH/ohk4S9K03kJirE= -github.com/aws/aws-sdk-go v1.39.4/go.mod h1:585smgzpB/KqRA+K3y/NL/oYRqQvpNJYvLm+LY1U59Q= +github.com/aws/aws-sdk-go v1.39.5 h1:yoJEE1NJxbpZ3CtPxvOSFJ9ByxiXmBTKk8J+XU5ldtg= +github.com/aws/aws-sdk-go v1.39.5/go.mod h1:585smgzpB/KqRA+K3y/NL/oYRqQvpNJYvLm+LY1U59Q= github.com/aws/aws-sdk-go-v2 v0.18.0/go.mod h1:JWVYvqSMppoMJC0x5wdwiImzgXTI9FuZwxzkQq9wy+g= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= @@ -589,8 +589,8 @@ github.com/lestrrat-go/httpcc v1.0.0/go.mod h1:tGS/u00Vh5N6FHNkExqGGNId8e0Big+++ github.com/lestrrat-go/iter v1.0.1 h1:q8faalr2dY6o8bV45uwrxq12bRa1ezKrB6oM9FUgN4A= github.com/lestrrat-go/iter v1.0.1/go.mod h1:zIdgO1mRKhn8l9vrZJZz9TUMMFbQbLeTsbqPDrJ/OJc= github.com/lestrrat-go/jwx v1.1.6/go.mod h1:c+R8G7qsaFNmTzYjU98A+sMh8Bo/MJqO9GnpqR+X024= -github.com/lestrrat-go/jwx v1.2.1 h1:WJ/3tiPUz1wV24KiwMEanbENwHnYub9UqzCbQ82mv9c= -github.com/lestrrat-go/jwx v1.2.1/go.mod h1:Tg2uP7bpxEHUDtuWjap/PxroJ4okxGzkQznXiG+a5Dc= +github.com/lestrrat-go/jwx v1.2.2 h1:sH9GeolQn9s3JyNbeEOXXPTko2fD4BwAMWXLYvcEl2k= +github.com/lestrrat-go/jwx v1.2.2/go.mod h1:Tg2uP7bpxEHUDtuWjap/PxroJ4okxGzkQznXiG+a5Dc= github.com/lestrrat-go/option v0.0.0-20210103042652-6f1ecfceda35/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I= github.com/lestrrat-go/option v1.0.0 h1:WqAWL8kh8VcSoD6xjSH34/1m8yxluXQbDeKNfvFeEO4= github.com/lestrrat-go/option v1.0.0/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I= @@ -1244,8 +1244,8 @@ google.golang.org/genproto v0.0.0-20210608205507-b6d2f5bf0d7d/go.mod h1:UODoCrxH google.golang.org/genproto v0.0.0-20210617175327-b9e0b3197ced/go.mod h1:SzzZ/N+nwJDaO1kznhnlzqS8ocJICar6hYhVyhi++24= google.golang.org/genproto v0.0.0-20210624174822-c5cf32407d0a/go.mod h1:SzzZ/N+nwJDaO1kznhnlzqS8ocJICar6hYhVyhi++24= google.golang.org/genproto v0.0.0-20210624195500-8bfb893ecb84/go.mod h1:SzzZ/N+nwJDaO1kznhnlzqS8ocJICar6hYhVyhi++24= -google.golang.org/genproto v0.0.0-20210708141623-e76da96a951f h1:khwpF3oSk7GIab/7DDMDyE8cPQEO6FAfOcWHIRAhO20= -google.golang.org/genproto v0.0.0-20210708141623-e76da96a951f/go.mod h1:AxrInvYm1dci+enl5hChSFPOmmUF1+uAa/UsgNRWd7k= +google.golang.org/genproto v0.0.0-20210713002101-d411969a0d9a h1:89EorDSnBRFywcvGsJvpxw2IsiDMI+DeM7iZOaunfHs= +google.golang.org/genproto v0.0.0-20210713002101-d411969a0d9a/go.mod h1:AxrInvYm1dci+enl5hChSFPOmmUF1+uAa/UsgNRWd7k= google.golang.org/grpc v1.8.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= google.golang.org/grpc v1.14.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= diff --git a/httpd/httpd.go b/httpd/httpd.go index 2b9db192..3e44ad56 100644 --- a/httpd/httpd.go +++ b/httpd/httpd.go @@ -302,7 +302,7 @@ func (c *Conf) getRedacted() Conf { // Initialize configures and starts the HTTP server func (c *Conf) Initialize(configDir string) error { - logger.Debug(logSender, "", "initializing HTTP server with config %v", c.getRedacted()) + logger.Debug(logSender, "", "initializing HTTP server with config %+v", c.getRedacted()) backupsPath = getConfigPath(c.BackupsPath, configDir) staticFilesPath := getConfigPath(c.StaticFilesPath, configDir) templatesPath := getConfigPath(c.TemplatesPath, configDir) diff --git a/kms/aws.go b/kms/aws.go deleted file mode 100644 index 23bde487..00000000 --- a/kms/aws.go +++ /dev/null @@ -1,51 +0,0 @@ -// +build !noawskms - -package kms - -import ( - // we import awskms here to be able to disable AWS KMS support using a build tag - _ "gocloud.dev/secrets/awskms" - - "github.com/drakkan/sftpgo/v2/version" -) - -type awsSecret struct { - baseGCloudSecret -} - -func init() { - version.AddFeature("+awskms") -} - -func newAWSSecret(base baseSecret, url, masterKey string) SecretProvider { - return &awsSecret{ - baseGCloudSecret{ - baseSecret: base, - url: url, - masterKey: masterKey, - }, - } -} - -func (s *awsSecret) Name() string { - return awsProviderName -} - -func (s *awsSecret) IsEncrypted() bool { - return s.Status == SecretStatusAWS -} - -func (s *awsSecret) Encrypt() error { - if err := s.baseGCloudSecret.Encrypt(); err != nil { - return err - } - s.Status = SecretStatusAWS - return nil -} - -func (s *awsSecret) Decrypt() error { - if !s.IsEncrypted() { - return errWrongSecretStatus - } - return s.baseGCloudSecret.Decrypt() -} diff --git a/kms/aws/aws.go b/kms/aws/aws.go new file mode 100644 index 00000000..a9baaffa --- /dev/null +++ b/kms/aws/aws.go @@ -0,0 +1,67 @@ +// +build !noawskms + +package aws + +import ( + // we import awskms here to be able to disable AWS KMS support using a build tag + _ "gocloud.dev/secrets/awskms" + + "github.com/drakkan/sftpgo/v2/kms" + "github.com/drakkan/sftpgo/v2/kms/gocloud" + "github.com/drakkan/sftpgo/v2/version" +) + +const encryptedStatus = kms.SecretStatusAWS + +type awsSecret struct { + gocloud.Secret +} + +func init() { + version.AddFeature("+awskms") + kms.RegisterSecretProvider(kms.SchemeAWS, encryptedStatus, newAWSSecret) +} + +func newAWSSecret(base kms.BaseSecret, url, masterKey string) kms.SecretProvider { + return &awsSecret{ + gocloud.Secret{ + BaseSecret: base, + URL: url, + MasterKey: masterKey, + }, + } +} + +func (s *awsSecret) Name() string { + return "AWS" +} + +func (s *awsSecret) IsEncrypted() bool { + return s.Status == encryptedStatus +} + +func (s *awsSecret) Encrypt() error { + if err := s.Secret.Encrypt(); err != nil { + return err + } + s.Status = encryptedStatus + return nil +} + +func (s *awsSecret) Decrypt() error { + if !s.IsEncrypted() { + return kms.ErrWrongSecretStatus + } + return s.Secret.Decrypt() +} + +func (s *awsSecret) Clone() kms.SecretProvider { + baseSecret := kms.BaseSecret{ + Status: s.Status, + Payload: s.Payload, + Key: s.Key, + AdditionalData: s.AdditionalData, + Mode: s.Mode, + } + return newAWSSecret(baseSecret, s.URL, s.MasterKey) +} diff --git a/kms/aws/aws_disabled.go b/kms/aws/aws_disabled.go new file mode 100644 index 00000000..65caa5f8 --- /dev/null +++ b/kms/aws/aws_disabled.go @@ -0,0 +1,11 @@ +// +build noawskms + +package aws + +import ( + "github.com/drakkan/sftpgo/v2/version" +) + +func init() { + version.AddFeature("-awskms") +} diff --git a/kms/aws_disabled.go b/kms/aws_disabled.go deleted file mode 100644 index 4cb9298b..00000000 --- a/kms/aws_disabled.go +++ /dev/null @@ -1,17 +0,0 @@ -// +build noawskms - -package kms - -import ( - "errors" - - "github.com/drakkan/sftpgo/v2/version" -) - -func init() { - version.AddFeature("-awskms") -} - -func newAWSSecret(base baseSecret, url, masterKey string) SecretProvider { - return newDisabledSecret(errors.New("AWS KMS disabled at build time")) -} diff --git a/kms/basesecret.go b/kms/basesecret.go index 1fc401a3..52ce99c5 100644 --- a/kms/basesecret.go +++ b/kms/basesecret.go @@ -1,7 +1,7 @@ package kms -// baseSecret defines the base struct shared among all the secret providers -type baseSecret struct { +// BaseSecret defines the base struct shared among all the secret providers +type BaseSecret struct { Status SecretStatus `json:"status,omitempty"` Payload string `json:"payload,omitempty"` Key string `json:"key,omitempty"` @@ -10,39 +10,39 @@ type baseSecret struct { Mode int `json:"mode,omitempty"` } -func (s *baseSecret) GetStatus() SecretStatus { +func (s *BaseSecret) GetStatus() SecretStatus { return s.Status } -func (s *baseSecret) GetPayload() string { +func (s *BaseSecret) GetPayload() string { return s.Payload } -func (s *baseSecret) GetKey() string { +func (s *BaseSecret) GetKey() string { return s.Key } -func (s *baseSecret) GetMode() int { +func (s *BaseSecret) GetMode() int { return s.Mode } -func (s *baseSecret) GetAdditionalData() string { +func (s *BaseSecret) GetAdditionalData() string { return s.AdditionalData } -func (s *baseSecret) SetKey(value string) { +func (s *BaseSecret) SetKey(value string) { s.Key = value } -func (s *baseSecret) SetAdditionalData(value string) { +func (s *BaseSecret) SetAdditionalData(value string) { s.AdditionalData = value } -func (s *baseSecret) SetStatus(value SecretStatus) { +func (s *BaseSecret) SetStatus(value SecretStatus) { s.Status = value } -func (s *baseSecret) isEmpty() bool { +func (s *BaseSecret) isEmpty() bool { if s.Status != "" { return false } diff --git a/kms/builtin.go b/kms/builtin.go index 4413f422..f842f844 100644 --- a/kms/builtin.go +++ b/kms/builtin.go @@ -10,17 +10,21 @@ import ( ) type builtinSecret struct { - baseSecret + BaseSecret } -func newBuiltinSecret(base baseSecret) SecretProvider { +func init() { + RegisterSecretProvider(SchemeBuiltin, SecretStatusAES256GCM, newBuiltinSecret) +} + +func newBuiltinSecret(base BaseSecret, url, masterKey string) SecretProvider { return &builtinSecret{ - baseSecret: base, + BaseSecret: base, } } func (s *builtinSecret) Name() string { - return builtinProviderName + return "Builtin" } func (s *builtinSecret) IsEncrypted() bool { @@ -40,7 +44,7 @@ func (s *builtinSecret) deriveKey(key []byte) []byte { func (s *builtinSecret) Encrypt() error { if s.Payload == "" { - return errInvalidSecret + return ErrInvalidSecret } switch s.Status { case SecretStatusPlain: @@ -70,7 +74,7 @@ func (s *builtinSecret) Encrypt() error { s.Status = SecretStatusAES256GCM return nil default: - return errWrongSecretStatus + return ErrWrongSecretStatus } } @@ -112,6 +116,17 @@ func (s *builtinSecret) Decrypt() error { s.AdditionalData = "" return nil default: - return errWrongSecretStatus + return ErrWrongSecretStatus } } + +func (s *builtinSecret) Clone() SecretProvider { + baseSecret := BaseSecret{ + Status: s.Status, + Payload: s.Payload, + Key: s.Key, + AdditionalData: s.AdditionalData, + Mode: s.Mode, + } + return newBuiltinSecret(baseSecret, "", "") +} diff --git a/kms/disabled.go b/kms/disabled.go deleted file mode 100644 index aa53e15b..00000000 --- a/kms/disabled.go +++ /dev/null @@ -1,33 +0,0 @@ -// +build noawskms nogcpkms novaultkms - -package kms - -const disabledProviderName = "Disabled" - -type disabledSecret struct { - baseSecret - err error -} - -func newDisabledSecret(err error) SecretProvider { - return &disabledSecret{ - baseSecret: baseSecret{}, - err: err, - } -} - -func (s *disabledSecret) Name() string { - return disabledProviderName -} - -func (s *disabledSecret) IsEncrypted() bool { - return false -} - -func (s *disabledSecret) Encrypt() error { - return s.err -} - -func (s *disabledSecret) Decrypt() error { - return s.err -} diff --git a/kms/gcp.go b/kms/gcp.go deleted file mode 100644 index 6289b4b1..00000000 --- a/kms/gcp.go +++ /dev/null @@ -1,51 +0,0 @@ -// +build !nogcpkms - -package kms - -import ( - // we import gcpkms here to be able to disable GCP KMS support using a build tag - _ "gocloud.dev/secrets/gcpkms" - - "github.com/drakkan/sftpgo/v2/version" -) - -type gcpSecret struct { - baseGCloudSecret -} - -func init() { - version.AddFeature("+gcpkms") -} - -func newGCPSecret(base baseSecret, url, masterKey string) SecretProvider { - return &gcpSecret{ - baseGCloudSecret{ - baseSecret: base, - url: url, - masterKey: masterKey, - }, - } -} - -func (s *gcpSecret) Name() string { - return gcpProviderName -} - -func (s *gcpSecret) IsEncrypted() bool { - return s.Status == SecretStatusGCP -} - -func (s *gcpSecret) Encrypt() error { - if err := s.baseGCloudSecret.Encrypt(); err != nil { - return err - } - s.Status = SecretStatusGCP - return nil -} - -func (s *gcpSecret) Decrypt() error { - if !s.IsEncrypted() { - return errWrongSecretStatus - } - return s.baseGCloudSecret.Decrypt() -} diff --git a/kms/gcp/gcp.go b/kms/gcp/gcp.go new file mode 100644 index 00000000..61ddc0ce --- /dev/null +++ b/kms/gcp/gcp.go @@ -0,0 +1,67 @@ +// +build !nogcpkms + +package gcp + +import ( + // we import gcpkms here to be able to disable GCP KMS support using a build tag + _ "gocloud.dev/secrets/gcpkms" + + "github.com/drakkan/sftpgo/v2/kms" + "github.com/drakkan/sftpgo/v2/kms/gocloud" + "github.com/drakkan/sftpgo/v2/version" +) + +const encryptedStatus = kms.SecretStatusGCP + +type gcpSecret struct { + gocloud.Secret +} + +func init() { + version.AddFeature("+gcpkms") + kms.RegisterSecretProvider(kms.SchemeGCP, encryptedStatus, newGCPSecret) +} + +func newGCPSecret(base kms.BaseSecret, url, masterKey string) kms.SecretProvider { + return &gcpSecret{ + gocloud.Secret{ + BaseSecret: base, + URL: url, + MasterKey: masterKey, + }, + } +} + +func (s *gcpSecret) Name() string { + return "GCP" +} + +func (s *gcpSecret) IsEncrypted() bool { + return s.Status == encryptedStatus +} + +func (s *gcpSecret) Encrypt() error { + if err := s.Secret.Encrypt(); err != nil { + return err + } + s.Status = encryptedStatus + return nil +} + +func (s *gcpSecret) Decrypt() error { + if !s.IsEncrypted() { + return kms.ErrWrongSecretStatus + } + return s.Secret.Decrypt() +} + +func (s *gcpSecret) Clone() kms.SecretProvider { + baseSecret := kms.BaseSecret{ + Status: s.Status, + Payload: s.Payload, + Key: s.Key, + AdditionalData: s.AdditionalData, + Mode: s.Mode, + } + return newGCPSecret(baseSecret, s.URL, s.MasterKey) +} diff --git a/kms/gcp/gcp_disabled.go b/kms/gcp/gcp_disabled.go new file mode 100644 index 00000000..bb9c62bc --- /dev/null +++ b/kms/gcp/gcp_disabled.go @@ -0,0 +1,11 @@ +// +build nogcpkms + +package gcp + +import ( + "github.com/drakkan/sftpgo/v2/version" +) + +func init() { + version.AddFeature("-gcpkms") +} diff --git a/kms/gcp_disabled.go b/kms/gcp_disabled.go deleted file mode 100644 index 9b1df3a9..00000000 --- a/kms/gcp_disabled.go +++ /dev/null @@ -1,17 +0,0 @@ -// +build nogcpkms - -package kms - -import ( - "errors" - - "github.com/drakkan/sftpgo/v2/version" -) - -func init() { - version.AddFeature("-gcpkms") -} - -func newGCPSecret(base baseSecret, url, masterKey string) SecretProvider { - return newDisabledSecret(errors.New("GCP KMS disabled at build time")) -} diff --git a/kms/basegocloud.go b/kms/gocloud/gocloud.go similarity index 65% rename from kms/basegocloud.go rename to kms/gocloud/gocloud.go index b0af907d..c726ed9c 100644 --- a/kms/basegocloud.go +++ b/kms/gocloud/gocloud.go @@ -1,4 +1,4 @@ -package kms +package gocloud import ( "context" @@ -6,27 +6,34 @@ import ( "time" "gocloud.dev/secrets" + + "github.com/drakkan/sftpgo/v2/kms" ) -type baseGCloudSecret struct { - baseSecret - masterKey string - url string +const ( + defaultTimeout = 10 * time.Second +) + +// Secret defines common methods for go-cloud based kms +type Secret struct { + kms.BaseSecret + MasterKey string + URL string } -func (s *baseGCloudSecret) Encrypt() error { - if s.Status != SecretStatusPlain { - return errWrongSecretStatus +func (s *Secret) Encrypt() error { + if s.Status != kms.SecretStatusPlain { + return kms.ErrWrongSecretStatus } if s.Payload == "" { - return errInvalidSecret + return kms.ErrInvalidSecret } payload := s.Payload key := "" mode := 0 - if s.masterKey != "" { - localSecret := newLocalSecret(s.baseSecret, s.masterKey) + if s.MasterKey != "" { + localSecret := kms.NewLocalSecret(s.BaseSecret, "", s.MasterKey) err := localSecret.Encrypt() if err != nil { return err @@ -39,7 +46,7 @@ func (s *baseGCloudSecret) Encrypt() error { ctx, cancelFn := context.WithDeadline(context.Background(), time.Now().Add(defaultTimeout)) defer cancelFn() - keeper, err := secrets.OpenKeeper(ctx, s.url) + keeper, err := secrets.OpenKeeper(ctx, s.URL) if err != nil { return err } @@ -55,7 +62,7 @@ func (s *baseGCloudSecret) Encrypt() error { return nil } -func (s *baseGCloudSecret) Decrypt() error { +func (s *Secret) Decrypt() error { encrypted, err := base64.StdEncoding.DecodeString(s.Payload) if err != nil { return err @@ -63,7 +70,7 @@ func (s *baseGCloudSecret) Decrypt() error { ctx, cancelFn := context.WithDeadline(context.Background(), time.Now().Add(defaultTimeout)) defer cancelFn() - keeper, err := secrets.OpenKeeper(ctx, s.url) + keeper, err := secrets.OpenKeeper(ctx, s.URL) if err != nil { return err } @@ -75,21 +82,21 @@ func (s *baseGCloudSecret) Decrypt() error { } payload := string(plaintext) if s.Key != "" { - baseSecret := baseSecret{ - Status: SecretStatusSecretBox, + baseSecret := kms.BaseSecret{ + Status: kms.SecretStatusSecretBox, Payload: string(plaintext), Key: s.Key, AdditionalData: s.AdditionalData, Mode: s.Mode, } - localSecret := newLocalSecret(baseSecret, s.masterKey) + localSecret := kms.NewLocalSecret(baseSecret, "", s.MasterKey) err = localSecret.Decrypt() if err != nil { return err } payload = localSecret.GetPayload() } - s.Status = SecretStatusPlain + s.Status = kms.SecretStatusPlain s.Payload = payload s.Key = "" s.AdditionalData = "" diff --git a/kms/kms.go b/kms/kms.go index 2c7366bd..f93298e6 100644 --- a/kms/kms.go +++ b/kms/kms.go @@ -7,8 +7,8 @@ import ( "os" "strings" "sync" - "time" + "github.com/drakkan/sftpgo/v2/logger" "github.com/drakkan/sftpgo/v2/util" ) @@ -26,8 +26,13 @@ type SecretProvider interface { SetKey(string) SetAdditionalData(string) SetStatus(SecretStatus) + Clone() SecretProvider } +const ( + logSender = "kms" +) + // SecretStatus defines the statuses of a Secret object type SecretStatus = string @@ -51,12 +56,16 @@ const ( SecretStatusRedacted SecretStatus = "Redacted" ) +// Scheme defines the supported URL scheme +type Scheme = string + +// supported URL schemes const ( - localProviderName = "Local" - builtinProviderName = "Builtin" - awsProviderName = "AWS" - gcpProviderName = "GCP" - vaultProviderName = "VaultTransit" + SchemeLocal Scheme = "local://" + SchemeBuiltin Scheme = "builtin://" + SchemeAWS Scheme = "awskms://" + SchemeGCP Scheme = "gcpkms://" + SchemeVaultTransit Scheme = "hashivault://" ) // Configuration defines the KMS configuration @@ -71,16 +80,32 @@ type Secrets struct { masterKey string } +type registeredSecretProvider struct { + encryptedStatus SecretStatus + newFn func(base BaseSecret, url, masterKey string) SecretProvider +} + var ( - errWrongSecretStatus = errors.New("wrong secret status") + // ErrWrongSecretStatus defines the error to return if the secret status is not appropriate + // for the request operation + ErrWrongSecretStatus = errors.New("wrong secret status") + // ErrInvalidSecret defines the error to return if a secret is not valid + ErrInvalidSecret = errors.New("invalid secret") errMalformedCiphertext = errors.New("malformed ciphertext") - errInvalidSecret = errors.New("invalid secret") validSecretStatuses = []string{SecretStatusPlain, SecretStatusAES256GCM, SecretStatusSecretBox, SecretStatusVaultTransit, SecretStatusAWS, SecretStatusGCP, SecretStatusRedacted} - config Configuration - defaultTimeout = 10 * time.Second + config Configuration + secretProviders = make(map[string]registeredSecretProvider) ) +// RegisterSecretProvider register a new secret provider +func RegisterSecretProvider(scheme string, encryptedStatus SecretStatus, fn func(base BaseSecret, url, masterKey string) SecretProvider) { + secretProviders[scheme] = registeredSecretProvider{ + encryptedStatus: encryptedStatus, + newFn: fn, + } +} + // NewSecret builds a new Secret using the provided arguments func NewSecret(status SecretStatus, payload, key, data string) *Secret { return config.newSecret(status, payload, key, data) @@ -115,11 +140,18 @@ func (c *Configuration) Initialize() error { c.Secrets.masterKey = strings.TrimSpace(string(mKey)) } config = *c + if config.Secrets.URL == "" { + config.Secrets.URL = "local://" + } + for k, v := range secretProviders { + logger.Debug(logSender, "", "secret provider registered for scheme: %#v, encrypted status: %#v", + k, v.encryptedStatus) + } return nil } func (c *Configuration) newSecret(status SecretStatus, payload, key, data string) *Secret { - base := baseSecret{ + base := BaseSecret{ Status: status, Key: key, Payload: payload, @@ -130,17 +162,13 @@ func (c *Configuration) newSecret(status SecretStatus, payload, key, data string } } -func (c *Configuration) getSecretProvider(base baseSecret) SecretProvider { - if strings.HasPrefix(c.Secrets.URL, "hashivault://") { - return newVaultSecret(base, c.Secrets.URL, c.Secrets.masterKey) +func (c *Configuration) getSecretProvider(base BaseSecret) SecretProvider { + for k, v := range secretProviders { + if strings.HasPrefix(c.Secrets.URL, k) { + return v.newFn(base, c.Secrets.URL, c.Secrets.masterKey) + } } - if strings.HasPrefix(c.Secrets.URL, "awskms://") { - return newAWSSecret(base, c.Secrets.URL, c.Secrets.masterKey) - } - if strings.HasPrefix(c.Secrets.URL, "gcpkms://") { - return newGCPSecret(base, c.Secrets.URL, c.Secrets.masterKey) - } - return newLocalSecret(base, c.Secrets.masterKey) + return NewLocalSecret(base, c.Secrets.URL, c.Secrets.masterKey) } // Secret defines the struct used to store confidential data @@ -154,7 +182,7 @@ func (s *Secret) MarshalJSON() ([]byte, error) { s.RLock() defer s.RUnlock() - return json.Marshal(&baseSecret{ + return json.Marshal(&BaseSecret{ Status: s.provider.GetStatus(), Payload: s.provider.GetPayload(), Key: s.provider.GetKey(), @@ -169,7 +197,7 @@ func (s *Secret) UnmarshalJSON(data []byte) error { s.Lock() defer s.Unlock() - baseSecret := baseSecret{} + baseSecret := BaseSecret{} err := json.Unmarshal(data, &baseSecret) if err != nil { return err @@ -178,23 +206,21 @@ func (s *Secret) UnmarshalJSON(data []byte) error { s.provider = config.getSecretProvider(baseSecret) return nil } - switch baseSecret.Status { - case SecretStatusAES256GCM: - s.provider = newBuiltinSecret(baseSecret) - case SecretStatusSecretBox: - s.provider = newLocalSecret(baseSecret, config.Secrets.masterKey) - case SecretStatusVaultTransit: - s.provider = newVaultSecret(baseSecret, config.Secrets.URL, config.Secrets.masterKey) - case SecretStatusAWS: - s.provider = newAWSSecret(baseSecret, config.Secrets.URL, config.Secrets.masterKey) - case SecretStatusGCP: - s.provider = newGCPSecret(baseSecret, config.Secrets.URL, config.Secrets.masterKey) - case SecretStatusPlain, SecretStatusRedacted: + + if baseSecret.Status == SecretStatusPlain || baseSecret.Status == SecretStatusRedacted { s.provider = config.getSecretProvider(baseSecret) - default: - return errInvalidSecret + return nil } - return nil + + for _, v := range secretProviders { + if v.encryptedStatus == baseSecret.Status { + s.provider = v.newFn(baseSecret, config.Secrets.URL, config.Secrets.masterKey) + return nil + } + } + logger.Debug(logSender, "", "no provider registered for status %#v", baseSecret.Status) + + return ErrInvalidSecret } // IsEqual returns true if all the secrets fields are equal @@ -222,36 +248,9 @@ func (s *Secret) Clone() *Secret { s.RLock() defer s.RUnlock() - baseSecret := baseSecret{ - Status: s.provider.GetStatus(), - Payload: s.provider.GetPayload(), - Key: s.provider.GetKey(), - AdditionalData: s.provider.GetAdditionalData(), - Mode: s.provider.GetMode(), + return &Secret{ + provider: s.provider.Clone(), } - switch s.provider.Name() { - case builtinProviderName: - return &Secret{ - provider: newBuiltinSecret(baseSecret), - } - case awsProviderName: - return &Secret{ - provider: newAWSSecret(baseSecret, config.Secrets.URL, config.Secrets.masterKey), - } - case gcpProviderName: - return &Secret{ - provider: newGCPSecret(baseSecret, config.Secrets.URL, config.Secrets.masterKey), - } - case localProviderName: - return &Secret{ - provider: newLocalSecret(baseSecret, config.Secrets.masterKey), - } - case vaultProviderName: - return &Secret{ - provider: newVaultSecret(baseSecret, config.Secrets.URL, config.Secrets.masterKey), - } - } - return NewSecret(s.GetStatus(), s.GetPayload(), s.GetKey(), s.GetAdditionalData()) } // IsEncrypted returns true if the secret is encrypted diff --git a/kms/local.go b/kms/local.go index 0398287d..a360b163 100644 --- a/kms/local.go +++ b/kms/local.go @@ -11,20 +11,25 @@ import ( "golang.org/x/crypto/hkdf" ) +func init() { + RegisterSecretProvider(SchemeLocal, SecretStatusSecretBox, NewLocalSecret) +} + type localSecret struct { - baseSecret + BaseSecret masterKey string } -func newLocalSecret(base baseSecret, masterKey string) SecretProvider { +// NewLocalSecret returns a SecretProvider that use a locally provided symmetric key +func NewLocalSecret(base BaseSecret, url, masterKey string) SecretProvider { return &localSecret{ - baseSecret: base, + BaseSecret: base, masterKey: masterKey, } } func (s *localSecret) Name() string { - return localProviderName + return "Local" } func (s *localSecret) IsEncrypted() bool { @@ -33,10 +38,10 @@ func (s *localSecret) IsEncrypted() bool { func (s *localSecret) Encrypt() error { if s.Status != SecretStatusPlain { - return errWrongSecretStatus + return ErrWrongSecretStatus } if s.Payload == "" { - return errInvalidSecret + return ErrInvalidSecret } secretKey, err := localsecrets.NewRandomKey() if err != nil { @@ -62,7 +67,7 @@ func (s *localSecret) Encrypt() error { func (s *localSecret) Decrypt() error { if !s.IsEncrypted() { - return errWrongSecretStatus + return ErrWrongSecretStatus } encrypted, err := base64.StdEncoding.DecodeString(s.Payload) if err != nil { @@ -123,3 +128,14 @@ func (s *localSecret) getEncryptionMode() int { } return 1 } + +func (s *localSecret) Clone() SecretProvider { + baseSecret := BaseSecret{ + Status: s.Status, + Payload: s.Payload, + Key: s.Key, + AdditionalData: s.AdditionalData, + Mode: s.Mode, + } + return NewLocalSecret(baseSecret, "", s.masterKey) +} diff --git a/kms/vault.go b/kms/vault.go deleted file mode 100644 index e9df29f3..00000000 --- a/kms/vault.go +++ /dev/null @@ -1,51 +0,0 @@ -// +build !novaultkms - -package kms - -import ( - // we import hashivault here to be able to disable Vault support using a build tag - _ "gocloud.dev/secrets/hashivault" - - "github.com/drakkan/sftpgo/v2/version" -) - -type vaultSecret struct { - baseGCloudSecret -} - -func init() { - version.AddFeature("+vaultkms") -} - -func newVaultSecret(base baseSecret, url, masterKey string) SecretProvider { - return &vaultSecret{ - baseGCloudSecret{ - baseSecret: base, - url: url, - masterKey: masterKey, - }, - } -} - -func (s *vaultSecret) Name() string { - return vaultProviderName -} - -func (s *vaultSecret) IsEncrypted() bool { - return s.Status == SecretStatusVaultTransit -} - -func (s *vaultSecret) Encrypt() error { - if err := s.baseGCloudSecret.Encrypt(); err != nil { - return err - } - s.Status = SecretStatusVaultTransit - return nil -} - -func (s *vaultSecret) Decrypt() error { - if !s.IsEncrypted() { - return errWrongSecretStatus - } - return s.baseGCloudSecret.Decrypt() -} diff --git a/kms/vault/vault.go b/kms/vault/vault.go new file mode 100644 index 00000000..cb5f895e --- /dev/null +++ b/kms/vault/vault.go @@ -0,0 +1,67 @@ +// +build !novaultkms + +package vault + +import ( + // we import hashivault here to be able to disable Vault support using a build tag + _ "gocloud.dev/secrets/hashivault" + + "github.com/drakkan/sftpgo/v2/kms" + "github.com/drakkan/sftpgo/v2/kms/gocloud" + "github.com/drakkan/sftpgo/v2/version" +) + +const encryptedStatus = kms.SecretStatusVaultTransit + +type vaultSecret struct { + gocloud.Secret +} + +func init() { + version.AddFeature("+vaultkms") + kms.RegisterSecretProvider(kms.SchemeVaultTransit, encryptedStatus, newVaultSecret) +} + +func newVaultSecret(base kms.BaseSecret, url, masterKey string) kms.SecretProvider { + return &vaultSecret{ + gocloud.Secret{ + BaseSecret: base, + URL: url, + MasterKey: masterKey, + }, + } +} + +func (s *vaultSecret) Name() string { + return "VaultTransit" +} + +func (s *vaultSecret) IsEncrypted() bool { + return s.Status == encryptedStatus +} + +func (s *vaultSecret) Encrypt() error { + if err := s.Secret.Encrypt(); err != nil { + return err + } + s.Status = encryptedStatus + return nil +} + +func (s *vaultSecret) Decrypt() error { + if !s.IsEncrypted() { + return kms.ErrWrongSecretStatus + } + return s.Secret.Decrypt() +} + +func (s *vaultSecret) Clone() kms.SecretProvider { + baseSecret := kms.BaseSecret{ + Status: s.Status, + Payload: s.Payload, + Key: s.Key, + AdditionalData: s.AdditionalData, + Mode: s.Mode, + } + return newVaultSecret(baseSecret, s.URL, s.MasterKey) +} diff --git a/kms/vault/vault_disabled.go b/kms/vault/vault_disabled.go new file mode 100644 index 00000000..050dd204 --- /dev/null +++ b/kms/vault/vault_disabled.go @@ -0,0 +1,11 @@ +// +build novaultkms + +package vault + +import ( + "github.com/drakkan/sftpgo/v2/version" +) + +func init() { + version.AddFeature("-vaultkms") +} diff --git a/kms/vault_disabled.go b/kms/vault_disabled.go deleted file mode 100644 index 8ca26049..00000000 --- a/kms/vault_disabled.go +++ /dev/null @@ -1,17 +0,0 @@ -// +build novaultkms - -package kms - -import ( - "errors" - - "github.com/drakkan/sftpgo/v2/version" -) - -func init() { - version.AddFeature("-vaultkms") -} - -func newVaultSecret(base baseSecret, url, masterKey string) SecretProvider { - return newDisabledSecret(errors.New("Vault KMS disabled at build time")) -} diff --git a/main.go b/main.go index 4bdac6e5..df3472c6 100644 --- a/main.go +++ b/main.go @@ -11,6 +11,9 @@ import ( "go.uber.org/automaxprocs/maxprocs" "github.com/drakkan/sftpgo/v2/cmd" + _ "github.com/drakkan/sftpgo/v2/kms/aws" + _ "github.com/drakkan/sftpgo/v2/kms/gcp" + _ "github.com/drakkan/sftpgo/v2/kms/vault" ) func main() {