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() {