From 634b723b5d46b63c0be5c2232ee9efa61ce5d899 Mon Sep 17 00:00:00 2001 From: Nicola Murino Date: Mon, 30 Nov 2020 21:46:34 +0100 Subject: [PATCH] add KMS support Fixes #226 --- cmd/portable.go | 28 +-- cmd/startsubsys.go | 5 + config/config.go | 20 ++ config/config_test.go | 13 ++ dataprovider/bolt.go | 1 + dataprovider/compat.go | 29 +-- dataprovider/dataprovider.go | 26 ++- dataprovider/memory.go | 9 +- dataprovider/mysql.go | 7 +- dataprovider/pgsql.go | 7 + dataprovider/sqlcommon.go | 1 + dataprovider/user.go | 21 +- docs/account.md | 7 +- docs/full-configuration.md | 4 + docs/kms.md | 65 +++++++ examples/OTP/authy/README.md | 2 +- examples/rest-api-cli/README.md | 26 ++- ftpd/ftpd_test.go | 26 +-- go.mod | 3 + go.sum | 163 ++++++++++++++++ httpd/api_user.go | 11 +- httpd/api_utils.go | 33 +++- httpd/httpd_test.go | 304 ++++++++++++++--------------- httpd/internal_test.go | 44 +++-- httpd/schema/openapi.yaml | 8 +- httpd/web.go | 24 ++- kms/aws.go | 42 ++++ kms/basegocloud.go | 99 ++++++++++ kms/basesecret.go | 53 ++++++ kms/builtin.go | 122 ++++++++++++ kms/gcp.go | 42 ++++ kms/kms.go | 327 ++++++++++++++++++++++++++++++++ kms/local.go | 120 ++++++++++++ kms/vault.go | 42 ++++ service/service.go | 7 + service/service_portable.go | 24 +++ sftpd/sftpd_test.go | 25 +-- sftpgo.json | 6 + templates/user.html | 4 +- vfs/azblobfs.go | 2 +- vfs/gcsfs.go | 17 +- vfs/s3fs.go | 12 +- vfs/secret.go | 209 -------------------- vfs/vfs.go | 49 +++-- webdavd/server.go | 4 +- webdavd/webdavd_test.go | 25 +-- 46 files changed, 1582 insertions(+), 536 deletions(-) create mode 100644 docs/kms.md create mode 100644 kms/aws.go create mode 100644 kms/basegocloud.go create mode 100644 kms/basesecret.go create mode 100644 kms/builtin.go create mode 100644 kms/gcp.go create mode 100644 kms/kms.go create mode 100644 kms/local.go create mode 100644 kms/vault.go delete mode 100644 vfs/secret.go diff --git a/cmd/portable.go b/cmd/portable.go index c9e69e16..bc74c297 100644 --- a/cmd/portable.go +++ b/cmd/portable.go @@ -14,6 +14,7 @@ import ( "github.com/drakkan/sftpgo/common" "github.com/drakkan/sftpgo/dataprovider" + "github.com/drakkan/sftpgo/kms" "github.com/drakkan/sftpgo/service" "github.com/drakkan/sftpgo/sftpd" "github.com/drakkan/sftpgo/version" @@ -143,13 +144,10 @@ Please take a look at the usage below to customize the serving parameters`, FsConfig: dataprovider.Filesystem{ Provider: dataprovider.FilesystemProvider(portableFsProvider), S3Config: vfs.S3FsConfig{ - Bucket: portableS3Bucket, - Region: portableS3Region, - AccessKey: portableS3AccessKey, - AccessSecret: vfs.Secret{ - Status: vfs.SecretStatusPlain, - Payload: portableS3AccessSecret, - }, + Bucket: portableS3Bucket, + Region: portableS3Region, + AccessKey: portableS3AccessKey, + AccessSecret: kms.NewPlainSecret(portableS3AccessSecret), Endpoint: portableS3Endpoint, StorageClass: portableS3StorageClass, KeyPrefix: portableS3KeyPrefix, @@ -157,22 +155,16 @@ Please take a look at the usage below to customize the serving parameters`, UploadConcurrency: portableS3ULConcurrency, }, GCSConfig: vfs.GCSFsConfig{ - Bucket: portableGCSBucket, - Credentials: vfs.Secret{ - Status: vfs.SecretStatusPlain, - Payload: string(portableGCSCredentials), - }, + Bucket: portableGCSBucket, + Credentials: kms.NewPlainSecret(string(portableGCSCredentials)), AutomaticCredentials: portableGCSAutoCredentials, StorageClass: portableGCSStorageClass, KeyPrefix: portableGCSKeyPrefix, }, AzBlobConfig: vfs.AzBlobFsConfig{ - Container: portableAzContainer, - AccountName: portableAzAccountName, - AccountKey: vfs.Secret{ - Status: vfs.SecretStatusPlain, - Payload: portableAzAccountKey, - }, + Container: portableAzContainer, + AccountName: portableAzAccountName, + AccountKey: kms.NewPlainSecret(portableAzAccountKey), Endpoint: portableAzEndpoint, AccessTier: portableAzAccessTier, SASURL: portableAzSASURL, diff --git a/cmd/startsubsys.go b/cmd/startsubsys.go index 7ef140e8..5f9c82bc 100644 --- a/cmd/startsubsys.go +++ b/cmd/startsubsys.go @@ -83,6 +83,11 @@ Command-line flags should be specified in the Subsystem declaration. } httpConfig := config.GetHTTPConfig() httpConfig.Initialize(configDir) + kmsConfig := config.GetKMSConfig() + if err := kmsConfig.Initialize(); err != nil { + logger.Error(logSender, connectionID, "unable to initialize KMS: %v", err) + os.Exit(1) + } user, err := dataprovider.UserExists(username) if err == nil { if user.HomeDir != filepath.Clean(homedir) && !preserveHomeDir { diff --git a/config/config.go b/config/config.go index 2a73aaaa..7f2eab05 100644 --- a/config/config.go +++ b/config/config.go @@ -12,6 +12,7 @@ import ( "github.com/drakkan/sftpgo/ftpd" "github.com/drakkan/sftpgo/httpclient" "github.com/drakkan/sftpgo/httpd" + "github.com/drakkan/sftpgo/kms" "github.com/drakkan/sftpgo/logger" "github.com/drakkan/sftpgo/sftpd" "github.com/drakkan/sftpgo/utils" @@ -43,6 +44,7 @@ type globalConfig struct { ProviderConf dataprovider.Config `json:"data_provider" mapstructure:"data_provider"` HTTPDConfig httpd.Conf `json:"httpd" mapstructure:"httpd"` HTTPConfig httpclient.Config `json:"http" mapstructure:"http"` + KMSConfig kms.Configuration `json:"kms" mapstructure:"kms"` } func init() { @@ -164,6 +166,12 @@ func init() { CACertificates: nil, SkipTLSVerify: false, }, + KMSConfig: kms.Configuration{ + Secrets: kms.Secrets{ + URL: "", + MasterKeyPath: "", + }, + }, } viper.SetEnvPrefix(configEnvPrefix) @@ -240,6 +248,16 @@ func GetHTTPConfig() httpclient.Config { return globalConf.HTTPConfig } +// GetKMSConfig returns the KMS configuration +func GetKMSConfig() kms.Configuration { + return globalConf.KMSConfig +} + +// SetKMSConfig sets the kms configuration +func SetKMSConfig(config kms.Configuration) { + globalConf.KMSConfig = config +} + // HasServicesToStart returns true if the config defines at least a service to start. // Supported services are SFTP, FTP and WebDAV func HasServicesToStart() bool { @@ -456,4 +474,6 @@ func setViperDefaults() { viper.SetDefault("http.timeout", globalConf.HTTPConfig.Timeout) viper.SetDefault("http.ca_certificates", globalConf.HTTPConfig.CACertificates) viper.SetDefault("http.skip_tls_verify", globalConf.HTTPConfig.SkipTLSVerify) + viper.SetDefault("kms.secrets.url", globalConf.KMSConfig.Secrets.URL) + viper.SetDefault("kms.secrets.master_key_path", globalConf.KMSConfig.Secrets.MasterKeyPath) } diff --git a/config/config_test.go b/config/config_test.go index a991ae89..cefd23df 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -279,6 +279,12 @@ func TestSetGetConfig(t *testing.T) { config.SetWebDAVDConfig(webDavConf) assert.Equal(t, webDavConf.CertificateFile, config.GetWebDAVDConfig().CertificateFile) assert.Equal(t, webDavConf.CertificateKeyFile, config.GetWebDAVDConfig().CertificateKeyFile) + kmsConf := config.GetKMSConfig() + kmsConf.Secrets.MasterKeyPath = "apath" + kmsConf.Secrets.URL = "aurl" + config.SetKMSConfig(kmsConf) + assert.Equal(t, kmsConf.Secrets.MasterKeyPath, config.GetKMSConfig().Secrets.MasterKeyPath) + assert.Equal(t, kmsConf.Secrets.URL, config.GetKMSConfig().Secrets.URL) } func TestServiceToStart(t *testing.T) { @@ -313,11 +319,15 @@ func TestConfigFromEnv(t *testing.T) { os.Setenv("SFTPGO_DATA_PROVIDER__PASSWORD_HASHING__ARGON2_OPTIONS__ITERATIONS", "41") os.Setenv("SFTPGO_DATA_PROVIDER__POOL_SIZE", "10") os.Setenv("SFTPGO_DATA_PROVIDER__ACTIONS__EXECUTE_ON", "add") + os.Setenv("SFTPGO_KMS__SECRETS__URL", "local") + os.Setenv("SFTPGO_KMS__SECRETS__MASTER_KEY_PATH", "path") t.Cleanup(func() { os.Unsetenv("SFTPGO_SFTPD__BIND_ADDRESS") os.Unsetenv("SFTPGO_DATA_PROVIDER__PASSWORD_HASHING__ARGON2_OPTIONS__ITERATIONS") os.Unsetenv("SFTPGO_DATA_PROVIDER__POOL_SIZE") os.Unsetenv("SFTPGO_DATA_PROVIDER__ACTIONS__EXECUTE_ON") + os.Unsetenv("SFTPGO_KMS__SECRETS__URL") + os.Unsetenv("SFTPGO_KMS__SECRETS__MASTER_KEY_PATH") }) err := config.LoadConfig(".", "invalid config") assert.NoError(t, err) @@ -328,4 +338,7 @@ func TestConfigFromEnv(t *testing.T) { assert.Equal(t, 10, dataProviderConf.PoolSize) assert.Len(t, dataProviderConf.Actions.ExecuteOn, 1) assert.Contains(t, dataProviderConf.Actions.ExecuteOn, "add") + kmsConfig := config.GetKMSConfig() + assert.Equal(t, "local", kmsConfig.Secrets.URL) + assert.Equal(t, "path", kmsConfig.Secrets.MasterKeyPath) } diff --git a/dataprovider/bolt.go b/dataprovider/bolt.go index d6e60961..54e49594 100644 --- a/dataprovider/bolt.go +++ b/dataprovider/bolt.go @@ -787,6 +787,7 @@ func joinUserAndFolders(u []byte, foldersBucket *bolt.Bucket) (User, error) { } user.VirtualFolders = folders } + user.SetEmptySecretsIfNil() return user, err } diff --git a/dataprovider/compat.go b/dataprovider/compat.go index 7d6abd88..098656c2 100644 --- a/dataprovider/compat.go +++ b/dataprovider/compat.go @@ -6,6 +6,7 @@ import ( "io/ioutil" "path/filepath" + "github.com/drakkan/sftpgo/kms" "github.com/drakkan/sftpgo/logger" "github.com/drakkan/sftpgo/utils" "github.com/drakkan/sftpgo/vfs" @@ -129,6 +130,7 @@ func createUserFromV4(u compatUserV4, fsConfig Filesystem) User { Filters: u.Filters, } user.FsConfig = fsConfig + user.SetEmptySecretsIfNil() return user } @@ -160,12 +162,11 @@ func convertUserToV4(u User, fsConfig compatFilesystemV4) compatUserV4 { return user } -func getCGSCredentialsFromV4(config compatGCSFsConfigV4) (vfs.Secret, error) { - var secret vfs.Secret +func getCGSCredentialsFromV4(config compatGCSFsConfigV4) (*kms.Secret, error) { + secret := kms.NewEmptySecret() var err error if len(config.Credentials) > 0 { - secret.Status = vfs.SecretStatusPlain - secret.Payload = string(config.Credentials) + secret = kms.NewPlainSecret(string(config.Credentials)) return secret, nil } if config.CredentialFile != "" { @@ -173,14 +174,16 @@ func getCGSCredentialsFromV4(config compatGCSFsConfigV4) (vfs.Secret, error) { if err != nil { return secret, err } - secret.Status = vfs.SecretStatusPlain - secret.Payload = string(creds) + secret = kms.NewPlainSecret(string(creds)) return secret, nil } return secret, err } func getCGSCredentialsFromV6(config vfs.GCSFsConfig, username string) (string, error) { + if config.Credentials == nil { + config.Credentials = kms.NewEmptySecret() + } if config.Credentials.IsEmpty() { config.CredentialFile = filepath.Join(credentialsDirPath, fmt.Sprintf("%v_gcs_credentials.json", username)) @@ -199,7 +202,7 @@ func getCGSCredentialsFromV6(config vfs.GCSFsConfig, username string) (string, e return "", err } // in V4 GCS credentials were not encrypted - return config.Credentials.Payload, nil + return config.Credentials.GetPayload(), nil } return "", nil } @@ -229,7 +232,7 @@ func convertFsConfigToV4(fs Filesystem, username string) (compatFilesystemV4, er if err != nil { return fsV4, err } - secretV4, err := utils.EncryptData(fs.S3Config.AccessSecret.Payload) + secretV4, err := utils.EncryptData(fs.S3Config.AccessSecret.GetPayload()) if err != nil { return fsV4, err } @@ -253,7 +256,7 @@ func convertFsConfigToV4(fs Filesystem, username string) (compatFilesystemV4, er if err != nil { return fsV4, err } - secretV4, err := utils.EncryptData(fs.AzBlobConfig.AccountKey.Payload) + secretV4, err := utils.EncryptData(fs.AzBlobConfig.AccountKey.GetPayload()) if err != nil { return fsV4, err } @@ -292,14 +295,14 @@ func convertFsConfigFromV4(compatFs compatFilesystemV4, username string) (Filesy KeyPrefix: compatFs.S3Config.KeyPrefix, Region: compatFs.S3Config.Region, AccessKey: compatFs.S3Config.AccessKey, - AccessSecret: vfs.Secret{}, + AccessSecret: kms.NewEmptySecret(), Endpoint: compatFs.S3Config.Endpoint, StorageClass: compatFs.S3Config.StorageClass, UploadPartSize: compatFs.S3Config.UploadPartSize, UploadConcurrency: compatFs.S3Config.UploadConcurrency, } if compatFs.S3Config.AccessSecret != "" { - secret, err := vfs.GetSecretFromCompatString(compatFs.S3Config.AccessSecret) + secret, err := kms.GetSecretFromCompatString(compatFs.S3Config.AccessSecret) if err != nil { providerLog(logger.LevelError, "unable to convert v4 filesystem for user %#v: %v", username, err) return fsConfig, err @@ -310,7 +313,7 @@ func convertFsConfigFromV4(compatFs compatFilesystemV4, username string) (Filesy fsConfig.AzBlobConfig = vfs.AzBlobFsConfig{ Container: compatFs.AzBlobConfig.Container, AccountName: compatFs.AzBlobConfig.AccountName, - AccountKey: vfs.Secret{}, + AccountKey: kms.NewEmptySecret(), Endpoint: compatFs.AzBlobConfig.Endpoint, SASURL: compatFs.AzBlobConfig.SASURL, KeyPrefix: compatFs.AzBlobConfig.KeyPrefix, @@ -320,7 +323,7 @@ func convertFsConfigFromV4(compatFs compatFilesystemV4, username string) (Filesy AccessTier: compatFs.AzBlobConfig.AccessTier, } if compatFs.AzBlobConfig.AccountKey != "" { - secret, err := vfs.GetSecretFromCompatString(compatFs.AzBlobConfig.AccountKey) + secret, err := kms.GetSecretFromCompatString(compatFs.AzBlobConfig.AccountKey) if err != nil { providerLog(logger.LevelError, "unable to convert v4 filesystem for user %#v: %v", username, err) return fsConfig, err diff --git a/dataprovider/dataprovider.go b/dataprovider/dataprovider.go index 256cf65c..15d97d20 100644 --- a/dataprovider/dataprovider.go +++ b/dataprovider/dataprovider.go @@ -42,6 +42,7 @@ import ( "golang.org/x/crypto/ssh" "github.com/drakkan/sftpgo/httpclient" + "github.com/drakkan/sftpgo/kms" "github.com/drakkan/sftpgo/logger" "github.com/drakkan/sftpgo/metrics" "github.com/drakkan/sftpgo/utils" @@ -124,6 +125,7 @@ var ( sqlTableFoldersMapping = "folders_mapping" sqlTableSchemaVersion = "schema_version" argon2Params *argon2id.Params + lastLoginMinDelay = 10 * time.Minute ) type schemaVersion struct { @@ -577,7 +579,12 @@ func UpdateLastLogin(user User) error { if config.ManageUsers == 0 { return &MethodDisabledError{err: manageUsersDisabledError} } - return provider.updateLastLogin(user.Username) + lastLogin := utils.GetTimeFromMsecSinceEpoch(user.LastLogin) + diff := -time.Until(lastLogin) + if diff < 0 || diff > lastLoginMinDelay { + return provider.updateLastLogin(user.Username) + } + return nil } // UpdateUserQuota updates the quota for the given SFTP user adding filesAdd and sizeAdd. @@ -1099,12 +1106,12 @@ func saveGCSCredentials(user *User) error { if user.FsConfig.Provider != GCSFilesystemProvider { return nil } - if user.FsConfig.GCSConfig.Credentials.Payload == "" { + if user.FsConfig.GCSConfig.Credentials.GetPayload() == "" { return nil } if config.PreferDatabaseCredentials { if user.FsConfig.GCSConfig.Credentials.IsPlain() { - user.FsConfig.GCSConfig.Credentials.AdditionalData = user.Username + user.FsConfig.GCSConfig.Credentials.SetAdditionalData(user.Username) err := user.FsConfig.GCSConfig.Credentials.Encrypt() if err != nil { return err @@ -1113,7 +1120,7 @@ func saveGCSCredentials(user *User) error { return nil } if user.FsConfig.GCSConfig.Credentials.IsPlain() { - user.FsConfig.GCSConfig.Credentials.AdditionalData = user.Username + user.FsConfig.GCSConfig.Credentials.SetAdditionalData(user.Username) err := user.FsConfig.GCSConfig.Credentials.Encrypt() if err != nil { return &ValidationError{err: fmt.Sprintf("could not encrypt GCS credentials: %v", err)} @@ -1132,7 +1139,7 @@ func saveGCSCredentials(user *User) error { if err != nil { return &ValidationError{err: fmt.Sprintf("could not save GCS credentials: %v", err)} } - user.FsConfig.GCSConfig.Credentials = vfs.Secret{} + user.FsConfig.GCSConfig.Credentials = kms.NewEmptySecret() return nil } @@ -1143,7 +1150,7 @@ func validateFilesystemConfig(user *User) error { return &ValidationError{err: fmt.Sprintf("could not validate s3config: %v", err)} } if user.FsConfig.S3Config.AccessSecret.IsPlain() { - user.FsConfig.S3Config.AccessSecret.AdditionalData = user.Username + user.FsConfig.S3Config.AccessSecret.SetAdditionalData(user.Username) err = user.FsConfig.S3Config.AccessSecret.Encrypt() if err != nil { return &ValidationError{err: fmt.Sprintf("could not encrypt s3 access secret: %v", err)} @@ -1166,7 +1173,7 @@ func validateFilesystemConfig(user *User) error { return &ValidationError{err: fmt.Sprintf("could not validate Azure Blob config: %v", err)} } if user.FsConfig.AzBlobConfig.AccountKey.IsPlain() { - user.FsConfig.AzBlobConfig.AccountKey.AdditionalData = user.Username + user.FsConfig.AzBlobConfig.AccountKey.SetAdditionalData(user.Username) err = user.FsConfig.AzBlobConfig.AccountKey.Encrypt() if err != nil { return &ValidationError{err: fmt.Sprintf("could not encrypt Azure blob account key: %v", err)} @@ -1220,6 +1227,7 @@ func validateFolder(folder *vfs.BaseVirtualFolder) error { } func validateUser(user *User) error { + user.SetEmptySecretsIfNil() buildUserHomeDir(user) if err := validateBaseParams(user); err != nil { return err @@ -2131,7 +2139,7 @@ func CacheWebDAVUser(cachedUser *CachedUser, maxSize int) { } } - if len(cachedUser.User.Username) > 0 { + if cachedUser.User.Username != "" { webDAVUsersCache.Store(cachedUser.User.Username, cachedUser) } } @@ -2143,7 +2151,7 @@ func GetCachedWebDAVUser(username string) (interface{}, bool) { // RemoveCachedWebDAVUser removes a cached WebDAV user func RemoveCachedWebDAVUser(username string) { - if len(username) > 0 { + if username != "" { webDAVUsersCache.Delete(username) } } diff --git a/dataprovider/memory.go b/dataprovider/memory.go index a54681a0..7a3e1975 100644 --- a/dataprovider/memory.go +++ b/dataprovider/memory.go @@ -264,7 +264,8 @@ func (p MemoryProvider) dumpUsers() ([]User, error) { return users, errMemoryProviderClosed } for _, username := range p.dbHandle.usernames { - user := p.dbHandle.users[username] + u := p.dbHandle.users[username] + user := u.getACopy() err = addCredentialsToUser(&user) if err != nil { return users, err @@ -315,7 +316,8 @@ func (p MemoryProvider) getUsers(limit int, offset int, order string, username s if itNum <= offset { continue } - user := p.dbHandle.users[username] + u := p.dbHandle.users[username] + user := u.getACopy() user.HideConfidentialData() users = append(users, user) if len(users) >= limit { @@ -329,7 +331,8 @@ func (p MemoryProvider) getUsers(limit int, offset int, order string, username s continue } username := p.dbHandle.usernames[i] - user := p.dbHandle.users[username] + u := p.dbHandle.users[username] + user := u.getACopy() user.HideConfidentialData() users = append(users, user) if len(users) >= limit { diff --git a/dataprovider/mysql.go b/dataprovider/mysql.go index 0a919f32..699a087d 100644 --- a/dataprovider/mysql.go +++ b/dataprovider/mysql.go @@ -59,7 +59,12 @@ func initializeMySQLProvider() error { providerLog(logger.LevelDebug, "mysql database handle created, connection string: %#v, pool size: %v", getMySQLConnectionString(true), config.PoolSize) dbHandle.SetMaxOpenConns(config.PoolSize) - dbHandle.SetConnMaxLifetime(1800 * time.Second) + if config.PoolSize > 0 { + dbHandle.SetMaxIdleConns(config.PoolSize) + } else { + dbHandle.SetMaxIdleConns(2) + } + dbHandle.SetConnMaxLifetime(240 * time.Second) provider = MySQLProvider{dbHandle: dbHandle} } else { providerLog(logger.LevelWarn, "error creating mysql database handler, connection string: %#v, error: %v", diff --git a/dataprovider/pgsql.go b/dataprovider/pgsql.go index 6b39110c..2bb46d7d 100644 --- a/dataprovider/pgsql.go +++ b/dataprovider/pgsql.go @@ -7,6 +7,7 @@ import ( "database/sql" "fmt" "strings" + "time" // we import lib/pq here to be able to disable PostgreSQL support using a build tag _ "github.com/lib/pq" @@ -58,6 +59,12 @@ func initializePGSQLProvider() error { providerLog(logger.LevelDebug, "postgres database handle created, connection string: %#v, pool size: %v", getPGSQLConnectionString(true), config.PoolSize) dbHandle.SetMaxOpenConns(config.PoolSize) + if config.PoolSize > 0 { + dbHandle.SetMaxIdleConns(config.PoolSize) + } else { + dbHandle.SetMaxIdleConns(2) + } + dbHandle.SetConnMaxLifetime(240 * time.Second) provider = PGSQLProvider{dbHandle: dbHandle} } else { providerLog(logger.LevelWarn, "error creating postgres database handler, connection string: %#v, error: %v", diff --git a/dataprovider/sqlcommon.go b/dataprovider/sqlcommon.go index 5c4d1909..b390403e 100644 --- a/dataprovider/sqlcommon.go +++ b/dataprovider/sqlcommon.go @@ -446,6 +446,7 @@ func getUserFromDbRow(row *sql.Row, rows *sql.Rows) (User, error) { if additionalInfo.Valid { user.AdditionalInfo = additionalInfo.String } + user.SetEmptySecretsIfNil() return user, err } diff --git a/dataprovider/user.go b/dataprovider/user.go index a9cfbce7..a6327829 100644 --- a/dataprovider/user.go +++ b/dataprovider/user.go @@ -14,6 +14,7 @@ import ( "golang.org/x/net/webdav" + "github.com/drakkan/sftpgo/kms" "github.com/drakkan/sftpgo/logger" "github.com/drakkan/sftpgo/utils" "github.com/drakkan/sftpgo/vfs" @@ -768,7 +769,21 @@ func (u User) GetDeniedIPAsString() string { return result } +// SetEmptySecretsIfNil sets the secrets to empty if nil +func (u *User) SetEmptySecretsIfNil() { + if u.FsConfig.S3Config.AccessSecret == nil { + u.FsConfig.S3Config.AccessSecret = kms.NewEmptySecret() + } + if u.FsConfig.GCSConfig.Credentials == nil { + u.FsConfig.GCSConfig.Credentials = kms.NewEmptySecret() + } + if u.FsConfig.AzBlobConfig.AccountKey == nil { + u.FsConfig.AzBlobConfig.AccountKey = kms.NewEmptySecret() + } +} + func (u *User) getACopy() User { + u.SetEmptySecretsIfNil() pubKeys := make([]string, len(u.PublicKeys)) copy(pubKeys, u.PublicKeys) virtualFolders := make([]vfs.VirtualFolder, len(u.VirtualFolders)) @@ -799,7 +814,7 @@ func (u *User) getACopy() User { Bucket: u.FsConfig.S3Config.Bucket, Region: u.FsConfig.S3Config.Region, AccessKey: u.FsConfig.S3Config.AccessKey, - AccessSecret: u.FsConfig.S3Config.AccessSecret, + AccessSecret: u.FsConfig.S3Config.AccessSecret.Clone(), Endpoint: u.FsConfig.S3Config.Endpoint, StorageClass: u.FsConfig.S3Config.StorageClass, KeyPrefix: u.FsConfig.S3Config.KeyPrefix, @@ -809,7 +824,7 @@ func (u *User) getACopy() User { GCSConfig: vfs.GCSFsConfig{ Bucket: u.FsConfig.GCSConfig.Bucket, CredentialFile: u.FsConfig.GCSConfig.CredentialFile, - Credentials: u.FsConfig.GCSConfig.Credentials, + Credentials: u.FsConfig.GCSConfig.Credentials.Clone(), AutomaticCredentials: u.FsConfig.GCSConfig.AutomaticCredentials, StorageClass: u.FsConfig.GCSConfig.StorageClass, KeyPrefix: u.FsConfig.GCSConfig.KeyPrefix, @@ -817,7 +832,7 @@ func (u *User) getACopy() User { AzBlobConfig: vfs.AzBlobFsConfig{ Container: u.FsConfig.AzBlobConfig.Container, AccountName: u.FsConfig.AzBlobConfig.AccountName, - AccountKey: u.FsConfig.AzBlobConfig.AccountKey, + AccountKey: u.FsConfig.AzBlobConfig.AccountKey.Clone(), Endpoint: u.FsConfig.AzBlobConfig.Endpoint, SASURL: u.FsConfig.AzBlobConfig.SASURL, KeyPrefix: u.FsConfig.AzBlobConfig.KeyPrefix, diff --git a/docs/account.md b/docs/account.md index be31e3cb..cb555b66 100644 --- a/docs/account.md +++ b/docs/account.md @@ -28,6 +28,7 @@ For each account, the following properties can be configured: - `chtimes` changing file or directory access and modification time is allowed - `upload_bandwidth` maximum upload bandwidth as KB/s, 0 means unlimited. - `download_bandwidth` maximum download bandwidth as KB/s, 0 means unlimited. +- `last_login` last user login as unix timestamp in milliseconds. It is saved at most once every 10 minutes - `allowed_ip`, List of IP/Mask allowed to login. Any IP address not contained in this list cannot login. IP/Mask must be in CIDR notation as defined in RFC 4632 and RFC 4291, for example "192.0.2.0/24" or "2001:db8::/32" - `denied_ip`, List of IP/Mask not allowed to login. If an IP address is both allowed and denied then login will be denied - `max_upload_file_size`, max allowed size, as bytes, for a single file upload. The upload will be aborted if/when the size of the file being sent exceeds this limit. 0 means unlimited. This restriction does not apply for SSH system commands such as `git` and `rsync` @@ -53,20 +54,20 @@ For each account, the following properties can be configured: - `s3_bucket`, required for S3 filesystem - `s3_region`, required for S3 filesystem. Must match the region for your bucket. You can find here the list of available [AWS regions](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/using-regions-availability-zones.html#concepts-available-regions). For example if your bucket is at `Frankfurt` you have to set the region to `eu-central-1` - `s3_access_key` -- `s3_access_secret`, if provided it is stored encrypted (AES-256-GCM). You can leave access key and access secret blank to use credentials from environment +- `s3_access_secret`, if provided it is stored encrypted based on kms configuration. You can leave access key and access secret blank to use credentials from environment - `s3_endpoint`, specifies a S3 endpoint (server) different from AWS. It is not required if you are connecting to AWS - `s3_storage_class`, leave blank to use the default or specify a valid AWS [storage class](https://docs.aws.amazon.com/AmazonS3/latest/dev/storage-class-intro.html) - `s3_key_prefix`, allows to restrict access to the folder identified by this prefix and its contents - `s3_upload_part_size`, the buffer size for multipart uploads (MB). Zero means the default (5 MB). Minimum is 5 - `s3_upload_concurrency` how many parts are uploaded in parallel - `gcs_bucket`, required for GCS filesystem -- `gcs_credentials`, Google Cloud Storage JSON credentials base64 encoded +- `gcs_credentials`, Google Cloud Storage JSON credentials base64 encoded. Credentials are stored encrypted based on kms configuration - `gcs_automatic_credentials`, integer. Set to 1 to use Application Default Credentials strategy or set to 0 to use explicit credentials via `gcs_credentials` - `gcs_storage_class` - `gcs_key_prefix`, allows to restrict access to the folder identified by this prefix and its contents - `az_container`, Azure Blob Storage container - `az_account_name`, Azure account name. leave blank to use SAS URL -- `az_account_key`, Azure account key. leave blank to use SAS URL. If provided it is stored encrypted (AES-256-GCM) +- `az_account_key`, Azure account key. leave blank to use SAS URL. If provided it is stored encrypted based on kms configuration - `az_sas_url`, Azure shared access signature URL - `az_endpoint`, Default is "blob.core.windows.net". If you use the emulator the endpoint must include the protocol, for example "http://127.0.0.1:10000" - `az_upload_part_size`, the buffer size for multipart uploads (MB). Zero means the default (4 MB) diff --git a/docs/full-configuration.md b/docs/full-configuration.md index 9bf176e9..c623a7c0 100644 --- a/docs/full-configuration.md +++ b/docs/full-configuration.md @@ -163,6 +163,10 @@ The configuration file contains the following sections: - `timeout`, integer. Timeout specifies a time limit, in seconds, for requests. - `ca_certificates`, list of strings. List of paths to extra CA certificates to trust. The paths can be absolute or relative to the config dir. Adding trusted CA certificates is a convenient way to use self-signed certificates without defeating the purpose of using TLS. - `skip_tls_verify`, boolean. if enabled the HTTP client accepts any TLS certificate presented by the server and any host name in that certificate. In this mode, TLS is susceptible to man-in-the-middle attacks. This should be used only for testing. +- **kms**, configuration for the Key Management Service, more details can be found [here](./kms.md) + - `secrets` + - `url` + - `master_key_path` A full example showing the default config (in JSON format) can be found [here](../sftpgo.json). diff --git a/docs/kms.md b/docs/kms.md new file mode 100644 index 00000000..85d985c1 --- /dev/null +++ b/docs/kms.md @@ -0,0 +1,65 @@ +# Key Management Services + +SFTPGo stores sensitive data such as Cloud accounts credentials. This data are stored as ciphertext and only loaded to RAM in plaintext when needed. + +## Supported Services for encryption and decryption + +The `secrets` section of the `kms` configuration allows to configure how to encrypt and decrypt sensitive data. The following configuration parameters are available: + +- `url` defines the URI to the KMS service +- `master_key_path` defines the absolute path to a file containing the master encryption key. This could be, for example, a docker secrets or a file protected with filesystem level permissions. + +We use [Go CDK](https://gocloud.dev/howto/secrets/) to access several key management services in a portable way. + +### Local provider + +If the `url` is empty SFTPGo uses local encryption for keeping secrets. Internally, it uses the [NaCl secret box](https://godoc.org/golang.org/x/crypto/nacl/secretbox) algorithm to perform encryption and authentication. + +We first generate a random key, then the per-object encryption key is derived from this random key in the following way: + +1. a master key is provided: the encryption key is derived using the HMAC-based Extract-and-Expand Key Derivation Function (HKDF) as defined in [RFC 5869](http://tools.ietf.org/html/rfc5869) +2. no master key is provided: the encryption key is derived as simple hash of the random key. This is the default configuration. + +For compatibility with SFTPGo versions 1.2.x and before we also support encryption based on `AES-256-GCM`. The data encrypted with this algorithm will never use the master key to keep backward compatibility. + +### Google Cloud Key Management Service + +To use keys from Google Cloud Platform’s [Key Management Service](https://cloud.google.com/kms/) (GCP KMS) you have to use `gcpkms` as URL scheme like this: + +```shell +gcpkms://projects/[PROJECT_ID]/locations/[LOCATION]/keyRings/[KEY_RING]/cryptoKeys/[KEY] +``` + +SFTPGo will use Application Default Credentials. See [here](https://cloud.google.com/docs/authentication/production) for alternatives such as environment variables. + +The URL host+path are used as the key resource ID; see [here](https://cloud.google.com/kms/docs/object-hierarchy#key) for more details. + +If a master key is provided we first encrypt the plaintext data using the local provider and then we encrypt the resulting payload using the Cloud provider and store this ciphertext. + +### AWS Key Management Service + +To use customer master keys from Amazon Web Service’s [Key Management Service](https://aws.amazon.com/kms/) (AWS KMS) you have to use `awskms` as URL scheme. You can use the key’s ID, alias, or Amazon Resource Name (ARN) to identify the key. You should specify the region query parameter to ensure your application connects to the correct region. + +Here are some examples: + +- By ID: `awskms://1234abcd-12ab-34cd-56ef-1234567890ab?region=us-east-1` +- By alias: `awskms://alias/ExampleAlias?region=us-east-1` +- By ARN: `arn:aws:kms:us-east-1:111122223333:key/1234abcd-12ab-34bc-56ef-1234567890ab?region=us-east-1` + +SFTPGo will use the default AWS session. See [AWS Session](https://docs.aws.amazon.com/sdk-for-go/api/aws/session/) to learn about authentication alternatives such as environment variables. + +If a master key is provided we first encrypt the plaintext data using the local provider and then we encrypt the resulting payload using the Cloud provider and store this ciphertext. + +### HashiCorp Vault + +To use the [transit secrets engine](https://www.vaultproject.io/docs/secrets/transit/index.html) in [Vault](https://www.vaultproject.io/) you have to use `hashivault` as URL scheme like this: `hashivault://mykey`. + +The Vault server endpoint and authentication token are specified using the environment variables `VAULT_SERVER_URL` and `VAULT_SERVER_TOKEN`, respectively. + +If a master key is provided we first encrypt the plaintext data using the local provider and then we encrypt the resulting payload using Vault and store this ciphertext. + +### Notes + +- The KMS configuration is global. +- If you set a master key you will be unable to decrypt the data without this key and the SFTPGo users that need the data as plain text will be unable to login. +- You can start using the local provider and then switch to an external one but you can't switch between external providers and still be able to decrypt the data encrypted using the previous provider. diff --git a/examples/OTP/authy/README.md b/examples/OTP/authy/README.md index aff1f8a6..31984ae0 100644 --- a/examples/OTP/authy/README.md +++ b/examples/OTP/authy/README.md @@ -29,7 +29,7 @@ The response is something like this: {"message":"User created successfully.","user":{"id":xxxxxxxx},"success":true} ``` -Save the user id somewhere and add a reference to the matching SFTPGo account. +Save the user id somewhere and add a reference to the matching SFTPGo account. You could also store this ID in the `additional_info` SFTPGo user field. After this step you can use the Authy app installed on your phone to generate TOTP codes. diff --git a/examples/rest-api-cli/README.md b/examples/rest-api-cli/README.md index 933a1d42..1ceadb92 100644 --- a/examples/rest-api-cli/README.md +++ b/examples/rest-api-cli/README.md @@ -60,8 +60,8 @@ Output: "s3config": { "access_key": "accesskey", "access_secret": { - "payload": "ac46cec75466ba77e47f536436783b729ca5bbbb53252fda0de51f785a6da11ffb03", - "status": "AES-256-GCM" + "payload": "dcd07e64a5ef5ede37b978198ca396ea9aee92453208ee2fee6f25407e47bf2119ba8edf2e81f91999bd5386c1a7", + "status": "Secretbox" }, "bucket": "test", "endpoint": "http://127.0.0.1:9000", @@ -178,9 +178,16 @@ Output: "download_bandwidth": 80, "expiration_date": 0, "filesystem": { - "gcsconfig": {}, + "azblobconfig": { + "account_key": {} + }, + "gcsconfig": { + "credentials": {} + }, "provider": 0, - "s3config": {} + "s3config": { + "access_secret": {} + } }, "filters": { "denied_ip": [ @@ -253,9 +260,16 @@ Output: "download_bandwidth": 80, "expiration_date": 0, "filesystem": { - "gcsconfig": {}, + "azblobconfig": { + "account_key": {} + }, + "gcsconfig": { + "credentials": {} + }, "provider": 0, - "s3config": {} + "s3config": { + "access_secret": {} + } }, "filters": { "denied_ip": [ diff --git a/ftpd/ftpd_test.go b/ftpd/ftpd_test.go index 9f88f7e0..3800e902 100644 --- a/ftpd/ftpd_test.go +++ b/ftpd/ftpd_test.go @@ -26,6 +26,7 @@ import ( "github.com/drakkan/sftpgo/dataprovider" "github.com/drakkan/sftpgo/ftpd" "github.com/drakkan/sftpgo/httpd" + "github.com/drakkan/sftpgo/kms" "github.com/drakkan/sftpgo/logger" "github.com/drakkan/sftpgo/vfs" ) @@ -131,6 +132,13 @@ func TestMain(m *testing.M) { httpConfig := config.GetHTTPConfig() httpConfig.Initialize(configDir) + kmsConfig := config.GetKMSConfig() + err = kmsConfig.Initialize() + if err != nil { + logger.ErrorToConsole("error initializing kms: %v", err) + os.Exit(1) + } + httpdConf := config.GetHTTPDConfig() httpdConf.BindPort = 8079 httpd.SetBaseURLAndCredentials("http://127.0.0.1:8079", "", "") @@ -876,10 +884,7 @@ func TestLoginWithDatabaseCredentials(t *testing.T) { u := getTestUser() u.FsConfig.Provider = dataprovider.GCSFilesystemProvider u.FsConfig.GCSConfig.Bucket = "test" - u.FsConfig.GCSConfig.Credentials = vfs.Secret{ - Status: vfs.SecretStatusPlain, - Payload: `{ "type": "service_account" }`, - } + u.FsConfig.GCSConfig.Credentials = kms.NewPlainSecret(`{ "type": "service_account" }`) providerConf := config.GetProviderConf() providerConf.PreferDatabaseCredentials = true @@ -900,10 +905,10 @@ func TestLoginWithDatabaseCredentials(t *testing.T) { user, _, err := httpd.AddUser(u, http.StatusOK) assert.NoError(t, err) - assert.Equal(t, vfs.SecretStatusAES256GCM, user.FsConfig.GCSConfig.Credentials.Status) - assert.NotEmpty(t, user.FsConfig.GCSConfig.Credentials.Payload) - assert.Empty(t, user.FsConfig.GCSConfig.Credentials.AdditionalData) - assert.Empty(t, user.FsConfig.GCSConfig.Credentials.Key) + assert.Equal(t, kms.SecretStatusSecretBox, user.FsConfig.GCSConfig.Credentials.GetStatus()) + assert.NotEmpty(t, user.FsConfig.GCSConfig.Credentials.GetPayload()) + assert.Empty(t, user.FsConfig.GCSConfig.Credentials.GetAdditionalData()) + assert.Empty(t, user.FsConfig.GCSConfig.Credentials.GetKey()) assert.NoFileExists(t, credentialsFile) @@ -928,10 +933,7 @@ func TestLoginInvalidFs(t *testing.T) { u := getTestUser() u.FsConfig.Provider = dataprovider.GCSFilesystemProvider u.FsConfig.GCSConfig.Bucket = "test" - u.FsConfig.GCSConfig.Credentials = vfs.Secret{ - Status: vfs.SecretStatusPlain, - Payload: "invalid JSON for credentials", - } + u.FsConfig.GCSConfig.Credentials = kms.NewPlainSecret("invalid JSON for credentials") user, _, err := httpd.AddUser(u, http.StatusOK) assert.NoError(t, err) diff --git a/go.mod b/go.mod index b71d7070..7c570f2e 100644 --- a/go.mod +++ b/go.mod @@ -23,6 +23,7 @@ require ( github.com/magiconair/properties v1.8.4 // indirect github.com/mattn/go-sqlite3 v1.14.5 github.com/miekg/dns v1.1.35 // indirect + github.com/minio/sha256-simd v0.1.1 github.com/mitchellh/mapstructure v1.3.3 // indirect github.com/otiai10/copy v1.2.0 github.com/pelletier/go-toml v1.8.1 // indirect @@ -43,6 +44,8 @@ require ( github.com/studio-b12/gowebdav v0.0.0-20200929080739-bdacfab94796 go.etcd.io/bbolt v1.3.5 go.uber.org/automaxprocs v1.3.0 + gocloud.dev v0.20.0 + gocloud.dev/secrets/hashivault v0.20.0 golang.org/x/crypto v0.0.0-20201117144127-c1f2f97bffc9 golang.org/x/net v0.0.0-20201110031124-69a78807bb2b golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58 // indirect diff --git a/go.sum b/go.sum index ca56761f..8d55031f 100644 --- a/go.sum +++ b/go.sum @@ -1,16 +1,21 @@ +bazil.org/fuse v0.0.0-20180421153158-65cc252bf669/go.mod h1:Xbm+BRKSBEpa4q4hTSxohYNQpsxXPbPry4JJWOB3LB8= cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= +cloud.google.com/go v0.39.0/go.mod h1:rVLT6fkc8chs9sfPtFc1SBH6em7n+ZoXaG+87tDISts= cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= +cloud.google.com/go v0.44.3/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= +cloud.google.com/go v0.55.0/go.mod h1:ZHmoY+/lIMNkN2+fBmuTiqZ4inFhvQad8ft7MT8IV5Y= cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= +cloud.google.com/go v0.58.0/go.mod h1:W+9FnSUw6nhVwXlFcp1eL+krq5+HQUJeUogSeJZZiWg= cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= cloud.google.com/go v0.66.0/go.mod h1:dgqGAjKCDxyhGTtC9dAREQGUJpkceNm1yt590Qno0Ko= @@ -25,6 +30,7 @@ cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM7 cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk= +cloud.google.com/go/firestore v1.2.0/go.mod h1:iISCjWnTpnoJT1R287xRdjvQHJrxQOpeah4phb5D3h0= cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= @@ -33,23 +39,61 @@ cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiy cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= +cloud.google.com/go/storage v1.9.0/go.mod h1:m+/etGaqZbylxaNT876QGXqEHp4PR2Rq5GMqICWb9bU= cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= cloud.google.com/go/storage v1.12.0 h1:4y3gHptW1EHVtcPAVE0eBBlFuGqEejTTG3KdIE0lUX4= cloud.google.com/go/storage v1.12.0/go.mod h1:fFLk2dp2oAhDz8QFKwqrjdJvxSp/W2g7nillojlL5Ho= +contrib.go.opencensus.io/exporter/aws v0.0.0-20181029163544-2befc13012d0/go.mod h1:uu1P0UCM/6RbsMrgPa98ll8ZcHM858i/AD06a9aLRCA= +contrib.go.opencensus.io/exporter/stackdriver v0.12.1/go.mod h1:iwB6wGarfphGGe/e5CWqyUk/cLzKnWsOKPVW3no6OTw= +contrib.go.opencensus.io/integrations/ocsql v0.1.4/go.mod h1:8DsSdjz3F+APR+0z0WkU1aRorQCFfRxvqjUUPMbF3fE= +contrib.go.opencensus.io/resource v0.1.1/go.mod h1:F361eGI91LCmW1I/Saf+rX0+OFcigGlFvXwEGEnkRLA= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= +github.com/Azure/azure-amqp-common-go/v3 v3.0.0/go.mod h1:SY08giD/XbhTz07tJdpw1SoxQXHPN30+DI3Z04SYqyg= +github.com/Azure/azure-pipeline-go v0.2.1/go.mod h1:UGSo8XybXnIGZ3epmeBw7Jdz+HiUVpqIlpz/HKHylF4= +github.com/Azure/azure-pipeline-go v0.2.2/go.mod h1:4rQ/NZncSvGqNkkOsNpOU1tgoNuIlp9AfUH5G1tvCHc= github.com/Azure/azure-pipeline-go v0.2.3 h1:7U9HBg1JFK3jHl5qmo4CTZKFTVgMwdFHMVtCdfBE21U= github.com/Azure/azure-pipeline-go v0.2.3/go.mod h1:x841ezTBIMG6O3lAcl8ATHnsOPVl2bqk7S3ta6S6u4k= +github.com/Azure/azure-sdk-for-go v37.1.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc= +github.com/Azure/azure-service-bus-go v0.10.1/go.mod h1:E/FOceuKAFUfpbIJDKWz/May6guE+eGibfGT6q+n1to= +github.com/Azure/azure-storage-blob-go v0.9.0/go.mod h1:8UBPbiOhrMQ4pLPi3gA1tXnpjrS76UYE/fo5A40vf4g= github.com/Azure/azure-storage-blob-go v0.11.0 h1:WCTHKKNkHlzm7lzUNXRSD11784LwJqdrxnwWJxsJQHg= github.com/Azure/azure-storage-blob-go v0.11.0/go.mod h1:A0u4VjtpgZJ7Y7um/+ix2DHBuEKFC6sEIlj0xc13a4Q= +github.com/Azure/go-amqp v0.12.6/go.mod h1:qApuH6OFTSKZFmCOxccvAv5rLizBQf4v8pRmG138DPo= +github.com/Azure/go-amqp v0.12.7/go.mod h1:qApuH6OFTSKZFmCOxccvAv5rLizBQf4v8pRmG138DPo= +github.com/Azure/go-autorest v14.2.0+incompatible h1:V5VMDjClD3GiElqLWO7mz2MxNAK/vTfRHdAubSIPRgs= github.com/Azure/go-autorest v14.2.0+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24= +github.com/Azure/go-autorest/autorest v0.9.0/go.mod h1:xyHB1BMZT0cuDHU7I0+g046+BFDTQ8rEZB0s4Yfa6bI= +github.com/Azure/go-autorest/autorest v0.9.3 h1:OZEIaBbMdUE/Js+BQKlpO81XlISgipr6yDJ+PSwsgi4= +github.com/Azure/go-autorest/autorest v0.9.3/go.mod h1:GsRuLYvwzLjjjRoWEIyMUaYq8GNUx2nRB378IPt/1p0= +github.com/Azure/go-autorest/autorest/adal v0.5.0/go.mod h1:8Z9fGy2MpX0PvDjB1pEgQTmVqjGhiHBW7RJJEciWzS0= +github.com/Azure/go-autorest/autorest/adal v0.8.0/go.mod h1:Z6vX6WXXuyieHAXwMj0S6HY6e6wcHn37qQMBQlvY3lc= +github.com/Azure/go-autorest/autorest/adal v0.8.1/go.mod h1:ZjhuQClTqx435SRJ2iMlOxPYt3d2C/T/7TiQCVZSn3Q= +github.com/Azure/go-autorest/autorest/adal v0.8.3/go.mod h1:ZjhuQClTqx435SRJ2iMlOxPYt3d2C/T/7TiQCVZSn3Q= +github.com/Azure/go-autorest/autorest/adal v0.9.2 h1:Aze/GQeAN1RRbGmnUJvUj+tFGBzFdIg3293/A9rbxC4= github.com/Azure/go-autorest/autorest/adal v0.9.2/go.mod h1:/3SMAM86bP6wC9Ev35peQDUeqFZBMH07vvUOmg4z/fE= +github.com/Azure/go-autorest/autorest/azure/auth v0.4.2/go.mod h1:90gmfKdlmKgfjUpnCEpOJzsUEjrWDSLwHIG73tSXddM= +github.com/Azure/go-autorest/autorest/azure/cli v0.3.1/go.mod h1:ZG5p860J94/0kI9mNJVoIoLgXcirM2gF5i2kWloofxw= +github.com/Azure/go-autorest/autorest/date v0.1.0/go.mod h1:plvfp3oPSKwf2DNjlBjWF/7vwR+cUD/ELuzDCXwHUVA= +github.com/Azure/go-autorest/autorest/date v0.2.0/go.mod h1:vcORJHLJEh643/Ioh9+vPmf1Ij9AEBM5FuBIXLmIy0g= +github.com/Azure/go-autorest/autorest/date v0.3.0 h1:7gUk1U5M/CQbp9WoqinNzJar+8KY+LPI6wiWrP/myHw= github.com/Azure/go-autorest/autorest/date v0.3.0/go.mod h1:BI0uouVdmngYNUzGWeSYnokU+TrmwEsOqdt8Y6sso74= +github.com/Azure/go-autorest/autorest/mocks v0.1.0/go.mod h1:OTyCOPRA2IgIlWxVYxBee2F5Gr4kF2zd2J5cFRaIDN0= +github.com/Azure/go-autorest/autorest/mocks v0.2.0/go.mod h1:OTyCOPRA2IgIlWxVYxBee2F5Gr4kF2zd2J5cFRaIDN0= +github.com/Azure/go-autorest/autorest/mocks v0.3.0/go.mod h1:a8FDP3DYzQ4RYfVAxAN3SVSiiO77gL2j2ronKKP0syM= +github.com/Azure/go-autorest/autorest/mocks v0.4.1 h1:K0laFcLE6VLTOwNgSxaGbUcLPuGXlNkbVvq4cW4nIHk= github.com/Azure/go-autorest/autorest/mocks v0.4.1/go.mod h1:LTp+uSrOhSkaKrUy935gNZuuIPPVsHlr9DSOxSayd+k= +github.com/Azure/go-autorest/autorest/to v0.3.0/go.mod h1:MgwOyqaIuKdG4TL/2ywSsIWKAfJfgHDo8ObuUk3t5sA= +github.com/Azure/go-autorest/autorest/validation v0.2.0/go.mod h1:3EEqHnBxQGHXRYq3HT1WyXAvT7LLY3tl70hw6tQIbjI= +github.com/Azure/go-autorest/logger v0.1.0/go.mod h1:oExouG+K6PryycPJfVSxi/koC6LSNgds39diKLz7Vrc= +github.com/Azure/go-autorest/tracing v0.5.0/go.mod h1:r/s2XiOKccPW3HrqB+W0TQzfbtp2fGCgRFtBroKn4Dk= +github.com/Azure/go-autorest/tracing v0.6.0 h1:TYi4+3m5t6K48TGI9AUdb+IzbnSxvnvUMfuitfgcfuo= github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBpUA79WCAKPPZVC2DeU= +github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/GehirnInc/crypt v0.0.0-20200316065508-bb7000b8a962 h1:KeNholpO2xKjgaaSyd+DyQRrsQjhbSeS7qe4nEw8aQw= github.com/GehirnInc/crypt v0.0.0-20200316065508-bb7000b8a962/go.mod h1:kC29dT1vFpj7py2OvG1khBdQpo3kInWP+6QipLbdngo= +github.com/GoogleCloudPlatform/cloudsql-proxy v0.0.0-20191009163259-e802c2cb94ae/go.mod h1:mjwGPas4yKduTyubHvD1Atl9r1rUq8DfVy+gkVvZ+oo= github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0= github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo= @@ -70,7 +114,10 @@ github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmV github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= github.com/aryann/difflib v0.0.0-20170710044230-e206f873d14a/go.mod h1:DAHtR1m6lCRdSC2Tm3DSWRPvIPr6xNKyeHdqDQSQT+A= github.com/aws/aws-lambda-go v1.13.3/go.mod h1:4UKl9IzQMoD+QF79YdCuzCwp8VbmG4VAQwij/eHl5CU= +github.com/aws/aws-sdk-go v1.15.27/go.mod h1:mFuSZ37Z9YOHbQEwBWztmVzqXrEkub65tZoCYDt7FT0= +github.com/aws/aws-sdk-go v1.19.18/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= github.com/aws/aws-sdk-go v1.27.0/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= +github.com/aws/aws-sdk-go v1.31.13/go.mod h1:5zCpMtNQVjRREroY7sYe8lOMRSxkhG6MZveU8YkpAk0= github.com/aws/aws-sdk-go v1.35.30 h1:ZT+70Tw1ar5U2bL81ZyIvcLorxlD1UoxoIgjsEkismY= github.com/aws/aws-sdk-go v1.35.30/go.mod h1:tlPOdRjfxPBpNIwqDj61rmsnA85v9jc0Ps9+muhnW+k= github.com/aws/aws-sdk-go-v2 v0.18.0/go.mod h1:JWVYvqSMppoMJC0x5wdwiImzgXTI9FuZwxzkQq9wy+g= @@ -83,6 +130,7 @@ github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJm github.com/casbin/casbin/v2 v2.1.2/go.mod h1:YcPU1XXisHhLzuxH9coDNf2FbKpjGlbCg3n9yuLkIJQ= github.com/cenkalti/backoff v2.2.1+incompatible h1:tNowT99t7UNflLxfYYSlKYsBpXdEet03Pg2g16Swow4= github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM= +github.com/census-instrumentation/opencensus-proto v0.2.0/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko= github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= @@ -113,8 +161,11 @@ github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7Do github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/devigned/tab v0.1.1/go.mod h1:XG9mPq0dFghrYvoBF3xdRrJzSTX1b7IQrvaL9mzjeJY= +github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM= github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= +github.com/dimchansky/utfbom v1.1.0/go.mod h1:rO41eb7gLfo8SF1jd9F8HplJm1Fewwi4mQvIirEdv+8= github.com/drakkan/crypto v0.0.0-20201118124913-1ba5185435c1 h1:a+m/8QzsuNhWTVBprULP341v5EZZeKLleVSDcOiAS0c= github.com/drakkan/crypto v0.0.0-20201118124913-1ba5185435c1/go.mod h1:HCh3rfXxsHzqOEbzc/nqz6WnUhb7Nv19n/o64V0Zmbg= github.com/drakkan/ftp v0.0.0-20201114075148-9b9adce499a9 h1:LPH1dEblAOO/LoG7yHPMtBLXhQmjaga91/DDjWk9jWA= @@ -134,8 +185,10 @@ github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.m github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= +github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= github.com/fclairamb/ftpserverlib v0.9.1-0.20201105003045-1edd6bf7ae53 h1:veX6+jZG1119HXbWAbU2tQ99Zz5BSaFf7tfLgjLjZGI= github.com/fclairamb/ftpserverlib v0.9.1-0.20201105003045-1edd6bf7ae53/go.mod h1:sMPjxPuoVwwoV87gdPkyTb0dVofmCKpVZCQ3rMVokjc= +github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g= github.com/franela/goblin v0.0.0-20200105215937-c9ffbefa60db/go.mod h1:7dvUGVsVBjqR7JHJk0brhHOZYGmfBYOrK0ZhYMEtBr4= github.com/franela/goreq v0.0.0-20171204163338-bcd34c9993f8/go.mod h1:ZhphrRTfi2rbfLwlschooIH4+wKKDR4Pdxhh+TRoA20= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= @@ -151,18 +204,24 @@ github.com/go-chi/render v1.0.1/go.mod h1:pq4Rr7HbnsdaeHagklXub+p6Wd16Af5l9koip1 github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-ini/ini v1.25.4/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-kit/kit v0.10.0 h1:dXFJfIHVvUcpSgDOV+Ne6t7jXri8Tfv2uOLHUZ2XNuo= github.com/go-kit/kit v0.10.0/go.mod h1:xUsJbQ/Fp4kEt7AFgCuvyX4a71u8h9jB8tj/ORgOZ7o= +github.com/go-ldap/ldap v3.0.2+incompatible/go.mod h1:qfd9rJvER9Q0/D/Sqn1DfHRoBp40uXYvFoEVrNEPqRc= github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= +github.com/go-logfmt/logfmt v0.5.0 h1:TrB8swr/68K7m9CcGut2g3UOihhbcbiMAYiuTXdEih4= github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= github.com/go-playground/locales v0.12.1/go.mod h1:IUMDtCfWo/w/mtMfIE/IG2K+Ey3ygWanZIBtBW0W2TM= github.com/go-playground/universal-translator v0.16.0/go.mod h1:1AnU7NaIRDWWzGEKwgtJRd2xk99HeFyHw3yid4rvQIY= github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= github.com/go-sql-driver/mysql v1.5.0 h1:ozyZYNQW3x3HtqT1jira07DN2PArx2v7/mN66gGcHOs= github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= +github.com/go-stack/stack v1.8.0 h1:5SgMzNM5HxrEjV0ww2lTmX6E2Izsfxas4+YHWRs3Lsk= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/go-test/deep v1.0.1/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA= github.com/gogo/googleapis v1.1.0/go.mod h1:gf4bu3Q80BeJ6H1S1vYPm8/ELATdvryBaNFGgqEef3s= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= @@ -197,6 +256,8 @@ github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw github.com/golang/protobuf v1.4.3 h1:JjCZWpVbqXDqFVmTfYWEVTMIYrL/NPdPSCHPJ0T/raM= github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/golang/snappy v0.0.1 h1:Qgr9rKW7uDUkrbSmQeiDsGa8SjGyCOGtuasMWwvp2P4= +github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= @@ -206,10 +267,18 @@ github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.2 h1:X2ev0eStA3AbceY54o37/0PQ/UWqKEiiO2dKL5OPaFM= github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-replayers/grpcreplay v0.1.0 h1:eNb1y9rZFmY4ax45uEEECSa8fsxGRU+8Bil52ASAwic= +github.com/google/go-replayers/grpcreplay v0.1.0/go.mod h1:8Ig2Idjpr6gifRd6pNVggX6TC1Zw6Jx74AKp7QNH2QE= +github.com/google/go-replayers/httpreplay v0.1.0 h1:AX7FUb4BjrrzNvblr/OlgwrmFiep6soj5K2QSDW7BGk= +github.com/google/go-replayers/httpreplay v0.1.0/go.mod h1:YKZViNhiGgqdBlUbI2MwGpq4pXxNmhJLPHQ7cv2b5no= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= +github.com/google/martian v2.1.1-0.20190517191504-25dcb96d9e51+incompatible h1:xmapqc1AyLoB+ddYT6r04bD9lIjlOqGaREovi0SzFaE= +github.com/google/martian v2.1.1-0.20190517191504-25dcb96d9e51+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= +github.com/google/martian/v3 v3.1.0 h1:wCKgOCHuUEVfsaQLpPSJb7VdYCdTVZQAuOdYm1yc/60= github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= @@ -217,19 +286,26 @@ github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hf github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200507031123-427632fa3b1c/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/pprof v0.0.0-20200905233945-acf8798be1f7/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= +github.com/google/subcommands v1.0.1/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk= github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.1.2 h1:EVhdT+1Kseyi1/pUmXKaFxYsDNy9RQYkMWRH68J/W7Y= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/wire v0.4.0 h1:kXcsA/rIGzJImVqPdhfnr6q0xsS9gU0515q1EPpJ9fE= +github.com/google/wire v0.4.0/go.mod h1:ngWDr9Qvq3yZA10YrxfyGELY/AFWGVpy9c1LTRi1EoU= +github.com/googleapis/gax-go v2.0.2+incompatible h1:silFMLAnr330+NRuag/VjIGF7TLp/LBrV2CJKFLWEww= +github.com/googleapis/gax-go v2.0.2+incompatible/go.mod h1:SFVmujtThgffbyetf+mdk2eWhX2bMyUtNHzFKcPA9HY= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.5 h1:sjZBwGj9Jlw33ImPtvFviGYvseOtDM7hkSKB7+Tv3SM= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= +github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg= github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= @@ -247,16 +323,29 @@ github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBt github.com/hashicorp/consul/api v1.3.0/go.mod h1:MmDNSzIMUjNpY/mQ398R4bk2FnqQLoPndWW5VkKPlCE= github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8= github.com/hashicorp/consul/sdk v0.3.0/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8= +github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= +github.com/hashicorp/go-cleanhttp v0.5.1 h1:dH3aiDG9Jvb5r5+bYHsikaOUIpcM0xvgMXVoDkXMzJM= github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= +github.com/hashicorp/go-hclog v0.0.0-20180709165350-ff2cf002a8dd/go.mod h1:9bjs9uLqI8l75knNv3lV1kA55veR+WUPSiKIWcQHudI= +github.com/hashicorp/go-hclog v0.8.0/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ= github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= +github.com/hashicorp/go-multierror v1.0.0 h1:iVjPR7a6H0tWELX5NxNe7bYopibicUzc7uPribsnS6o= github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= +github.com/hashicorp/go-plugin v1.0.0/go.mod h1:++UyYGoz3o5w9ZzAdZxtQKrWWP+iqPBn3cQptSMzBuY= +github.com/hashicorp/go-retryablehttp v0.5.3 h1:QlWt0KvWT0lq8MFppF9tsJGF+ynG7ztc2KIPhzRGk7s= +github.com/hashicorp/go-retryablehttp v0.5.3/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs= +github.com/hashicorp/go-rootcerts v1.0.0 h1:Rqb66Oo1X/eSV1x66xbDccZjhJigjg0+e82kpwzSwCI= github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU= github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= +github.com/hashicorp/go-sockaddr v1.0.2 h1:ztczhD1jLxIRjVejw8gFomI1BQZOe2WoVOu0SyteCQc= +github.com/hashicorp/go-sockaddr v1.0.2/go.mod h1:rB4wwRAUzs07qva3c5SdrY/NEtAUjGlgmH/UkBUC97A= github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go-version v1.1.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= github.com/hashicorp/go-version v1.2.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= @@ -267,16 +356,27 @@ github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ= github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I= github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc= +github.com/hashicorp/vault/api v1.0.2 h1:/V9fULvLwt58vme/6Rkt/p/GtlresQv+Z9E6dgdANhs= +github.com/hashicorp/vault/api v1.0.2/go.mod h1:AV/+M5VPDpB90arloVX0rVDUIHkONiwz5Uza9HRtpUE= +github.com/hashicorp/vault/sdk v0.1.8 h1:pfF3KwA1yPlfpmcumNsFM4uo91WMasX5gTuIkItu9r0= +github.com/hashicorp/vault/sdk v0.1.8/go.mod h1:tHZfc6St71twLizWNHvnnbiGFo1aq0eD2jGPLtP8kAU= +github.com/hashicorp/yamux v0.0.0-20180604194846-3520598351bb/go.mod h1:+NfK9FKeTrX5uv1uIXGdwYDTeHna2qgaIlx54MXqjAM= +github.com/hashicorp/yamux v0.0.0-20181012175058-2f1d1f20f75d/go.mod h1:+NfK9FKeTrX5uv1uIXGdwYDTeHna2qgaIlx54MXqjAM= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/hudl/fargo v1.3.0/go.mod h1:y3CKSmjA+wD2gak7sUSXTAoopbhU08POFhmITJgmKTg= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= github.com/influxdata/influxdb1-client v0.0.0-20191209144304-8bf82d3c094d/go.mod h1:qj24IKcXYK6Iy9ceXlo3Tc+vtHo9lIhSX5JddghvEPo= +github.com/jmespath/go-jmespath v0.0.0-20160202185014-0b12d6b521d8/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= +github.com/jmespath/go-jmespath v0.3.0/go.mod h1:9QtRXoHjLGCJ5IBSaohpXITPlowMeeYCZ7fLUTSywik= github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= +github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8= github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= +github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg= github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= @@ -284,7 +384,9 @@ github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/u github.com/json-iterator/go v1.1.8/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= +github.com/jstemmer/go-junit-report v0.9.1 h1:6QPYqodiu3GuPL+7mfx+NwDdp2eTkp9IfEUpgAwUN0o= github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= +github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= @@ -297,8 +399,10 @@ github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/leodido/go-urn v1.1.0/go.mod h1:+cyI34gQWZcE1eQU7NVgKkkzdXDQHr1dBMtdAPozLkw= +github.com/lib/pq v1.1.1/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.8.0 h1:9xohqzkUwzR4Ga4ivdTcawVS89YSDVxXMa3xJX3cGzg= github.com/lib/pq v1.8.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/lightstep/lightstep-tracer-common/golang/gogo v0.0.0-20190605223551-bc2310a04743/go.mod h1:qklhhLq1aX+mtWk9cPHPzaBjWImj5ULL6C7HFJtXQMM= @@ -308,6 +412,8 @@ github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czP github.com/magiconair/properties v1.8.4 h1:8KGKTcQQGm0Kv7vEbKFErAoAOFyyacLStRtQSeYtvkY= github.com/magiconair/properties v1.8.4/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= +github.com/mattn/go-ieproxy v0.0.0-20190610004146-91bb50d98149/go.mod h1:31jz6HNzdxOmlERGGEc4v/dMssOfmp2p5bT/okiKFFc= +github.com/mattn/go-ieproxy v0.0.0-20190702010315-6dee0af9227d/go.mod h1:31jz6HNzdxOmlERGGEc4v/dMssOfmp2p5bT/okiKFFc= github.com/mattn/go-ieproxy v0.0.1 h1:qiyop7gCflfhwCzGyeT0gro3sF9AIg9HU98JORTkqfI= github.com/mattn/go-ieproxy v0.0.1/go.mod h1:pYabZ6IHcRpFh7vIaLfK7rdcWgFEb3SFJ6/gNWuh88E= github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= @@ -325,15 +431,20 @@ github.com/miekg/dns v1.1.35/go.mod h1:KNUDUusw/aVsxyTYZM1oqvCicbwhgbNgztCETuNZ7 github.com/minio/sha256-simd v0.1.1 h1:5QHSlgo3nt5yKOJrC7W8w7X+NFl8cMPZm96iu8kKUJU= github.com/minio/sha256-simd v0.1.1/go.mod h1:B5e1o+1/KgNmWrSQK08Y6Z1Vb5pwIktudl0J58iy0KM= github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= +github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw= github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/go-testing-interface v0.0.0-20171004221916-a61a99592b77/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= +github.com/mitchellh/go-wordwrap v1.0.0/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo= github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg= github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY= github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/mitchellh/mapstructure v1.3.3 h1:SzB1nHZ2Xi+17FP0zVQBHIZqvwRN9408fJO8h+eeNA8= github.com/mitchellh/mapstructure v1.3.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= @@ -347,6 +458,7 @@ github.com/nats-io/nats.go v1.9.1/go.mod h1:ZjDU1L/7fJ09jvUSRVBR2e7+RnLiiIQyqyzE github.com/nats-io/nkeys v0.1.0/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w= github.com/nats-io/nkeys v0.1.3/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w= github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/oklog/oklog v0.3.2/go.mod h1:FCV+B7mhrz4o+ueLpx+KqkyXRGMWOYEvfiXtdGtbWGs= github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA= @@ -367,17 +479,21 @@ github.com/openzipkin/zipkin-go v0.2.2/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnh github.com/otiai10/copy v1.2.0 h1:HvG945u96iNadPoG2/Ja2+AUJeW5YuFQMixq9yirC+k= github.com/otiai10/copy v1.2.0/go.mod h1:rrF5dJ5F0t/EWSYODDu4j9/vEeYHMkc8jt0zJChqQWw= github.com/otiai10/curr v0.0.0-20150429015615-9b4961190c95/go.mod h1:9qAhocn7zKJG+0mI8eUu6xqkFDYS2kb2saOteoSB3cE= +github.com/otiai10/curr v1.0.0 h1:TJIWdbX0B+kpNagQrjgq8bCMrbhiuX73M2XwgtDMoOI= github.com/otiai10/curr v1.0.0/go.mod h1:LskTG5wDwr8Rs+nNQ+1LlxRjAtTZZjtJW4rMXl6j4vs= github.com/otiai10/mint v1.3.0/go.mod h1:F5AjcsTsWUqX+Na9fpHb52P8pcRX2CI6A3ctIT91xUo= +github.com/otiai10/mint v1.3.1 h1:BCmzIS3n71sGfHB5NMNDB3lHYPz8fWSkCAErHed//qc= github.com/otiai10/mint v1.3.1/go.mod h1:/yxELlJQ0ufhjUwhshSj+wFjZ78CnZ48/1wtmBH1OTc= github.com/pact-foundation/pact-go v1.0.4/go.mod h1:uExwJY4kCzNPcHRj+hCR/HBbOOIwwtUjcrb0b5/5kLM= github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= +github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k= github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= github.com/pelletier/go-toml v1.8.1 h1:1Nf83orprkJyknT6h7zbuEGUEjcyVlCxSUGTENmNCRM= github.com/pelletier/go-toml v1.8.1/go.mod h1:T2/BmBdy8dvIRq1a/8aqjN41wvWlN4lrapLU/GW4pbc= github.com/performancecopilot/speed v3.0.0+incompatible/go.mod h1:/CLtqpZ5gBg1M9iaPbIdPPGyKcA8hKdoy6hAWba7Yac= github.com/pierrec/lz4 v1.0.2-0.20190131084431-473cd7ce01a1/go.mod h1:3/3N9NVKO0jef7pBehbT1qWhCMrIgbYNnFAZCqQ5LRc= +github.com/pierrec/lz4 v2.0.5+incompatible h1:2xWsjqPFWcplujydGg4WmhC/6fZqK42wMM8aXeqhl0I= github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= github.com/pires/go-proxyproto v0.3.2 h1:E5ig1h9SFGne7IWVY6yRu3UCzyAFkQIukXHMkdFUOCA= github.com/pires/go-proxyproto v0.3.2/go.mod h1:Odh9VFOZJCf9G8cLW5o435Xf1J95Jw9Gw5rnCjcwzAY= @@ -438,14 +554,20 @@ github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQD github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= +github.com/ryanuber/columnize v2.1.0+incompatible/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= +github.com/ryanuber/go-glob v1.0.0 h1:iQh3xXAumdQ+4Ufa5b25cRpC5TYKlno6hsv6Cb3pkBk= +github.com/ryanuber/go-glob v1.0.0/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc= github.com/samuel/go-zookeeper v0.0.0-20190923202752-2cc03de413da/go.mod h1:gi+0XIa01GRL2eRQVjQkKGqKF3SF9vZR/HnPullcV2E= github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= +github.com/secsy/goftp v0.0.0-20200609142545-aa2de14babf4 h1:PT+ElG/UUFMfqy5HrxJxNzj3QBOf7dZwupeVC+mG1Lo= github.com/secsy/goftp v0.0.0-20200609142545-aa2de14babf4/go.mod h1:MnkX001NG75g3p8bhFycnyIjeQoOjGL6CEIsdE/nKSY= github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= +github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= +github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s= github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= github.com/sony/gobreaker v0.4.1/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY= @@ -474,6 +596,7 @@ github.com/streadway/amqp v0.0.0-20190827072141-edfb9018d271/go.mod h1:AZpEONHx3 github.com/streadway/handy v0.0.0-20190108123426-d5acb3125c2a/go.mod h1:qNTQ5P5JnDBl6z3cMAg/SywNDC5ABu5ApDIw6lUbRmI= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= @@ -500,6 +623,7 @@ go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= go.etcd.io/bbolt v1.3.5 h1:XAzx9gjCb0Rxj7EoqcClPD1d5ZBxZJk0jbuoPHenBt0= go.etcd.io/bbolt v1.3.5/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ= go.etcd.io/etcd v0.0.0-20191023171146-3cf2f69b5738/go.mod h1:dnLIgRNXwCJa5e+c6mIZCrds/GIG4ncV9HhK5PX7jPg= +go.opencensus.io v0.15.0/go.mod h1:UffZAU+4sDEINUGP/B7UfBBkq4fqLu9zXAX7ke6CHW0= go.opencensus.io v0.20.1/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk= go.opencensus.io v0.20.2/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= @@ -519,6 +643,10 @@ go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+ go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM= +gocloud.dev v0.20.0 h1:mbEKMfnyPV7W1Rj35R1xXfjszs9dXkwSOq2KoFr25g8= +gocloud.dev v0.20.0/go.mod h1:+Y/RpSXrJthIOM8uFNzWp6MRu9pFPNFEEZrQMxpkfIc= +gocloud.dev/secrets/hashivault v0.20.0 h1:919urzRWksXrZNNqqlHvek9IE6lWvYM4m8dvFqXq4HU= +gocloud.dev/secrets/hashivault v0.20.0/go.mod h1:2nNlZ76i4JlT9qrPVKbINB01L8BGgv4wmq2Cqyz22dA= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -540,6 +668,7 @@ golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHl golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20200302205851-738671d3881b h1:Wh+f8QHJXR411sJR8/vRBTZ7YapZaRvUcLFFJhusH0k= golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= @@ -548,9 +677,11 @@ golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0 h1:RM4zey1++hCTbCVQfnWeKs9/IEsaBLA8vTkd0WVtmH4= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20190402181905-9f3314589c9a/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -565,6 +696,7 @@ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9 h1:SQFwaSi55rU7vdNs9Yr0Z324VNlrF+0wMqRXT4St8ck= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -574,8 +706,10 @@ golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5h golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190129075346-302c3dd5f1cc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -600,11 +734,13 @@ golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200317113312-5766fd39f98d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200602225109-6fdc65e7d980/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -614,10 +750,12 @@ golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20201015000850-e3ed0017c211/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201117222635-ba5294a509c7 h1:s330+6z/Ko3J0o6rvOcwXe5nzs7UT9tLKHoOXYn6uE0= golang.org/x/sys v0.0.0-20201117222635-ba5294a509c7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/term v0.0.0-20201117132131-f5c789dd3221 h1:/ZHdbVpdR/jk3g30/d4yUL0JU9kksj8+F/bnQUVLGDM= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.1-0.20181227161524-e6919f6577db/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.4 h1:0YWbFKbhXG/wIiuHDSKpS0Iy7FSA+u45VtBMfQcFTTc= @@ -625,6 +763,7 @@ golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20191024005414-555d28b269f0 h1:/5xXl8Y5W96D+TtHSlonuFqGHIWVuyCkGJLwGh9JJFs= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -635,6 +774,7 @@ golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3 golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190422233926-fe54fb35175b/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= @@ -667,10 +807,15 @@ golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapK golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200317043434-63da46f3035e/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= +golang.org/x/tools v0.0.0-20200325010219-a49f79bcc224/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200601175630-2caf76543d99/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200606014950-c42cb6316fb6/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200608174601-1b747fd94509/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= @@ -680,13 +825,16 @@ golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82u golang.org/x/tools v0.0.0-20200915173823-2db8f0ff891c/go.mod h1:z6u4i615ZeAfBE4XtMziQW1fSVJXACjjbWkB/mvPzlU= golang.org/x/tools v0.0.0-20200918232735-d647fc253266/go.mod h1:z6u4i615ZeAfBE4XtMziQW1fSVJXACjjbWkB/mvPzlU= golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20201118030313-598b068a9102 h1:kr6Ik/EJgxdTSLX+rSiDounHdHWMBu9Ks/ghr2hWNpo= golang.org/x/tools v0.0.0-20201118030313-598b068a9102/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/api v0.3.1/go.mod h1:6wY9I6uQWHQ8EM57III9mq/AjF+i8G65rmVagqKMtkk= google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= +google.golang.org/api v0.5.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= @@ -699,6 +847,7 @@ google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/ google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= +google.golang.org/api v0.26.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= @@ -713,12 +862,15 @@ google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7 google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190404172233-64821d5d2107/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190508193815-b515fa19cec8/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190530194941-fb225487d101/go.mod h1:z3L6/3dTEVtUr6QSP8miRzeRqwQOioJ9I66odjN4I7s= google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= @@ -735,11 +887,15 @@ google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfG google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200317114155-1f3552e48f24/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200325114520-5b2d0af7952b/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/genproto v0.0.0-20200603110839-e855014d5736/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= +google.golang.org/genproto v0.0.0-20200608115520-7c474a2e3482/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= @@ -751,8 +907,10 @@ google.golang.org/genproto v0.0.0-20200921151605-7abf4a1a14d5/go.mod h1:FWY/as6D google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20201117123952-62d171c70ae1 h1:EVow1AaDgdoMjdO64/fntn4+RGTVor8YE/mkmIYsqFM= google.golang.org/genproto v0.0.0-20201117123952-62d171c70ae1/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +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= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.19.1/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.0/go.mod h1:chYK+tFQF0nDUGJgXMSgLCQk3phJEuONr2DCgLDdAQM= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= @@ -784,11 +942,14 @@ google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGj google.golang.org/protobuf v1.25.0 h1:Ejskq+SyPohKW+1uil0JJMtmHCgJPJ/qWTxr8qp+R4c= google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= +gopkg.in/asn1-ber.v1 v1.0.0-20181015200546-f715ec2f112d/go.mod h1:cuepJuh7vyXfUyUwEgHQXw849cJrilpS5NeIjOWESAw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/cheggaaa/pb.v1 v1.0.25/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw= +gopkg.in/dutchcoders/goftp.v1 v1.0.0-20170301105846-ed59a591ce14 h1:tHqNpm9sPaE6BSuMLXBzgTwukQLdBEt4OYU2coQjEQQ= gopkg.in/dutchcoders/goftp.v1 v1.0.0-20170301105846-ed59a591ce14/go.mod h1:nzmlZQ+UqB5+55CRTV/dOaiK8OrPl6Co96Ob8lH4Wxw= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= @@ -801,6 +962,8 @@ gopkg.in/ini.v1 v1.62.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/natefinch/lumberjack.v2 v2.0.0 h1:1Lc07Kr7qY4U2YPouBjpCLxpiyxIVoxqXgkXLknAOE8= gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k= gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= +gopkg.in/square/go-jose.v2 v2.3.1 h1:SK5KegNXmKmqE342YYN2qPHEnUYeoMiXXl1poUlI+o4= +gopkg.in/square/go-jose.v2 v2.3.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= diff --git a/httpd/api_user.go b/httpd/api_user.go index 5d786b02..4bf46f4b 100644 --- a/httpd/api_user.go +++ b/httpd/api_user.go @@ -11,6 +11,7 @@ import ( "github.com/drakkan/sftpgo/common" "github.com/drakkan/sftpgo/dataprovider" + "github.com/drakkan/sftpgo/kms" "github.com/drakkan/sftpgo/vfs" ) @@ -82,6 +83,7 @@ func addUser(w http.ResponseWriter, r *http.Request) { sendAPIResponse(w, r, err, "", http.StatusBadRequest) return } + user.SetEmptySecretsIfNil() switch user.FsConfig.Provider { case dataprovider.S3FilesystemProvider: if user.FsConfig.S3Config.AccessSecret.IsRedacted() { @@ -136,9 +138,9 @@ func updateUser(w http.ResponseWriter, r *http.Request) { return } currentPermissions := user.Permissions - var currentS3AccessSecret vfs.Secret - var currentAzAccountKey vfs.Secret - var currentGCSCredentials vfs.Secret + var currentS3AccessSecret *kms.Secret + var currentAzAccountKey *kms.Secret + var currentGCSCredentials *kms.Secret if user.FsConfig.Provider == dataprovider.S3FilesystemProvider { currentS3AccessSecret = user.FsConfig.S3Config.AccessSecret } @@ -157,6 +159,7 @@ func updateUser(w http.ResponseWriter, r *http.Request) { sendAPIResponse(w, r, err, "", http.StatusBadRequest) return } + user.SetEmptySecretsIfNil() // we use new Permissions if passed otherwise the old ones if len(user.Permissions) == 0 { user.Permissions = currentPermissions @@ -207,7 +210,7 @@ func disconnectUser(username string) { } } -func updateEncryptedSecrets(user *dataprovider.User, currentS3AccessSecret, currentAzAccountKey, currentGCSCredentials vfs.Secret) { +func updateEncryptedSecrets(user *dataprovider.User, currentS3AccessSecret, currentAzAccountKey, currentGCSCredentials *kms.Secret) { // we use the new access secret if plain or empty, otherwise the old value if user.FsConfig.Provider == dataprovider.S3FilesystemProvider { if !user.FsConfig.S3Config.AccessSecret.IsPlain() && !user.FsConfig.S3Config.AccessSecret.IsEmpty() { diff --git a/httpd/api_utils.go b/httpd/api_utils.go index 66ce925b..5c30cae7 100644 --- a/httpd/api_utils.go +++ b/httpd/api_utils.go @@ -21,6 +21,7 @@ import ( "github.com/drakkan/sftpgo/common" "github.com/drakkan/sftpgo/dataprovider" "github.com/drakkan/sftpgo/httpclient" + "github.com/drakkan/sftpgo/kms" "github.com/drakkan/sftpgo/utils" "github.com/drakkan/sftpgo/version" "github.com/drakkan/sftpgo/vfs" @@ -710,19 +711,41 @@ func compareAzBlobConfig(expected *dataprovider.User, actual *dataprovider.User) return nil } -func checkEncryptedSecret(expected, actual vfs.Secret) error { +func areSecretEquals(expected, actual *kms.Secret) bool { + if expected == nil && actual == nil { + return true + } + if expected != nil && expected.IsEmpty() && actual == nil { + return true + } + if actual != nil && actual.IsEmpty() && expected == nil { + return true + } + return false +} + +func checkEncryptedSecret(expected, actual *kms.Secret) error { + if areSecretEquals(expected, actual) { + return nil + } + if expected == nil && actual != nil && !actual.IsEmpty() { + return errors.New("secret mismatch") + } + if actual == nil && expected != nil && !expected.IsEmpty() { + return errors.New("secret mismatch") + } if expected.IsPlain() && actual.IsEncrypted() { - if actual.Payload == "" { + if actual.GetPayload() == "" { return errors.New("invalid secret payload") } - if actual.AdditionalData != "" { + if actual.GetAdditionalData() != "" { return errors.New("invalid secret additional data") } - if actual.Key != "" { + if actual.GetKey() != "" { return errors.New("invalid secret key") } } else { - if expected.Status != actual.Status || expected.Payload != actual.Payload { + if expected.GetStatus() != actual.GetStatus() || expected.GetPayload() != actual.GetPayload() { return errors.New("secret mismatch") } } diff --git a/httpd/httpd_test.go b/httpd/httpd_test.go index d1916433..99aac3ed 100644 --- a/httpd/httpd_test.go +++ b/httpd/httpd_test.go @@ -32,6 +32,7 @@ import ( "github.com/drakkan/sftpgo/config" "github.com/drakkan/sftpgo/dataprovider" "github.com/drakkan/sftpgo/httpd" + "github.com/drakkan/sftpgo/kms" "github.com/drakkan/sftpgo/logger" "github.com/drakkan/sftpgo/utils" "github.com/drakkan/sftpgo/vfs" @@ -140,6 +141,12 @@ func TestMain(m *testing.M) { httpConfig := config.GetHTTPConfig() httpConfig.Initialize(configDir) + kmsConfig := config.GetKMSConfig() + err = kmsConfig.Initialize() + if err != nil { + logger.ErrorToConsole("error initializing kms: %v", err) + os.Exit(1) + } httpdConf := config.GetHTTPDConfig() @@ -191,7 +198,7 @@ func TestMain(m *testing.M) { defer testServer.Close() exitCode := m.Run() - //os.Remove(logfilePath) //nolint:errcheck + os.Remove(logfilePath) //nolint:errcheck os.RemoveAll(backupsPath) //nolint:errcheck os.RemoveAll(credentialsPath) //nolint:errcheck os.Remove(certPath) //nolint:errcheck @@ -439,14 +446,13 @@ func TestAddUserInvalidFsConfig(t *testing.T) { u.FsConfig.S3Config.Bucket = "testbucket" u.FsConfig.S3Config.Region = "eu-west-1" u.FsConfig.S3Config.AccessKey = "access-key" - u.FsConfig.S3Config.AccessSecret.Payload = "access-secret" - u.FsConfig.S3Config.AccessSecret.Status = vfs.SecretStatusRedacted + u.FsConfig.S3Config.AccessSecret = kms.NewSecret(kms.SecretStatusRedacted, "access-secret", "", "") u.FsConfig.S3Config.Endpoint = "http://127.0.0.1:9000/path?a=b" u.FsConfig.S3Config.StorageClass = "Standard" //nolint:goconst u.FsConfig.S3Config.KeyPrefix = "/adir/subdir/" _, _, err = httpd.AddUser(u, http.StatusBadRequest) assert.NoError(t, err) - u.FsConfig.S3Config.AccessSecret.Status = vfs.SecretStatusPlain + u.FsConfig.S3Config.AccessSecret.SetStatus(kms.SecretStatusPlain) _, _, err = httpd.AddUser(u, http.StatusBadRequest) assert.NoError(t, err) u.FsConfig.S3Config.KeyPrefix = "" @@ -468,20 +474,18 @@ func TestAddUserInvalidFsConfig(t *testing.T) { u.FsConfig.GCSConfig.Bucket = "abucket" u.FsConfig.GCSConfig.StorageClass = "Standard" u.FsConfig.GCSConfig.KeyPrefix = "/somedir/subdir/" - u.FsConfig.GCSConfig.Credentials.Payload = "test" //nolint:goconst - u.FsConfig.GCSConfig.Credentials.Status = vfs.SecretStatusRedacted + u.FsConfig.GCSConfig.Credentials = kms.NewSecret(kms.SecretStatusRedacted, "test", "", "") //nolint:goconst _, _, err = httpd.AddUser(u, http.StatusBadRequest) assert.NoError(t, err) - u.FsConfig.GCSConfig.Credentials.Status = vfs.SecretStatusPlain + u.FsConfig.GCSConfig.Credentials.SetStatus(kms.SecretStatusPlain) _, _, err = httpd.AddUser(u, http.StatusBadRequest) assert.NoError(t, err) u.FsConfig.GCSConfig.KeyPrefix = "somedir/subdir/" //nolint:goconst - u.FsConfig.GCSConfig.Credentials = vfs.Secret{} + u.FsConfig.GCSConfig.Credentials = kms.NewEmptySecret() u.FsConfig.GCSConfig.AutomaticCredentials = 0 _, _, err = httpd.AddUser(u, http.StatusBadRequest) assert.NoError(t, err) - u.FsConfig.GCSConfig.Credentials.Payload = "invalid" - u.FsConfig.GCSConfig.Credentials.Status = vfs.SecretStatusAES256GCM + u.FsConfig.GCSConfig.Credentials = kms.NewSecret(kms.SecretStatusSecretBox, "invalid", "", "") _, _, err = httpd.AddUser(u, http.StatusBadRequest) assert.NoError(t, err) @@ -497,12 +501,11 @@ func TestAddUserInvalidFsConfig(t *testing.T) { u.FsConfig.AzBlobConfig.Container = "container" _, _, err = httpd.AddUser(u, http.StatusBadRequest) assert.NoError(t, err) - u.FsConfig.AzBlobConfig.AccountKey.Payload = "key" - u.FsConfig.AzBlobConfig.AccountKey.Status = vfs.SecretStatusRedacted + u.FsConfig.AzBlobConfig.AccountKey = kms.NewSecret(kms.SecretStatusRedacted, "key", "", "") u.FsConfig.AzBlobConfig.KeyPrefix = "/amedir/subdir/" _, _, err = httpd.AddUser(u, http.StatusBadRequest) assert.NoError(t, err) - u.FsConfig.AzBlobConfig.AccountKey.Status = vfs.SecretStatusPlain + u.FsConfig.AzBlobConfig.AccountKey.SetStatus(kms.SecretStatusPlain) _, _, err = httpd.AddUser(u, http.StatusBadRequest) assert.NoError(t, err) u.FsConfig.AzBlobConfig.KeyPrefix = "amedir/subdir/" @@ -1013,35 +1016,31 @@ func TestUserS3Config(t *testing.T) { user.FsConfig.S3Config.Bucket = "test" //nolint:goconst user.FsConfig.S3Config.Region = "us-east-1" //nolint:goconst user.FsConfig.S3Config.AccessKey = "Server-Access-Key" - user.FsConfig.S3Config.AccessSecret.Payload = "Server-Access-Secret" - user.FsConfig.S3Config.AccessSecret.Status = vfs.SecretStatusPlain + user.FsConfig.S3Config.AccessSecret = kms.NewPlainSecret("Server-Access-Secret") user.FsConfig.S3Config.Endpoint = "http://127.0.0.1:9000" user.FsConfig.S3Config.UploadPartSize = 8 user, body, err := httpd.UpdateUser(user, http.StatusOK, "") assert.NoError(t, err, string(body)) - assert.Equal(t, vfs.SecretStatusAES256GCM, user.FsConfig.S3Config.AccessSecret.Status) - assert.NotEmpty(t, user.FsConfig.S3Config.AccessSecret.Payload) - assert.Empty(t, user.FsConfig.S3Config.AccessSecret.AdditionalData) - assert.Empty(t, user.FsConfig.S3Config.AccessSecret.Key) + assert.Equal(t, kms.SecretStatusSecretBox, user.FsConfig.S3Config.AccessSecret.GetStatus()) + assert.NotEmpty(t, user.FsConfig.S3Config.AccessSecret.GetPayload()) + assert.Empty(t, user.FsConfig.S3Config.AccessSecret.GetAdditionalData()) + assert.Empty(t, user.FsConfig.S3Config.AccessSecret.GetKey()) _, err = httpd.RemoveUser(user, http.StatusOK) assert.NoError(t, err) user.Password = defaultPassword user.ID = 0 - secret := vfs.Secret{ - Payload: "Server-Access-Secret", - Status: vfs.SecretStatusAES256GCM, - } + secret := kms.NewSecret(kms.SecretStatusSecretBox, "Server-Access-Secret", "", "") user.FsConfig.S3Config.AccessSecret = secret _, _, err = httpd.AddUser(user, http.StatusOK) assert.Error(t, err) - user.FsConfig.S3Config.AccessSecret.Status = vfs.SecretStatusPlain + user.FsConfig.S3Config.AccessSecret.SetStatus(kms.SecretStatusPlain) user, _, err = httpd.AddUser(user, http.StatusOK) assert.NoError(t, err) - initialSecretPayload := user.FsConfig.S3Config.AccessSecret.Payload - assert.Equal(t, vfs.SecretStatusAES256GCM, user.FsConfig.S3Config.AccessSecret.Status) + initialSecretPayload := user.FsConfig.S3Config.AccessSecret.GetPayload() + assert.Equal(t, kms.SecretStatusSecretBox, user.FsConfig.S3Config.AccessSecret.GetStatus()) assert.NotEmpty(t, initialSecretPayload) - assert.Empty(t, user.FsConfig.S3Config.AccessSecret.AdditionalData) - assert.Empty(t, user.FsConfig.S3Config.AccessSecret.Key) + assert.Empty(t, user.FsConfig.S3Config.AccessSecret.GetAdditionalData()) + assert.Empty(t, user.FsConfig.S3Config.AccessSecret.GetKey()) user.FsConfig.Provider = dataprovider.S3FilesystemProvider user.FsConfig.S3Config.Bucket = "test-bucket" user.FsConfig.S3Config.Region = "us-east-1" //nolint:goconst @@ -1051,16 +1050,16 @@ func TestUserS3Config(t *testing.T) { user.FsConfig.S3Config.UploadConcurrency = 5 user, _, err = httpd.UpdateUser(user, http.StatusOK, "") assert.NoError(t, err) - assert.Equal(t, vfs.SecretStatusAES256GCM, user.FsConfig.S3Config.AccessSecret.Status) - assert.Equal(t, initialSecretPayload, user.FsConfig.S3Config.AccessSecret.Payload) - assert.Empty(t, user.FsConfig.S3Config.AccessSecret.AdditionalData) - assert.Empty(t, user.FsConfig.S3Config.AccessSecret.Key) + assert.Equal(t, kms.SecretStatusSecretBox, user.FsConfig.S3Config.AccessSecret.GetStatus()) + assert.Equal(t, initialSecretPayload, user.FsConfig.S3Config.AccessSecret.GetPayload()) + assert.Empty(t, user.FsConfig.S3Config.AccessSecret.GetAdditionalData()) + assert.Empty(t, user.FsConfig.S3Config.AccessSecret.GetKey()) // test user without access key and access secret (shared config state) user.FsConfig.Provider = dataprovider.S3FilesystemProvider user.FsConfig.S3Config.Bucket = "testbucket" user.FsConfig.S3Config.Region = "us-east-1" user.FsConfig.S3Config.AccessKey = "" - user.FsConfig.S3Config.AccessSecret = vfs.Secret{} + user.FsConfig.S3Config.AccessSecret = kms.NewEmptySecret() user.FsConfig.S3Config.Endpoint = "" user.FsConfig.S3Config.KeyPrefix = "somedir/subdir" user.FsConfig.S3Config.UploadPartSize = 6 @@ -1089,49 +1088,46 @@ func TestUserGCSConfig(t *testing.T) { assert.NoError(t, err) user.FsConfig.Provider = dataprovider.GCSFilesystemProvider user.FsConfig.GCSConfig.Bucket = "test" - user.FsConfig.GCSConfig.Credentials.Payload = "fake credentials" //nolint:goconst - user.FsConfig.GCSConfig.Credentials.Status = vfs.SecretStatusPlain + user.FsConfig.GCSConfig.Credentials = kms.NewPlainSecret("fake credentials") //nolint:goconst user, _, err = httpd.UpdateUser(user, http.StatusOK, "") assert.NoError(t, err) credentialFile := filepath.Join(credentialsPath, fmt.Sprintf("%v_gcs_credentials.json", user.Username)) assert.FileExists(t, credentialFile) creds, err := ioutil.ReadFile(credentialFile) assert.NoError(t, err) - secret := &vfs.Secret{} + secret := kms.NewEmptySecret() err = json.Unmarshal(creds, secret) assert.NoError(t, err) err = secret.Decrypt() assert.NoError(t, err) - assert.Equal(t, "fake credentials", secret.Payload) - user.FsConfig.GCSConfig.Credentials.Payload = "fake encrypted credentials" - user.FsConfig.GCSConfig.Credentials.Status = vfs.SecretStatusAES256GCM + assert.Equal(t, "fake credentials", secret.GetPayload()) + user.FsConfig.GCSConfig.Credentials = kms.NewSecret(kms.SecretStatusSecretBox, "fake encrypted credentials", "", "") user, _, err = httpd.UpdateUser(user, http.StatusOK, "") assert.NoError(t, err) assert.FileExists(t, credentialFile) creds, err = ioutil.ReadFile(credentialFile) assert.NoError(t, err) - secret = &vfs.Secret{} + secret = kms.NewEmptySecret() err = json.Unmarshal(creds, secret) assert.NoError(t, err) err = secret.Decrypt() assert.NoError(t, err) - assert.Equal(t, "fake credentials", secret.Payload) + assert.Equal(t, "fake credentials", secret.GetPayload()) _, err = httpd.RemoveUser(user, http.StatusOK) assert.NoError(t, err) user.Password = defaultPassword user.ID = 0 - user.FsConfig.GCSConfig.Credentials.Payload = "fake credentials" - user.FsConfig.GCSConfig.Credentials.Status = vfs.SecretStatusAES256GCM + user.FsConfig.GCSConfig.Credentials = kms.NewSecret(kms.SecretStatusSecretBox, "fake credentials", "", "") _, _, err = httpd.AddUser(user, http.StatusOK) assert.Error(t, err) - user.FsConfig.GCSConfig.Credentials.Status = vfs.SecretStatusPlain + user.FsConfig.GCSConfig.Credentials.SetStatus(kms.SecretStatusPlain) user, body, err := httpd.AddUser(user, http.StatusOK) assert.NoError(t, err, string(body)) err = os.RemoveAll(credentialsPath) assert.NoError(t, err) err = os.MkdirAll(credentialsPath, 0700) assert.NoError(t, err) - user.FsConfig.GCSConfig.Credentials = vfs.Secret{} + user.FsConfig.GCSConfig.Credentials = kms.NewEmptySecret() user.FsConfig.GCSConfig.AutomaticCredentials = 1 user, _, err = httpd.UpdateUser(user, http.StatusOK, "") assert.NoError(t, err) @@ -1141,8 +1137,7 @@ func TestUserGCSConfig(t *testing.T) { user.FsConfig.S3Config.Bucket = "test1" user.FsConfig.S3Config.Region = "us-east-1" user.FsConfig.S3Config.AccessKey = "Server-Access-Key1" - user.FsConfig.S3Config.AccessSecret.Payload = "secret" - user.FsConfig.S3Config.AccessSecret.Status = vfs.SecretStatusPlain + user.FsConfig.S3Config.AccessSecret = kms.NewPlainSecret("secret") user.FsConfig.S3Config.Endpoint = "http://localhost:9000" user.FsConfig.S3Config.KeyPrefix = "somedir/subdir" user, _, err = httpd.UpdateUser(user, http.StatusOK, "") @@ -1150,8 +1145,7 @@ func TestUserGCSConfig(t *testing.T) { user.FsConfig.S3Config = vfs.S3FsConfig{} user.FsConfig.Provider = dataprovider.GCSFilesystemProvider user.FsConfig.GCSConfig.Bucket = "test1" - user.FsConfig.GCSConfig.Credentials.Payload = "fake credentials" - user.FsConfig.GCSConfig.Credentials.Status = vfs.SecretStatusPlain + user.FsConfig.GCSConfig.Credentials = kms.NewPlainSecret("fake credentials") user, _, err = httpd.UpdateUser(user, http.StatusOK, "") assert.NoError(t, err) @@ -1165,49 +1159,42 @@ func TestUserAzureBlobConfig(t *testing.T) { user.FsConfig.Provider = dataprovider.AzureBlobFilesystemProvider user.FsConfig.AzBlobConfig.Container = "test" user.FsConfig.AzBlobConfig.AccountName = "Server-Account-Name" - user.FsConfig.AzBlobConfig.AccountKey.Payload = "Server-Account-Key" - user.FsConfig.AzBlobConfig.AccountKey.Status = vfs.SecretStatusPlain + user.FsConfig.AzBlobConfig.AccountKey = kms.NewPlainSecret("Server-Account-Key") user.FsConfig.AzBlobConfig.Endpoint = "http://127.0.0.1:9000" user.FsConfig.AzBlobConfig.UploadPartSize = 8 user, _, err = httpd.UpdateUser(user, http.StatusOK, "") assert.NoError(t, err) - initialPayload := user.FsConfig.AzBlobConfig.AccountKey.Payload - assert.Equal(t, vfs.SecretStatusAES256GCM, user.FsConfig.AzBlobConfig.AccountKey.Status) + initialPayload := user.FsConfig.AzBlobConfig.AccountKey.GetPayload() + assert.Equal(t, kms.SecretStatusSecretBox, user.FsConfig.AzBlobConfig.AccountKey.GetStatus()) assert.NotEmpty(t, initialPayload) - assert.Empty(t, user.FsConfig.AzBlobConfig.AccountKey.AdditionalData) - assert.Empty(t, user.FsConfig.AzBlobConfig.AccountKey.Key) - user.FsConfig.AzBlobConfig.AccountKey.Status = vfs.SecretStatusAES256GCM - user.FsConfig.AzBlobConfig.AccountKey.AdditionalData = "data" - user.FsConfig.AzBlobConfig.AccountKey.Key = "fake key" + assert.Empty(t, user.FsConfig.AzBlobConfig.AccountKey.GetAdditionalData()) + assert.Empty(t, user.FsConfig.AzBlobConfig.AccountKey.GetKey()) + user.FsConfig.AzBlobConfig.AccountKey.SetStatus(kms.SecretStatusSecretBox) + user.FsConfig.AzBlobConfig.AccountKey.SetAdditionalData("data") + user.FsConfig.AzBlobConfig.AccountKey.SetKey("fake key") user, _, err = httpd.UpdateUser(user, http.StatusOK, "") assert.NoError(t, err) - assert.Equal(t, vfs.SecretStatusAES256GCM, user.FsConfig.AzBlobConfig.AccountKey.Status) - assert.Equal(t, initialPayload, user.FsConfig.AzBlobConfig.AccountKey.Payload) - assert.Empty(t, user.FsConfig.AzBlobConfig.AccountKey.AdditionalData) - assert.Empty(t, user.FsConfig.AzBlobConfig.AccountKey.Key) + assert.Equal(t, kms.SecretStatusSecretBox, user.FsConfig.AzBlobConfig.AccountKey.GetStatus()) + assert.Equal(t, initialPayload, user.FsConfig.AzBlobConfig.AccountKey.GetPayload()) + assert.Empty(t, user.FsConfig.AzBlobConfig.AccountKey.GetAdditionalData()) + assert.Empty(t, user.FsConfig.AzBlobConfig.AccountKey.GetKey()) _, err = httpd.RemoveUser(user, http.StatusOK) assert.NoError(t, err) user.Password = defaultPassword user.ID = 0 - secret := vfs.Secret{ - Payload: "Server-Account-Key", - Status: vfs.SecretStatusAES256GCM, - } + secret := kms.NewSecret(kms.SecretStatusSecretBox, "Server-Account-Key", "", "") user.FsConfig.AzBlobConfig.AccountKey = secret _, _, err = httpd.AddUser(user, http.StatusOK) assert.Error(t, err) - user.FsConfig.AzBlobConfig.AccountKey = vfs.Secret{ - Payload: "Server-Account-Key-Test", - Status: vfs.SecretStatusPlain, - } + user.FsConfig.AzBlobConfig.AccountKey = kms.NewPlainSecret("Server-Account-Key-Test") user, _, err = httpd.AddUser(user, http.StatusOK) assert.NoError(t, err) - initialPayload = user.FsConfig.AzBlobConfig.AccountKey.Payload - assert.Equal(t, vfs.SecretStatusAES256GCM, user.FsConfig.AzBlobConfig.AccountKey.Status) + initialPayload = user.FsConfig.AzBlobConfig.AccountKey.GetPayload() + assert.Equal(t, kms.SecretStatusSecretBox, user.FsConfig.AzBlobConfig.AccountKey.GetStatus()) assert.NotEmpty(t, initialPayload) - assert.Empty(t, user.FsConfig.AzBlobConfig.AccountKey.AdditionalData) - assert.Empty(t, user.FsConfig.AzBlobConfig.AccountKey.Key) + assert.Empty(t, user.FsConfig.AzBlobConfig.AccountKey.GetAdditionalData()) + assert.Empty(t, user.FsConfig.AzBlobConfig.AccountKey.GetKey()) user.FsConfig.Provider = dataprovider.AzureBlobFilesystemProvider user.FsConfig.AzBlobConfig.Container = "test-container" user.FsConfig.AzBlobConfig.Endpoint = "http://localhost:9001" @@ -1215,16 +1202,16 @@ func TestUserAzureBlobConfig(t *testing.T) { user.FsConfig.AzBlobConfig.UploadConcurrency = 5 user, _, err = httpd.UpdateUser(user, http.StatusOK, "") assert.NoError(t, err) - assert.Equal(t, vfs.SecretStatusAES256GCM, user.FsConfig.AzBlobConfig.AccountKey.Status) + assert.Equal(t, kms.SecretStatusSecretBox, user.FsConfig.AzBlobConfig.AccountKey.GetStatus()) assert.NotEmpty(t, initialPayload) - assert.Empty(t, user.FsConfig.AzBlobConfig.AccountKey.AdditionalData) - assert.Empty(t, user.FsConfig.AzBlobConfig.AccountKey.Key) + assert.Empty(t, user.FsConfig.AzBlobConfig.AccountKey.GetAdditionalData()) + assert.Empty(t, user.FsConfig.AzBlobConfig.AccountKey.GetKey()) // test user without access key and access secret (sas) user.FsConfig.Provider = dataprovider.AzureBlobFilesystemProvider user.FsConfig.AzBlobConfig.SASURL = "https://myaccount.blob.core.windows.net/pictures/profile.jpg?sv=2012-02-12&st=2009-02-09&se=2009-02-10&sr=c&sp=r&si=YWJjZGVmZw%3d%3d&sig=dD80ihBh5jfNpymO5Hg1IdiJIEvHcJpCMiCMnN%2fRnbI%3d" user.FsConfig.AzBlobConfig.KeyPrefix = "somedir/subdir" user.FsConfig.AzBlobConfig.AccountName = "" - user.FsConfig.AzBlobConfig.AccountKey = vfs.Secret{} + user.FsConfig.AzBlobConfig.AccountKey = kms.NewEmptySecret() user.FsConfig.AzBlobConfig.UploadPartSize = 6 user.FsConfig.AzBlobConfig.UploadConcurrency = 4 user, _, err = httpd.UpdateUser(user, http.StatusOK, "") @@ -1260,8 +1247,7 @@ func TestUserHiddenFields(t *testing.T) { u1.FsConfig.S3Config.Bucket = "test" u1.FsConfig.S3Config.Region = "us-east-1" u1.FsConfig.S3Config.AccessKey = "S3-Access-Key" - u1.FsConfig.S3Config.AccessSecret.Payload = "S3-Access-Secret" - u1.FsConfig.S3Config.AccessSecret.Status = vfs.SecretStatusPlain + u1.FsConfig.S3Config.AccessSecret = kms.NewPlainSecret("S3-Access-Secret") user1, _, err := httpd.AddUser(u1, http.StatusOK) assert.NoError(t, err) @@ -1269,8 +1255,7 @@ func TestUserHiddenFields(t *testing.T) { u2.Username = usernames[1] u2.FsConfig.Provider = dataprovider.GCSFilesystemProvider u2.FsConfig.GCSConfig.Bucket = "test" - u2.FsConfig.GCSConfig.Credentials.Payload = "fake credentials" - u2.FsConfig.GCSConfig.Credentials.Status = vfs.SecretStatusPlain + u2.FsConfig.GCSConfig.Credentials = kms.NewPlainSecret("fake credentials") user2, _, err := httpd.AddUser(u2, http.StatusOK) assert.NoError(t, err) @@ -1279,8 +1264,7 @@ func TestUserHiddenFields(t *testing.T) { u3.FsConfig.Provider = dataprovider.AzureBlobFilesystemProvider u3.FsConfig.AzBlobConfig.Container = "test" u3.FsConfig.AzBlobConfig.AccountName = "Server-Account-Name" - u3.FsConfig.AzBlobConfig.AccountKey.Payload = "Server-Account-Key" - u3.FsConfig.AzBlobConfig.AccountKey.Status = vfs.SecretStatusPlain + u3.FsConfig.AzBlobConfig.AccountKey = kms.NewPlainSecret("Server-Account-Key") user3, _, err := httpd.AddUser(u3, http.StatusOK) assert.NoError(t, err) @@ -1298,69 +1282,69 @@ func TestUserHiddenFields(t *testing.T) { user1, _, err = httpd.GetUserByID(user1.ID, http.StatusOK) assert.NoError(t, err) assert.Empty(t, user1.Password) - assert.Empty(t, user1.FsConfig.S3Config.AccessSecret.Key) - assert.Empty(t, user1.FsConfig.S3Config.AccessSecret.AdditionalData) - assert.NotEmpty(t, user1.FsConfig.S3Config.AccessSecret.Status) - assert.NotEmpty(t, user1.FsConfig.S3Config.AccessSecret.Payload) + assert.Empty(t, user1.FsConfig.S3Config.AccessSecret.GetKey()) + assert.Empty(t, user1.FsConfig.S3Config.AccessSecret.GetAdditionalData()) + assert.NotEmpty(t, user1.FsConfig.S3Config.AccessSecret.GetStatus()) + assert.NotEmpty(t, user1.FsConfig.S3Config.AccessSecret.GetPayload()) user2, _, err = httpd.GetUserByID(user2.ID, http.StatusOK) assert.NoError(t, err) assert.Empty(t, user2.Password) - assert.Empty(t, user2.FsConfig.GCSConfig.Credentials.Key) - assert.Empty(t, user2.FsConfig.GCSConfig.Credentials.AdditionalData) - assert.NotEmpty(t, user2.FsConfig.GCSConfig.Credentials.Status) - assert.NotEmpty(t, user2.FsConfig.GCSConfig.Credentials.Payload) + assert.Empty(t, user2.FsConfig.GCSConfig.Credentials.GetKey()) + assert.Empty(t, user2.FsConfig.GCSConfig.Credentials.GetAdditionalData()) + assert.NotEmpty(t, user2.FsConfig.GCSConfig.Credentials.GetStatus()) + assert.NotEmpty(t, user2.FsConfig.GCSConfig.Credentials.GetPayload()) user3, _, err = httpd.GetUserByID(user3.ID, http.StatusOK) assert.NoError(t, err) assert.Empty(t, user3.Password) - assert.Empty(t, user3.FsConfig.AzBlobConfig.AccountKey.Key) - assert.Empty(t, user3.FsConfig.AzBlobConfig.AccountKey.AdditionalData) - assert.NotEmpty(t, user3.FsConfig.AzBlobConfig.AccountKey.Status) - assert.NotEmpty(t, user3.FsConfig.AzBlobConfig.AccountKey.Payload) + assert.Empty(t, user3.FsConfig.AzBlobConfig.AccountKey.GetKey()) + assert.Empty(t, user3.FsConfig.AzBlobConfig.AccountKey.GetAdditionalData()) + assert.NotEmpty(t, user3.FsConfig.AzBlobConfig.AccountKey.GetStatus()) + assert.NotEmpty(t, user3.FsConfig.AzBlobConfig.AccountKey.GetPayload()) // finally check that we have all the data inside the data provider user1, err = dataprovider.GetUserByID(user1.ID) assert.NoError(t, err) assert.NotEmpty(t, user1.Password) - assert.NotEmpty(t, user1.FsConfig.S3Config.AccessSecret.Key) - assert.NotEmpty(t, user1.FsConfig.S3Config.AccessSecret.AdditionalData) - assert.NotEmpty(t, user1.FsConfig.S3Config.AccessSecret.Status) - assert.NotEmpty(t, user1.FsConfig.S3Config.AccessSecret.Payload) + assert.NotEmpty(t, user1.FsConfig.S3Config.AccessSecret.GetKey()) + assert.NotEmpty(t, user1.FsConfig.S3Config.AccessSecret.GetAdditionalData()) + assert.NotEmpty(t, user1.FsConfig.S3Config.AccessSecret.GetStatus()) + assert.NotEmpty(t, user1.FsConfig.S3Config.AccessSecret.GetPayload()) err = user1.FsConfig.S3Config.AccessSecret.Decrypt() assert.NoError(t, err) - assert.Equal(t, vfs.SecretStatusPlain, user1.FsConfig.S3Config.AccessSecret.Status) - assert.Equal(t, u1.FsConfig.S3Config.AccessSecret.Payload, user1.FsConfig.S3Config.AccessSecret.Payload) - assert.Empty(t, user1.FsConfig.S3Config.AccessSecret.Key) - assert.Empty(t, user1.FsConfig.S3Config.AccessSecret.AdditionalData) + assert.Equal(t, kms.SecretStatusPlain, user1.FsConfig.S3Config.AccessSecret.GetStatus()) + assert.Equal(t, u1.FsConfig.S3Config.AccessSecret.GetPayload(), user1.FsConfig.S3Config.AccessSecret.GetPayload()) + assert.Empty(t, user1.FsConfig.S3Config.AccessSecret.GetKey()) + assert.Empty(t, user1.FsConfig.S3Config.AccessSecret.GetAdditionalData()) user2, err = dataprovider.GetUserByID(user2.ID) assert.NoError(t, err) assert.NotEmpty(t, user2.Password) - assert.NotEmpty(t, user2.FsConfig.GCSConfig.Credentials.Key) - assert.NotEmpty(t, user2.FsConfig.GCSConfig.Credentials.AdditionalData) - assert.NotEmpty(t, user2.FsConfig.GCSConfig.Credentials.Status) - assert.NotEmpty(t, user2.FsConfig.GCSConfig.Credentials.Payload) + assert.NotEmpty(t, user2.FsConfig.GCSConfig.Credentials.GetKey()) + assert.NotEmpty(t, user2.FsConfig.GCSConfig.Credentials.GetAdditionalData()) + assert.NotEmpty(t, user2.FsConfig.GCSConfig.Credentials.GetStatus()) + assert.NotEmpty(t, user2.FsConfig.GCSConfig.Credentials.GetPayload()) err = user2.FsConfig.GCSConfig.Credentials.Decrypt() assert.NoError(t, err) - assert.Equal(t, vfs.SecretStatusPlain, user2.FsConfig.GCSConfig.Credentials.Status) - assert.Equal(t, u2.FsConfig.GCSConfig.Credentials.Payload, user2.FsConfig.GCSConfig.Credentials.Payload) - assert.Empty(t, user2.FsConfig.GCSConfig.Credentials.Key) - assert.Empty(t, user2.FsConfig.GCSConfig.Credentials.AdditionalData) + assert.Equal(t, kms.SecretStatusPlain, user2.FsConfig.GCSConfig.Credentials.GetStatus()) + assert.Equal(t, u2.FsConfig.GCSConfig.Credentials.GetPayload(), user2.FsConfig.GCSConfig.Credentials.GetPayload()) + assert.Empty(t, user2.FsConfig.GCSConfig.Credentials.GetKey()) + assert.Empty(t, user2.FsConfig.GCSConfig.Credentials.GetAdditionalData()) user3, err = dataprovider.GetUserByID(user3.ID) assert.NoError(t, err) assert.NotEmpty(t, user3.Password) - assert.NotEmpty(t, user3.FsConfig.AzBlobConfig.AccountKey.Key) - assert.NotEmpty(t, user3.FsConfig.AzBlobConfig.AccountKey.AdditionalData) - assert.NotEmpty(t, user3.FsConfig.AzBlobConfig.AccountKey.Status) - assert.NotEmpty(t, user3.FsConfig.AzBlobConfig.AccountKey.Payload) + assert.NotEmpty(t, user3.FsConfig.AzBlobConfig.AccountKey.GetKey()) + assert.NotEmpty(t, user3.FsConfig.AzBlobConfig.AccountKey.GetAdditionalData()) + assert.NotEmpty(t, user3.FsConfig.AzBlobConfig.AccountKey.GetStatus()) + assert.NotEmpty(t, user3.FsConfig.AzBlobConfig.AccountKey.GetPayload()) err = user3.FsConfig.AzBlobConfig.AccountKey.Decrypt() assert.NoError(t, err) - assert.Equal(t, vfs.SecretStatusPlain, user3.FsConfig.AzBlobConfig.AccountKey.Status) - assert.Equal(t, u3.FsConfig.AzBlobConfig.AccountKey.Payload, user3.FsConfig.AzBlobConfig.AccountKey.Payload) - assert.Empty(t, user3.FsConfig.AzBlobConfig.AccountKey.Key) - assert.Empty(t, user3.FsConfig.AzBlobConfig.AccountKey.AdditionalData) + assert.Equal(t, kms.SecretStatusPlain, user3.FsConfig.AzBlobConfig.AccountKey.GetStatus()) + assert.Equal(t, u3.FsConfig.AzBlobConfig.AccountKey.GetPayload(), user3.FsConfig.AzBlobConfig.AccountKey.GetPayload()) + assert.Empty(t, user3.FsConfig.AzBlobConfig.AccountKey.GetKey()) + assert.Empty(t, user3.FsConfig.AzBlobConfig.AccountKey.GetAdditionalData()) _, err = httpd.RemoveUser(user1, http.StatusOK) assert.NoError(t, err) @@ -1382,31 +1366,28 @@ func TestUserHiddenFields(t *testing.T) { } func TestSecretObject(t *testing.T) { - s := vfs.Secret{ - Status: vfs.SecretStatusPlain, - Payload: "test data", - AdditionalData: "username", - } + s := kms.NewPlainSecret("test data") + s.SetAdditionalData("username") require.True(t, s.IsValid()) err := s.Encrypt() require.NoError(t, err) - require.Equal(t, vfs.SecretStatusAES256GCM, s.Status) - require.NotEmpty(t, s.Payload) - require.NotEmpty(t, s.Key) + require.Equal(t, kms.SecretStatusSecretBox, s.GetStatus()) + require.NotEmpty(t, s.GetPayload()) + require.NotEmpty(t, s.GetKey()) require.True(t, s.IsValid()) err = s.Decrypt() require.NoError(t, err) - require.Equal(t, vfs.SecretStatusPlain, s.Status) - require.Equal(t, "test data", s.Payload) - require.Empty(t, s.Key) + require.Equal(t, kms.SecretStatusPlain, s.GetStatus()) + require.Equal(t, "test data", s.GetPayload()) + require.Empty(t, s.GetKey()) oldFormat := "$aes$5b97e3a3324a2f53e2357483383367c0$0ed3132b584742ab217866219da633266782b69b13e50ebc6ddfb7c4fbf2f2a414c6d5f813" - s, err = vfs.GetSecretFromCompatString(oldFormat) + s, err = kms.GetSecretFromCompatString(oldFormat) require.NoError(t, err) require.True(t, s.IsValid()) - require.Equal(t, vfs.SecretStatusPlain, s.Status) - require.Equal(t, "test data", s.Payload) - require.Empty(t, s.Key) + require.Equal(t, kms.SecretStatusPlain, s.GetStatus()) + require.Equal(t, "test data", s.GetPayload()) + require.Empty(t, s.GetKey()) } func TestUpdateUserNoCredentials(t *testing.T) { @@ -3003,8 +2984,7 @@ func TestWebUserS3Mock(t *testing.T) { user.FsConfig.S3Config.Bucket = "test" user.FsConfig.S3Config.Region = "eu-west-1" user.FsConfig.S3Config.AccessKey = "access-key" - user.FsConfig.S3Config.AccessSecret.Payload = "access-secret" - user.FsConfig.S3Config.AccessSecret.Status = vfs.SecretStatusPlain + user.FsConfig.S3Config.AccessSecret = kms.NewPlainSecret("access-secret") user.FsConfig.S3Config.Endpoint = "http://127.0.0.1:9000/path?a=b" user.FsConfig.S3Config.StorageClass = "Standard" user.FsConfig.S3Config.KeyPrefix = "somedir/subdir/" @@ -3030,7 +3010,7 @@ func TestWebUserS3Mock(t *testing.T) { form.Set("s3_bucket", user.FsConfig.S3Config.Bucket) form.Set("s3_region", user.FsConfig.S3Config.Region) form.Set("s3_access_key", user.FsConfig.S3Config.AccessKey) - form.Set("s3_access_secret", user.FsConfig.S3Config.AccessSecret.Payload) + form.Set("s3_access_secret", user.FsConfig.S3Config.AccessSecret.GetPayload()) form.Set("s3_storage_class", user.FsConfig.S3Config.StorageClass) form.Set("s3_endpoint", user.FsConfig.S3Config.Endpoint) form.Set("s3_key_prefix", user.FsConfig.S3Config.KeyPrefix) @@ -3077,10 +3057,10 @@ func TestWebUserS3Mock(t *testing.T) { assert.Equal(t, updateUser.FsConfig.S3Config.UploadPartSize, user.FsConfig.S3Config.UploadPartSize) assert.Equal(t, updateUser.FsConfig.S3Config.UploadConcurrency, user.FsConfig.S3Config.UploadConcurrency) assert.Equal(t, 2, len(updateUser.Filters.FileExtensions)) - assert.Equal(t, vfs.SecretStatusAES256GCM, updateUser.FsConfig.S3Config.AccessSecret.Status) - assert.NotEmpty(t, updateUser.FsConfig.S3Config.AccessSecret.Payload) - assert.Empty(t, updateUser.FsConfig.S3Config.AccessSecret.Key) - assert.Empty(t, updateUser.FsConfig.S3Config.AccessSecret.AdditionalData) + assert.Equal(t, kms.SecretStatusSecretBox, updateUser.FsConfig.S3Config.AccessSecret.GetStatus()) + assert.NotEmpty(t, updateUser.FsConfig.S3Config.AccessSecret.GetPayload()) + assert.Empty(t, updateUser.FsConfig.S3Config.AccessSecret.GetKey()) + assert.Empty(t, updateUser.FsConfig.S3Config.AccessSecret.GetAdditionalData()) // now check that a redacted password is not saved form.Set("s3_access_secret", "[**redacted**] ") b, contentType, _ = getMultipartFormData(form, "", "") @@ -3096,10 +3076,10 @@ func TestWebUserS3Mock(t *testing.T) { assert.NoError(t, err) assert.Equal(t, 1, len(users)) lastUpdatedUser := users[0] - assert.Equal(t, vfs.SecretStatusAES256GCM, lastUpdatedUser.FsConfig.S3Config.AccessSecret.Status) - assert.Equal(t, updateUser.FsConfig.S3Config.AccessSecret.Payload, lastUpdatedUser.FsConfig.S3Config.AccessSecret.Payload) - assert.Empty(t, lastUpdatedUser.FsConfig.S3Config.AccessSecret.Key) - assert.Empty(t, lastUpdatedUser.FsConfig.S3Config.AccessSecret.AdditionalData) + assert.Equal(t, kms.SecretStatusSecretBox, lastUpdatedUser.FsConfig.S3Config.AccessSecret.GetStatus()) + assert.Equal(t, updateUser.FsConfig.S3Config.AccessSecret.GetPayload(), lastUpdatedUser.FsConfig.S3Config.AccessSecret.GetPayload()) + assert.Empty(t, lastUpdatedUser.FsConfig.S3Config.AccessSecret.GetKey()) + assert.Empty(t, lastUpdatedUser.FsConfig.S3Config.AccessSecret.GetAdditionalData()) // now clear credentials form.Set("s3_access_key", "") form.Set("s3_access_secret", "") @@ -3222,8 +3202,7 @@ func TestWebUserAzureBlobMock(t *testing.T) { user.FsConfig.Provider = dataprovider.AzureBlobFilesystemProvider user.FsConfig.AzBlobConfig.Container = "container" user.FsConfig.AzBlobConfig.AccountName = "aname" - user.FsConfig.AzBlobConfig.AccountKey.Payload = "access-skey" - user.FsConfig.AzBlobConfig.AccountKey.Status = vfs.SecretStatusPlain + user.FsConfig.AzBlobConfig.AccountKey = kms.NewPlainSecret("access-skey") user.FsConfig.AzBlobConfig.Endpoint = "http://127.0.0.1:9000/path?b=c" user.FsConfig.AzBlobConfig.KeyPrefix = "somedir/subdir/" user.FsConfig.AzBlobConfig.UploadPartSize = 5 @@ -3248,7 +3227,7 @@ func TestWebUserAzureBlobMock(t *testing.T) { form.Set("fs_provider", "3") form.Set("az_container", user.FsConfig.AzBlobConfig.Container) form.Set("az_account_name", user.FsConfig.AzBlobConfig.AccountName) - form.Set("az_account_key", user.FsConfig.AzBlobConfig.AccountKey.Payload) + form.Set("az_account_key", user.FsConfig.AzBlobConfig.AccountKey.GetPayload()) form.Set("az_sas_url", user.FsConfig.AzBlobConfig.SASURL) form.Set("az_endpoint", user.FsConfig.AzBlobConfig.Endpoint) form.Set("az_key_prefix", user.FsConfig.AzBlobConfig.KeyPrefix) @@ -3295,10 +3274,10 @@ func TestWebUserAzureBlobMock(t *testing.T) { assert.Equal(t, updateUser.FsConfig.AzBlobConfig.UploadPartSize, user.FsConfig.AzBlobConfig.UploadPartSize) assert.Equal(t, updateUser.FsConfig.AzBlobConfig.UploadConcurrency, user.FsConfig.AzBlobConfig.UploadConcurrency) assert.Equal(t, 2, len(updateUser.Filters.FileExtensions)) - assert.Equal(t, vfs.SecretStatusAES256GCM, updateUser.FsConfig.AzBlobConfig.AccountKey.Status) - assert.NotEmpty(t, updateUser.FsConfig.AzBlobConfig.AccountKey.Payload) - assert.Empty(t, updateUser.FsConfig.AzBlobConfig.AccountKey.Key) - assert.Empty(t, updateUser.FsConfig.AzBlobConfig.AccountKey.AdditionalData) + assert.Equal(t, kms.SecretStatusSecretBox, updateUser.FsConfig.AzBlobConfig.AccountKey.GetStatus()) + assert.NotEmpty(t, updateUser.FsConfig.AzBlobConfig.AccountKey.GetPayload()) + assert.Empty(t, updateUser.FsConfig.AzBlobConfig.AccountKey.GetKey()) + assert.Empty(t, updateUser.FsConfig.AzBlobConfig.AccountKey.GetAdditionalData()) // now check that a redacted password is not saved form.Set("az_account_key", "[**redacted**] ") b, contentType, _ = getMultipartFormData(form, "", "") @@ -3314,10 +3293,10 @@ func TestWebUserAzureBlobMock(t *testing.T) { assert.NoError(t, err) assert.Equal(t, 1, len(users)) lastUpdatedUser := users[0] - assert.Equal(t, vfs.SecretStatusAES256GCM, lastUpdatedUser.FsConfig.AzBlobConfig.AccountKey.Status) - assert.Equal(t, updateUser.FsConfig.AzBlobConfig.AccountKey.Payload, lastUpdatedUser.FsConfig.AzBlobConfig.AccountKey.Payload) - assert.Empty(t, lastUpdatedUser.FsConfig.AzBlobConfig.AccountKey.Key) - assert.Empty(t, lastUpdatedUser.FsConfig.AzBlobConfig.AccountKey.AdditionalData) + assert.Equal(t, kms.SecretStatusSecretBox, lastUpdatedUser.FsConfig.AzBlobConfig.AccountKey.GetStatus()) + assert.Equal(t, updateUser.FsConfig.AzBlobConfig.AccountKey.GetPayload(), lastUpdatedUser.FsConfig.AzBlobConfig.AccountKey.GetPayload()) + assert.Empty(t, lastUpdatedUser.FsConfig.AzBlobConfig.AccountKey.GetKey()) + assert.Empty(t, lastUpdatedUser.FsConfig.AzBlobConfig.AccountKey.GetAdditionalData()) req, _ = http.NewRequest(http.MethodDelete, userPath+"/"+strconv.FormatInt(user.ID, 10), nil) rr = executeRequest(req) checkResponseCode(t, http.StatusOK, rr.Code) @@ -3546,3 +3525,14 @@ func getMultipartFormData(values url.Values, fileFieldName, filePath string) (by err := w.Close() return b, w.FormDataContentType(), err } + +func BenchmarkSecretDecryption(b *testing.B) { + s := kms.NewPlainSecret("test data") + s.SetAdditionalData("username") + err := s.Encrypt() + require.NoError(b, err) + for i := 0; i < b.N; i++ { + err = s.Clone().Decrypt() + require.NoError(b, err) + } +} diff --git a/httpd/internal_test.go b/httpd/internal_test.go index 2852752f..58238e76 100644 --- a/httpd/internal_test.go +++ b/httpd/internal_test.go @@ -19,6 +19,7 @@ import ( "github.com/drakkan/sftpgo/common" "github.com/drakkan/sftpgo/dataprovider" + "github.com/drakkan/sftpgo/kms" "github.com/drakkan/sftpgo/utils" "github.com/drakkan/sftpgo/vfs" ) @@ -318,8 +319,11 @@ func TestCompareUserFields(t *testing.T) { } func TestCompareUserFsConfig(t *testing.T) { + secretString := "access secret" expected := &dataprovider.User{} actual := &dataprovider.User{} + expected.SetEmptySecretsIfNil() + actual.SetEmptySecretsIfNil() expected.FsConfig.Provider = dataprovider.S3FilesystemProvider err := compareUserFsConfig(expected, actual) assert.Error(t, err) @@ -336,36 +340,36 @@ func TestCompareUserFsConfig(t *testing.T) { err = compareUserFsConfig(expected, actual) assert.Error(t, err) expected.FsConfig.S3Config.AccessKey = "" - actual.FsConfig.S3Config.AccessSecret.Payload = "access secret" + actual.FsConfig.S3Config.AccessSecret = kms.NewPlainSecret(secretString) err = compareUserFsConfig(expected, actual) assert.Error(t, err) - secret, _ := utils.EncryptData("access secret") - actual.FsConfig.S3Config.AccessSecret.Payload = "" - expected.FsConfig.S3Config.AccessSecret.Payload = secret + secret, err := utils.EncryptData(secretString) + assert.NoError(t, err) + actual.FsConfig.S3Config.AccessSecret = kms.NewEmptySecret() + kmsSecret, err := kms.GetSecretFromCompatString(secret) + assert.NoError(t, err) + expected.FsConfig.S3Config.AccessSecret = kmsSecret err = compareUserFsConfig(expected, actual) assert.Error(t, err) - expected.FsConfig.S3Config.AccessSecret.Payload = "test" - actual.FsConfig.S3Config.AccessSecret.Payload = "" + expected.FsConfig.S3Config.AccessSecret = kms.NewPlainSecret(secretString) + actual.FsConfig.S3Config.AccessSecret = kms.NewEmptySecret() err = compareUserFsConfig(expected, actual) assert.Error(t, err) - expected.FsConfig.S3Config.AccessSecret.Status = vfs.SecretStatusPlain - actual.FsConfig.S3Config.AccessSecret.Status = vfs.SecretStatusAES256GCM + expected.FsConfig.S3Config.AccessSecret = kms.NewPlainSecret(secretString) + actual.FsConfig.S3Config.AccessSecret = kms.NewSecret(kms.SecretStatusSecretBox, "", "", "") err = compareUserFsConfig(expected, actual) assert.Error(t, err) - actual.FsConfig.S3Config.AccessSecret.Payload = "payload" - actual.FsConfig.S3Config.AccessSecret.AdditionalData = "data" + actual.FsConfig.S3Config.AccessSecret = kms.NewSecret(kms.SecretStatusSecretBox, secretString, "", "data") err = compareUserFsConfig(expected, actual) assert.Error(t, err) - actual.FsConfig.S3Config.AccessSecret.AdditionalData = "" - actual.FsConfig.S3Config.AccessSecret.Key = "key" + actual.FsConfig.S3Config.AccessSecret = kms.NewSecret(kms.SecretStatusSecretBox, secretString, "key", "") err = compareUserFsConfig(expected, actual) assert.Error(t, err) - expected.FsConfig.S3Config.AccessSecret.Status = "" - expected.FsConfig.S3Config.AccessSecret.Payload = "" - actual.FsConfig.S3Config.AccessSecret.Status = "" - actual.FsConfig.S3Config.AccessSecret.Payload = "" - actual.FsConfig.S3Config.AccessSecret.AdditionalData = "" - actual.FsConfig.S3Config.AccessSecret.Key = "" + expected.FsConfig.S3Config.AccessSecret = nil + err = compareUserFsConfig(expected, actual) + assert.Error(t, err) + expected.FsConfig.S3Config.AccessSecret = kms.NewEmptySecret() + actual.FsConfig.S3Config.AccessSecret = kms.NewEmptySecret() expected.FsConfig.S3Config.Endpoint = "http://127.0.0.1:9000/" err = compareUserFsConfig(expected, actual) assert.Error(t, err) @@ -419,10 +423,10 @@ func TestCompareUserAzureConfig(t *testing.T) { err = compareUserFsConfig(expected, actual) assert.Error(t, err) expected.FsConfig.AzBlobConfig.AccountName = "" - expected.FsConfig.AzBlobConfig.AccountKey.Payload = "akey" + expected.FsConfig.AzBlobConfig.AccountKey = kms.NewSecret(kms.SecretStatusAWS, "payload", "", "") err = compareUserFsConfig(expected, actual) assert.Error(t, err) - expected.FsConfig.AzBlobConfig.AccountKey.Payload = "" + expected.FsConfig.AzBlobConfig.AccountKey = kms.NewEmptySecret() expected.FsConfig.AzBlobConfig.Endpoint = "endpt" err = compareUserFsConfig(expected, actual) assert.Error(t, err) diff --git a/httpd/schema/openapi.yaml b/httpd/schema/openapi.yaml index b7561b9a..2b5a9352 100644 --- a/httpd/schema/openapi.yaml +++ b/httpd/schema/openapi.yaml @@ -2,7 +2,7 @@ openapi: 3.0.3 info: title: SFTPGo description: 'SFTPGo REST API' - version: 2.1.1 + version: 2.1.2 servers: - url: /api/v1 @@ -965,6 +965,10 @@ components: enum: - Plain - AES-256-GCM + - Secretbox + - GCP + - AWS + - VaultTransit - Redacted description: Set to "Plain" to add or update an existing secret, set to "Redacted" to preserve the existing value payload: @@ -1234,7 +1238,7 @@ components: last_login: type: integer format: int64 - description: Last user login as unix timestamp in milliseconds + description: Last user login as unix timestamp in milliseconds. It is saved at most once every 10 minutes filters: $ref: '#/components/schemas/UserFilters' filesystem: diff --git a/httpd/web.go b/httpd/web.go index 00d9457e..0d37f5e0 100644 --- a/httpd/web.go +++ b/httpd/web.go @@ -16,6 +16,7 @@ import ( "github.com/drakkan/sftpgo/common" "github.com/drakkan/sftpgo/dataprovider" + "github.com/drakkan/sftpgo/kms" "github.com/drakkan/sftpgo/utils" "github.com/drakkan/sftpgo/version" "github.com/drakkan/sftpgo/vfs" @@ -205,6 +206,7 @@ func renderNotFoundPage(w http.ResponseWriter, err error) { } func renderAddUserPage(w http.ResponseWriter, user dataprovider.User, error string) { + user.SetEmptySecretsIfNil() data := userPage{ basePage: getBasePageData("Add a new user", webUserPath), IsAdd: true, @@ -222,6 +224,7 @@ func renderAddUserPage(w http.ResponseWriter, user dataprovider.User, error stri } func renderUpdateUserPage(w http.ResponseWriter, user dataprovider.User, error string) { + user.SetEmptySecretsIfNil() data := userPage{ basePage: getBasePageData("Update user", fmt.Sprintf("%v/%v", webUserPath, user.ID)), IsAdd: false, @@ -430,16 +433,13 @@ func getFiltersFromUserPostFields(r *http.Request) dataprovider.UserFilters { return filters } -func getSecretFromFormField(r *http.Request, field string) vfs.Secret { - secret := vfs.Secret{ - Payload: r.Form.Get(field), - Status: vfs.SecretStatusPlain, +func getSecretFromFormField(r *http.Request, field string) *kms.Secret { + secret := kms.NewPlainSecret(r.Form.Get(field)) + if strings.TrimSpace(secret.GetPayload()) == redactedSecret { + secret.SetStatus(kms.SecretStatusRedacted) } - if strings.TrimSpace(secret.Payload) == redactedSecret { - secret.Status = vfs.SecretStatusRedacted - } - if strings.TrimSpace(secret.Payload) == "" { - secret.Status = "" + if strings.TrimSpace(secret.GetPayload()) == "" { + secret.SetStatus("") } return secret } @@ -492,10 +492,7 @@ func getFsConfigFromUserPostFields(r *http.Request) (dataprovider.Filesystem, er } return fs, err } - fs.GCSConfig.Credentials = vfs.Secret{ - Status: vfs.SecretStatusPlain, - Payload: string(fileBytes), - } + fs.GCSConfig.Credentials = kms.NewPlainSecret(string(fileBytes)) fs.GCSConfig.AutomaticCredentials = 0 } else if fs.Provider == dataprovider.AzureBlobFilesystemProvider { fs.AzBlobConfig.Container = r.Form.Get("az_container") @@ -680,6 +677,7 @@ func handleWebUpdateUserPost(w http.ResponseWriter, r *http.Request) { return } updatedUser.ID = user.ID + updatedUser.SetEmptySecretsIfNil() if len(updatedUser.Password) == 0 { updatedUser.Password = user.Password } diff --git a/kms/aws.go b/kms/aws.go new file mode 100644 index 00000000..6f83ad3c --- /dev/null +++ b/kms/aws.go @@ -0,0 +1,42 @@ +package kms + +const ( + awsProviderName = "AWS" +) + +type awsSecret struct { + baseGCloudSecret +} + +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/basegocloud.go b/kms/basegocloud.go new file mode 100644 index 00000000..442ed6ec --- /dev/null +++ b/kms/basegocloud.go @@ -0,0 +1,99 @@ +package kms + +import ( + "context" + "encoding/base64" + "time" + + "gocloud.dev/secrets" + // import awskms package + _ "gocloud.dev/secrets/awskms" + // import gcpkms package + _ "gocloud.dev/secrets/gcpkms" + // import hashivault package + _ "gocloud.dev/secrets/hashivault" +) + +type baseGCloudSecret struct { + baseSecret + masterKey string + url string +} + +func (s *baseGCloudSecret) Encrypt() error { + if s.Status != SecretStatusPlain { + return errWrongSecretStatus + } + if s.Payload == "" { + return errInvalidSecret + } + + payload := s.Payload + key := "" + if s.masterKey != "" { + localSecret := newLocalSecret(s.baseSecret, s.masterKey) + err := localSecret.Encrypt() + if err != nil { + return err + } + payload = localSecret.GetPayload() + key = localSecret.GetKey() + } + + ctx, cancelFn := context.WithDeadline(context.Background(), time.Now().Add(defaultTimeout)) + defer cancelFn() + + keeper, err := secrets.OpenKeeper(ctx, s.url) + if err != nil { + return err + } + + defer keeper.Close() + ciphertext, err := keeper.Encrypt(context.Background(), []byte(payload)) + if err != nil { + return err + } + s.Payload = base64.StdEncoding.EncodeToString(ciphertext) + s.Key = key + return nil +} + +func (s *baseGCloudSecret) Decrypt() error { + encrypted, err := base64.StdEncoding.DecodeString(s.Payload) + if err != nil { + return err + } + ctx, cancelFn := context.WithDeadline(context.Background(), time.Now().Add(defaultTimeout)) + defer cancelFn() + + keeper, err := secrets.OpenKeeper(ctx, s.url) + if err != nil { + return err + } + + defer keeper.Close() + plaintext, err := keeper.Decrypt(context.Background(), encrypted) + if err != nil { + return err + } + payload := string(plaintext) + if s.Key != "" { + baseSecret := baseSecret{ + Status: SecretStatusSecretBox, + Payload: string(plaintext), + Key: s.Key, + AdditionalData: s.AdditionalData, + } + localSecret := newLocalSecret(baseSecret, s.masterKey) + err = localSecret.Decrypt() + if err != nil { + return err + } + payload = localSecret.GetPayload() + } + s.Status = SecretStatusPlain + s.Payload = payload + s.Key = "" + s.AdditionalData = "" + return nil +} diff --git a/kms/basesecret.go b/kms/basesecret.go new file mode 100644 index 00000000..a5790da0 --- /dev/null +++ b/kms/basesecret.go @@ -0,0 +1,53 @@ +package kms + +// 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"` + AdditionalData string `json:"additional_data,omitempty"` +} + +func (s *baseSecret) GetStatus() SecretStatus { + return s.Status +} + +func (s *baseSecret) GetPayload() string { + return s.Payload +} + +func (s *baseSecret) GetKey() string { + return s.Key +} + +func (s *baseSecret) GetAdditionalData() string { + return s.AdditionalData +} + +func (s *baseSecret) SetKey(value string) { + s.Key = value +} + +func (s *baseSecret) SetAdditionalData(value string) { + s.AdditionalData = value +} + +func (s *baseSecret) SetStatus(value SecretStatus) { + s.Status = value +} + +func (s *baseSecret) isEmpty() bool { + if s.Status != "" { + return false + } + if s.Payload != "" { + return false + } + if s.Key != "" { + return false + } + if s.AdditionalData != "" { + return false + } + return true +} diff --git a/kms/builtin.go b/kms/builtin.go new file mode 100644 index 00000000..1ac584b9 --- /dev/null +++ b/kms/builtin.go @@ -0,0 +1,122 @@ +package kms + +import ( + "crypto/aes" + "crypto/cipher" + "crypto/rand" + "encoding/hex" + "io" + + "github.com/minio/sha256-simd" +) + +const ( + builtinProviderName = "Builtin" +) + +type builtinSecret struct { + baseSecret +} + +func newBuiltinSecret(base baseSecret) SecretProvider { + return &builtinSecret{ + baseSecret: base, + } +} + +func (s *builtinSecret) Name() string { + return builtinProviderName +} + +func (s *builtinSecret) IsEncrypted() bool { + return s.Status == SecretStatusAES256GCM +} + +func (s *builtinSecret) deriveKey(key []byte) []byte { + var combined []byte + combined = append(combined, key...) + if s.AdditionalData != "" { + combined = append(combined, []byte(s.AdditionalData)...) + } + combined = append(combined, key...) + hash := sha256.Sum256(combined) + return hash[:] +} + +func (s *builtinSecret) Encrypt() error { + if s.Payload == "" { + return errInvalidSecret + } + switch s.Status { + case SecretStatusPlain: + key := make([]byte, 32) + if _, err := io.ReadFull(rand.Reader, key); err != nil { + return err + } + block, err := aes.NewCipher(s.deriveKey(key)) + if err != nil { + return err + } + gcm, err := cipher.NewGCM(block) + if err != nil { + return err + } + nonce := make([]byte, gcm.NonceSize()) + if _, err = io.ReadFull(rand.Reader, nonce); err != nil { + return err + } + var aad []byte + if s.AdditionalData != "" { + aad = []byte(s.AdditionalData) + } + ciphertext := gcm.Seal(nonce, nonce, []byte(s.Payload), aad) + s.Key = hex.EncodeToString(key) + s.Payload = hex.EncodeToString(ciphertext) + s.Status = SecretStatusAES256GCM + return nil + default: + return errWrongSecretStatus + } +} + +func (s *builtinSecret) Decrypt() error { + switch s.Status { + case SecretStatusAES256GCM: + encrypted, err := hex.DecodeString(s.Payload) + if err != nil { + return err + } + key, err := hex.DecodeString(s.Key) + if err != nil { + return err + } + block, err := aes.NewCipher(s.deriveKey(key)) + if err != nil { + return err + } + gcm, err := cipher.NewGCM(block) + if err != nil { + return err + } + nonceSize := gcm.NonceSize() + if len(encrypted) < nonceSize { + return errMalformedCiphertext + } + nonce, ciphertext := encrypted[:nonceSize], encrypted[nonceSize:] + var aad []byte + if s.AdditionalData != "" { + aad = []byte(s.AdditionalData) + } + plaintext, err := gcm.Open(nil, nonce, ciphertext, aad) + if err != nil { + return err + } + s.Status = SecretStatusPlain + s.Payload = string(plaintext) + s.Key = "" + s.AdditionalData = "" + return nil + default: + return errWrongSecretStatus + } +} diff --git a/kms/gcp.go b/kms/gcp.go new file mode 100644 index 00000000..158cce8a --- /dev/null +++ b/kms/gcp.go @@ -0,0 +1,42 @@ +package kms + +const ( + gcpProviderName = "GCP" +) + +type gcpSecret struct { + baseGCloudSecret +} + +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/kms.go b/kms/kms.go new file mode 100644 index 00000000..b80368bb --- /dev/null +++ b/kms/kms.go @@ -0,0 +1,327 @@ +package kms + +import ( + "encoding/json" + "errors" + "io/ioutil" + "strings" + "time" + + "github.com/drakkan/sftpgo/utils" +) + +// SecretProvider defines the interface for a KMS secrets provider +type SecretProvider interface { + Name() string + Encrypt() error + Decrypt() error + IsEncrypted() bool + GetStatus() SecretStatus + GetPayload() string + GetKey() string + GetAdditionalData() string + SetKey(string) + SetAdditionalData(string) + SetStatus(SecretStatus) +} + +// SecretStatus defines the statuses of a Secret object +type SecretStatus = string + +const ( + // SecretStatusPlain means the secret is in plain text and must be encrypted + SecretStatusPlain SecretStatus = "Plain" + // SecretStatusAES256GCM means the secret is encrypted using AES-256-GCM + SecretStatusAES256GCM SecretStatus = "AES-256-GCM" + // SecretStatusSecretBox means the secret is encrypted using a locally provided symmetric key + SecretStatusSecretBox SecretStatus = "Secretbox" + // SecretStatusGCP means we use keys from Google Cloud Platform’s Key Management Service + // (GCP KMS) to keep information secret + SecretStatusGCP SecretStatus = "GCP" + // SecretStatusAWS means we use customer master keys from Amazon Web Service’s + // Key Management Service (AWS KMS) to keep information secret + SecretStatusAWS SecretStatus = "AWS" + // SecretStatusVaultTransit means we use the transit secrets engine in Vault + // to keep information secret + SecretStatusVaultTransit SecretStatus = "VaultTransit" + // SecretStatusRedacted means the secret is redacted + SecretStatusRedacted SecretStatus = "Redacted" +) + +// Configuration defines the KMS configuration +type Configuration struct { + Secrets Secrets `json:"secrets" mapstructure:"secrets"` +} + +// Secrets define the KMS configuration for encryption/decryption +type Secrets struct { + URL string `json:"url" mapstructure:"url"` + MasterKeyPath string `json:"master_key_path" mapstructure:"master_key_path"` + masterKey string +} + +var ( + errWrongSecretStatus = errors.New("wrong secret status") + 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 +) + +// 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) +} + +// NewEmptySecret returns an empty secret +func NewEmptySecret() *Secret { + return NewSecret("", "", "", "") +} + +// NewPlainSecret stores the give payload in a plain text secret +func NewPlainSecret(payload string) *Secret { + return NewSecret(SecretStatusPlain, payload, "", "") +} + +// GetSecretFromCompatString returns a secret from the previous format +func GetSecretFromCompatString(secret string) (*Secret, error) { + plain, err := utils.DecryptData(secret) + if err != nil { + return &Secret{}, errMalformedCiphertext + } + return NewSecret(SecretStatusPlain, plain, "", ""), nil +} + +// Initialize configures the KMS support +func (c *Configuration) Initialize() error { + if c.Secrets.MasterKeyPath != "" { + mKey, err := ioutil.ReadFile(c.Secrets.MasterKeyPath) + if err != nil { + return err + } + c.Secrets.masterKey = strings.TrimSpace(string(mKey)) + } + config = *c + return nil +} + +func (c *Configuration) newSecret(status SecretStatus, payload, key, data string) *Secret { + base := baseSecret{ + Status: status, + Key: key, + Payload: payload, + AdditionalData: data, + } + return &Secret{ + provider: c.getSecretProvider(base), + } +} + +func (c *Configuration) getSecretProvider(base baseSecret) SecretProvider { + if strings.HasPrefix(c.Secrets.URL, "hashivault://") { + return newVaultSecret(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) +} + +// Secret defines the struct used to store confidential data +type Secret struct { + provider SecretProvider +} + +// MarshalJSON return the JSON encoding of the Secret object +func (s *Secret) MarshalJSON() ([]byte, error) { + return json.Marshal(&baseSecret{ + Status: s.provider.GetStatus(), + Payload: s.provider.GetPayload(), + Key: s.provider.GetKey(), + AdditionalData: s.provider.GetAdditionalData(), + }) +} + +// UnmarshalJSON parses the JSON-encoded data and stores the result +// in the Secret object +func (s *Secret) UnmarshalJSON(data []byte) error { + baseSecret := baseSecret{} + err := json.Unmarshal(data, &baseSecret) + if err != nil { + return err + } + if baseSecret.isEmpty() { + 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: + s.provider = config.getSecretProvider(baseSecret) + default: + return errInvalidSecret + } + return nil +} + +// Clone returns a copy of the secret object +func (s *Secret) Clone() *Secret { + baseSecret := baseSecret{ + Status: s.provider.GetStatus(), + Payload: s.provider.GetPayload(), + Key: s.provider.GetKey(), + AdditionalData: s.provider.GetAdditionalData(), + } + 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 +// This isn't a pointer receiver because we don't want to pass +// a pointer to html template +func (s *Secret) IsEncrypted() bool { + return s.provider.IsEncrypted() +} + +// IsPlain returns true if the secret is in plain text +func (s *Secret) IsPlain() bool { + return s.provider.GetStatus() == SecretStatusPlain +} + +// IsRedacted returns true if the secret is redacted +func (s *Secret) IsRedacted() bool { + return s.provider.GetStatus() == SecretStatusRedacted +} + +// GetPayload returns the secret payload +func (s *Secret) GetPayload() string { + return s.provider.GetPayload() +} + +// GetAdditionalData returns the secret additional data +func (s *Secret) GetAdditionalData() string { + return s.provider.GetAdditionalData() +} + +// GetStatus returns the secret status +func (s *Secret) GetStatus() SecretStatus { + return s.provider.GetStatus() +} + +// GetKey returns the secret key +func (s *Secret) GetKey() string { + return s.provider.GetKey() +} + +// SetAdditionalData sets the given additional data +func (s *Secret) SetAdditionalData(value string) { + s.provider.SetAdditionalData(value) +} + +// SetStatus sets the status for this secret +func (s *Secret) SetStatus(value SecretStatus) { + s.provider.SetStatus(value) +} + +// SetKey sets the key for this secret +func (s *Secret) SetKey(value string) { + s.provider.SetKey(value) +} + +// IsEmpty returns true if all fields are empty +func (s *Secret) IsEmpty() bool { + if s.provider.GetStatus() != "" { + return false + } + if s.provider.GetPayload() != "" { + return false + } + if s.provider.GetKey() != "" { + return false + } + if s.provider.GetAdditionalData() != "" { + return false + } + return true +} + +// IsValid returns true if the secret is not empty and valid +func (s *Secret) IsValid() bool { + if !s.IsValidInput() { + return false + } + switch s.provider.GetStatus() { + case SecretStatusAES256GCM, SecretStatusSecretBox: + if len(s.provider.GetKey()) != 64 { + return false + } + case SecretStatusAWS, SecretStatusGCP, SecretStatusVaultTransit: + key := s.provider.GetKey() + if key != "" && len(key) != 64 { + return false + } + } + return true +} + +// IsValidInput returns true if the secret is a valid user input +func (s *Secret) IsValidInput() bool { + if !utils.IsStringInSlice(s.provider.GetStatus(), validSecretStatuses) { + return false + } + if s.provider.GetPayload() == "" { + return false + } + return true +} + +// Hide hides info to decrypt data +func (s *Secret) Hide() { + s.provider.SetKey("") + s.provider.SetAdditionalData("") +} + +// Encrypt encrypts a plain text Secret object +func (s *Secret) Encrypt() error { + return s.provider.Encrypt() +} + +// Decrypt decrypts a Secret object +func (s *Secret) Decrypt() error { + return s.provider.Decrypt() +} diff --git a/kms/local.go b/kms/local.go new file mode 100644 index 00000000..bff8395d --- /dev/null +++ b/kms/local.go @@ -0,0 +1,120 @@ +package kms + +import ( + "context" + "encoding/base64" + "encoding/hex" + "io" + + "github.com/minio/sha256-simd" + "gocloud.dev/secrets/localsecrets" + "golang.org/x/crypto/hkdf" +) + +const ( + localProviderName = "Local" +) + +type localSecret struct { + baseSecret + masterKey string +} + +func newLocalSecret(base baseSecret, masterKey string) SecretProvider { + return &localSecret{ + baseSecret: base, + masterKey: masterKey, + } +} + +func (s *localSecret) Name() string { + return localProviderName +} + +func (s *localSecret) IsEncrypted() bool { + return s.Status == SecretStatusSecretBox +} + +func (s *localSecret) Encrypt() error { + if s.Status != SecretStatusPlain { + return errWrongSecretStatus + } + if s.Payload == "" { + return errInvalidSecret + } + secretKey, err := localsecrets.NewRandomKey() + if err != nil { + return err + } + key, err := s.deriveKey(secretKey[:]) + if err != nil { + return err + } + keeper := localsecrets.NewKeeper(key) + defer keeper.Close() + + ciphertext, err := keeper.Encrypt(context.Background(), []byte(s.Payload)) + if err != nil { + return err + } + s.Key = hex.EncodeToString(secretKey[:]) + s.Payload = base64.StdEncoding.EncodeToString(ciphertext) + s.Status = SecretStatusSecretBox + return nil +} + +func (s *localSecret) Decrypt() error { + if !s.IsEncrypted() { + return errWrongSecretStatus + } + encrypted, err := base64.StdEncoding.DecodeString(s.Payload) + if err != nil { + return err + } + secretKey, err := hex.DecodeString(s.Key) + if err != nil { + return err + } + key, err := s.deriveKey(secretKey[:]) + if err != nil { + return err + } + keeper := localsecrets.NewKeeper(key) + defer keeper.Close() + + plaintext, err := keeper.Decrypt(context.Background(), encrypted) + if err != nil { + return err + } + s.Status = SecretStatusPlain + s.Payload = string(plaintext) + s.Key = "" + s.AdditionalData = "" + return nil +} + +func (s *localSecret) deriveKey(key []byte) ([32]byte, error) { + var masterKey []byte + if s.masterKey == "" { + var combined []byte + combined = append(combined, key...) + if s.AdditionalData != "" { + combined = append(combined, []byte(s.AdditionalData)...) + } + combined = append(combined, key...) + hash := sha256.Sum256(combined) + masterKey = hash[:] + } else { + masterKey = []byte(s.masterKey) + } + var derivedKey [32]byte + var info []byte + if s.AdditionalData != "" { + info = []byte(s.AdditionalData) + } + kdf := hkdf.New(sha256.New, masterKey, key, info) + if _, err := io.ReadFull(kdf, derivedKey[:]); err != nil { + return derivedKey, err + } + return derivedKey, nil +} diff --git a/kms/vault.go b/kms/vault.go new file mode 100644 index 00000000..af549992 --- /dev/null +++ b/kms/vault.go @@ -0,0 +1,42 @@ +package kms + +const ( + vaultProviderName = "VaultTransit" +) + +type vaultSecret struct { + baseGCloudSecret +} + +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/service/service.go b/service/service.go index 98a9ad84..384d2df2 100644 --- a/service/service.go +++ b/service/service.go @@ -109,6 +109,13 @@ func (s *Service) Start() error { httpConfig := config.GetHTTPConfig() httpConfig.Initialize(s.ConfigDir) + kmsConfig := config.GetKMSConfig() + err = kmsConfig.Initialize() + if err != nil { + logger.Error(logSender, "", "unable to initialize KMS: %v", err) + logger.ErrorToConsole("unable to initialize KMS: %v", err) + os.Exit(1) + } s.startServices() diff --git a/service/service_portable.go b/service/service_portable.go index e419838b..769d71fd 100644 --- a/service/service_portable.go +++ b/service/service_portable.go @@ -15,6 +15,7 @@ import ( "github.com/drakkan/sftpgo/config" "github.com/drakkan/sftpgo/dataprovider" + "github.com/drakkan/sftpgo/kms" "github.com/drakkan/sftpgo/logger" "github.com/drakkan/sftpgo/sftpd" "github.com/drakkan/sftpgo/utils" @@ -32,6 +33,11 @@ func (s *Service) StartPortableMode(sftpdPort, ftpPort, webdavPort int, enabledS if err != nil { fmt.Printf("error loading configuration file: %v using defaults\n", err) } + kmsConfig := config.GetKMSConfig() + err = kmsConfig.Initialize() + if err != nil { + return err + } printablePassword := s.configurePortableUser() dataProviderConf := config.GetProviderConf() dataProviderConf.Driver = dataprovider.MemoryDataProviderName @@ -237,5 +243,23 @@ func (s *Service) configurePortableUser() string { s.PortableUser.Password = b.String() printablePassword = s.PortableUser.Password } + // we created the user before to initialize the KMS so we need to create the secret here + switch s.PortableUser.FsConfig.Provider { + case dataprovider.S3FilesystemProvider: + payload := s.PortableUser.FsConfig.S3Config.AccessSecret.GetPayload() + if payload != "" { + s.PortableUser.FsConfig.S3Config.AccessSecret = kms.NewPlainSecret(payload) + } + case dataprovider.GCSFilesystemProvider: + payload := s.PortableUser.FsConfig.GCSConfig.Credentials.GetPayload() + if payload != "" { + s.PortableUser.FsConfig.GCSConfig.Credentials = kms.NewPlainSecret(payload) + } + case dataprovider.AzureBlobFilesystemProvider: + payload := s.PortableUser.FsConfig.AzBlobConfig.AccountKey.GetPayload() + if payload != "" { + s.PortableUser.FsConfig.AzBlobConfig.AccountKey = kms.NewPlainSecret(payload) + } + } return printablePassword } diff --git a/sftpd/sftpd_test.go b/sftpd/sftpd_test.go index 5460e580..0c3640a1 100644 --- a/sftpd/sftpd_test.go +++ b/sftpd/sftpd_test.go @@ -41,6 +41,7 @@ import ( "github.com/drakkan/sftpgo/config" "github.com/drakkan/sftpgo/dataprovider" "github.com/drakkan/sftpgo/httpd" + "github.com/drakkan/sftpgo/kms" "github.com/drakkan/sftpgo/logger" "github.com/drakkan/sftpgo/utils" "github.com/drakkan/sftpgo/vfs" @@ -177,6 +178,12 @@ func TestMain(m *testing.M) { httpConfig := config.GetHTTPConfig() httpConfig.Initialize(configDir) + kmsConfig := config.GetKMSConfig() + err = kmsConfig.Initialize() + if err != nil { + logger.ErrorToConsole("error initializing kms: %v", err) + os.Exit(1) + } sftpdConf := config.GetSFTPDConfig() httpdConf := config.GetHTTPDConfig() @@ -1312,10 +1319,7 @@ func TestLoginWithDatabaseCredentials(t *testing.T) { u := getTestUser(usePubKey) u.FsConfig.Provider = dataprovider.GCSFilesystemProvider u.FsConfig.GCSConfig.Bucket = "testbucket" - u.FsConfig.GCSConfig.Credentials = vfs.Secret{ - Status: vfs.SecretStatusPlain, - Payload: `{ "type": "service_account" }`, - } + u.FsConfig.GCSConfig.Credentials = kms.NewPlainSecret(`{ "type": "service_account" }`) providerConf := config.GetProviderConf() providerConf.PreferDatabaseCredentials = true @@ -1336,10 +1340,10 @@ func TestLoginWithDatabaseCredentials(t *testing.T) { user, _, err := httpd.AddUser(u, http.StatusOK) assert.NoError(t, err) - assert.Equal(t, vfs.SecretStatusAES256GCM, user.FsConfig.GCSConfig.Credentials.Status) - assert.NotEmpty(t, user.FsConfig.GCSConfig.Credentials.Payload) - assert.Empty(t, user.FsConfig.GCSConfig.Credentials.AdditionalData) - assert.Empty(t, user.FsConfig.GCSConfig.Credentials.Key) + assert.Equal(t, kms.SecretStatusSecretBox, user.FsConfig.GCSConfig.Credentials.GetStatus()) + assert.NotEmpty(t, user.FsConfig.GCSConfig.Credentials.GetPayload()) + assert.Empty(t, user.FsConfig.GCSConfig.Credentials.GetAdditionalData()) + assert.Empty(t, user.FsConfig.GCSConfig.Credentials.GetKey()) assert.NoFileExists(t, credentialsFile) @@ -1364,10 +1368,7 @@ func TestLoginInvalidFs(t *testing.T) { u := getTestUser(usePubKey) u.FsConfig.Provider = dataprovider.GCSFilesystemProvider u.FsConfig.GCSConfig.Bucket = "test" - u.FsConfig.GCSConfig.Credentials = vfs.Secret{ - Status: vfs.SecretStatusPlain, - Payload: "invalid JSON for credentials", - } + u.FsConfig.GCSConfig.Credentials = kms.NewPlainSecret("invalid JSON for credentials") user, _, err := httpd.AddUser(u, http.StatusOK) assert.NoError(t, err) diff --git a/sftpgo.json b/sftpgo.json index f962a698..17e48f00 100644 --- a/sftpgo.json +++ b/sftpgo.json @@ -122,5 +122,11 @@ "timeout": 20, "ca_certificates": [], "skip_tls_verify": false + }, + "kms": { + "secrets": { + "url": "", + "master_key_path": "" + } } } \ No newline at end of file diff --git a/templates/user.html b/templates/user.html index 30694550..901a6a1d 100644 --- a/templates/user.html +++ b/templates/user.html @@ -337,7 +337,7 @@
+ value="{{if .IsS3SecretEnc}}{{.RedactedSecret}}{{else}}{{.User.FsConfig.S3Config.AccessSecret.GetPayload}}{{end}}" maxlength="1000">
@@ -448,7 +448,7 @@
+ value="{{if .IsAzSecretEnc}}{{.RedactedSecret}}{{else}}{{.User.FsConfig.AzBlobConfig.AccountKey.GetPayload}}{{end}}" maxlength="1000">
diff --git a/vfs/azblobfs.go b/vfs/azblobfs.go index 61a094ee..b6293bc5 100644 --- a/vfs/azblobfs.go +++ b/vfs/azblobfs.go @@ -104,7 +104,7 @@ func NewAzBlobFs(connectionID, localTempDir string, config AzBlobFsConfig) (Fs, return fs, nil } - credential, err := azblob.NewSharedKeyCredential(fs.config.AccountName, fs.config.AccountKey.Payload) + credential, err := azblob.NewSharedKeyCredential(fs.config.AccountName, fs.config.AccountKey.GetPayload()) if err != nil { return fs, fmt.Errorf("invalid credentials: %v", err) } diff --git a/vfs/gcsfs.go b/vfs/gcsfs.go index d09d24b9..56645708 100644 --- a/vfs/gcsfs.go +++ b/vfs/gcsfs.go @@ -23,6 +23,7 @@ import ( "google.golang.org/api/iterator" "google.golang.org/api/option" + "github.com/drakkan/sftpgo/kms" "github.com/drakkan/sftpgo/logger" "github.com/drakkan/sftpgo/metrics" "github.com/drakkan/sftpgo/version" @@ -62,19 +63,21 @@ func NewGCSFs(connectionID, localTempDir string, config GCSFsConfig) (Fs, error) ctx := context.Background() if fs.config.AutomaticCredentials > 0 { fs.svc, err = storage.NewClient(ctx) - } else if fs.config.Credentials.IsEncrypted() { - err = fs.config.Credentials.Decrypt() - if err != nil { - return fs, err + } else if !fs.config.Credentials.IsEmpty() { + if fs.config.Credentials.IsEncrypted() { + err = fs.config.Credentials.Decrypt() + if err != nil { + return fs, err + } } - fs.svc, err = storage.NewClient(ctx, option.WithCredentialsJSON([]byte(fs.config.Credentials.Payload))) + fs.svc, err = storage.NewClient(ctx, option.WithCredentialsJSON([]byte(fs.config.Credentials.GetPayload()))) } else { var creds []byte creds, err = ioutil.ReadFile(fs.config.CredentialFile) if err != nil { return fs, err } - secret := &Secret{} + secret := kms.NewEmptySecret() err = json.Unmarshal(creds, secret) if err != nil { return fs, err @@ -83,7 +86,7 @@ func NewGCSFs(connectionID, localTempDir string, config GCSFsConfig) (Fs, error) if err != nil { return fs, err } - fs.svc, err = storage.NewClient(ctx, option.WithCredentialsJSON([]byte(secret.Payload))) + fs.svc, err = storage.NewClient(ctx, option.WithCredentialsJSON([]byte(secret.GetPayload()))) } return fs, err } diff --git a/vfs/s3fs.go b/vfs/s3fs.go index f77a7677..3fc1d391 100644 --- a/vfs/s3fs.go +++ b/vfs/s3fs.go @@ -60,12 +60,14 @@ func NewS3Fs(connectionID, localTempDir string, config S3FsConfig) (Fs, error) { awsConfig.WithRegion(fs.config.Region) } - if fs.config.AccessSecret.IsEncrypted() { - err := fs.config.AccessSecret.Decrypt() - if err != nil { - return fs, err + if !fs.config.AccessSecret.IsEmpty() { + if fs.config.AccessSecret.IsEncrypted() { + err := fs.config.AccessSecret.Decrypt() + if err != nil { + return fs, err + } } - awsConfig.Credentials = credentials.NewStaticCredentials(fs.config.AccessKey, fs.config.AccessSecret.Payload, "") + awsConfig.Credentials = credentials.NewStaticCredentials(fs.config.AccessKey, fs.config.AccessSecret.GetPayload(), "") } if fs.config.Endpoint != "" { diff --git a/vfs/secret.go b/vfs/secret.go deleted file mode 100644 index 9d16f088..00000000 --- a/vfs/secret.go +++ /dev/null @@ -1,209 +0,0 @@ -package vfs - -import ( - "crypto/aes" - "crypto/cipher" - "crypto/rand" - "crypto/sha256" - "encoding/hex" - "errors" - "io" - - "github.com/drakkan/sftpgo/utils" -) - -// SecretStatus defines the statuses of a Secret object -type SecretStatus = string - -const ( - // SecretStatusPlain means the secret is in plain text and must be encrypted - SecretStatusPlain SecretStatus = "Plain" - // SecretStatusAES256GCM means the secret is encrypted using AES-256-GCM - SecretStatusAES256GCM SecretStatus = "AES-256-GCM" - // SecretStatusRedacted means the secret is redacted - SecretStatusRedacted SecretStatus = "Redacted" -) - -var ( - errWrongSecretStatus = errors.New("wrong secret status") - errMalformedCiphertext = errors.New("malformed ciphertext") - errInvalidSecret = errors.New("invalid secret") - validSecretStatuses = []string{SecretStatusPlain, SecretStatusAES256GCM, SecretStatusRedacted} -) - -// Secret defines the struct used to store confidential data -type Secret struct { - Status SecretStatus `json:"status,omitempty"` - Payload string `json:"payload,omitempty"` - Key string `json:"key,omitempty"` - AdditionalData string `json:"additional_data,omitempty"` -} - -// GetSecretFromCompatString returns a secret from the previous format -func GetSecretFromCompatString(secret string) (Secret, error) { - s := Secret{} - plain, err := utils.DecryptData(secret) - if err != nil { - return s, errMalformedCiphertext - } - s.Status = SecretStatusPlain - s.Payload = plain - return s, nil -} - -// IsEncrypted returns true if the secret is encrypted -// This isn't a pointer receiver because we don't want to pass -// a pointer to html template -func (s *Secret) IsEncrypted() bool { - return s.Status == SecretStatusAES256GCM -} - -// IsPlain returns true if the secret is in plain text -func (s *Secret) IsPlain() bool { - return s.Status == SecretStatusPlain -} - -// IsRedacted returns true if the secret is redacted -func (s *Secret) IsRedacted() bool { - return s.Status == SecretStatusRedacted -} - -// IsEmpty returns true if all fields are empty -func (s *Secret) IsEmpty() bool { - if s.Status != "" { - return false - } - if s.Payload != "" { - return false - } - if s.Key != "" { - return false - } - if s.AdditionalData != "" { - return false - } - return true -} - -// IsValid returns true if the secret is not empty and valid -func (s *Secret) IsValid() bool { - if !s.IsValidInput() { - return false - } - if s.Status == SecretStatusAES256GCM { - if len(s.Key) != 64 { - return false - } - } - return true -} - -// IsValidInput returns true if the secret is a valid user input -func (s *Secret) IsValidInput() bool { - if !utils.IsStringInSlice(s.Status, validSecretStatuses) { - return false - } - if s.Payload == "" { - return false - } - return true -} - -// Hide hides info to decrypt data -func (s *Secret) Hide() { - s.Key = "" - s.AdditionalData = "" -} - -// deriveKey is a weak method of deriving a key but it is still better than using the key as it is. -// We should use a KMS in future -func (s *Secret) deriveKey(key []byte) []byte { - var combined []byte - combined = append(combined, key...) - if s.AdditionalData != "" { - combined = append(combined, []byte(s.AdditionalData)...) - } - combined = append(combined, key...) - hash := sha256.Sum256(combined) - return hash[:] -} - -// Encrypt encrypts a plain text Secret object -func (s *Secret) Encrypt() error { - if s.Payload == "" { - return errInvalidSecret - } - switch s.Status { - case SecretStatusPlain: - key := make([]byte, 32) - if _, err := io.ReadFull(rand.Reader, key); err != nil { - return err - } - block, err := aes.NewCipher(s.deriveKey(key)) - if err != nil { - return err - } - gcm, err := cipher.NewGCM(block) - if err != nil { - return err - } - nonce := make([]byte, gcm.NonceSize()) - if _, err = io.ReadFull(rand.Reader, nonce); err != nil { - return err - } - var aad []byte - if s.AdditionalData != "" { - aad = []byte(s.AdditionalData) - } - ciphertext := gcm.Seal(nonce, nonce, []byte(s.Payload), aad) - s.Key = hex.EncodeToString(key) - s.Payload = hex.EncodeToString(ciphertext) - s.Status = SecretStatusAES256GCM - return nil - default: - return errWrongSecretStatus - } -} - -// Decrypt decrypts a Secret object -func (s *Secret) Decrypt() error { - switch s.Status { - case SecretStatusAES256GCM: - encrypted, err := hex.DecodeString(s.Payload) - if err != nil { - return err - } - key, err := hex.DecodeString(s.Key) - if err != nil { - return err - } - block, err := aes.NewCipher(s.deriveKey(key)) - if err != nil { - return err - } - gcm, err := cipher.NewGCM(block) - if err != nil { - return err - } - nonceSize := gcm.NonceSize() - if len(encrypted) < nonceSize { - return errMalformedCiphertext - } - nonce, ciphertext := encrypted[:nonceSize], encrypted[nonceSize:] - var aad []byte - if s.AdditionalData != "" { - aad = []byte(s.AdditionalData) - } - plaintext, err := gcm.Open(nil, nonce, ciphertext, aad) - if err != nil { - return err - } - s.Status = SecretStatusPlain - s.Payload = string(plaintext) - s.Key = "" - s.AdditionalData = "" - return nil - default: - return errWrongSecretStatus - } -} diff --git a/vfs/vfs.go b/vfs/vfs.go index 1842955b..5c553b75 100644 --- a/vfs/vfs.go +++ b/vfs/vfs.go @@ -15,6 +15,7 @@ import ( "github.com/eikenb/pipeat" + "github.com/drakkan/sftpgo/kms" "github.com/drakkan/sftpgo/logger" "github.com/drakkan/sftpgo/utils" ) @@ -110,12 +111,12 @@ type S3FsConfig struct { // folder. The prefix, if not empty, must not start with "/" and must // end with "/". // If empty the whole bucket contents will be available - KeyPrefix string `json:"key_prefix,omitempty"` - Region string `json:"region,omitempty"` - AccessKey string `json:"access_key,omitempty"` - AccessSecret Secret `json:"access_secret,omitempty"` - Endpoint string `json:"endpoint,omitempty"` - StorageClass string `json:"storage_class,omitempty"` + KeyPrefix string `json:"key_prefix,omitempty"` + Region string `json:"region,omitempty"` + AccessKey string `json:"access_key,omitempty"` + AccessSecret *kms.Secret `json:"access_secret,omitempty"` + Endpoint string `json:"endpoint,omitempty"` + StorageClass string `json:"storage_class,omitempty"` // The buffer size (in MB) to use for multipart uploads. The minimum allowed part size is 5MB, // and if this value is set to zero, the default value (5MB) for the AWS SDK will be used. // The minimum allowed value is 5. @@ -137,9 +138,9 @@ type GCSFsConfig struct { // folder. The prefix, if not empty, must not start with "/" and must // end with "/". // If empty the whole bucket contents will be available - KeyPrefix string `json:"key_prefix,omitempty"` - CredentialFile string `json:"-"` - Credentials Secret `json:"credentials,omitempty"` + KeyPrefix string `json:"key_prefix,omitempty"` + CredentialFile string `json:"-"` + Credentials *kms.Secret `json:"credentials,omitempty"` // 0 explicit, 1 automatic AutomaticCredentials int `json:"automatic_credentials,omitempty"` StorageClass string `json:"storage_class,omitempty"` @@ -151,8 +152,8 @@ type AzBlobFsConfig struct { // Storage Account Name, leave blank to use SAS URL AccountName string `json:"account_name,omitempty"` // Storage Account Key leave blank to use SAS URL. - // The access key is stored encrypted (AES-256-GCM) - AccountKey Secret `json:"account_key,omitempty"` + // The access key is stored encrypted based on the kms configuration + AccountKey *kms.Secret `json:"account_key,omitempty"` // Optional endpoint. Default is "blob.core.windows.net". // If you use the emulator the endpoint must include the protocol, // for example "http://127.0.0.1:10000" @@ -254,6 +255,9 @@ func checkS3Credentials(config *S3FsConfig) error { // ValidateS3FsConfig returns nil if the specified s3 config is valid, otherwise an error func ValidateS3FsConfig(config *S3FsConfig) error { + if config.AccessSecret == nil { + config.AccessSecret = kms.NewEmptySecret() + } if config.Bucket == "" { return errors.New("bucket cannot be empty") } @@ -283,6 +287,9 @@ func ValidateS3FsConfig(config *S3FsConfig) error { // ValidateGCSFsConfig returns nil if the specified GCS config is valid, otherwise an error func ValidateGCSFsConfig(config *GCSFsConfig, credentialsFilePath string) error { + if config.Credentials == nil { + config.Credentials = kms.NewEmptySecret() + } if config.Bucket == "" { return errors.New("bucket cannot be empty") } @@ -310,8 +317,21 @@ func ValidateGCSFsConfig(config *GCSFsConfig, credentialsFilePath string) error return nil } +func checkAzCredentials(config *AzBlobFsConfig) error { + if config.AccountName == "" || !config.AccountKey.IsValidInput() { + return errors.New("credentials cannot be empty or invalid") + } + if config.AccountKey.IsEncrypted() && !config.AccountKey.IsValid() { + return errors.New("invalid encrypted account_key") + } + return nil +} + // ValidateAzBlobFsConfig returns nil if the specified Azure Blob config is valid, otherwise an error func ValidateAzBlobFsConfig(config *AzBlobFsConfig) error { + if config.AccountKey == nil { + config.AccountKey = kms.NewEmptySecret() + } if config.SASURL != "" { _, err := url.Parse(config.SASURL) return err @@ -319,11 +339,8 @@ func ValidateAzBlobFsConfig(config *AzBlobFsConfig) error { if config.Container == "" { return errors.New("container cannot be empty") } - if config.AccountName == "" || !config.AccountKey.IsValidInput() { - return errors.New("credentials cannot be empty or invalid") - } - if config.AccountKey.IsEncrypted() && !config.AccountKey.IsValid() { - return errors.New("invalid encrypted account_key") + if err := checkAzCredentials(config); err != nil { + return err } if config.KeyPrefix != "" { if strings.HasPrefix(config.KeyPrefix, "/") { diff --git a/webdavd/server.go b/webdavd/server.go index 4bd35001..a7ee27aa 100644 --- a/webdavd/server.go +++ b/webdavd/server.go @@ -136,10 +136,10 @@ func (s *webDavServer) ServeHTTP(w http.ResponseWriter, r *http.Request) { defer common.Connections.Remove(connection.GetID()) if !isCached { - // we update last login and check for home directory only if the user is not cached + // we check the home directory only if the user is not cached connection.Fs.CheckRootPath(connection.GetUsername(), user.GetUID(), user.GetGID()) - dataprovider.UpdateLastLogin(user) //nolint:errcheck } + dataprovider.UpdateLastLogin(user) //nolint:errcheck prefix := path.Join("/", user.Username) // see RFC4918, section 9.4 diff --git a/webdavd/webdavd_test.go b/webdavd/webdavd_test.go index 6f9838d1..756d4ad4 100644 --- a/webdavd/webdavd_test.go +++ b/webdavd/webdavd_test.go @@ -27,6 +27,7 @@ import ( "github.com/drakkan/sftpgo/dataprovider" "github.com/drakkan/sftpgo/httpclient" "github.com/drakkan/sftpgo/httpd" + "github.com/drakkan/sftpgo/kms" "github.com/drakkan/sftpgo/logger" "github.com/drakkan/sftpgo/vfs" "github.com/drakkan/sftpgo/webdavd" @@ -125,6 +126,12 @@ func TestMain(m *testing.M) { httpConfig := config.GetHTTPConfig() httpConfig.Initialize(configDir) + kmsConfig := config.GetKMSConfig() + err = kmsConfig.Initialize() + if err != nil { + logger.ErrorToConsole("error initializing kms: %v", err) + os.Exit(1) + } httpdConf := config.GetHTTPDConfig() httpdConf.BindPort = 8078 @@ -861,10 +868,7 @@ func TestLoginWithDatabaseCredentials(t *testing.T) { u := getTestUser() u.FsConfig.Provider = dataprovider.GCSFilesystemProvider u.FsConfig.GCSConfig.Bucket = "test" - u.FsConfig.GCSConfig.Credentials = vfs.Secret{ - Status: vfs.SecretStatusPlain, - Payload: `{ "type": "service_account" }`, - } + u.FsConfig.GCSConfig.Credentials = kms.NewPlainSecret(`{ "type": "service_account" }`) providerConf := config.GetProviderConf() providerConf.PreferDatabaseCredentials = true @@ -885,10 +889,10 @@ func TestLoginWithDatabaseCredentials(t *testing.T) { user, _, err := httpd.AddUser(u, http.StatusOK) assert.NoError(t, err) - assert.Equal(t, vfs.SecretStatusAES256GCM, user.FsConfig.GCSConfig.Credentials.Status) - assert.NotEmpty(t, user.FsConfig.GCSConfig.Credentials.Payload) - assert.Empty(t, user.FsConfig.GCSConfig.Credentials.AdditionalData) - assert.Empty(t, user.FsConfig.GCSConfig.Credentials.Key) + assert.Equal(t, kms.SecretStatusSecretBox, user.FsConfig.GCSConfig.Credentials.GetStatus()) + assert.NotEmpty(t, user.FsConfig.GCSConfig.Credentials.GetPayload()) + assert.Empty(t, user.FsConfig.GCSConfig.Credentials.GetAdditionalData()) + assert.Empty(t, user.FsConfig.GCSConfig.Credentials.GetKey()) assert.NoFileExists(t, credentialsFile) @@ -912,10 +916,7 @@ func TestLoginInvalidFs(t *testing.T) { u := getTestUser() u.FsConfig.Provider = dataprovider.GCSFilesystemProvider u.FsConfig.GCSConfig.Bucket = "test" - u.FsConfig.GCSConfig.Credentials = vfs.Secret{ - Status: vfs.SecretStatusPlain, - Payload: "invalid JSON for credentials", - } + u.FsConfig.GCSConfig.Credentials = kms.NewPlainSecret("invalid JSON for credentials") user, _, err := httpd.AddUser(u, http.StatusOK) assert.NoError(t, err)