diff --git a/config/config.go b/config/config.go index 57c19b0d..5e2abe4f 100644 --- a/config/config.go +++ b/config/config.go @@ -597,6 +597,16 @@ func getPluginsFromEnv(idx int) { isSet = true } + kmsScheme, ok := os.LookupEnv(fmt.Sprintf("SFTPGO_PLUGINS__%v__KMS_OPTIONS__SCHEME", idx)) + if ok { + pluginConfig.KMSOptions.Scheme = kmsScheme + } + + kmsEncStatus, ok := os.LookupEnv(fmt.Sprintf("SFTPGO_PLUGINS__%v__KMS_OPTIONS__ENCRYPTED_STATUS", idx)) + if ok { + pluginConfig.KMSOptions.EncryptedStatus = kmsEncStatus + } + cmd, ok := os.LookupEnv(fmt.Sprintf("SFTPGO_PLUGINS__%v__CMD", idx)) if ok { pluginConfig.Cmd = cmd diff --git a/config/config_test.go b/config/config_test.go index 6688c08b..250b6378 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -17,6 +17,7 @@ import ( "github.com/drakkan/sftpgo/v2/ftpd" "github.com/drakkan/sftpgo/v2/httpclient" "github.com/drakkan/sftpgo/v2/httpd" + "github.com/drakkan/sftpgo/v2/kms" "github.com/drakkan/sftpgo/v2/sftpd" "github.com/drakkan/sftpgo/v2/util" ) @@ -300,6 +301,8 @@ func TestPluginsFromEnv(t *testing.T) { os.Setenv("SFTPGO_PLUGINS__0__ARGS", "arg1,arg2") os.Setenv("SFTPGO_PLUGINS__0__SHA256SUM", "0a71ded61fccd59c4f3695b51c1b3d180da8d2d77ea09ccee20dac242675c193") os.Setenv("SFTPGO_PLUGINS__0__AUTO_MTLS", "1") + os.Setenv("SFTPGO_PLUGINS__0__KMS_OPTIONS__SCHEME", kms.SchemeAWS) + os.Setenv("SFTPGO_PLUGINS__0__KMS_OPTIONS__ENCRYPTED_STATUS", kms.SecretStatusAWS) t.Cleanup(func() { os.Unsetenv("SFTPGO_PLUGINS__0__TYPE") os.Unsetenv("SFTPGO_PLUGINS__0__NOTIFIER_OPTIONS__FS_EVENTS") @@ -308,6 +311,8 @@ func TestPluginsFromEnv(t *testing.T) { os.Unsetenv("SFTPGO_PLUGINS__0__ARGS") os.Unsetenv("SFTPGO_PLUGINS__0__SHA256SUM") os.Unsetenv("SFTPGO_PLUGINS__0__AUTO_MTLS") + os.Unsetenv("SFTPGO_PLUGINS__0__KMS_OPTIONS__SCHEME") + os.Unsetenv("SFTPGO_PLUGINS__0__KMS_OPTIONS__ENCRYPTED_STATUS") }) configDir := ".." @@ -329,6 +334,8 @@ func TestPluginsFromEnv(t *testing.T) { require.Equal(t, "arg2", pluginConf.Args[1]) require.Equal(t, "0a71ded61fccd59c4f3695b51c1b3d180da8d2d77ea09ccee20dac242675c193", pluginConf.SHA256Sum) require.True(t, pluginConf.AutoMTLS) + require.Equal(t, kms.SchemeAWS, pluginConf.KMSOptions.Scheme) + require.Equal(t, kms.SecretStatusAWS, pluginConf.KMSOptions.EncryptedStatus) configAsJSON, err := json.Marshal(pluginsConf) require.NoError(t, err) @@ -340,6 +347,8 @@ func TestPluginsFromEnv(t *testing.T) { os.Setenv("SFTPGO_PLUGINS__0__CMD", "plugin_start_cmd1") os.Setenv("SFTPGO_PLUGINS__0__ARGS", "") os.Setenv("SFTPGO_PLUGINS__0__AUTO_MTLS", "0") + os.Setenv("SFTPGO_PLUGINS__0__KMS_OPTIONS__SCHEME", kms.SchemeVaultTransit) + os.Setenv("SFTPGO_PLUGINS__0__KMS_OPTIONS__ENCRYPTED_STATUS", kms.SecretStatusVaultTransit) err = config.LoadConfig(configDir, confName) assert.NoError(t, err) pluginsConf = config.GetPluginsConfig() @@ -356,6 +365,8 @@ func TestPluginsFromEnv(t *testing.T) { require.Len(t, pluginConf.Args, 0) require.Equal(t, "0a71ded61fccd59c4f3695b51c1b3d180da8d2d77ea09ccee20dac242675c193", pluginConf.SHA256Sum) require.False(t, pluginConf.AutoMTLS) + require.Equal(t, kms.SchemeVaultTransit, pluginConf.KMSOptions.Scheme) + require.Equal(t, kms.SecretStatusVaultTransit, pluginConf.KMSOptions.EncryptedStatus) err = os.Remove(configFilePath) assert.NoError(t, err) diff --git a/docs/full-configuration.md b/docs/full-configuration.md index ea606b35..ede57221 100644 --- a/docs/full-configuration.md +++ b/docs/full-configuration.md @@ -243,6 +243,9 @@ The configuration file contains the following sections: - `notifier_options`, struct. Defines the options for notifier plugins. - `fs_events`, list of strings. Defines the filesystem events that will be notified to this plugin. - `user_events`, list of strings. Defines the user events that will be notified to this plugin. + - `kms_options`, struct. Defines the options for kms plugins. + - `scheme`, string. KMS scheme. Supported schemes are: `awskms`, `gcpkms`, `hashivault`, `azurekeyvault`. + - `encrypted_status`, string. Encrypted status for a KMS secret. Supported statuses are: `AWS`, `GCP`, `VaultTransit`, `AzureKeyVault`. - `cmd`, string. Path to the plugin executable. - `args`, list of strings. Optional arguments to pass to the plugin executable. - `sha256sum`, string. SHA256 checksum for the plugin executable. If not empty it will be used to verify the integrity of the executable. diff --git a/go.mod b/go.mod index 722049c7..bd9cc435 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.5 + github.com/aws/aws-sdk-go v1.40.1 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 @@ -19,7 +19,6 @@ require ( github.com/go-chi/render v1.0.1 github.com/go-ole/go-ole v1.2.5 // indirect github.com/go-sql-driver/mysql v1.6.0 - github.com/goccy/go-json v0.7.4 // indirect github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 github.com/grandcat/zeroconf v1.0.0 github.com/hashicorp/go-hclog v0.16.2 @@ -30,10 +29,10 @@ 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.2 + github.com/lestrrat-go/jwx v1.2.4 github.com/lib/pq v1.10.2 github.com/mattn/go-isatty v0.0.13 // indirect - github.com/mattn/go-sqlite3 v1.14.7 + github.com/mattn/go-sqlite3 v1.14.8 github.com/miekg/dns v1.1.43 // indirect github.com/minio/sio v0.3.0 github.com/mitchellh/go-testing-interface v1.14.1 // indirect @@ -63,7 +62,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-20210713002101-d411969a0d9a // indirect + google.golang.org/genproto v0.0.0-20210716133855-ce7ef5c701ea // 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 2971e4cc..c1cc2cb1 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.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 v1.40.1 h1:MlWasxqPLnFKZLPdEFzlEtzIdV7044o44YILe6A9zys= +github.com/aws/aws-sdk-go v1.40.1/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.2 h1:sH9GeolQn9s3JyNbeEOXXPTko2fD4BwAMWXLYvcEl2k= -github.com/lestrrat-go/jwx v1.2.2/go.mod h1:Tg2uP7bpxEHUDtuWjap/PxroJ4okxGzkQznXiG+a5Dc= +github.com/lestrrat-go/jwx v1.2.4 h1:EuVGI/hPUSRstxWpWjVcklOe1odJLVrFY9zt4k1pa30= +github.com/lestrrat-go/jwx v1.2.4/go.mod h1:CAe9Z479rJwIYDR2DqWwMm9c+gCNoYB6+0wBxPkEh0Q= 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= @@ -630,8 +630,8 @@ github.com/mattn/go-isatty v0.0.13 h1:qdl+GuBjcsKKDco5BsxPJlId98mSWNKqYA+Co0SC1y github.com/mattn/go-isatty v0.0.13/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= -github.com/mattn/go-sqlite3 v1.14.7 h1:fxWBnXkxfM6sRiuH3bqJ4CfzZojMOLVc0UTsTglEghA= -github.com/mattn/go-sqlite3 v1.14.7/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= +github.com/mattn/go-sqlite3 v1.14.8 h1:gDp86IdQsN/xWjIEmr9MF6o9mpksUgh0fu+9ByFxzIU= +github.com/mattn/go-sqlite3 v1.14.8/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= @@ -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-20210713002101-d411969a0d9a h1:89EorDSnBRFywcvGsJvpxw2IsiDMI+DeM7iZOaunfHs= -google.golang.org/genproto v0.0.0-20210713002101-d411969a0d9a/go.mod h1:AxrInvYm1dci+enl5hChSFPOmmUF1+uAa/UsgNRWd7k= +google.golang.org/genproto v0.0.0-20210716133855-ce7ef5c701ea h1:8ZyCcgugUqamxp/vZSEJw9CMy7VZlSWYJLLJPi/dSDA= +google.golang.org/genproto v0.0.0-20210716133855-ce7ef5c701ea/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/kms/gocloud/gocloud.go b/kms/gocloud/gocloud.go index c726ed9c..11511a6f 100644 --- a/kms/gocloud/gocloud.go +++ b/kms/gocloud/gocloud.go @@ -84,7 +84,7 @@ func (s *Secret) Decrypt() error { if s.Key != "" { baseSecret := kms.BaseSecret{ Status: kms.SecretStatusSecretBox, - Payload: string(plaintext), + Payload: payload, Key: s.Key, AdditionalData: s.AdditionalData, Mode: s.Mode, diff --git a/kms/kms.go b/kms/kms.go index f93298e6..dff231e7 100644 --- a/kms/kms.go +++ b/kms/kms.go @@ -52,6 +52,8 @@ const ( // SecretStatusVaultTransit means we use the transit secrets engine in Vault // to keep information secret SecretStatusVaultTransit SecretStatus = "VaultTransit" + // SecretStatusAzureKeyVault means we use Azure KeyVault to keep information secret + SecretStatusAzureKeyVault SecretStatus = "AzureKeyVault" // SecretStatusRedacted means the secret is redacted SecretStatusRedacted SecretStatus = "Redacted" ) @@ -61,11 +63,12 @@ type Scheme = string // supported URL schemes const ( - SchemeLocal Scheme = "local://" - SchemeBuiltin Scheme = "builtin://" - SchemeAWS Scheme = "awskms://" - SchemeGCP Scheme = "gcpkms://" - SchemeVaultTransit Scheme = "hashivault://" + SchemeLocal Scheme = "local" + SchemeBuiltin Scheme = "builtin" + SchemeAWS Scheme = "awskms" + SchemeGCP Scheme = "gcpkms" + SchemeVaultTransit Scheme = "hashivault" + SchemeAzureKeyVault Scheme = "azurekeyvault" ) // Configuration defines the KMS configuration @@ -141,7 +144,7 @@ func (c *Configuration) Initialize() error { } config = *c if config.Secrets.URL == "" { - config.Secrets.URL = "local://" + config.Secrets.URL = SchemeLocal + "://" } for k, v := range secretProviders { logger.Debug(logSender, "", "secret provider registered for scheme: %#v, encrypted status: %#v", diff --git a/sdk/plugin/kms.go b/sdk/plugin/kms.go new file mode 100644 index 00000000..5f8580f7 --- /dev/null +++ b/sdk/plugin/kms.go @@ -0,0 +1,181 @@ +package plugin + +import ( + "crypto/sha256" + "fmt" + "os/exec" + "path/filepath" + + "github.com/hashicorp/go-hclog" + "github.com/hashicorp/go-plugin" + + "github.com/drakkan/sftpgo/v2/kms" + "github.com/drakkan/sftpgo/v2/logger" + kmsplugin "github.com/drakkan/sftpgo/v2/sdk/plugin/kms" + "github.com/drakkan/sftpgo/v2/util" +) + +var ( + validKMSSchemes = []string{kms.SchemeAWS, kms.SchemeGCP, kms.SchemeVaultTransit, kms.SchemeAzureKeyVault} + validKMSEncryptedStatuses = []string{kms.SecretStatusVaultTransit, kms.SecretStatusAWS, kms.SecretStatusGCP, + kms.SecretStatusAzureKeyVault} +) + +// KMSConfig defines configuration parameters for kms plugins +type KMSConfig struct { + Scheme string `json:"scheme" mapstructure:"scheme"` + EncryptedStatus string `json:"encrypted_status" mapstructure:"encrypted_status"` +} + +func (c *KMSConfig) isValid() error { + if !util.IsStringInSlice(c.Scheme, validKMSSchemes) { + return fmt.Errorf("invalid kms scheme: %v", c.Scheme) + } + if !util.IsStringInSlice(c.EncryptedStatus, validKMSEncryptedStatuses) { + return fmt.Errorf("invalid kms encrypted status: %v", c.EncryptedStatus) + } + return nil +} + +type kmsPlugin struct { + config Config + service kmsplugin.Service + client *plugin.Client +} + +func newKMSPlugin(config Config) (*kmsPlugin, error) { + p := &kmsPlugin{ + config: config, + } + if err := p.initialize(); err != nil { + logger.Warn(logSender, "", "unable to create kms plugin: %v, config %+v", err, config) + return nil, err + } + return p, nil +} + +func (p *kmsPlugin) initialize() error { + killProcess(p.config.Cmd) + logger.Debug(logSender, "", "create new kms plugin %#v", p.config.Cmd) + if err := p.config.KMSOptions.isValid(); err != nil { + return fmt.Errorf("invalid options for kms plugin %#v: %v", p.config.Cmd, err) + } + var secureConfig *plugin.SecureConfig + if p.config.SHA256Sum != "" { + secureConfig.Checksum = []byte(p.config.SHA256Sum) + secureConfig.Hash = sha256.New() + } + client := plugin.NewClient(&plugin.ClientConfig{ + HandshakeConfig: kmsplugin.Handshake, + Plugins: kmsplugin.PluginMap, + Cmd: exec.Command(p.config.Cmd, p.config.Args...), + AllowedProtocols: []plugin.Protocol{ + plugin.ProtocolGRPC, + }, + AutoMTLS: p.config.AutoMTLS, + SecureConfig: secureConfig, + Managed: false, + Logger: &logger.HCLogAdapter{ + Logger: hclog.New(&hclog.LoggerOptions{ + Name: fmt.Sprintf("%v.%v", logSender, kmsplugin.PluginName), + Level: pluginsLogLevel, + DisableTime: true, + }), + }, + }) + rpcClient, err := client.Client() + if err != nil { + logger.Debug(logSender, "", "unable to get rpc client for kms plugin %#v: %v", p.config.Cmd, err) + return err + } + raw, err := rpcClient.Dispense(kmsplugin.PluginName) + if err != nil { + logger.Debug(logSender, "", "unable to get plugin %v from rpc client for command %#v: %v", + kmsplugin.PluginName, p.config.Cmd, err) + return err + } + + p.client = client + p.service = raw.(kmsplugin.Service) + + return nil +} + +func (p *kmsPlugin) exited() bool { + return p.client.Exited() +} + +func (p *kmsPlugin) cleanup() { + p.client.Kill() +} + +func (p *kmsPlugin) Encrypt(secret kms.BaseSecret, url string, masterKey string) (string, string, int32, error) { + return p.service.Encrypt(secret.Payload, secret.AdditionalData, url, masterKey) +} + +func (p *kmsPlugin) Decrypt(secret kms.BaseSecret, url string, masterKey string) (string, error) { + return p.service.Decrypt(secret.Payload, secret.Key, secret.AdditionalData, secret.Mode, url, masterKey) +} + +type kmsPluginSecretProvider struct { + kms.BaseSecret + URL string + MasterKey string + config *Config +} + +func (s *kmsPluginSecretProvider) Name() string { + return fmt.Sprintf("KMSPlugin_%v_%v_%v", filepath.Base(s.config.Cmd), s.config.KMSOptions.Scheme, s.config.kmsID) +} + +func (s *kmsPluginSecretProvider) IsEncrypted() bool { + return s.Status == s.config.KMSOptions.EncryptedStatus +} + +func (s *kmsPluginSecretProvider) Encrypt() error { + if s.Status != kms.SecretStatusPlain { + return kms.ErrWrongSecretStatus + } + if s.Payload == "" { + return kms.ErrInvalidSecret + } + + payload, key, mode, err := Handler.kmsEncrypt(s.BaseSecret, s.URL, s.MasterKey, s.config.kmsID) + if err != nil { + return err + } + s.Status = s.config.KMSOptions.EncryptedStatus + s.Payload = payload + s.Key = key + s.Mode = int(mode) + + return nil +} + +func (s *kmsPluginSecretProvider) Decrypt() error { + if !s.IsEncrypted() { + return kms.ErrWrongSecretStatus + } + payload, err := Handler.kmsDecrypt(s.BaseSecret, s.URL, s.MasterKey, s.config.kmsID) + if err != nil { + return err + } + s.Status = kms.SecretStatusPlain + s.Payload = payload + s.Key = "" + s.AdditionalData = "" + s.Mode = 0 + + return nil +} + +func (s *kmsPluginSecretProvider) Clone() kms.SecretProvider { + baseSecret := kms.BaseSecret{ + Status: s.Status, + Payload: s.Payload, + Key: s.Key, + AdditionalData: s.AdditionalData, + Mode: s.Mode, + } + return s.config.newKMSPluginSecretProvider(baseSecret, s.URL, s.MasterKey) +} diff --git a/sdk/plugin/kms/grpc.go b/sdk/plugin/kms/grpc.go new file mode 100644 index 00000000..36f8fbec --- /dev/null +++ b/sdk/plugin/kms/grpc.go @@ -0,0 +1,84 @@ +package kms + +import ( + "context" + "time" + + "github.com/drakkan/sftpgo/v2/sdk/plugin/kms/proto" +) + +const ( + rpcTimeout = 20 * time.Second +) + +// GRPCClient is an implementation of KMS interface that talks over RPC. +type GRPCClient struct { + client proto.KMSClient +} + +// Encrypt implements the KMSService interface +func (c *GRPCClient) Encrypt(payload, additionalData, URL, masterKey string) (string, string, int32, error) { + ctx, cancel := context.WithTimeout(context.Background(), rpcTimeout) + defer cancel() + + resp, err := c.client.Encrypt(ctx, &proto.EncryptRequest{ + Payload: payload, + AdditionalData: additionalData, + Url: URL, + MasterKey: masterKey, + }) + if err != nil { + return "", "", 0, err + } + return resp.Payload, resp.Key, resp.Mode, nil +} + +// Decrypt implements the KMSService interface +func (c *GRPCClient) Decrypt(payload, key, additionalData string, mode int, URL, masterKey string) (string, error) { + ctx, cancel := context.WithTimeout(context.Background(), rpcTimeout) + defer cancel() + + resp, err := c.client.Decrypt(ctx, &proto.DecryptRequest{ + Payload: payload, + Key: key, + AdditionalData: additionalData, + Mode: int32(mode), + Url: URL, + MasterKey: masterKey, + }) + if err != nil { + return "", err + } + return resp.Payload, nil +} + +// GRPCServer defines the gRPC server that GRPCClient talks to. +type GRPCServer struct { + Impl Service +} + +// Encrypt implements the serve side encrypt method +func (s *GRPCServer) Encrypt(ctx context.Context, req *proto.EncryptRequest) (*proto.EncryptResponse, error) { + payload, key, mode, err := s.Impl.Encrypt(req.Payload, req.AdditionalData, req.Url, req.MasterKey) + if err != nil { + return nil, err + } + + return &proto.EncryptResponse{ + Payload: payload, + Key: key, + Mode: mode, + }, nil +} + +// Decrypt implements the serve side decrypt method +func (s *GRPCServer) Decrypt(ctx context.Context, req *proto.DecryptRequest) (*proto.DecryptResponse, error) { + payload, err := s.Impl.Decrypt(req.Payload, req.Key, req.AdditionalData, int(req.Mode), req.Url, req.MasterKey) + if err != nil { + return nil, err + } + + return &proto.DecryptResponse{ + Payload: payload, + }, nil +} diff --git a/sdk/plugin/kms/kms.go b/sdk/plugin/kms/kms.go new file mode 100644 index 00000000..54853bff --- /dev/null +++ b/sdk/plugin/kms/kms.go @@ -0,0 +1,56 @@ +// Package kms defines the implementation for kms plugins. +// KMS plugins allow to encrypt/decrypt sensitive data. +package kms + +import ( + "context" + + "github.com/hashicorp/go-plugin" + "google.golang.org/grpc" + + "github.com/drakkan/sftpgo/v2/sdk/plugin/kms/proto" +) + +const ( + // PluginName defines the name for a kms plugin + PluginName = "kms" +) + +// Handshake is a common handshake that is shared by plugin and host. +var Handshake = plugin.HandshakeConfig{ + ProtocolVersion: 1, + MagicCookieKey: "SFTPGO_KMS_PLUGIN", + MagicCookieValue: "223e3571-7ed2-4b96-b4b3-c7eb87d7ca1d", +} + +// PluginMap is the map of plugins we can dispense. +var PluginMap = map[string]plugin.Plugin{ + PluginName: &Plugin{}, +} + +// Service defines the interface for kms plugins +type Service interface { + Encrypt(payload, additionalData, URL, masterKey string) (string, string, int32, error) + Decrypt(payload, key, additionalData string, mode int, URL, masterKey string) (string, error) +} + +// Plugin defines the implementation to serve/connect to a notifier plugin +type Plugin struct { + plugin.Plugin + Impl Service +} + +// GRPCServer defines the GRPC server implementation for this plugin +func (p *Plugin) GRPCServer(broker *plugin.GRPCBroker, s *grpc.Server) error { + proto.RegisterKMSServer(s, &GRPCServer{ + Impl: p.Impl, + }) + return nil +} + +// GRPCClient defines the GRPC client implementation for this plugin +func (p *Plugin) GRPCClient(ctx context.Context, broker *plugin.GRPCBroker, c *grpc.ClientConn) (interface{}, error) { + return &GRPCClient{ + client: proto.NewKMSClient(c), + }, nil +} diff --git a/sdk/plugin/kms/proto/kms.pb.go b/sdk/plugin/kms/proto/kms.pb.go new file mode 100644 index 00000000..e20fa389 --- /dev/null +++ b/sdk/plugin/kms/proto/kms.pb.go @@ -0,0 +1,559 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.26.0 +// protoc v3.17.3 +// source: kms/proto/kms.proto + +package proto + +import ( + context "context" + grpc "google.golang.org/grpc" + codes "google.golang.org/grpc/codes" + status "google.golang.org/grpc/status" + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type EncryptRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Payload string `protobuf:"bytes,1,opt,name=payload,proto3" json:"payload,omitempty"` + AdditionalData string `protobuf:"bytes,2,opt,name=additional_data,json=additionalData,proto3" json:"additional_data,omitempty"` + Url string `protobuf:"bytes,3,opt,name=url,proto3" json:"url,omitempty"` + MasterKey string `protobuf:"bytes,4,opt,name=master_key,json=masterKey,proto3" json:"master_key,omitempty"` +} + +func (x *EncryptRequest) Reset() { + *x = EncryptRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_kms_proto_kms_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *EncryptRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*EncryptRequest) ProtoMessage() {} + +func (x *EncryptRequest) ProtoReflect() protoreflect.Message { + mi := &file_kms_proto_kms_proto_msgTypes[0] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use EncryptRequest.ProtoReflect.Descriptor instead. +func (*EncryptRequest) Descriptor() ([]byte, []int) { + return file_kms_proto_kms_proto_rawDescGZIP(), []int{0} +} + +func (x *EncryptRequest) GetPayload() string { + if x != nil { + return x.Payload + } + return "" +} + +func (x *EncryptRequest) GetAdditionalData() string { + if x != nil { + return x.AdditionalData + } + return "" +} + +func (x *EncryptRequest) GetUrl() string { + if x != nil { + return x.Url + } + return "" +} + +func (x *EncryptRequest) GetMasterKey() string { + if x != nil { + return x.MasterKey + } + return "" +} + +type EncryptResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Payload string `protobuf:"bytes,1,opt,name=payload,proto3" json:"payload,omitempty"` + Key string `protobuf:"bytes,2,opt,name=key,proto3" json:"key,omitempty"` + Mode int32 `protobuf:"varint,3,opt,name=mode,proto3" json:"mode,omitempty"` +} + +func (x *EncryptResponse) Reset() { + *x = EncryptResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_kms_proto_kms_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *EncryptResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*EncryptResponse) ProtoMessage() {} + +func (x *EncryptResponse) ProtoReflect() protoreflect.Message { + mi := &file_kms_proto_kms_proto_msgTypes[1] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use EncryptResponse.ProtoReflect.Descriptor instead. +func (*EncryptResponse) Descriptor() ([]byte, []int) { + return file_kms_proto_kms_proto_rawDescGZIP(), []int{1} +} + +func (x *EncryptResponse) GetPayload() string { + if x != nil { + return x.Payload + } + return "" +} + +func (x *EncryptResponse) GetKey() string { + if x != nil { + return x.Key + } + return "" +} + +func (x *EncryptResponse) GetMode() int32 { + if x != nil { + return x.Mode + } + return 0 +} + +type DecryptRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Payload string `protobuf:"bytes,1,opt,name=payload,proto3" json:"payload,omitempty"` + Key string `protobuf:"bytes,2,opt,name=key,proto3" json:"key,omitempty"` + AdditionalData string `protobuf:"bytes,3,opt,name=additional_data,json=additionalData,proto3" json:"additional_data,omitempty"` + Mode int32 `protobuf:"varint,4,opt,name=mode,proto3" json:"mode,omitempty"` + Url string `protobuf:"bytes,5,opt,name=url,proto3" json:"url,omitempty"` + MasterKey string `protobuf:"bytes,6,opt,name=master_key,json=masterKey,proto3" json:"master_key,omitempty"` +} + +func (x *DecryptRequest) Reset() { + *x = DecryptRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_kms_proto_kms_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *DecryptRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DecryptRequest) ProtoMessage() {} + +func (x *DecryptRequest) ProtoReflect() protoreflect.Message { + mi := &file_kms_proto_kms_proto_msgTypes[2] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DecryptRequest.ProtoReflect.Descriptor instead. +func (*DecryptRequest) Descriptor() ([]byte, []int) { + return file_kms_proto_kms_proto_rawDescGZIP(), []int{2} +} + +func (x *DecryptRequest) GetPayload() string { + if x != nil { + return x.Payload + } + return "" +} + +func (x *DecryptRequest) GetKey() string { + if x != nil { + return x.Key + } + return "" +} + +func (x *DecryptRequest) GetAdditionalData() string { + if x != nil { + return x.AdditionalData + } + return "" +} + +func (x *DecryptRequest) GetMode() int32 { + if x != nil { + return x.Mode + } + return 0 +} + +func (x *DecryptRequest) GetUrl() string { + if x != nil { + return x.Url + } + return "" +} + +func (x *DecryptRequest) GetMasterKey() string { + if x != nil { + return x.MasterKey + } + return "" +} + +type DecryptResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Payload string `protobuf:"bytes,1,opt,name=payload,proto3" json:"payload,omitempty"` +} + +func (x *DecryptResponse) Reset() { + *x = DecryptResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_kms_proto_kms_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *DecryptResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DecryptResponse) ProtoMessage() {} + +func (x *DecryptResponse) ProtoReflect() protoreflect.Message { + mi := &file_kms_proto_kms_proto_msgTypes[3] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DecryptResponse.ProtoReflect.Descriptor instead. +func (*DecryptResponse) Descriptor() ([]byte, []int) { + return file_kms_proto_kms_proto_rawDescGZIP(), []int{3} +} + +func (x *DecryptResponse) GetPayload() string { + if x != nil { + return x.Payload + } + return "" +} + +var File_kms_proto_kms_proto protoreflect.FileDescriptor + +var file_kms_proto_kms_proto_rawDesc = []byte{ + 0x0a, 0x13, 0x6b, 0x6d, 0x73, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x6b, 0x6d, 0x73, 0x2e, + 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x05, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x84, 0x01, 0x0a, + 0x0e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, + 0x18, 0x0a, 0x07, 0x70, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x07, 0x70, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x12, 0x27, 0x0a, 0x0f, 0x61, 0x64, 0x64, + 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x61, 0x6c, 0x5f, 0x64, 0x61, 0x74, 0x61, 0x18, 0x02, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x0e, 0x61, 0x64, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x61, 0x6c, 0x44, 0x61, + 0x74, 0x61, 0x12, 0x10, 0x0a, 0x03, 0x75, 0x72, 0x6c, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x03, 0x75, 0x72, 0x6c, 0x12, 0x1d, 0x0a, 0x0a, 0x6d, 0x61, 0x73, 0x74, 0x65, 0x72, 0x5f, 0x6b, + 0x65, 0x79, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x6d, 0x61, 0x73, 0x74, 0x65, 0x72, + 0x4b, 0x65, 0x79, 0x22, 0x51, 0x0a, 0x0f, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x52, 0x65, + 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x70, 0x61, 0x79, 0x6c, 0x6f, 0x61, + 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x70, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, + 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, + 0x65, 0x79, 0x12, 0x12, 0x0a, 0x04, 0x6d, 0x6f, 0x64, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x05, + 0x52, 0x04, 0x6d, 0x6f, 0x64, 0x65, 0x22, 0xaa, 0x01, 0x0a, 0x0e, 0x44, 0x65, 0x63, 0x72, 0x79, + 0x70, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x18, 0x0a, 0x07, 0x70, 0x61, 0x79, + 0x6c, 0x6f, 0x61, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x70, 0x61, 0x79, 0x6c, + 0x6f, 0x61, 0x64, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x27, 0x0a, 0x0f, 0x61, 0x64, 0x64, 0x69, 0x74, 0x69, 0x6f, + 0x6e, 0x61, 0x6c, 0x5f, 0x64, 0x61, 0x74, 0x61, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, + 0x61, 0x64, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x61, 0x6c, 0x44, 0x61, 0x74, 0x61, 0x12, 0x12, + 0x0a, 0x04, 0x6d, 0x6f, 0x64, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x05, 0x52, 0x04, 0x6d, 0x6f, + 0x64, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x75, 0x72, 0x6c, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x03, 0x75, 0x72, 0x6c, 0x12, 0x1d, 0x0a, 0x0a, 0x6d, 0x61, 0x73, 0x74, 0x65, 0x72, 0x5f, 0x6b, + 0x65, 0x79, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x6d, 0x61, 0x73, 0x74, 0x65, 0x72, + 0x4b, 0x65, 0x79, 0x22, 0x2b, 0x0a, 0x0f, 0x44, 0x65, 0x63, 0x72, 0x79, 0x70, 0x74, 0x52, 0x65, + 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x70, 0x61, 0x79, 0x6c, 0x6f, 0x61, + 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x70, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, + 0x32, 0x79, 0x0a, 0x03, 0x4b, 0x4d, 0x53, 0x12, 0x38, 0x0a, 0x07, 0x45, 0x6e, 0x63, 0x72, 0x79, + 0x70, 0x74, 0x12, 0x15, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, + 0x70, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, + 0x65, 0x12, 0x38, 0x0a, 0x07, 0x44, 0x65, 0x63, 0x72, 0x79, 0x70, 0x74, 0x12, 0x15, 0x2e, 0x70, + 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x44, 0x65, 0x63, 0x72, 0x79, 0x70, 0x74, 0x52, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x44, 0x65, 0x63, 0x72, + 0x79, 0x70, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x16, 0x5a, 0x14, 0x73, + 0x64, 0x6b, 0x2f, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x2f, 0x6b, 0x6d, 0x73, 0x2f, 0x70, 0x72, + 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, +} + +var ( + file_kms_proto_kms_proto_rawDescOnce sync.Once + file_kms_proto_kms_proto_rawDescData = file_kms_proto_kms_proto_rawDesc +) + +func file_kms_proto_kms_proto_rawDescGZIP() []byte { + file_kms_proto_kms_proto_rawDescOnce.Do(func() { + file_kms_proto_kms_proto_rawDescData = protoimpl.X.CompressGZIP(file_kms_proto_kms_proto_rawDescData) + }) + return file_kms_proto_kms_proto_rawDescData +} + +var file_kms_proto_kms_proto_msgTypes = make([]protoimpl.MessageInfo, 4) +var file_kms_proto_kms_proto_goTypes = []interface{}{ + (*EncryptRequest)(nil), // 0: proto.EncryptRequest + (*EncryptResponse)(nil), // 1: proto.EncryptResponse + (*DecryptRequest)(nil), // 2: proto.DecryptRequest + (*DecryptResponse)(nil), // 3: proto.DecryptResponse +} +var file_kms_proto_kms_proto_depIdxs = []int32{ + 0, // 0: proto.KMS.Encrypt:input_type -> proto.EncryptRequest + 2, // 1: proto.KMS.Decrypt:input_type -> proto.DecryptRequest + 1, // 2: proto.KMS.Encrypt:output_type -> proto.EncryptResponse + 3, // 3: proto.KMS.Decrypt:output_type -> proto.DecryptResponse + 2, // [2:4] is the sub-list for method output_type + 0, // [0:2] is the sub-list for method input_type + 0, // [0:0] is the sub-list for extension type_name + 0, // [0:0] is the sub-list for extension extendee + 0, // [0:0] is the sub-list for field type_name +} + +func init() { file_kms_proto_kms_proto_init() } +func file_kms_proto_kms_proto_init() { + if File_kms_proto_kms_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_kms_proto_kms_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*EncryptRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_kms_proto_kms_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*EncryptResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_kms_proto_kms_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*DecryptRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_kms_proto_kms_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*DecryptResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_kms_proto_kms_proto_rawDesc, + NumEnums: 0, + NumMessages: 4, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_kms_proto_kms_proto_goTypes, + DependencyIndexes: file_kms_proto_kms_proto_depIdxs, + MessageInfos: file_kms_proto_kms_proto_msgTypes, + }.Build() + File_kms_proto_kms_proto = out.File + file_kms_proto_kms_proto_rawDesc = nil + file_kms_proto_kms_proto_goTypes = nil + file_kms_proto_kms_proto_depIdxs = nil +} + +// Reference imports to suppress errors if they are not otherwise used. +var _ context.Context +var _ grpc.ClientConnInterface + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +const _ = grpc.SupportPackageIsVersion6 + +// KMSClient is the client API for KMS service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://godoc.org/google.golang.org/grpc#ClientConn.NewStream. +type KMSClient interface { + Encrypt(ctx context.Context, in *EncryptRequest, opts ...grpc.CallOption) (*EncryptResponse, error) + Decrypt(ctx context.Context, in *DecryptRequest, opts ...grpc.CallOption) (*DecryptResponse, error) +} + +type kMSClient struct { + cc grpc.ClientConnInterface +} + +func NewKMSClient(cc grpc.ClientConnInterface) KMSClient { + return &kMSClient{cc} +} + +func (c *kMSClient) Encrypt(ctx context.Context, in *EncryptRequest, opts ...grpc.CallOption) (*EncryptResponse, error) { + out := new(EncryptResponse) + err := c.cc.Invoke(ctx, "/proto.KMS/Encrypt", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *kMSClient) Decrypt(ctx context.Context, in *DecryptRequest, opts ...grpc.CallOption) (*DecryptResponse, error) { + out := new(DecryptResponse) + err := c.cc.Invoke(ctx, "/proto.KMS/Decrypt", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +// KMSServer is the server API for KMS service. +type KMSServer interface { + Encrypt(context.Context, *EncryptRequest) (*EncryptResponse, error) + Decrypt(context.Context, *DecryptRequest) (*DecryptResponse, error) +} + +// UnimplementedKMSServer can be embedded to have forward compatible implementations. +type UnimplementedKMSServer struct { +} + +func (*UnimplementedKMSServer) Encrypt(context.Context, *EncryptRequest) (*EncryptResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method Encrypt not implemented") +} +func (*UnimplementedKMSServer) Decrypt(context.Context, *DecryptRequest) (*DecryptResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method Decrypt not implemented") +} + +func RegisterKMSServer(s *grpc.Server, srv KMSServer) { + s.RegisterService(&_KMS_serviceDesc, srv) +} + +func _KMS_Encrypt_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(EncryptRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(KMSServer).Encrypt(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/proto.KMS/Encrypt", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(KMSServer).Encrypt(ctx, req.(*EncryptRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _KMS_Decrypt_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(DecryptRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(KMSServer).Decrypt(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/proto.KMS/Decrypt", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(KMSServer).Decrypt(ctx, req.(*DecryptRequest)) + } + return interceptor(ctx, in, info, handler) +} + +var _KMS_serviceDesc = grpc.ServiceDesc{ + ServiceName: "proto.KMS", + HandlerType: (*KMSServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "Encrypt", + Handler: _KMS_Encrypt_Handler, + }, + { + MethodName: "Decrypt", + Handler: _KMS_Decrypt_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "kms/proto/kms.proto", +} diff --git a/sdk/plugin/kms/proto/kms.proto b/sdk/plugin/kms/proto/kms.proto new file mode 100644 index 00000000..fb443e75 --- /dev/null +++ b/sdk/plugin/kms/proto/kms.proto @@ -0,0 +1,35 @@ +syntax = "proto3"; +package proto; + +option go_package = "sdk/plugin/kms/proto"; + +message EncryptRequest { + string payload = 1; + string additional_data = 2; + string url = 3; + string master_key = 4; +} + +message EncryptResponse { + string payload = 1; + string key = 2; + int32 mode = 3; +} + +message DecryptRequest { + string payload = 1; + string key = 2; + string additional_data = 3; + int32 mode = 4; + string url = 5; + string master_key = 6; +} + +message DecryptResponse { + string payload = 1; +} + +service KMS { + rpc Encrypt(EncryptRequest) returns (EncryptResponse); + rpc Decrypt(DecryptRequest) returns (DecryptResponse); +} diff --git a/sdk/plugin/mkproto.sh b/sdk/plugin/mkproto.sh index bb13c400..9e20ec63 100755 --- a/sdk/plugin/mkproto.sh +++ b/sdk/plugin/mkproto.sh @@ -1,5 +1,6 @@ #!/bin/bash protoc notifier/proto/notifier.proto --go_out=plugins=grpc:../.. --go_out=../../.. +protoc kms/proto/kms.proto --go_out=plugins=grpc:../.. --go_out=../../.. diff --git a/sdk/plugin/notifier.go b/sdk/plugin/notifier.go index c5635692..fcf3e242 100644 --- a/sdk/plugin/notifier.go +++ b/sdk/plugin/notifier.go @@ -40,7 +40,7 @@ func newNotifierPlugin(config Config) (*notifierPlugin, error) { config: config, } if err := p.initialize(); err != nil { - logger.Warn(logSender, "", "unable to create notifier plugin: %v, config %v", err, config) + logger.Warn(logSender, "", "unable to create notifier plugin: %v, config %+v", err, config) return nil, err } return p, nil @@ -56,9 +56,9 @@ func (p *notifierPlugin) cleanup() { func (p *notifierPlugin) initialize() error { killProcess(p.config.Cmd) - logger.Debug(logSender, "", "create new plugin %v", p.config.Cmd) + logger.Debug(logSender, "", "create new notifier plugin %#v", p.config.Cmd) if !p.config.NotifierOptions.hasActions() { - return fmt.Errorf("no actions defined for the notifier plugin %v", p.config.Cmd) + return fmt.Errorf("no actions defined for the notifier plugin %#v", p.config.Cmd) } var secureConfig *plugin.SecureConfig if p.config.SHA256Sum != "" { @@ -85,12 +85,12 @@ func (p *notifierPlugin) initialize() error { }) rpcClient, err := client.Client() if err != nil { - logger.Debug(logSender, "", "unable to get rpc client for plugin %v: %v", p.config.Cmd, err) + logger.Debug(logSender, "", "unable to get rpc client for plugin %#v: %v", p.config.Cmd, err) return err } raw, err := rpcClient.Dispense(notifier.PluginName) if err != nil { - logger.Debug(logSender, "", "unable to get plugin %v from rpc client for plugin %v: %v", + logger.Debug(logSender, "", "unable to get plugin %v from rpc client for command %#v: %v", notifier.PluginName, p.config.Cmd, err) return err } diff --git a/sdk/plugin/notifier/notifier.go b/sdk/plugin/notifier/notifier.go index 4f805823..f94b401c 100644 --- a/sdk/plugin/notifier/notifier.go +++ b/sdk/plugin/notifier/notifier.go @@ -1,4 +1,4 @@ -// Package notifier defines the implementation for event notifier plugin. +// Package notifier defines the implementation for event notifier plugins. // Notifier plugins allow to receive filesystem events such as file uploads, // downloads etc. and user events such as add, update, delete. package notifier diff --git a/sdk/plugin/plugin.go b/sdk/plugin/plugin.go index 233c6398..5fd0212f 100644 --- a/sdk/plugin/plugin.go +++ b/sdk/plugin/plugin.go @@ -4,10 +4,14 @@ package plugin import ( "fmt" "sync" + "sync/atomic" + "time" "github.com/hashicorp/go-hclog" + "github.com/drakkan/sftpgo/v2/kms" "github.com/drakkan/sftpgo/v2/logger" + kmsplugin "github.com/drakkan/sftpgo/v2/sdk/plugin/kms" "github.com/drakkan/sftpgo/v2/sdk/plugin/notifier" ) @@ -30,8 +34,10 @@ type Renderer interface { type Config struct { // Plugin type Type string `json:"type" mapstructure:"type"` - // NotifierOptions defines additional options for notifiers plugins + // NotifierOptions defines options for notifiers plugins NotifierOptions NotifierConfig `json:"notifier_options" mapstructure:"notifier_options"` + // KMSOptions defines options for a KMS plugin + KMSOptions KMSConfig `json:"kms_options" mapstructure:"kms_options"` // Path to the plugin executable Cmd string `json:"cmd" mapstructure:"cmd"` // Args to pass to the plugin executable @@ -45,20 +51,43 @@ type Config struct { // rejected. The client will also refuse to connect to any server that isn't // the original instance started by the client. AutoMTLS bool `json:"auto_mtls" mapstructure:"auto_mtls"` + // unique identifier for kms plugins + kmsID int +} + +func (c *Config) newKMSPluginSecretProvider(base kms.BaseSecret, url, masterKey string) kms.SecretProvider { + return &kmsPluginSecretProvider{ + BaseSecret: base, + URL: url, + MasterKey: masterKey, + config: c, + } } // Manager handles enabled plugins type Manager struct { + closed int32 + done chan bool // List of configured plugins Configs []Config `json:"plugins" mapstructure:"plugins"` - mu sync.RWMutex + notifLock sync.RWMutex notifiers []*notifierPlugin + kmsLock sync.RWMutex + kms []*kmsPlugin } // Initialize initializes the configured plugins func Initialize(configs []Config, logVerbose bool) error { Handler = Manager{ Configs: configs, + done: make(chan bool), + closed: 0, + } + if len(configs) == 0 { + return nil + } + if err := Handler.validateConfigs(); err != nil { + return err } if logVerbose { @@ -67,7 +96,8 @@ func Initialize(configs []Config, logVerbose bool) error { pluginsLogLevel = hclog.Info } - for _, config := range configs { + kmsID := 0 + for idx, config := range Handler.Configs { switch config.Type { case notifier.PluginName: plugin, err := newNotifierPlugin(config) @@ -75,92 +105,172 @@ func Initialize(configs []Config, logVerbose bool) error { return err } Handler.notifiers = append(Handler.notifiers, plugin) + case kmsplugin.PluginName: + plugin, err := newKMSPlugin(config) + if err != nil { + return err + } + Handler.kms = append(Handler.kms, plugin) + Handler.Configs[idx].kmsID = kmsID + kmsID++ + kms.RegisterSecretProvider(config.KMSOptions.Scheme, config.KMSOptions.EncryptedStatus, + Handler.Configs[idx].newKMSPluginSecretProvider) + logger.Debug(logSender, "", "registered secret provider for scheme: %v, encrypted status: %v", + config.KMSOptions.Scheme, config.KMSOptions.EncryptedStatus) default: return fmt.Errorf("unsupported plugin type: %v", config.Type) } } + startCheckTicker() + return nil +} + +func (m *Manager) validateConfigs() error { + kmsSchemes := make(map[string]bool) + kmsEncryptions := make(map[string]bool) + + for _, config := range m.Configs { + if config.Type == kmsplugin.PluginName { + if _, ok := kmsSchemes[config.KMSOptions.Scheme]; ok { + return fmt.Errorf("invalid KMS configuration, duplicated scheme %#v", config.KMSOptions.Scheme) + } + if _, ok := kmsEncryptions[config.KMSOptions.EncryptedStatus]; ok { + return fmt.Errorf("invalid KMS configuration, duplicated encrypted status %#v", config.KMSOptions.EncryptedStatus) + } + kmsSchemes[config.KMSOptions.Scheme] = true + kmsEncryptions[config.KMSOptions.EncryptedStatus] = true + } + } return nil } // NotifyFsEvent sends the fs event notifications using any defined notifier plugins func (m *Manager) NotifyFsEvent(action, username, fsPath, fsTargetPath, sshCmd, protocol string, fileSize int64, err error) { - m.mu.RLock() + m.notifLock.RLock() + defer m.notifLock.RUnlock() - var crashedIdxs []int - for idx, n := range m.notifiers { + for _, n := range m.notifiers { if n.exited() { - crashedIdxs = append(crashedIdxs, idx) - } else { - n.notifyFsAction(action, username, fsPath, fsTargetPath, sshCmd, protocol, fileSize, err) - } - } - - m.mu.RUnlock() - - if len(crashedIdxs) > 0 { - m.restartCrashedNotifiers(crashedIdxs) - - m.mu.RLock() - defer m.mu.RUnlock() - - for idx := range crashedIdxs { - if !m.notifiers[idx].exited() { - m.notifiers[idx].notifyFsAction(action, username, fsPath, fsTargetPath, sshCmd, protocol, fileSize, err) - } + logger.Warn(logSender, "", "notifer plugin %v is not active, unable to send fs event", n.config.Cmd) + continue } + n.notifyFsAction(action, username, fsPath, fsTargetPath, sshCmd, protocol, fileSize, err) } } // NotifyUserEvent sends the user event notifications using any defined notifier plugins func (m *Manager) NotifyUserEvent(action string, user Renderer) { - m.mu.RLock() + m.notifLock.RLock() + defer m.notifLock.RUnlock() - var crashedIdxs []int - for idx, n := range m.notifiers { + for _, n := range m.notifiers { if n.exited() { - crashedIdxs = append(crashedIdxs, idx) - } else { - n.notifyUserAction(action, user) - } - } - - m.mu.RUnlock() - - if len(crashedIdxs) > 0 { - m.restartCrashedNotifiers(crashedIdxs) - - m.mu.RLock() - defer m.mu.RUnlock() - - for idx := range crashedIdxs { - if !m.notifiers[idx].exited() { - m.notifiers[idx].notifyUserAction(action, user) - } + logger.Warn(logSender, "", "notifer plugin %v is not active, unable to send user event", n.config.Cmd) + continue } + n.notifyUserAction(action, user) } } -func (m *Manager) restartCrashedNotifiers(crashedIdxs []int) { - for _, idx := range crashedIdxs { - m.mu.Lock() - defer m.mu.Unlock() +func (m *Manager) kmsEncrypt(secret kms.BaseSecret, url string, masterKey string, kmsID int) (string, string, int32, error) { + m.kmsLock.RLock() + plugin := m.kms[kmsID] + m.kmsLock.RUnlock() - if m.notifiers[idx].exited() { - logger.Info(logSender, "", "try to restart crashed plugin %v", m.Configs[idx].Cmd) - plugin, err := newNotifierPlugin(m.Configs[idx]) - if err == nil { - m.notifiers[idx] = plugin - } else { - logger.Warn(logSender, "", "plugin %v crashed and restart failed: %v", m.Configs[idx].Cmd, err) - } + return plugin.Encrypt(secret, url, masterKey) +} + +func (m *Manager) kmsDecrypt(secret kms.BaseSecret, url string, masterKey string, kmsID int) (string, error) { + m.kmsLock.RLock() + plugin := m.kms[kmsID] + m.kmsLock.RUnlock() + + return plugin.Decrypt(secret, url, masterKey) +} + +func (m *Manager) checkCrashedPlugins() { + m.notifLock.RLock() + for idx, n := range m.notifiers { + if n.exited() { + defer Handler.restartNotifierPlugin(n.config, idx) } } + m.notifLock.RUnlock() + + m.kmsLock.RLock() + for idx, k := range m.kms { + if k.exited() { + defer Handler.restartKMSPlugin(k.config, idx) + } + } + m.kmsLock.RUnlock() +} + +func (m *Manager) restartNotifierPlugin(config Config, idx int) { + if atomic.LoadInt32(&m.closed) == 1 { + return + } + logger.Info(logSender, "", "try to restart notifier crashed plugin %#v, idx: %v", config.Cmd, idx) + plugin, err := newNotifierPlugin(config) + if err != nil { + logger.Warn(logSender, "", "unable to restart notifier plugin %#v, err: %v", config.Cmd, err) + return + } + + m.notifLock.Lock() + m.notifiers[idx] = plugin + m.notifLock.Unlock() +} + +func (m *Manager) restartKMSPlugin(config Config, idx int) { + if atomic.LoadInt32(&m.closed) == 1 { + return + } + logger.Info(logSender, "", "try to restart crashed kms plugin %#v, idx: %v", config.Cmd, idx) + plugin, err := newKMSPlugin(config) + if err != nil { + logger.Warn(logSender, "", "unable to restart kms plugin %#v, err: %v", config.Cmd, err) + return + } + + m.kmsLock.Lock() + m.kms[idx] = plugin + m.kmsLock.Unlock() } // Cleanup releases all the active plugins func (m *Manager) Cleanup() { + atomic.StoreInt32(&m.closed, 1) + close(m.done) + m.notifLock.Lock() for _, n := range m.notifiers { - logger.Debug(logSender, "", "cleanup plugin %v", n.config.Cmd) + logger.Debug(logSender, "", "cleanup notifier plugin %v", n.config.Cmd) n.cleanup() } + m.notifLock.Unlock() + + m.kmsLock.Lock() + for _, k := range m.kms { + logger.Debug(logSender, "", "cleanup kms plugin %v", k.config.Cmd) + k.cleanup() + } + m.kmsLock.Unlock() +} + +func startCheckTicker() { + logger.Debug(logSender, "", "start plugins checker") + checker := time.NewTicker(30 * time.Second) + + go func() { + for { + select { + case <-Handler.done: + logger.Debug(logSender, "", "handler done, stop plugins checker") + checker.Stop() + return + case <-checker.C: + Handler.checkCrashedPlugins() + } + } + }() }