Browse Source

move kms implementation outside the sdk package

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
Nicola Murino 3 years ago
parent
commit
6d3d94a01f

+ 15 - 15
cmd/portable.go

@@ -14,8 +14,8 @@ import (
 
 	"github.com/drakkan/sftpgo/v2/common"
 	"github.com/drakkan/sftpgo/v2/dataprovider"
+	"github.com/drakkan/sftpgo/v2/kms"
 	"github.com/drakkan/sftpgo/v2/sdk"
-	"github.com/drakkan/sftpgo/v2/sdk/kms"
 	"github.com/drakkan/sftpgo/v2/service"
 	"github.com/drakkan/sftpgo/v2/sftpd"
 	"github.com/drakkan/sftpgo/v2/version"
@@ -157,18 +157,19 @@ Please take a look at the usage below to customize the serving parameters`,
 						Permissions: permissions,
 						HomeDir:     portableDir,
 						Status:      1,
-						Filters: sdk.UserFilters{
+					},
+					Filters: dataprovider.UserFilters{
+						BaseUserFilters: sdk.BaseUserFilters{
 							FilePatterns: parsePatternsFilesFilters(),
 						},
 					},
 					FsConfig: vfs.Filesystem{
 						Provider: sdk.GetProviderByName(portableFsProvider),
 						S3Config: vfs.S3FsConfig{
-							S3FsConfig: sdk.S3FsConfig{
+							BaseS3FsConfig: sdk.BaseS3FsConfig{
 								Bucket:            portableS3Bucket,
 								Region:            portableS3Region,
 								AccessKey:         portableS3AccessKey,
-								AccessSecret:      kms.NewPlainSecret(portableS3AccessSecret),
 								Endpoint:          portableS3Endpoint,
 								StorageClass:      portableS3StorageClass,
 								ACL:               portableS3ACL,
@@ -177,46 +178,45 @@ Please take a look at the usage below to customize the serving parameters`,
 								UploadConcurrency: portableS3ULConcurrency,
 								ForcePathStyle:    portableS3ForcePathStyle,
 							},
+							AccessSecret: kms.NewPlainSecret(portableS3AccessSecret),
 						},
 						GCSConfig: vfs.GCSFsConfig{
-							GCSFsConfig: sdk.GCSFsConfig{
+							BaseGCSFsConfig: sdk.BaseGCSFsConfig{
 								Bucket:               portableGCSBucket,
-								Credentials:          kms.NewPlainSecret(portableGCSCredentials),
 								AutomaticCredentials: portableGCSAutoCredentials,
 								StorageClass:         portableGCSStorageClass,
 								KeyPrefix:            portableGCSKeyPrefix,
 							},
+							Credentials: kms.NewPlainSecret(portableGCSCredentials),
 						},
 						AzBlobConfig: vfs.AzBlobFsConfig{
-							AzBlobFsConfig: sdk.AzBlobFsConfig{
+							BaseAzBlobFsConfig: sdk.BaseAzBlobFsConfig{
 								Container:         portableAzContainer,
 								AccountName:       portableAzAccountName,
-								AccountKey:        kms.NewPlainSecret(portableAzAccountKey),
 								Endpoint:          portableAzEndpoint,
 								AccessTier:        portableAzAccessTier,
-								SASURL:            kms.NewPlainSecret(portableAzSASURL),
 								KeyPrefix:         portableAzKeyPrefix,
 								UseEmulator:       portableAzUseEmulator,
 								UploadPartSize:    int64(portableAzULPartSize),
 								UploadConcurrency: portableAzULConcurrency,
 							},
+							AccountKey: kms.NewPlainSecret(portableAzAccountKey),
+							SASURL:     kms.NewPlainSecret(portableAzSASURL),
 						},
 						CryptConfig: vfs.CryptFsConfig{
-							CryptFsConfig: sdk.CryptFsConfig{
-								Passphrase: kms.NewPlainSecret(portableCryptPassphrase),
-							},
+							Passphrase: kms.NewPlainSecret(portableCryptPassphrase),
 						},
 						SFTPConfig: vfs.SFTPFsConfig{
-							SFTPFsConfig: sdk.SFTPFsConfig{
+							BaseSFTPFsConfig: sdk.BaseSFTPFsConfig{
 								Endpoint:                portableSFTPEndpoint,
 								Username:                portableSFTPUsername,
-								Password:                kms.NewPlainSecret(portableSFTPPassword),
-								PrivateKey:              kms.NewPlainSecret(portableSFTPPrivateKey),
 								Fingerprints:            portableSFTPFingerprints,
 								Prefix:                  portableSFTPPrefix,
 								DisableCouncurrentReads: portableSFTPDisableConcurrentReads,
 								BufferSize:              portableSFTPDBufferSize,
 							},
+							Password:   kms.NewPlainSecret(portableSFTPPassword),
+							PrivateKey: kms.NewPlainSecret(portableSFTPPrivateKey),
 						},
 					},
 				},

+ 4 - 4
common/actions_test.go

@@ -28,24 +28,24 @@ func TestNewActionNotification(t *testing.T) {
 	}
 	user.FsConfig.Provider = sdk.LocalFilesystemProvider
 	user.FsConfig.S3Config = vfs.S3FsConfig{
-		S3FsConfig: sdk.S3FsConfig{
+		BaseS3FsConfig: sdk.BaseS3FsConfig{
 			Bucket:   "s3bucket",
 			Endpoint: "endpoint",
 		},
 	}
 	user.FsConfig.GCSConfig = vfs.GCSFsConfig{
-		GCSFsConfig: sdk.GCSFsConfig{
+		BaseGCSFsConfig: sdk.BaseGCSFsConfig{
 			Bucket: "gcsbucket",
 		},
 	}
 	user.FsConfig.AzBlobConfig = vfs.AzBlobFsConfig{
-		AzBlobFsConfig: sdk.AzBlobFsConfig{
+		BaseAzBlobFsConfig: sdk.BaseAzBlobFsConfig{
 			Container: "azcontainer",
 			Endpoint:  "azendpoint",
 		},
 	}
 	user.FsConfig.SFTPConfig = vfs.SFTPFsConfig{
-		SFTPFsConfig: sdk.SFTPFsConfig{
+		BaseSFTPFsConfig: sdk.BaseSFTPFsConfig{
 			Endpoint: "sftpendpoint",
 		},
 	}

+ 3 - 7
common/common_test.go

@@ -19,8 +19,8 @@ import (
 	"golang.org/x/crypto/bcrypt"
 
 	"github.com/drakkan/sftpgo/v2/dataprovider"
+	"github.com/drakkan/sftpgo/v2/kms"
 	"github.com/drakkan/sftpgo/v2/sdk"
-	"github.com/drakkan/sftpgo/v2/sdk/kms"
 	"github.com/drakkan/sftpgo/v2/util"
 	"github.com/drakkan/sftpgo/v2/vfs"
 )
@@ -731,9 +731,7 @@ func TestPostConnectHook(t *testing.T) {
 func TestCryptoConvertFileInfo(t *testing.T) {
 	name := "name"
 	fs, err := vfs.NewCryptFs("connID1", os.TempDir(), "", vfs.CryptFsConfig{
-		CryptFsConfig: sdk.CryptFsConfig{
-			Passphrase: kms.NewPlainSecret("secret"),
-		},
+		Passphrase: kms.NewPlainSecret("secret"),
 	})
 	require.NoError(t, err)
 	cryptFs := fs.(*vfs.CryptFs)
@@ -772,9 +770,7 @@ func TestFolderCopy(t *testing.T) {
 
 	folder.FsConfig = vfs.Filesystem{
 		CryptConfig: vfs.CryptFsConfig{
-			CryptFsConfig: sdk.CryptFsConfig{
-				Passphrase: kms.NewPlainSecret("crypto secret"),
-			},
+			Passphrase: kms.NewPlainSecret("crypto secret"),
 		},
 	}
 	folderCopy = folder.GetACopy()

+ 6 - 6
common/connection_test.go

@@ -13,8 +13,8 @@ import (
 	"github.com/stretchr/testify/assert"
 
 	"github.com/drakkan/sftpgo/v2/dataprovider"
+	"github.com/drakkan/sftpgo/v2/kms"
 	"github.com/drakkan/sftpgo/v2/sdk"
-	"github.com/drakkan/sftpgo/v2/sdk/kms"
 	"github.com/drakkan/sftpgo/v2/vfs"
 )
 
@@ -419,12 +419,12 @@ func TestCheckParentDirsErrors(t *testing.T) {
 		FsConfig: vfs.Filesystem{
 			Provider: sdk.S3FilesystemProvider,
 			S3Config: vfs.S3FsConfig{
-				S3FsConfig: sdk.S3FsConfig{
-					Bucket:       "buck",
-					Region:       "us-east-1",
-					AccessKey:    "key",
-					AccessSecret: kms.NewPlainSecret("s3secret"),
+				BaseS3FsConfig: sdk.BaseS3FsConfig{
+					Bucket:    "buck",
+					Region:    "us-east-1",
+					AccessKey: "key",
 				},
+				AccessSecret: kms.NewPlainSecret("s3secret"),
 			},
 		},
 	}

+ 10 - 14
common/protocol_test.go

@@ -34,10 +34,10 @@ import (
 	"github.com/drakkan/sftpgo/v2/dataprovider"
 	"github.com/drakkan/sftpgo/v2/httpclient"
 	"github.com/drakkan/sftpgo/v2/httpdtest"
+	"github.com/drakkan/sftpgo/v2/kms"
 	"github.com/drakkan/sftpgo/v2/logger"
 	"github.com/drakkan/sftpgo/v2/mfa"
 	"github.com/drakkan/sftpgo/v2/sdk"
-	"github.com/drakkan/sftpgo/v2/sdk/kms"
 	"github.com/drakkan/sftpgo/v2/util"
 	"github.com/drakkan/sftpgo/v2/vfs"
 )
@@ -2758,7 +2758,7 @@ func TestBuiltinKeyboardInteractiveAuthentication(t *testing.T) {
 	configName, _, secret, _, err := mfa.GenerateTOTPSecret(mfa.GetAvailableTOTPConfigNames()[0], user.Username)
 	assert.NoError(t, err)
 	user.Password = defaultPassword
-	user.Filters.TOTPConfig = sdk.TOTPConfig{
+	user.Filters.TOTPConfig = dataprovider.UserTOTPConfig{
 		Enabled:    true,
 		ConfigName: configName,
 		Secret:     kms.NewPlainSecret(secret),
@@ -2926,11 +2926,11 @@ func TestSFTPLoopError(t *testing.T) {
 			FsConfig: vfs.Filesystem{
 				Provider: sdk.SFTPFilesystemProvider,
 				SFTPConfig: vfs.SFTPFsConfig{
-					SFTPFsConfig: sdk.SFTPFsConfig{
+					BaseSFTPFsConfig: sdk.BaseSFTPFsConfig{
 						Endpoint: sftpServerAddr,
 						Username: user2.Username,
-						Password: kms.NewPlainSecret(defaultPassword),
 					},
+					Password: kms.NewPlainSecret(defaultPassword),
 				},
 			},
 		},
@@ -2939,11 +2939,11 @@ func TestSFTPLoopError(t *testing.T) {
 
 	user2.FsConfig.Provider = sdk.SFTPFilesystemProvider
 	user2.FsConfig.SFTPConfig = vfs.SFTPFsConfig{
-		SFTPFsConfig: sdk.SFTPFsConfig{
+		BaseSFTPFsConfig: sdk.BaseSFTPFsConfig{
 			Endpoint: sftpServerAddr,
 			Username: user1.Username,
-			Password: kms.NewPlainSecret(defaultPassword),
 		},
+		Password: kms.NewPlainSecret(defaultPassword),
 	}
 
 	user1, resp, err := httpdtest.AddUser(user1, http.StatusCreated)
@@ -2995,11 +2995,11 @@ func TestNonLocalCrossRename(t *testing.T) {
 			FsConfig: vfs.Filesystem{
 				Provider: sdk.SFTPFilesystemProvider,
 				SFTPConfig: vfs.SFTPFsConfig{
-					SFTPFsConfig: sdk.SFTPFsConfig{
+					BaseSFTPFsConfig: sdk.BaseSFTPFsConfig{
 						Endpoint: sftpServerAddr,
 						Username: baseUser.Username,
-						Password: kms.NewPlainSecret(defaultPassword),
 					},
+					Password: kms.NewPlainSecret(defaultPassword),
 				},
 			},
 		},
@@ -3014,9 +3014,7 @@ func TestNonLocalCrossRename(t *testing.T) {
 			FsConfig: vfs.Filesystem{
 				Provider: sdk.CryptedFilesystemProvider,
 				CryptConfig: vfs.CryptFsConfig{
-					CryptFsConfig: sdk.CryptFsConfig{
-						Passphrase: kms.NewPlainSecret(defaultPassword),
-					},
+					Passphrase: kms.NewPlainSecret(defaultPassword),
 				},
 			},
 			MappedPath: mappedPathCrypt,
@@ -3117,9 +3115,7 @@ func TestNonLocalCrossRenameNonLocalBaseUser(t *testing.T) {
 			FsConfig: vfs.Filesystem{
 				Provider: sdk.CryptedFilesystemProvider,
 				CryptConfig: vfs.CryptFsConfig{
-					CryptFsConfig: sdk.CryptFsConfig{
-						Passphrase: kms.NewPlainSecret(defaultPassword),
-					},
+					Passphrase: kms.NewPlainSecret(defaultPassword),
 				},
 			},
 			MappedPath: mappedPathCrypt,

+ 2 - 2
common/transfer_test.go

@@ -11,8 +11,8 @@ import (
 	"github.com/stretchr/testify/require"
 
 	"github.com/drakkan/sftpgo/v2/dataprovider"
+	"github.com/drakkan/sftpgo/v2/kms"
 	"github.com/drakkan/sftpgo/v2/sdk"
-	"github.com/drakkan/sftpgo/v2/sdk/kms"
 	"github.com/drakkan/sftpgo/v2/vfs"
 )
 
@@ -264,7 +264,7 @@ func TestTransferErrors(t *testing.T) {
 
 func TestRemovePartialCryptoFile(t *testing.T) {
 	testFile := filepath.Join(os.TempDir(), "transfer_test_file")
-	fs, err := vfs.NewCryptFs("id", os.TempDir(), "", vfs.CryptFsConfig{CryptFsConfig: sdk.CryptFsConfig{Passphrase: kms.NewPlainSecret("secret")}})
+	fs, err := vfs.NewCryptFs("id", os.TempDir(), "", vfs.CryptFsConfig{Passphrase: kms.NewPlainSecret("secret")})
 	require.NoError(t, err)
 	u := dataprovider.User{
 		BaseUser: sdk.BaseUser{

+ 1 - 1
config/config.go

@@ -16,10 +16,10 @@ import (
 	"github.com/drakkan/sftpgo/v2/ftpd"
 	"github.com/drakkan/sftpgo/v2/httpclient"
 	"github.com/drakkan/sftpgo/v2/httpd"
+	"github.com/drakkan/sftpgo/v2/kms"
 	"github.com/drakkan/sftpgo/v2/logger"
 	"github.com/drakkan/sftpgo/v2/mfa"
 	"github.com/drakkan/sftpgo/v2/plugin"
-	"github.com/drakkan/sftpgo/v2/sdk/kms"
 	"github.com/drakkan/sftpgo/v2/sftpd"
 	"github.com/drakkan/sftpgo/v2/smtp"
 	"github.com/drakkan/sftpgo/v2/telemetry"

+ 9 - 9
config/config_test.go

@@ -19,7 +19,7 @@ import (
 	"github.com/drakkan/sftpgo/v2/httpd"
 	"github.com/drakkan/sftpgo/v2/mfa"
 	"github.com/drakkan/sftpgo/v2/plugin"
-	"github.com/drakkan/sftpgo/v2/sdk/kms"
+	sdkkms "github.com/drakkan/sftpgo/v2/sdk/kms"
 	"github.com/drakkan/sftpgo/v2/sftpd"
 	"github.com/drakkan/sftpgo/v2/smtp"
 	"github.com/drakkan/sftpgo/v2/util"
@@ -467,8 +467,8 @@ func TestPluginsFromEnv(t *testing.T) {
 	os.Setenv("SFTPGO_PLUGINS__0__ARGS", "arg1,arg2")
 	os.Setenv("SFTPGO_PLUGINS__0__SHA256SUM", "0a71ded61fccd59c4f3695b51c1b3d180da8d2d77ea09ccee20dac242675c193")
 	os.Setenv("SFTPGO_PLUGINS__0__AUTO_MTLS", "1")
-	os.Setenv("SFTPGO_PLUGINS__0__KMS_OPTIONS__SCHEME", kms.SchemeAWS)
-	os.Setenv("SFTPGO_PLUGINS__0__KMS_OPTIONS__ENCRYPTED_STATUS", kms.SecretStatusAWS)
+	os.Setenv("SFTPGO_PLUGINS__0__KMS_OPTIONS__SCHEME", sdkkms.SchemeAWS)
+	os.Setenv("SFTPGO_PLUGINS__0__KMS_OPTIONS__ENCRYPTED_STATUS", sdkkms.SecretStatusAWS)
 	os.Setenv("SFTPGO_PLUGINS__0__AUTH_OPTIONS__SCOPE", "14")
 	t.Cleanup(func() {
 		os.Unsetenv("SFTPGO_PLUGINS__0__TYPE")
@@ -510,8 +510,8 @@ func TestPluginsFromEnv(t *testing.T) {
 	require.Equal(t, "arg2", pluginConf.Args[1])
 	require.Equal(t, "0a71ded61fccd59c4f3695b51c1b3d180da8d2d77ea09ccee20dac242675c193", pluginConf.SHA256Sum)
 	require.True(t, pluginConf.AutoMTLS)
-	require.Equal(t, kms.SchemeAWS, pluginConf.KMSOptions.Scheme)
-	require.Equal(t, kms.SecretStatusAWS, pluginConf.KMSOptions.EncryptedStatus)
+	require.Equal(t, sdkkms.SchemeAWS, pluginConf.KMSOptions.Scheme)
+	require.Equal(t, sdkkms.SecretStatusAWS, pluginConf.KMSOptions.EncryptedStatus)
 	require.Equal(t, 14, pluginConf.AuthOptions.Scope)
 
 	configAsJSON, err := json.Marshal(pluginsConf)
@@ -524,8 +524,8 @@ func TestPluginsFromEnv(t *testing.T) {
 	os.Setenv("SFTPGO_PLUGINS__0__CMD", "plugin_start_cmd1")
 	os.Setenv("SFTPGO_PLUGINS__0__ARGS", "")
 	os.Setenv("SFTPGO_PLUGINS__0__AUTO_MTLS", "0")
-	os.Setenv("SFTPGO_PLUGINS__0__KMS_OPTIONS__SCHEME", kms.SchemeVaultTransit)
-	os.Setenv("SFTPGO_PLUGINS__0__KMS_OPTIONS__ENCRYPTED_STATUS", kms.SecretStatusVaultTransit)
+	os.Setenv("SFTPGO_PLUGINS__0__KMS_OPTIONS__SCHEME", sdkkms.SchemeVaultTransit)
+	os.Setenv("SFTPGO_PLUGINS__0__KMS_OPTIONS__ENCRYPTED_STATUS", sdkkms.SecretStatusVaultTransit)
 	err = config.LoadConfig(configDir, confName)
 	assert.NoError(t, err)
 	pluginsConf = config.GetPluginsConfig()
@@ -547,8 +547,8 @@ func TestPluginsFromEnv(t *testing.T) {
 	require.Len(t, pluginConf.Args, 0)
 	require.Equal(t, "0a71ded61fccd59c4f3695b51c1b3d180da8d2d77ea09ccee20dac242675c193", pluginConf.SHA256Sum)
 	require.False(t, pluginConf.AutoMTLS)
-	require.Equal(t, kms.SchemeVaultTransit, pluginConf.KMSOptions.Scheme)
-	require.Equal(t, kms.SecretStatusVaultTransit, pluginConf.KMSOptions.EncryptedStatus)
+	require.Equal(t, sdkkms.SchemeVaultTransit, pluginConf.KMSOptions.Scheme)
+	require.Equal(t, sdkkms.SecretStatusVaultTransit, pluginConf.KMSOptions.EncryptedStatus)
 	require.Equal(t, 14, pluginConf.AuthOptions.Scope)
 
 	err = os.Remove(configFilePath)

+ 8 - 9
dataprovider/admin.go

@@ -15,10 +15,9 @@ import (
 	passwordvalidator "github.com/wagslane/go-password-validator"
 	"golang.org/x/crypto/bcrypt"
 
+	"github.com/drakkan/sftpgo/v2/kms"
 	"github.com/drakkan/sftpgo/v2/logger"
 	"github.com/drakkan/sftpgo/v2/mfa"
-	"github.com/drakkan/sftpgo/v2/sdk"
-	"github.com/drakkan/sftpgo/v2/sdk/kms"
 	"github.com/drakkan/sftpgo/v2/util"
 )
 
@@ -52,14 +51,14 @@ var (
 		PermAdminViewEvents}
 )
 
-// TOTPConfig defines the time-based one time password configuration
-type TOTPConfig struct {
+// AdminTOTPConfig defines the time-based one time password configuration
+type AdminTOTPConfig struct {
 	Enabled    bool        `json:"enabled,omitempty"`
 	ConfigName string      `json:"config_name,omitempty"`
 	Secret     *kms.Secret `json:"secret,omitempty"`
 }
 
-func (c *TOTPConfig) validate(username string) error {
+func (c *AdminTOTPConfig) validate(username string) error {
 	if !c.Enabled {
 		c.ConfigName = ""
 		c.Secret = kms.NewEmptySecret()
@@ -93,11 +92,11 @@ type AdminFilters struct {
 	// API key auth allows to impersonate this administrator with an API key
 	AllowAPIKeyAuth bool `json:"allow_api_key_auth,omitempty"`
 	// Time-based one time passwords configuration
-	TOTPConfig TOTPConfig `json:"totp_config,omitempty"`
+	TOTPConfig AdminTOTPConfig `json:"totp_config,omitempty"`
 	// Recovery codes to use if the user loses access to their second factor auth device.
 	// Each code can only be used once, you should use these codes to login and disable or
 	// reset 2FA for your account
-	RecoveryCodes []sdk.RecoveryCode `json:"recovery_codes,omitempty"`
+	RecoveryCodes []RecoveryCode `json:"recovery_codes,omitempty"`
 }
 
 // Admin defines a SFTPGo admin
@@ -403,12 +402,12 @@ func (a *Admin) getACopy() Admin {
 	filters.TOTPConfig.ConfigName = a.Filters.TOTPConfig.ConfigName
 	filters.TOTPConfig.Secret = a.Filters.TOTPConfig.Secret.Clone()
 	copy(filters.AllowList, a.Filters.AllowList)
-	filters.RecoveryCodes = make([]sdk.RecoveryCode, 0)
+	filters.RecoveryCodes = make([]RecoveryCode, 0)
 	for _, code := range a.Filters.RecoveryCodes {
 		if code.Secret == nil {
 			code.Secret = kms.NewEmptySecret()
 		}
-		filters.RecoveryCodes = append(filters.RecoveryCodes, sdk.RecoveryCode{
+		filters.RecoveryCodes = append(filters.RecoveryCodes, RecoveryCode{
 			Secret: code.Secret.Clone(),
 			Used:   code.Used,
 		})

+ 4 - 4
dataprovider/dataprovider.go

@@ -46,12 +46,12 @@ import (
 	"golang.org/x/crypto/ssh"
 
 	"github.com/drakkan/sftpgo/v2/httpclient"
+	"github.com/drakkan/sftpgo/v2/kms"
 	"github.com/drakkan/sftpgo/v2/logger"
 	"github.com/drakkan/sftpgo/v2/metric"
 	"github.com/drakkan/sftpgo/v2/mfa"
 	"github.com/drakkan/sftpgo/v2/plugin"
 	"github.com/drakkan/sftpgo/v2/sdk"
-	"github.com/drakkan/sftpgo/v2/sdk/kms"
 	"github.com/drakkan/sftpgo/v2/util"
 	"github.com/drakkan/sftpgo/v2/vfs"
 )
@@ -1153,7 +1153,7 @@ func HasAdmin() bool {
 // AddAdmin adds a new SFTPGo admin
 func AddAdmin(admin *Admin, executor, ipAddress string) error {
 	admin.Filters.RecoveryCodes = nil
-	admin.Filters.TOTPConfig = TOTPConfig{
+	admin.Filters.TOTPConfig = AdminTOTPConfig{
 		Enabled: false,
 	}
 	err := provider.addAdmin(admin)
@@ -1199,7 +1199,7 @@ func UserExists(username string) (User, error) {
 // AddUser adds a new SFTPGo user.
 func AddUser(user *User, executor, ipAddress string) error {
 	user.Filters.RecoveryCodes = nil
-	user.Filters.TOTPConfig = sdk.TOTPConfig{
+	user.Filters.TOTPConfig = UserTOTPConfig{
 		Enabled: false,
 	}
 	err := provider.addUser(user)
@@ -1559,7 +1559,7 @@ func validateUserVirtualFolders(user *User) error {
 	return nil
 }
 
-func validateUserTOTPConfig(c *sdk.TOTPConfig, username string) error {
+func validateUserTOTPConfig(c *UserTOTPConfig, username string) error {
 	if !c.Enabled {
 		c.ConfigName = ""
 		c.Secret = kms.NewEmptySecret()

+ 1 - 2
dataprovider/sqlcommon.go

@@ -13,7 +13,6 @@ import (
 	"github.com/cockroachdb/cockroach-go/v2/crdb"
 
 	"github.com/drakkan/sftpgo/v2/logger"
-	"github.com/drakkan/sftpgo/v2/sdk"
 	"github.com/drakkan/sftpgo/v2/util"
 	"github.com/drakkan/sftpgo/v2/vfs"
 )
@@ -1386,7 +1385,7 @@ func getUserFromDbRow(row sqlScanner) (User, error) {
 		user.Permissions = perms
 	}
 	if filters.Valid {
-		var userFilters sdk.UserFilters
+		var userFilters UserFilters
 		err = json.Unmarshal([]byte(filters.String), &userFilters)
 		if err == nil {
 			user.Filters = userFilters

+ 40 - 5
dataprovider/user.go

@@ -15,10 +15,10 @@ import (
 	"strings"
 	"time"
 
+	"github.com/drakkan/sftpgo/v2/kms"
 	"github.com/drakkan/sftpgo/v2/logger"
 	"github.com/drakkan/sftpgo/v2/mfa"
 	"github.com/drakkan/sftpgo/v2/sdk"
-	"github.com/drakkan/sftpgo/v2/sdk/kms"
 	"github.com/drakkan/sftpgo/v2/util"
 	"github.com/drakkan/sftpgo/v2/vfs"
 )
@@ -79,9 +79,44 @@ var (
 	permsCreateAny             = []string{PermUpload, PermCreateDirs}
 )
 
+// RecoveryCode defines a 2FA recovery code
+type RecoveryCode struct {
+	Secret *kms.Secret `json:"secret"`
+	Used   bool        `json:"used,omitempty"`
+}
+
+// UserTOTPConfig defines the time-based one time password configuration
+type UserTOTPConfig struct {
+	Enabled    bool        `json:"enabled,omitempty"`
+	ConfigName string      `json:"config_name,omitempty"`
+	Secret     *kms.Secret `json:"secret,omitempty"`
+	// TOTP will be required for the specified protocols.
+	// SSH protocol (SFTP/SCP/SSH commands) will ask for the TOTP passcode if the client uses keyboard interactive
+	// authentication.
+	// FTP have no standard way to support two factor authentication, if you
+	// enable the support for this protocol you have to add the TOTP passcode after the password.
+	// For example if your password is "password" and your one time passcode is
+	// "123456" you have to use "password123456" as password.
+	Protocols []string `json:"protocols,omitempty"`
+}
+
+// UserFilters defines additional restrictions for a user
+// TODO: rename to UserOptions in v3
+type UserFilters struct {
+	sdk.BaseUserFilters
+	// Time-based one time passwords configuration
+	TOTPConfig UserTOTPConfig `json:"totp_config,omitempty"`
+	// Recovery codes to use if the user loses access to their second factor auth device.
+	// Each code can only be used once, you should use these codes to login and disable or
+	// reset 2FA for your account
+	RecoveryCodes []RecoveryCode `json:"recovery_codes,omitempty"`
+}
+
 // User defines a SFTPGo user
 type User struct {
 	sdk.BaseUser
+	// Additional restrictions
+	Filters UserFilters `json:"filters"`
 	// Mapping between virtual paths and virtual folders
 	VirtualFolders []vfs.VirtualFolder `json:"virtual_folders,omitempty"`
 	// Filesystem configuration details
@@ -1168,7 +1203,7 @@ func (u *User) getACopy() User {
 		copy(perms, v)
 		permissions[k] = perms
 	}
-	filters := sdk.UserFilters{}
+	filters := UserFilters{}
 	filters.MaxUploadFileSize = u.Filters.MaxUploadFileSize
 	filters.TLSUsername = u.Filters.TLSUsername
 	filters.UserType = u.Filters.UserType
@@ -1194,12 +1229,12 @@ func (u *User) getACopy() User {
 	filters.AllowAPIKeyAuth = u.Filters.AllowAPIKeyAuth
 	filters.WebClient = make([]string, len(u.Filters.WebClient))
 	copy(filters.WebClient, u.Filters.WebClient)
-	filters.RecoveryCodes = make([]sdk.RecoveryCode, 0, len(u.Filters.RecoveryCodes))
+	filters.RecoveryCodes = make([]RecoveryCode, 0, len(u.Filters.RecoveryCodes))
 	for _, code := range u.Filters.RecoveryCodes {
 		if code.Secret == nil {
 			code.Secret = kms.NewEmptySecret()
 		}
-		filters.RecoveryCodes = append(filters.RecoveryCodes, sdk.RecoveryCode{
+		filters.RecoveryCodes = append(filters.RecoveryCodes, RecoveryCode{
 			Secret: code.Secret.Clone(),
 			Used:   code.Used,
 		})
@@ -1238,12 +1273,12 @@ func (u *User) getACopy() User {
 			Status:            u.Status,
 			ExpirationDate:    u.ExpirationDate,
 			LastLogin:         u.LastLogin,
-			Filters:           filters,
 			AdditionalInfo:    u.AdditionalInfo,
 			Description:       u.Description,
 			CreatedAt:         u.CreatedAt,
 			UpdatedAt:         u.UpdatedAt,
 		},
+		Filters:        filters,
 		VirtualFolders: virtualFolders,
 		FsConfig:       u.FsConfig.GetACopy(),
 	}

+ 1 - 1
ftpd/cryptfs_test.go

@@ -18,8 +18,8 @@ import (
 	"github.com/drakkan/sftpgo/v2/common"
 	"github.com/drakkan/sftpgo/v2/dataprovider"
 	"github.com/drakkan/sftpgo/v2/httpdtest"
+	"github.com/drakkan/sftpgo/v2/kms"
 	"github.com/drakkan/sftpgo/v2/sdk"
-	"github.com/drakkan/sftpgo/v2/sdk/kms"
 )
 
 func TestBasicFTPHandlingCryptFs(t *testing.T) {

+ 5 - 6
ftpd/ftpd_test.go

@@ -32,10 +32,11 @@ import (
 	"github.com/drakkan/sftpgo/v2/dataprovider"
 	"github.com/drakkan/sftpgo/v2/ftpd"
 	"github.com/drakkan/sftpgo/v2/httpdtest"
+	"github.com/drakkan/sftpgo/v2/kms"
 	"github.com/drakkan/sftpgo/v2/logger"
 	"github.com/drakkan/sftpgo/v2/mfa"
 	"github.com/drakkan/sftpgo/v2/sdk"
-	"github.com/drakkan/sftpgo/v2/sdk/kms"
+	sdkkms "github.com/drakkan/sftpgo/v2/sdk/kms"
 	"github.com/drakkan/sftpgo/v2/sftpd"
 	"github.com/drakkan/sftpgo/v2/vfs"
 )
@@ -604,7 +605,7 @@ func TestMultiFactorAuth(t *testing.T) {
 	configName, _, secret, _, err := mfa.GenerateTOTPSecret(mfa.GetAvailableTOTPConfigNames()[0], user.Username)
 	assert.NoError(t, err)
 	user.Password = defaultPassword
-	user.Filters.TOTPConfig = sdk.TOTPConfig{
+	user.Filters.TOTPConfig = dataprovider.UserTOTPConfig{
 		Enabled:    true,
 		ConfigName: configName,
 		Secret:     kms.NewPlainSecret(secret),
@@ -1686,7 +1687,7 @@ func TestLoginWithDatabaseCredentials(t *testing.T) {
 
 	user, _, err := httpdtest.AddUser(u, http.StatusCreated)
 	assert.NoError(t, err)
-	assert.Equal(t, kms.SecretStatusSecretBox, user.FsConfig.GCSConfig.Credentials.GetStatus())
+	assert.Equal(t, sdkkms.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())
@@ -2808,9 +2809,7 @@ func TestNestedVirtualFolders(t *testing.T) {
 			FsConfig: vfs.Filesystem{
 				Provider: sdk.CryptedFilesystemProvider,
 				CryptConfig: vfs.CryptFsConfig{
-					CryptFsConfig: sdk.CryptFsConfig{
-						Passphrase: kms.NewPlainSecret(defaultPassword),
-					},
+					Passphrase: kms.NewPlainSecret(defaultPassword),
 				},
 			},
 			MappedPath: mappedPathCrypt,

+ 2 - 2
httpd/api_admin.go

@@ -83,7 +83,7 @@ func disableAdmin2FA(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 	admin.Filters.RecoveryCodes = nil
-	admin.Filters.TOTPConfig = dataprovider.TOTPConfig{
+	admin.Filters.TOTPConfig = dataprovider.AdminTOTPConfig{
 		Enabled: false,
 	}
 	if err := dataprovider.UpdateAdmin(&admin, claims.Username, util.GetIPFromRemoteAddress(r.RemoteAddr)); err != nil {
@@ -105,7 +105,7 @@ func updateAdmin(w http.ResponseWriter, r *http.Request) {
 	adminID := admin.ID
 	totpConfig := admin.Filters.TOTPConfig
 	recoveryCodes := admin.Filters.RecoveryCodes
-	admin.Filters.TOTPConfig = dataprovider.TOTPConfig{}
+	admin.Filters.TOTPConfig = dataprovider.AdminTOTPConfig{}
 	admin.Filters.RecoveryCodes = nil
 	err = render.DecodeJSON(r.Body, &admin)
 	if err != nil {

+ 8 - 9
httpd/api_mfa.go

@@ -8,9 +8,8 @@ import (
 	"github.com/go-chi/render"
 
 	"github.com/drakkan/sftpgo/v2/dataprovider"
+	"github.com/drakkan/sftpgo/v2/kms"
 	"github.com/drakkan/sftpgo/v2/mfa"
-	"github.com/drakkan/sftpgo/v2/sdk"
-	"github.com/drakkan/sftpgo/v2/sdk/kms"
 	"github.com/drakkan/sftpgo/v2/util"
 )
 
@@ -81,10 +80,10 @@ func saveTOTPConfig(w http.ResponseWriter, r *http.Request) {
 		sendAPIResponse(w, r, err, "Invalid token claims", http.StatusBadRequest)
 		return
 	}
-	recoveryCodes := make([]sdk.RecoveryCode, 0, 12)
+	recoveryCodes := make([]dataprovider.RecoveryCode, 0, 12)
 	for i := 0; i < 12; i++ {
 		code := getNewRecoveryCode()
-		recoveryCodes = append(recoveryCodes, sdk.RecoveryCode{Secret: kms.NewPlainSecret(code)})
+		recoveryCodes = append(recoveryCodes, dataprovider.RecoveryCode{Secret: kms.NewPlainSecret(code)})
 	}
 	if claims.hasUserAudience() {
 		if err := saveUserTOTPConfig(claims.Username, r, recoveryCodes); err != nil {
@@ -125,7 +124,7 @@ func getRecoveryCodes(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 	recoveryCodes := make([]recoveryCode, 0, 12)
-	var accountRecoveryCodes []sdk.RecoveryCode
+	var accountRecoveryCodes []dataprovider.RecoveryCode
 	if claims.hasUserAudience() {
 		user, err := dataprovider.UserExists(claims.Username)
 		if err != nil {
@@ -163,11 +162,11 @@ func generateRecoveryCodes(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 	recoveryCodes := make([]string, 0, 12)
-	accountRecoveryCodes := make([]sdk.RecoveryCode, 0, 12)
+	accountRecoveryCodes := make([]dataprovider.RecoveryCode, 0, 12)
 	for i := 0; i < 12; i++ {
 		code := getNewRecoveryCode()
 		recoveryCodes = append(recoveryCodes, code)
-		accountRecoveryCodes = append(accountRecoveryCodes, sdk.RecoveryCode{Secret: kms.NewPlainSecret(code)})
+		accountRecoveryCodes = append(accountRecoveryCodes, dataprovider.RecoveryCode{Secret: kms.NewPlainSecret(code)})
 	}
 	if claims.hasUserAudience() {
 		user, err := dataprovider.UserExists(claims.Username)
@@ -200,7 +199,7 @@ func getNewRecoveryCode() string {
 	return fmt.Sprintf("RC-%v", strings.ToUpper(util.GenerateUniqueID()))
 }
 
-func saveUserTOTPConfig(username string, r *http.Request, recoveryCodes []sdk.RecoveryCode) error {
+func saveUserTOTPConfig(username string, r *http.Request, recoveryCodes []dataprovider.RecoveryCode) error {
 	user, err := dataprovider.UserExists(username)
 	if err != nil {
 		return err
@@ -220,7 +219,7 @@ func saveUserTOTPConfig(username string, r *http.Request, recoveryCodes []sdk.Re
 	return dataprovider.UpdateUser(&user, dataprovider.ActionExecutorSelf, util.GetIPFromRemoteAddress(r.RemoteAddr))
 }
 
-func saveAdminTOTPConfig(username string, r *http.Request, recoveryCodes []sdk.RecoveryCode) error {
+func saveAdminTOTPConfig(username string, r *http.Request, recoveryCodes []dataprovider.RecoveryCode) error {
 	admin, err := dataprovider.AdminExists(username)
 	if err != nil {
 		return err

+ 3 - 3
httpd/api_user.go

@@ -10,8 +10,8 @@ import (
 
 	"github.com/drakkan/sftpgo/v2/common"
 	"github.com/drakkan/sftpgo/v2/dataprovider"
+	"github.com/drakkan/sftpgo/v2/kms"
 	"github.com/drakkan/sftpgo/v2/sdk"
-	"github.com/drakkan/sftpgo/v2/sdk/kms"
 	"github.com/drakkan/sftpgo/v2/smtp"
 	"github.com/drakkan/sftpgo/v2/util"
 	"github.com/drakkan/sftpgo/v2/vfs"
@@ -89,7 +89,7 @@ func disableUser2FA(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 	user.Filters.RecoveryCodes = nil
-	user.Filters.TOTPConfig = sdk.TOTPConfig{
+	user.Filters.TOTPConfig = dataprovider.UserTOTPConfig{
 		Enabled: false,
 	}
 	if err := dataprovider.UpdateUser(&user, claims.Username, util.GetIPFromRemoteAddress(r.RemoteAddr)); err != nil {
@@ -140,7 +140,7 @@ func updateUser(w http.ResponseWriter, r *http.Request) {
 	user.FsConfig.GCSConfig = vfs.GCSFsConfig{}
 	user.FsConfig.CryptConfig = vfs.CryptFsConfig{}
 	user.FsConfig.SFTPConfig = vfs.SFTPFsConfig{}
-	user.Filters.TOTPConfig = sdk.TOTPConfig{}
+	user.Filters.TOTPConfig = dataprovider.UserTOTPConfig{}
 	user.Filters.RecoveryCodes = nil
 	user.VirtualFolders = nil
 	err = render.DecodeJSON(r.Body, &user)

File diff suppressed because it is too large
+ 134 - 139
httpd/httpd_test.go


+ 9 - 9
httpd/internal_test.go

@@ -33,9 +33,9 @@ import (
 
 	"github.com/drakkan/sftpgo/v2/common"
 	"github.com/drakkan/sftpgo/v2/dataprovider"
+	"github.com/drakkan/sftpgo/v2/kms"
 	"github.com/drakkan/sftpgo/v2/plugin"
 	"github.com/drakkan/sftpgo/v2/sdk"
-	"github.com/drakkan/sftpgo/v2/sdk/kms"
 	"github.com/drakkan/sftpgo/v2/util"
 	"github.com/drakkan/sftpgo/v2/vfs"
 )
@@ -1773,10 +1773,10 @@ func TestConnection(t *testing.T) {
 		FsConfig: vfs.Filesystem{
 			Provider: sdk.GCSFilesystemProvider,
 			GCSConfig: vfs.GCSFsConfig{
-				GCSFsConfig: sdk.GCSFsConfig{
-					Bucket:      "test_bucket_name",
-					Credentials: kms.NewPlainSecret("invalid JSON payload"),
+				BaseGCSFsConfig: sdk.BaseGCSFsConfig{
+					Bucket: "test_bucket_name",
 				},
+				Credentials: kms.NewPlainSecret("invalid JSON payload"),
 			},
 		},
 	}
@@ -1815,12 +1815,12 @@ func TestGetFileWriterErrors(t *testing.T) {
 
 	user.FsConfig.Provider = sdk.S3FilesystemProvider
 	user.FsConfig.S3Config = vfs.S3FsConfig{
-		S3FsConfig: sdk.S3FsConfig{
-			Bucket:       "b",
-			Region:       "us-west-1",
-			AccessKey:    "key",
-			AccessSecret: kms.NewPlainSecret("secret"),
+		BaseS3FsConfig: sdk.BaseS3FsConfig{
+			Bucket:    "b",
+			Region:    "us-west-1",
+			AccessKey: "key",
 		},
+		AccessSecret: kms.NewPlainSecret("secret"),
 	}
 	connection = &Connection{
 		BaseConnection: common.NewBaseConnection(xid.New().String(), common.ProtocolHTTP, "", "", user),

+ 9 - 6
httpd/webadmin.go

@@ -16,9 +16,10 @@ import (
 
 	"github.com/drakkan/sftpgo/v2/common"
 	"github.com/drakkan/sftpgo/v2/dataprovider"
+	"github.com/drakkan/sftpgo/v2/kms"
 	"github.com/drakkan/sftpgo/v2/mfa"
 	"github.com/drakkan/sftpgo/v2/sdk"
-	"github.com/drakkan/sftpgo/v2/sdk/kms"
+	sdkkms "github.com/drakkan/sftpgo/v2/sdk/kms"
 	"github.com/drakkan/sftpgo/v2/smtp"
 	"github.com/drakkan/sftpgo/v2/util"
 	"github.com/drakkan/sftpgo/v2/version"
@@ -179,7 +180,7 @@ type changePasswordPage struct {
 type mfaPage struct {
 	basePage
 	TOTPConfigs     []string
-	TOTPConfig      dataprovider.TOTPConfig
+	TOTPConfig      dataprovider.AdminTOTPConfig
 	GenerateTOTPURL string
 	ValidateTOTPURL string
 	SaveTOTPURL     string
@@ -821,8 +822,8 @@ func getFilePatternsFromPostField(r *http.Request) []sdk.PatternsFilter {
 	return result
 }
 
-func getFiltersFromUserPostFields(r *http.Request) (sdk.UserFilters, error) {
-	var filters sdk.UserFilters
+func getFiltersFromUserPostFields(r *http.Request) (sdk.BaseUserFilters, error) {
+	var filters sdk.BaseUserFilters
 	bwLimits, err := getBandwidthLimitsFromPostFields(r)
 	if err != nil {
 		return filters, err
@@ -853,7 +854,7 @@ func getFiltersFromUserPostFields(r *http.Request) (sdk.UserFilters, error) {
 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)
+		secret.SetStatus(sdkkms.SecretStatusRedacted)
 	}
 	if strings.TrimSpace(secret.GetPayload()) == "" {
 		secret.SetStatus("")
@@ -1204,10 +1205,12 @@ func getUserFromPostFields(r *http.Request) (dataprovider.User, error) {
 			DownloadBandwidth: bandwidthDL,
 			Status:            status,
 			ExpirationDate:    expirationDateMillis,
-			Filters:           filters,
 			AdditionalInfo:    r.Form.Get("additional_info"),
 			Description:       r.Form.Get("description"),
 		},
+		Filters: dataprovider.UserFilters{
+			BaseUserFilters: filters,
+		},
 		VirtualFolders: getVirtualFoldersFromPostFields(r),
 		FsConfig:       fsConfig,
 	}

+ 1 - 1
httpd/webclient.go

@@ -158,7 +158,7 @@ type changeClientPasswordPage struct {
 type clientMFAPage struct {
 	baseClientPage
 	TOTPConfigs     []string
-	TOTPConfig      sdk.TOTPConfig
+	TOTPConfig      dataprovider.UserTOTPConfig
 	GenerateTOTPURL string
 	ValidateTOTPURL string
 	SaveTOTPURL     string

+ 1 - 1
httpdtest/httpdtest.go

@@ -20,7 +20,7 @@ import (
 	"github.com/drakkan/sftpgo/v2/dataprovider"
 	"github.com/drakkan/sftpgo/v2/httpclient"
 	"github.com/drakkan/sftpgo/v2/httpd"
-	"github.com/drakkan/sftpgo/v2/sdk/kms"
+	"github.com/drakkan/sftpgo/v2/kms"
 	"github.com/drakkan/sftpgo/v2/util"
 	"github.com/drakkan/sftpgo/v2/version"
 	"github.com/drakkan/sftpgo/v2/vfs"

+ 10 - 6
sdk/kms/basesecret.go → kms/basesecret.go

@@ -1,17 +1,21 @@
 package kms
 
+import (
+	sdkkms "github.com/drakkan/sftpgo/v2/sdk/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"`
+	Status         sdkkms.SecretStatus `json:"status,omitempty"`
+	Payload        string              `json:"payload,omitempty"`
+	Key            string              `json:"key,omitempty"`
+	AdditionalData string              `json:"additional_data,omitempty"`
 	// 1 means encrypted using a master key
 	Mode int `json:"mode,omitempty"`
 }
 
 // GetStatus returns the secret's status
-func (s *BaseSecret) GetStatus() SecretStatus {
+func (s *BaseSecret) GetStatus() sdkkms.SecretStatus {
 	return s.Status
 }
 
@@ -46,7 +50,7 @@ func (s *BaseSecret) SetAdditionalData(value string) {
 }
 
 // SetStatus sets the secret's status
-func (s *BaseSecret) SetStatus(value SecretStatus) {
+func (s *BaseSecret) SetStatus(value sdkkms.SecretStatus) {
 	s.Status = value
 }
 

+ 8 - 6
sdk/kms/builtin.go → kms/builtin.go

@@ -8,6 +8,8 @@ import (
 	"encoding/hex"
 	"errors"
 	"io"
+
+	sdkkms "github.com/drakkan/sftpgo/v2/sdk/kms"
 )
 
 var (
@@ -19,7 +21,7 @@ type builtinSecret struct {
 }
 
 func init() {
-	RegisterSecretProvider(SchemeBuiltin, SecretStatusAES256GCM, newBuiltinSecret)
+	RegisterSecretProvider(sdkkms.SchemeBuiltin, sdkkms.SecretStatusAES256GCM, newBuiltinSecret)
 }
 
 func newBuiltinSecret(base BaseSecret, url, masterKey string) SecretProvider {
@@ -33,7 +35,7 @@ func (s *builtinSecret) Name() string {
 }
 
 func (s *builtinSecret) IsEncrypted() bool {
-	return s.Status == SecretStatusAES256GCM
+	return s.Status == sdkkms.SecretStatusAES256GCM
 }
 
 func (s *builtinSecret) deriveKey(key []byte) []byte {
@@ -52,7 +54,7 @@ func (s *builtinSecret) Encrypt() error {
 		return ErrInvalidSecret
 	}
 	switch s.Status {
-	case SecretStatusPlain:
+	case sdkkms.SecretStatusPlain:
 		key := make([]byte, 32)
 		if _, err := io.ReadFull(rand.Reader, key); err != nil {
 			return err
@@ -76,7 +78,7 @@ func (s *builtinSecret) Encrypt() error {
 		ciphertext := gcm.Seal(nonce, nonce, []byte(s.Payload), aad)
 		s.Key = hex.EncodeToString(key)
 		s.Payload = hex.EncodeToString(ciphertext)
-		s.Status = SecretStatusAES256GCM
+		s.Status = sdkkms.SecretStatusAES256GCM
 		return nil
 	default:
 		return ErrWrongSecretStatus
@@ -85,7 +87,7 @@ func (s *builtinSecret) Encrypt() error {
 
 func (s *builtinSecret) Decrypt() error {
 	switch s.Status {
-	case SecretStatusAES256GCM:
+	case sdkkms.SecretStatusAES256GCM:
 		encrypted, err := hex.DecodeString(s.Payload)
 		if err != nil {
 			return err
@@ -115,7 +117,7 @@ func (s *builtinSecret) Decrypt() error {
 		if err != nil {
 			return err
 		}
-		s.Status = SecretStatusPlain
+		s.Status = sdkkms.SecretStatusPlain
 		s.Payload = string(plaintext)
 		s.Key = ""
 		s.AdditionalData = ""

+ 417 - 0
kms/kms.go

@@ -0,0 +1,417 @@
+// Package kms provides Key Management Services support
+package kms
+
+import (
+	"encoding/json"
+	"errors"
+	"os"
+	"strings"
+	"sync"
+
+	"github.com/drakkan/sftpgo/v2/logger"
+	sdkkms "github.com/drakkan/sftpgo/v2/sdk/kms"
+)
+
+// SecretProvider defines the interface for a KMS secrets provider
+type SecretProvider interface {
+	Name() string
+	Encrypt() error
+	Decrypt() error
+	IsEncrypted() bool
+	GetStatus() sdkkms.SecretStatus
+	GetPayload() string
+	GetKey() string
+	GetAdditionalData() string
+	GetMode() int
+	SetKey(string)
+	SetAdditionalData(string)
+	SetStatus(sdkkms.SecretStatus)
+	Clone() SecretProvider
+}
+
+const (
+	logSender = "kms"
+)
+
+// 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"`
+	MasterKeyString string `json:"master_key" mapstructure:"master_key"`
+	masterKey       string
+}
+
+type registeredSecretProvider struct {
+	encryptedStatus sdkkms.SecretStatus
+	newFn           func(base BaseSecret, url, masterKey string) SecretProvider
+}
+
+var (
+	// ErrWrongSecretStatus defines the error to return if the secret status is not appropriate
+	// for the request operation
+	ErrWrongSecretStatus = errors.New("wrong secret status")
+	// ErrInvalidSecret defines the error to return if a secret is not valid
+	ErrInvalidSecret    = errors.New("invalid secret")
+	validSecretStatuses = []string{sdkkms.SecretStatusPlain, sdkkms.SecretStatusAES256GCM, sdkkms.SecretStatusSecretBox,
+		sdkkms.SecretStatusVaultTransit, sdkkms.SecretStatusAWS, sdkkms.SecretStatusGCP, sdkkms.SecretStatusRedacted}
+	config          Configuration
+	secretProviders = make(map[string]registeredSecretProvider)
+)
+
+// RegisterSecretProvider register a new secret provider
+func RegisterSecretProvider(scheme string, encryptedStatus sdkkms.SecretStatus,
+	fn func(base BaseSecret, url, masterKey string) SecretProvider,
+) {
+	secretProviders[scheme] = registeredSecretProvider{
+		encryptedStatus: encryptedStatus,
+		newFn:           fn,
+	}
+}
+
+// NewSecret builds a new Secret using the provided arguments
+func NewSecret(status sdkkms.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(sdkkms.SecretStatusPlain, payload, "", "")
+}
+
+// Initialize configures the KMS support
+func (c *Configuration) Initialize() error {
+	if c.Secrets.MasterKeyString != "" {
+		c.Secrets.masterKey = c.Secrets.MasterKeyString
+	}
+	if c.Secrets.masterKey == "" && c.Secrets.MasterKeyPath != "" {
+		mKey, err := os.ReadFile(c.Secrets.MasterKeyPath)
+		if err != nil {
+			return err
+		}
+		c.Secrets.masterKey = strings.TrimSpace(string(mKey))
+	}
+	config = *c
+	if config.Secrets.URL == "" {
+		config.Secrets.URL = sdkkms.SchemeLocal + "://"
+	}
+	for k, v := range secretProviders {
+		logger.Info(logSender, "", "secret provider registered for scheme: %#v, encrypted status: %#v",
+			k, v.encryptedStatus)
+	}
+	return nil
+}
+
+func (c *Configuration) newSecret(status sdkkms.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 {
+	for k, v := range secretProviders {
+		if strings.HasPrefix(c.Secrets.URL, k) {
+			return v.newFn(base, c.Secrets.URL, c.Secrets.masterKey)
+		}
+	}
+	logger.Warn(logSender, "", "no secret provider registered for URL %v, fallback to local provider", c.Secrets.URL)
+	return NewLocalSecret(base, c.Secrets.URL, c.Secrets.masterKey)
+}
+
+// Secret defines the struct used to store confidential data
+type Secret struct {
+	sync.RWMutex
+	provider SecretProvider
+}
+
+// MarshalJSON return the JSON encoding of the Secret object
+func (s *Secret) MarshalJSON() ([]byte, error) {
+	s.RLock()
+	defer s.RUnlock()
+
+	return json.Marshal(&BaseSecret{
+		Status:         s.provider.GetStatus(),
+		Payload:        s.provider.GetPayload(),
+		Key:            s.provider.GetKey(),
+		AdditionalData: s.provider.GetAdditionalData(),
+		Mode:           s.provider.GetMode(),
+	})
+}
+
+// UnmarshalJSON parses the JSON-encoded data and stores the result
+// in the Secret object
+func (s *Secret) UnmarshalJSON(data []byte) error {
+	s.Lock()
+	defer s.Unlock()
+
+	baseSecret := BaseSecret{}
+	err := json.Unmarshal(data, &baseSecret)
+	if err != nil {
+		return err
+	}
+	if baseSecret.isEmpty() {
+		s.provider = config.getSecretProvider(baseSecret)
+		return nil
+	}
+
+	if baseSecret.Status == sdkkms.SecretStatusPlain || baseSecret.Status == sdkkms.SecretStatusRedacted {
+		s.provider = config.getSecretProvider(baseSecret)
+		return nil
+	}
+
+	for _, v := range secretProviders {
+		if v.encryptedStatus == baseSecret.Status {
+			s.provider = v.newFn(baseSecret, config.Secrets.URL, config.Secrets.masterKey)
+			return nil
+		}
+	}
+	logger.Error(logSender, "", "no provider registered for status %#v", baseSecret.Status)
+	return ErrInvalidSecret
+}
+
+// IsEqual returns true if all the secrets fields are equal
+func (s *Secret) IsEqual(other *Secret) bool {
+	if s.GetStatus() != other.GetStatus() {
+		return false
+	}
+	if s.GetPayload() != other.GetPayload() {
+		return false
+	}
+	if s.GetKey() != other.GetKey() {
+		return false
+	}
+	if s.GetAdditionalData() != other.GetAdditionalData() {
+		return false
+	}
+	if s.GetMode() != other.GetMode() {
+		return false
+	}
+	return true
+}
+
+// Clone returns a copy of the secret object
+func (s *Secret) Clone() *Secret {
+	s.RLock()
+	defer s.RUnlock()
+
+	return &Secret{
+		provider: s.provider.Clone(),
+	}
+}
+
+// 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 {
+	s.RLock()
+	defer s.RUnlock()
+
+	return s.provider.IsEncrypted()
+}
+
+// IsPlain returns true if the secret is in plain text
+func (s *Secret) IsPlain() bool {
+	s.RLock()
+	defer s.RUnlock()
+
+	return s.provider.GetStatus() == sdkkms.SecretStatusPlain
+}
+
+// IsNotPlainAndNotEmpty returns true if the secret is not plain and not empty.
+// This is an utility method, we update the secret for an existing user
+// if it is empty or plain
+func (s *Secret) IsNotPlainAndNotEmpty() bool {
+	s.RLock()
+	defer s.RUnlock()
+
+	return !s.IsPlain() && !s.IsEmpty()
+}
+
+// IsRedacted returns true if the secret is redacted
+func (s *Secret) IsRedacted() bool {
+	s.RLock()
+	defer s.RUnlock()
+
+	return s.provider.GetStatus() == sdkkms.SecretStatusRedacted
+}
+
+// GetPayload returns the secret payload
+func (s *Secret) GetPayload() string {
+	s.RLock()
+	defer s.RUnlock()
+
+	return s.provider.GetPayload()
+}
+
+// GetAdditionalData returns the secret additional data
+func (s *Secret) GetAdditionalData() string {
+	s.RLock()
+	defer s.RUnlock()
+
+	return s.provider.GetAdditionalData()
+}
+
+// GetStatus returns the secret status
+func (s *Secret) GetStatus() sdkkms.SecretStatus {
+	s.RLock()
+	defer s.RUnlock()
+
+	return s.provider.GetStatus()
+}
+
+// GetKey returns the secret key
+func (s *Secret) GetKey() string {
+	s.RLock()
+	defer s.RUnlock()
+
+	return s.provider.GetKey()
+}
+
+// GetMode returns the secret mode
+func (s *Secret) GetMode() int {
+	s.RLock()
+	defer s.RUnlock()
+
+	return s.provider.GetMode()
+}
+
+// SetAdditionalData sets the given additional data
+func (s *Secret) SetAdditionalData(value string) {
+	s.Lock()
+	defer s.Unlock()
+
+	s.provider.SetAdditionalData(value)
+}
+
+// SetStatus sets the status for this secret
+func (s *Secret) SetStatus(value sdkkms.SecretStatus) {
+	s.Lock()
+	defer s.Unlock()
+
+	s.provider.SetStatus(value)
+}
+
+// SetKey sets the key for this secret
+func (s *Secret) SetKey(value string) {
+	s.Lock()
+	defer s.Unlock()
+
+	s.provider.SetKey(value)
+}
+
+// IsEmpty returns true if all fields are empty
+func (s *Secret) IsEmpty() bool {
+	s.RLock()
+	defer s.RUnlock()
+
+	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 {
+	s.RLock()
+	defer s.RUnlock()
+
+	if !s.IsValidInput() {
+		return false
+	}
+	switch s.provider.GetStatus() {
+	case sdkkms.SecretStatusAES256GCM, sdkkms.SecretStatusSecretBox:
+		if len(s.provider.GetKey()) != 64 {
+			return false
+		}
+	case sdkkms.SecretStatusAWS, sdkkms.SecretStatusGCP, sdkkms.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 {
+	s.RLock()
+	defer s.RUnlock()
+
+	if !isSecretStatusValid(s.provider.GetStatus()) {
+		return false
+	}
+	if s.provider.GetPayload() == "" {
+		return false
+	}
+	return true
+}
+
+// Hide hides info to decrypt data
+func (s *Secret) Hide() {
+	s.Lock()
+	defer s.Unlock()
+
+	s.provider.SetKey("")
+	s.provider.SetAdditionalData("")
+}
+
+// Encrypt encrypts a plain text Secret object
+func (s *Secret) Encrypt() error {
+	s.Lock()
+	defer s.Unlock()
+
+	return s.provider.Encrypt()
+}
+
+// Decrypt decrypts a Secret object
+func (s *Secret) Decrypt() error {
+	s.Lock()
+	defer s.Unlock()
+
+	return s.provider.Decrypt()
+}
+
+// TryDecrypt decrypts a Secret object if encrypted.
+// It returns a nil error if the object is not encrypted
+func (s *Secret) TryDecrypt() error {
+	s.Lock()
+	defer s.Unlock()
+
+	if s.provider.IsEncrypted() {
+		return s.provider.Decrypt()
+	}
+	return nil
+}
+
+func isSecretStatusValid(status string) bool {
+	for idx := range validSecretStatuses {
+		if validSecretStatuses[idx] == status {
+			return true
+		}
+	}
+	return false
+}

+ 7 - 5
sdk/kms/local.go → kms/local.go

@@ -9,10 +9,12 @@ import (
 
 	"gocloud.dev/secrets/localsecrets"
 	"golang.org/x/crypto/hkdf"
+
+	sdkkms "github.com/drakkan/sftpgo/v2/sdk/kms"
 )
 
 func init() {
-	RegisterSecretProvider(SchemeLocal, SecretStatusSecretBox, NewLocalSecret)
+	RegisterSecretProvider(sdkkms.SchemeLocal, sdkkms.SecretStatusSecretBox, NewLocalSecret)
 }
 
 type localSecret struct {
@@ -33,11 +35,11 @@ func (s *localSecret) Name() string {
 }
 
 func (s *localSecret) IsEncrypted() bool {
-	return s.Status == SecretStatusSecretBox
+	return s.Status == sdkkms.SecretStatusSecretBox
 }
 
 func (s *localSecret) Encrypt() error {
-	if s.Status != SecretStatusPlain {
+	if s.Status != sdkkms.SecretStatusPlain {
 		return ErrWrongSecretStatus
 	}
 	if s.Payload == "" {
@@ -60,7 +62,7 @@ func (s *localSecret) Encrypt() error {
 	}
 	s.Key = hex.EncodeToString(secretKey[:])
 	s.Payload = base64.StdEncoding.EncodeToString(ciphertext)
-	s.Status = SecretStatusSecretBox
+	s.Status = sdkkms.SecretStatusSecretBox
 	s.Mode = s.getEncryptionMode()
 	return nil
 }
@@ -88,7 +90,7 @@ func (s *localSecret) Decrypt() error {
 	if err != nil {
 		return err
 	}
-	s.Status = SecretStatusPlain
+	s.Status = sdkkms.SecretStatusPlain
 	s.Payload = string(plaintext)
 	s.Key = ""
 	s.AdditionalData = ""

+ 0 - 21
logger/logger.go

@@ -18,8 +18,6 @@ import (
 	ftpserverlog "github.com/fclairamb/go-log"
 	"github.com/rs/zerolog"
 	lumberjack "gopkg.in/natefinch/lumberjack.v2"
-
-	sdklogger "github.com/drakkan/sftpgo/v2/sdk/logger"
 )
 
 const (
@@ -43,22 +41,6 @@ var (
 	rollingLogger *lumberjack.Logger
 )
 
-type logWrapper struct{}
-
-// Log logs at the specified level for the specified sender
-func (l *logWrapper) Log(level int, sender, format string, v ...interface{}) {
-	switch level {
-	case 1:
-		Log(LevelInfo, sender, "", format, v...)
-	case 2:
-		Log(LevelWarn, sender, "", format, v...)
-	case 3:
-		Log(LevelError, sender, "", format, v...)
-	default:
-		Log(LevelDebug, sender, "", format, v...)
-	}
-}
-
 // StdLoggerWrapper is a wrapper for standard logger compatibility
 type StdLoggerWrapper struct {
 	Sender string
@@ -194,7 +176,6 @@ func InitLogger(logFilePath string, logMaxSize int, logMaxBackups int, logMaxAge
 		consoleLogger = zerolog.Nop()
 	}
 	logger = logger.Level(level)
-	sdklogger.SetLogger(&logWrapper{})
 }
 
 // InitStdErrLogger configures the logger to write to stderr
@@ -203,7 +184,6 @@ func InitStdErrLogger(level zerolog.Level) {
 		output: os.Stderr,
 	}).Level(level)
 	consoleLogger = zerolog.Nop()
-	sdklogger.SetLogger(&logWrapper{})
 }
 
 // DisableLogger disable the main logger.
@@ -211,7 +191,6 @@ func InitStdErrLogger(level zerolog.Level) {
 func DisableLogger() {
 	logger = zerolog.Nop()
 	rollingLogger = nil
-	sdklogger.DisableLogger()
 }
 
 // EnableConsoleLogger enables the console logger

+ 7 - 6
plugin/kms.go

@@ -9,16 +9,17 @@ import (
 	"github.com/hashicorp/go-hclog"
 	"github.com/hashicorp/go-plugin"
 
+	"github.com/drakkan/sftpgo/v2/kms"
 	"github.com/drakkan/sftpgo/v2/logger"
-	"github.com/drakkan/sftpgo/v2/sdk/kms"
+	sdkkms "github.com/drakkan/sftpgo/v2/sdk/kms"
 	kmsplugin "github.com/drakkan/sftpgo/v2/sdk/plugin/kms"
 	"github.com/drakkan/sftpgo/v2/util"
 )
 
 var (
-	validKMSSchemes           = []string{kms.SchemeAWS, kms.SchemeGCP, kms.SchemeVaultTransit, kms.SchemeAzureKeyVault}
-	validKMSEncryptedStatuses = []string{kms.SecretStatusVaultTransit, kms.SecretStatusAWS, kms.SecretStatusGCP,
-		kms.SecretStatusAzureKeyVault}
+	validKMSSchemes           = []string{sdkkms.SchemeAWS, sdkkms.SchemeGCP, sdkkms.SchemeVaultTransit, sdkkms.SchemeAzureKeyVault}
+	validKMSEncryptedStatuses = []string{sdkkms.SecretStatusVaultTransit, sdkkms.SecretStatusAWS, sdkkms.SecretStatusGCP,
+		sdkkms.SecretStatusAzureKeyVault}
 )
 
 // KMSConfig defines configuration parameters for kms plugins
@@ -133,7 +134,7 @@ func (s *kmsPluginSecretProvider) IsEncrypted() bool {
 }
 
 func (s *kmsPluginSecretProvider) Encrypt() error {
-	if s.Status != kms.SecretStatusPlain {
+	if s.Status != sdkkms.SecretStatusPlain {
 		return kms.ErrWrongSecretStatus
 	}
 	if s.Payload == "" {
@@ -160,7 +161,7 @@ func (s *kmsPluginSecretProvider) Decrypt() error {
 	if err != nil {
 		return err
 	}
-	s.Status = kms.SecretStatusPlain
+	s.Status = sdkkms.SecretStatusPlain
 	s.Payload = payload
 	s.Key = ""
 	s.AdditionalData = ""

+ 2 - 2
plugin/plugin.go

@@ -11,8 +11,8 @@ import (
 
 	"github.com/hashicorp/go-hclog"
 
+	"github.com/drakkan/sftpgo/v2/kms"
 	"github.com/drakkan/sftpgo/v2/logger"
-	"github.com/drakkan/sftpgo/v2/sdk/kms"
 	"github.com/drakkan/sftpgo/v2/sdk/plugin/auth"
 	"github.com/drakkan/sftpgo/v2/sdk/plugin/eventsearcher"
 	kmsplugin "github.com/drakkan/sftpgo/v2/sdk/plugin/kms"
@@ -135,7 +135,7 @@ func Initialize(configs []Config, logVerbose bool) error {
 			kmsID++
 			kms.RegisterSecretProvider(config.KMSOptions.Scheme, config.KMSOptions.EncryptedStatus,
 				Handler.Configs[idx].newKMSPluginSecretProvider)
-			logger.Debug(logSender, "", "registered secret provider for scheme: %v, encrypted status: %v",
+			logger.Info(logSender, "", "registered secret provider for scheme: %v, encrypted status: %v",
 				config.KMSOptions.Scheme, config.KMSOptions.EncryptedStatus)
 		case auth.PluginName:
 			plugin, err := newAuthPlugin(config)

+ 48 - 28
sdk/filesystem.go

@@ -84,8 +84,8 @@ func ListProviders() []FilesystemProvider {
 	}
 }
 
-// S3FsConfig defines the configuration for S3 based filesystem
-type S3FsConfig struct {
+// BaseS3FsConfig defines the base configuration for S3 based filesystems
+type BaseS3FsConfig struct {
 	Bucket string `json:"bucket,omitempty"`
 	// KeyPrefix is similar to a chroot directory for local filesystem.
 	// If specified then the SFTP user will only see objects that starts
@@ -93,12 +93,11 @@ 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 *kms.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"`
+	Endpoint     string `json:"endpoint,omitempty"`
+	StorageClass string `json:"storage_class,omitempty"`
 	// The canned ACL to apply to uploaded objects. Leave empty to use the default ACL.
 	// For more information and available ACLs, see here:
 	// https://docs.aws.amazon.com/AmazonS3/latest/userguide/acl-overview.html#canned-acl
@@ -129,8 +128,14 @@ type S3FsConfig struct {
 	ForcePathStyle bool `json:"force_path_style,omitempty"`
 }
 
-// GCSFsConfig defines the configuration for Google Cloud Storage based filesystem
-type GCSFsConfig struct {
+// S3FsConfig defines the base configuration for S3 based filesystems
+type S3FsConfig struct {
+	BaseS3FsConfig
+	AccessSecret kms.BaseSecret `json:"access_secret,omitempty"`
+}
+
+// BaseGCSFsConfig defines the base configuration for Google Cloud Storage based filesystems
+type BaseGCSFsConfig struct {
 	Bucket string `json:"bucket,omitempty"`
 	// KeyPrefix is similar to a chroot directory for local filesystem.
 	// If specified then the SFTP user will only see objects that starts
@@ -138,9 +143,8 @@ 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    *kms.Secret `json:"credentials,omitempty"`
+	KeyPrefix      string `json:"key_prefix,omitempty"`
+	CredentialFile string `json:"-"`
 	// 0 explicit, 1 automatic
 	AutomaticCredentials int    `json:"automatic_credentials,omitempty"`
 	StorageClass         string `json:"storage_class,omitempty"`
@@ -150,20 +154,21 @@ type GCSFsConfig struct {
 	ACL string `json:"acl,omitempty"`
 }
 
-// AzBlobFsConfig defines the configuration for Azure Blob Storage based filesystem
-type AzBlobFsConfig struct {
+// GCSFsConfig defines the configuration for Google Cloud Storage based filesystems
+type GCSFsConfig struct {
+	BaseGCSFsConfig
+	Credentials kms.BaseSecret `json:"credentials,omitempty"`
+}
+
+// BaseAzBlobFsConfig defines the base configuration for Azure Blob Storage based filesystem
+type BaseAzBlobFsConfig struct {
 	Container string `json:"container,omitempty"`
 	// 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 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"
 	Endpoint string `json:"endpoint,omitempty"`
-	// Shared access signature URL, leave blank if using account/key
-	SASURL *kms.Secret `json:"sas_url,omitempty"`
 	// KeyPrefix is similar to a chroot directory for local filesystem.
 	// If specified then the SFTPGo user will only see objects that starts
 	// with this prefix and so you can restrict access to a specific
@@ -187,18 +192,26 @@ type AzBlobFsConfig struct {
 	AccessTier string `json:"access_tier,omitempty"`
 }
 
+// AzBlobFsConfig defines the configuration for Azure Blob Storage based filesystem
+type AzBlobFsConfig struct {
+	BaseAzBlobFsConfig
+	// Storage Account Key leave blank to use SAS URL.
+	// The access key is stored encrypted based on the kms configuration
+	AccountKey kms.BaseSecret `json:"account_key,omitempty"`
+	// Shared access signature URL, leave blank if using account/key
+	SASURL kms.BaseSecret `json:"sas_url,omitempty"`
+}
+
 // CryptFsConfig defines the configuration to store local files as encrypted
 type CryptFsConfig struct {
-	Passphrase *kms.Secret `json:"passphrase,omitempty"`
+	Passphrase kms.BaseSecret `json:"passphrase,omitempty"`
 }
 
-// SFTPFsConfig defines the configuration for SFTP based filesystem
-type SFTPFsConfig struct {
-	Endpoint     string      `json:"endpoint,omitempty"`
-	Username     string      `json:"username,omitempty"`
-	Password     *kms.Secret `json:"password,omitempty"`
-	PrivateKey   *kms.Secret `json:"private_key,omitempty"`
-	Fingerprints []string    `json:"fingerprints,omitempty"`
+// BaseSFTPFsConfig defines the base configuration for SFTP based filesystem
+type BaseSFTPFsConfig struct {
+	Endpoint     string   `json:"endpoint,omitempty"`
+	Username     string   `json:"username,omitempty"`
+	Fingerprints []string `json:"fingerprints,omitempty"`
 	// Prefix is the path prefix to strip from SFTP resource paths.
 	Prefix string `json:"prefix,omitempty"`
 	// Concurrent reads are safe to use and disabling them will degrade performance.
@@ -213,6 +226,13 @@ type SFTPFsConfig struct {
 	BufferSize int64 `json:"buffer_size,omitempty"`
 }
 
+// SFTPFsConfig defines the configuration for SFTP based filesystem
+type SFTPFsConfig struct {
+	BaseSFTPFsConfig
+	Password   kms.BaseSecret `json:"password,omitempty"`
+	PrivateKey kms.BaseSecret `json:"private_key,omitempty"`
+}
+
 // Filesystem defines filesystem details
 type Filesystem struct {
 	Provider     FilesystemProvider `json:"provider"`

+ 8 - 411
sdk/kms/kms.go

@@ -1,37 +1,6 @@
 // Package kms provides Key Management Services support
 package kms
 
-import (
-	"encoding/json"
-	"errors"
-	"os"
-	"strings"
-	"sync"
-
-	"github.com/drakkan/sftpgo/v2/sdk/logger"
-)
-
-// 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
-	GetMode() int
-	SetKey(string)
-	SetAdditionalData(string)
-	SetStatus(SecretStatus)
-	Clone() SecretProvider
-}
-
-const (
-	logSender = "kms"
-)
-
 // SecretStatus defines the statuses of a Secret object
 type SecretStatus = string
 
@@ -70,384 +39,12 @@ const (
 	SchemeAzureKeyVault Scheme = "azurekeyvault"
 )
 
-// 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"`
-	MasterKeyString string `json:"master_key" mapstructure:"master_key"`
-	masterKey       string
-}
-
-type registeredSecretProvider struct {
-	encryptedStatus SecretStatus
-	newFn           func(base BaseSecret, url, masterKey string) SecretProvider
-}
-
-var (
-	// ErrWrongSecretStatus defines the error to return if the secret status is not appropriate
-	// for the request operation
-	ErrWrongSecretStatus = errors.New("wrong secret status")
-	// ErrInvalidSecret defines the error to return if a secret is not valid
-	ErrInvalidSecret    = errors.New("invalid secret")
-	validSecretStatuses = []string{SecretStatusPlain, SecretStatusAES256GCM, SecretStatusSecretBox,
-		SecretStatusVaultTransit, SecretStatusAWS, SecretStatusGCP, SecretStatusRedacted}
-	config          Configuration
-	secretProviders = make(map[string]registeredSecretProvider)
-)
-
-// RegisterSecretProvider register a new secret provider
-func RegisterSecretProvider(scheme string, encryptedStatus SecretStatus, fn func(base BaseSecret, url, masterKey string) SecretProvider) {
-	secretProviders[scheme] = registeredSecretProvider{
-		encryptedStatus: encryptedStatus,
-		newFn:           fn,
-	}
-}
-
-// NewSecret builds a new Secret using the provided arguments
-func NewSecret(status SecretStatus, payload, key, data string) *Secret {
-	return config.newSecret(status, payload, key, data)
-}
-
-// 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, "", "")
-}
-
-// Initialize configures the KMS support
-func (c *Configuration) Initialize() error {
-	if c.Secrets.MasterKeyString != "" {
-		c.Secrets.masterKey = c.Secrets.MasterKeyString
-	}
-	if c.Secrets.masterKey == "" && c.Secrets.MasterKeyPath != "" {
-		mKey, err := os.ReadFile(c.Secrets.MasterKeyPath)
-		if err != nil {
-			return err
-		}
-		c.Secrets.masterKey = strings.TrimSpace(string(mKey))
-	}
-	config = *c
-	if config.Secrets.URL == "" {
-		config.Secrets.URL = SchemeLocal + "://"
-	}
-	for k, v := range secretProviders {
-		logger.Info(logSender, "secret provider registered for scheme: %#v, encrypted status: %#v",
-			k, v.encryptedStatus)
-	}
-	return nil
-}
-
-func (c *Configuration) newSecret(status SecretStatus, payload, key, data string) *Secret {
-	base := BaseSecret{
-		Status:         status,
-		Key:            key,
-		Payload:        payload,
-		AdditionalData: data,
-	}
-	return &Secret{
-		provider: c.getSecretProvider(base),
-	}
-}
-
-func (c *Configuration) getSecretProvider(base BaseSecret) SecretProvider {
-	for k, v := range secretProviders {
-		if strings.HasPrefix(c.Secrets.URL, k) {
-			return v.newFn(base, c.Secrets.URL, c.Secrets.masterKey)
-		}
-	}
-	// we assume that SchemeLocal is always registered
-	logger.Warn(logSender, "no secret provider registered for URL %v, fallback to local provider", c.Secrets.URL)
-	return NewLocalSecret(base, c.Secrets.URL, c.Secrets.masterKey)
-}
-
-// Secret defines the struct used to store confidential data
-type Secret struct {
-	sync.RWMutex
-	provider SecretProvider
-}
-
-// MarshalJSON return the JSON encoding of the Secret object
-func (s *Secret) MarshalJSON() ([]byte, error) {
-	s.RLock()
-	defer s.RUnlock()
-
-	return json.Marshal(&BaseSecret{
-		Status:         s.provider.GetStatus(),
-		Payload:        s.provider.GetPayload(),
-		Key:            s.provider.GetKey(),
-		AdditionalData: s.provider.GetAdditionalData(),
-		Mode:           s.provider.GetMode(),
-	})
-}
-
-// UnmarshalJSON parses the JSON-encoded data and stores the result
-// in the Secret object
-func (s *Secret) UnmarshalJSON(data []byte) error {
-	s.Lock()
-	defer s.Unlock()
-
-	baseSecret := BaseSecret{}
-	err := json.Unmarshal(data, &baseSecret)
-	if err != nil {
-		return err
-	}
-	if baseSecret.isEmpty() {
-		s.provider = config.getSecretProvider(baseSecret)
-		return nil
-	}
-
-	if baseSecret.Status == SecretStatusPlain || baseSecret.Status == SecretStatusRedacted {
-		s.provider = config.getSecretProvider(baseSecret)
-		return nil
-	}
-
-	for _, v := range secretProviders {
-		if v.encryptedStatus == baseSecret.Status {
-			s.provider = v.newFn(baseSecret, config.Secrets.URL, config.Secrets.masterKey)
-			return nil
-		}
-	}
-	logger.Error(logSender, "no provider registered for status %#v", baseSecret.Status)
-	return ErrInvalidSecret
-}
-
-// IsEqual returns true if all the secrets fields are equal
-func (s *Secret) IsEqual(other *Secret) bool {
-	if s.GetStatus() != other.GetStatus() {
-		return false
-	}
-	if s.GetPayload() != other.GetPayload() {
-		return false
-	}
-	if s.GetKey() != other.GetKey() {
-		return false
-	}
-	if s.GetAdditionalData() != other.GetAdditionalData() {
-		return false
-	}
-	if s.GetMode() != other.GetMode() {
-		return false
-	}
-	return true
-}
-
-// Clone returns a copy of the secret object
-func (s *Secret) Clone() *Secret {
-	s.RLock()
-	defer s.RUnlock()
-
-	return &Secret{
-		provider: s.provider.Clone(),
-	}
-}
-
-// 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 {
-	s.RLock()
-	defer s.RUnlock()
-
-	return s.provider.IsEncrypted()
-}
-
-// IsPlain returns true if the secret is in plain text
-func (s *Secret) IsPlain() bool {
-	s.RLock()
-	defer s.RUnlock()
-
-	return s.provider.GetStatus() == SecretStatusPlain
-}
-
-// IsNotPlainAndNotEmpty returns true if the secret is not plain and not empty.
-// This is an utility method, we update the secret for an existing user
-// if it is empty or plain
-func (s *Secret) IsNotPlainAndNotEmpty() bool {
-	s.RLock()
-	defer s.RUnlock()
-
-	return !s.IsPlain() && !s.IsEmpty()
-}
-
-// IsRedacted returns true if the secret is redacted
-func (s *Secret) IsRedacted() bool {
-	s.RLock()
-	defer s.RUnlock()
-
-	return s.provider.GetStatus() == SecretStatusRedacted
-}
-
-// GetPayload returns the secret payload
-func (s *Secret) GetPayload() string {
-	s.RLock()
-	defer s.RUnlock()
-
-	return s.provider.GetPayload()
-}
-
-// GetAdditionalData returns the secret additional data
-func (s *Secret) GetAdditionalData() string {
-	s.RLock()
-	defer s.RUnlock()
-
-	return s.provider.GetAdditionalData()
-}
-
-// GetStatus returns the secret status
-func (s *Secret) GetStatus() SecretStatus {
-	s.RLock()
-	defer s.RUnlock()
-
-	return s.provider.GetStatus()
-}
-
-// GetKey returns the secret key
-func (s *Secret) GetKey() string {
-	s.RLock()
-	defer s.RUnlock()
-
-	return s.provider.GetKey()
-}
-
-// GetMode returns the secret mode
-func (s *Secret) GetMode() int {
-	s.RLock()
-	defer s.RUnlock()
-
-	return s.provider.GetMode()
-}
-
-// SetAdditionalData sets the given additional data
-func (s *Secret) SetAdditionalData(value string) {
-	s.Lock()
-	defer s.Unlock()
-
-	s.provider.SetAdditionalData(value)
-}
-
-// SetStatus sets the status for this secret
-func (s *Secret) SetStatus(value SecretStatus) {
-	s.Lock()
-	defer s.Unlock()
-
-	s.provider.SetStatus(value)
-}
-
-// SetKey sets the key for this secret
-func (s *Secret) SetKey(value string) {
-	s.Lock()
-	defer s.Unlock()
-
-	s.provider.SetKey(value)
-}
-
-// IsEmpty returns true if all fields are empty
-func (s *Secret) IsEmpty() bool {
-	s.RLock()
-	defer s.RUnlock()
-
-	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 {
-	s.RLock()
-	defer s.RUnlock()
-
-	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 {
-	s.RLock()
-	defer s.RUnlock()
-
-	if !isSecretStatusValid(s.provider.GetStatus()) {
-		return false
-	}
-	if s.provider.GetPayload() == "" {
-		return false
-	}
-	return true
-}
-
-// Hide hides info to decrypt data
-func (s *Secret) Hide() {
-	s.Lock()
-	defer s.Unlock()
-
-	s.provider.SetKey("")
-	s.provider.SetAdditionalData("")
-}
-
-// Encrypt encrypts a plain text Secret object
-func (s *Secret) Encrypt() error {
-	s.Lock()
-	defer s.Unlock()
-
-	return s.provider.Encrypt()
-}
-
-// Decrypt decrypts a Secret object
-func (s *Secret) Decrypt() error {
-	s.Lock()
-	defer s.Unlock()
-
-	return s.provider.Decrypt()
-}
-
-// TryDecrypt decrypts a Secret object if encrypted.
-// It returns a nil error if the object is not encrypted
-func (s *Secret) TryDecrypt() error {
-	s.Lock()
-	defer s.Unlock()
-
-	if s.provider.IsEncrypted() {
-		return s.provider.Decrypt()
-	}
-	return nil
-}
-
-func isSecretStatusValid(status string) bool {
-	for idx := range validSecretStatuses {
-		if validSecretStatuses[idx] == status {
-			return true
-		}
-	}
-	return false
+// 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"`
+	// 1 means encrypted using a master key
+	Mode int `json:"mode,omitempty"`
 }

+ 0 - 57
sdk/logger/logger.go

@@ -1,57 +0,0 @@
-// Package logger provides logging capabilities.
-package logger
-
-const (
-	levelDebug = iota
-	levelInfo
-	levelWarn
-	levelError
-)
-
-var (
-	logger Logger
-)
-
-func init() {
-	DisableLogger()
-}
-
-// Logger interface
-type Logger interface {
-	// Log logs at the specified level for the specified sender
-	Log(level int, sender, format string, v ...interface{})
-}
-
-// SetLogger sets the specified logger
-func SetLogger(l Logger) {
-	logger = l
-}
-
-// DisableLogger disables logging
-func DisableLogger() {
-	logger = &noLogger{}
-}
-
-type noLogger struct{}
-
-func (*noLogger) Log(level int, sender, format string, v ...interface{}) {}
-
-// Debug logs at debug level for the specified sender
-func Debug(sender, format string, v ...interface{}) {
-	logger.Log(levelDebug, sender, format, v...)
-}
-
-// Info logs at info level for the specified sender
-func Info(sender, format string, v ...interface{}) {
-	logger.Log(levelInfo, sender, format, v...)
-}
-
-// Warn logs at warn level for the specified sender
-func Warn(sender, format string, v ...interface{}) {
-	logger.Log(levelWarn, sender, format, v...)
-}
-
-// Error logs at error level for the specified sender
-func Error(sender, format string, v ...interface{}) {
-	logger.Log(levelError, sender, format, v...)
-}

+ 20 - 15
sdk/user.go

@@ -100,15 +100,15 @@ type HooksFilter struct {
 
 // RecoveryCode defines a 2FA recovery code
 type RecoveryCode struct {
-	Secret *kms.Secret `json:"secret"`
-	Used   bool        `json:"used,omitempty"`
+	Secret kms.BaseSecret `json:"secret"`
+	Used   bool           `json:"used,omitempty"`
 }
 
 // TOTPConfig defines the time-based one time password configuration
 type TOTPConfig struct {
-	Enabled    bool        `json:"enabled,omitempty"`
-	ConfigName string      `json:"config_name,omitempty"`
-	Secret     *kms.Secret `json:"secret,omitempty"`
+	Enabled    bool           `json:"enabled,omitempty"`
+	ConfigName string         `json:"config_name,omitempty"`
+	Secret     kms.BaseSecret `json:"secret,omitempty"`
 	// TOTP will be required for the specified protocols.
 	// SSH protocol (SFTP/SCP/SSH commands) will ask for the TOTP passcode if the client uses keyboard interactive
 	// authentication.
@@ -136,9 +136,8 @@ func (l *BandwidthLimit) GetSourcesAsString() string {
 	return strings.Join(l.Sources, ",")
 }
 
-// UserFilters defines additional restrictions for a user
-// TODO: rename to UserOptions in v3
-type UserFilters struct {
+// BaseUserFilters defines additional restrictions for a user
+type BaseUserFilters struct {
 	// only clients connecting from these IP/Mask are allowed.
 	// 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"
@@ -175,17 +174,23 @@ type UserFilters struct {
 	WebClient []string `json:"web_client,omitempty"`
 	// API key auth allows to impersonate this user with an API key
 	AllowAPIKeyAuth bool `json:"allow_api_key_auth,omitempty"`
+	// UserType is an hint for authentication plugins.
+	// It is ignored when using SFTPGo internal authentication
+	UserType string `json:"user_type,omitempty"`
+	// Per-source bandwidth limits
+	BandwidthLimits []BandwidthLimit `json:"bandwidth_limits,omitempty"`
+}
+
+// UserFilters defines additional restrictions for a user
+// TODO: rename to UserOptions in v3
+type UserFilters struct {
+	BaseUserFilters
 	// Time-based one time passwords configuration
 	TOTPConfig TOTPConfig `json:"totp_config,omitempty"`
 	// Recovery codes to use if the user loses access to their second factor auth device.
 	// Each code can only be used once, you should use these codes to login and disable or
 	// reset 2FA for your account
 	RecoveryCodes []RecoveryCode `json:"recovery_codes,omitempty"`
-	// UserType is an hint for authentication plugins.
-	// It is ignored when using SFTPGo internal authentication
-	UserType string `json:"user_type,omitempty"`
-	// Per-source bandwidth limits
-	BandwidthLimits []BandwidthLimit `json:"bandwidth_limits,omitempty"`
 }
 
 // BaseUser defines the shared user fields
@@ -239,8 +244,6 @@ type BaseUser struct {
 	CreatedAt int64 `json:"created_at"`
 	// last update time as unix timestamp in milliseconds
 	UpdatedAt int64 `json:"updated_at"`
-	// Additional restrictions
-	Filters UserFilters `json:"filters"`
 	// optional description, for example full name
 	Description string `json:"description,omitempty"`
 	// free form text field for external systems
@@ -250,6 +253,8 @@ type BaseUser struct {
 // User defines a SFTPGo user
 type User struct {
 	BaseUser
+	// Additional restrictions
+	Filters UserFilters `json:"filters"`
 	// Mapping between virtual paths and virtual folders
 	VirtualFolders []VirtualFolder `json:"virtual_folders,omitempty"`
 	// Filesystem configuration details

+ 1 - 1
service/service_portable.go

@@ -17,9 +17,9 @@ import (
 	"github.com/drakkan/sftpgo/v2/config"
 	"github.com/drakkan/sftpgo/v2/dataprovider"
 	"github.com/drakkan/sftpgo/v2/ftpd"
+	"github.com/drakkan/sftpgo/v2/kms"
 	"github.com/drakkan/sftpgo/v2/logger"
 	"github.com/drakkan/sftpgo/v2/sdk"
-	"github.com/drakkan/sftpgo/v2/sdk/kms"
 	"github.com/drakkan/sftpgo/v2/sftpd"
 	"github.com/drakkan/sftpgo/v2/util"
 	"github.com/drakkan/sftpgo/v2/version"

+ 1 - 1
sftpd/cryptfs_test.go

@@ -15,8 +15,8 @@ import (
 
 	"github.com/drakkan/sftpgo/v2/dataprovider"
 	"github.com/drakkan/sftpgo/v2/httpdtest"
+	"github.com/drakkan/sftpgo/v2/kms"
 	"github.com/drakkan/sftpgo/v2/sdk"
-	"github.com/drakkan/sftpgo/v2/sdk/kms"
 	"github.com/drakkan/sftpgo/v2/vfs"
 )
 

+ 3 - 5
sftpd/internal_test.go

@@ -20,8 +20,8 @@ import (
 
 	"github.com/drakkan/sftpgo/v2/common"
 	"github.com/drakkan/sftpgo/v2/dataprovider"
+	"github.com/drakkan/sftpgo/v2/kms"
 	"github.com/drakkan/sftpgo/v2/sdk"
-	"github.com/drakkan/sftpgo/v2/sdk/kms"
 	"github.com/drakkan/sftpgo/v2/util"
 	"github.com/drakkan/sftpgo/v2/vfs"
 )
@@ -667,7 +667,7 @@ func TestSSHCommandsRemoteFs(t *testing.T) {
 	user.FsConfig = vfs.Filesystem{
 		Provider: sdk.S3FilesystemProvider,
 		S3Config: vfs.S3FsConfig{
-			S3FsConfig: sdk.S3FsConfig{
+			BaseS3FsConfig: sdk.BaseS3FsConfig{
 				Bucket:   "s3bucket",
 				Endpoint: "endpoint",
 				Region:   "eu-west-1",
@@ -1753,9 +1753,7 @@ func TestTransferFailingReader(t *testing.T) {
 		FsConfig: vfs.Filesystem{
 			Provider: sdk.CryptedFilesystemProvider,
 			CryptConfig: vfs.CryptFsConfig{
-				CryptFsConfig: sdk.CryptFsConfig{
-					Passphrase: kms.NewPlainSecret("crypt secret"),
-				},
+				Passphrase: kms.NewPlainSecret("crypt secret"),
 			},
 		},
 	}

+ 40 - 43
sftpd/sftpd_test.go

@@ -42,10 +42,11 @@ import (
 	"github.com/drakkan/sftpgo/v2/config"
 	"github.com/drakkan/sftpgo/v2/dataprovider"
 	"github.com/drakkan/sftpgo/v2/httpdtest"
+	"github.com/drakkan/sftpgo/v2/kms"
 	"github.com/drakkan/sftpgo/v2/logger"
 	"github.com/drakkan/sftpgo/v2/mfa"
 	"github.com/drakkan/sftpgo/v2/sdk"
-	"github.com/drakkan/sftpgo/v2/sdk/kms"
+	sdkkms "github.com/drakkan/sftpgo/v2/sdk/kms"
 	"github.com/drakkan/sftpgo/v2/sftpd"
 	"github.com/drakkan/sftpgo/v2/util"
 	"github.com/drakkan/sftpgo/v2/vfs"
@@ -1999,7 +2000,7 @@ func TestLoginWithDatabaseCredentials(t *testing.T) {
 
 	user, _, err := httpdtest.AddUser(u, http.StatusCreated)
 	assert.NoError(t, err)
-	assert.Equal(t, kms.SecretStatusSecretBox, user.FsConfig.GCSConfig.Credentials.GetStatus())
+	assert.Equal(t, sdkkms.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())
@@ -2283,7 +2284,7 @@ func TestInteractiveLoginWithPasscode(t *testing.T) {
 	configName, _, secret, _, err := mfa.GenerateTOTPSecret(mfa.GetAvailableTOTPConfigNames()[0], user.Username)
 	assert.NoError(t, err)
 	user.Password = defaultPassword
-	user.Filters.TOTPConfig = sdk.TOTPConfig{
+	user.Filters.TOTPConfig = dataprovider.UserTOTPConfig{
 		Enabled:    true,
 		ConfigName: configName,
 		Secret:     kms.NewPlainSecret(secret),
@@ -2332,7 +2333,7 @@ func TestInteractiveLoginWithPasscode(t *testing.T) {
 	configName, _, secret, _, err = mfa.GenerateTOTPSecret(mfa.GetAvailableTOTPConfigNames()[0], user.Username)
 	assert.NoError(t, err)
 	user.Password = defaultPassword
-	user.Filters.TOTPConfig = sdk.TOTPConfig{
+	user.Filters.TOTPConfig = dataprovider.UserTOTPConfig{
 		Enabled:    true,
 		ConfigName: configName,
 		Secret:     kms.NewPlainSecret(secret),
@@ -2521,14 +2522,14 @@ func TestPreLoginHookPreserveMFAConfig(t *testing.T) {
 	configName, _, secret, _, err := mfa.GenerateTOTPSecret(mfa.GetAvailableTOTPConfigNames()[0], user.Username)
 	assert.NoError(t, err)
 	user.Password = defaultPassword
-	user.Filters.TOTPConfig = sdk.TOTPConfig{
+	user.Filters.TOTPConfig = dataprovider.UserTOTPConfig{
 		Enabled:    true,
 		ConfigName: configName,
 		Secret:     kms.NewPlainSecret(secret),
 		Protocols:  []string{common.ProtocolSSH},
 	}
 	for i := 0; i < 12; i++ {
-		user.Filters.RecoveryCodes = append(user.Filters.RecoveryCodes, sdk.RecoveryCode{
+		user.Filters.RecoveryCodes = append(user.Filters.RecoveryCodes, dataprovider.RecoveryCode{
 			Secret: kms.NewPlainSecret(fmt.Sprintf("RC-%v", strings.ToUpper(util.GenerateUniqueID()))),
 		})
 	}
@@ -2548,7 +2549,7 @@ func TestPreLoginHookPreserveMFAConfig(t *testing.T) {
 	assert.True(t, user.Filters.TOTPConfig.Enabled)
 	assert.Equal(t, configName, user.Filters.TOTPConfig.ConfigName)
 	assert.Equal(t, []string{common.ProtocolSSH}, user.Filters.TOTPConfig.Protocols)
-	assert.Equal(t, kms.SecretStatusSecretBox, user.Filters.TOTPConfig.Secret.GetStatus())
+	assert.Equal(t, sdkkms.SecretStatusSecretBox, user.Filters.TOTPConfig.Secret.GetStatus())
 
 	err = os.WriteFile(extAuthPath, getExitCodeScriptContent(0), os.ModePerm)
 	assert.NoError(t, err)
@@ -2566,7 +2567,7 @@ func TestPreLoginHookPreserveMFAConfig(t *testing.T) {
 	assert.True(t, user.Filters.TOTPConfig.Enabled)
 	assert.Equal(t, configName, user.Filters.TOTPConfig.ConfigName)
 	assert.Equal(t, []string{common.ProtocolSSH}, user.Filters.TOTPConfig.Protocols)
-	assert.Equal(t, kms.SecretStatusSecretBox, user.Filters.TOTPConfig.Secret.GetStatus())
+	assert.Equal(t, sdkkms.SecretStatusSecretBox, user.Filters.TOTPConfig.Secret.GetStatus())
 
 	_, err = httpdtest.RemoveUser(user, http.StatusOK)
 	assert.NoError(t, err)
@@ -3463,14 +3464,14 @@ func TestExternalAuthPreserveMFAConfig(t *testing.T) {
 	configName, _, secret, _, err := mfa.GenerateTOTPSecret(mfa.GetAvailableTOTPConfigNames()[0], user.Username)
 	assert.NoError(t, err)
 	user.Password = defaultPassword
-	user.Filters.TOTPConfig = sdk.TOTPConfig{
+	user.Filters.TOTPConfig = dataprovider.UserTOTPConfig{
 		Enabled:    true,
 		ConfigName: configName,
 		Secret:     kms.NewPlainSecret(secret),
 		Protocols:  []string{common.ProtocolSSH},
 	}
 	for i := 0; i < 12; i++ {
-		user.Filters.RecoveryCodes = append(user.Filters.RecoveryCodes, sdk.RecoveryCode{
+		user.Filters.RecoveryCodes = append(user.Filters.RecoveryCodes, dataprovider.RecoveryCode{
 			Secret: kms.NewPlainSecret(fmt.Sprintf("RC-%v", strings.ToUpper(util.GenerateUniqueID()))),
 		})
 	}
@@ -3490,7 +3491,7 @@ func TestExternalAuthPreserveMFAConfig(t *testing.T) {
 	assert.True(t, user.Filters.TOTPConfig.Enabled)
 	assert.Equal(t, configName, user.Filters.TOTPConfig.ConfigName)
 	assert.Equal(t, []string{common.ProtocolSSH}, user.Filters.TOTPConfig.Protocols)
-	assert.Equal(t, kms.SecretStatusSecretBox, user.Filters.TOTPConfig.Secret.GetStatus())
+	assert.Equal(t, sdkkms.SecretStatusSecretBox, user.Filters.TOTPConfig.Secret.GetStatus())
 
 	err = os.WriteFile(extAuthPath, getExtAuthScriptContent(u, false, true, ""), os.ModePerm)
 	assert.NoError(t, err)
@@ -3508,7 +3509,7 @@ func TestExternalAuthPreserveMFAConfig(t *testing.T) {
 	assert.True(t, user.Filters.TOTPConfig.Enabled)
 	assert.Equal(t, configName, user.Filters.TOTPConfig.ConfigName)
 	assert.Equal(t, []string{common.ProtocolSSH}, user.Filters.TOTPConfig.Protocols)
-	assert.Equal(t, kms.SecretStatusSecretBox, user.Filters.TOTPConfig.Secret.GetStatus())
+	assert.Equal(t, sdkkms.SecretStatusSecretBox, user.Filters.TOTPConfig.Secret.GetStatus())
 
 	_, err = httpdtest.RemoveUser(user, http.StatusOK)
 	assert.NoError(t, err)
@@ -4428,18 +4429,18 @@ func TestSFTPLoopSimple(t *testing.T) {
 	user1.FsConfig.Provider = sdk.SFTPFilesystemProvider
 	user2.FsConfig.Provider = sdk.SFTPFilesystemProvider
 	user1.FsConfig.SFTPConfig = vfs.SFTPFsConfig{
-		SFTPFsConfig: sdk.SFTPFsConfig{
+		BaseSFTPFsConfig: sdk.BaseSFTPFsConfig{
 			Endpoint: sftpServerAddr,
 			Username: user2.Username,
-			Password: kms.NewPlainSecret(defaultPassword),
 		},
+		Password: kms.NewPlainSecret(defaultPassword),
 	}
 	user2.FsConfig.SFTPConfig = vfs.SFTPFsConfig{
-		SFTPFsConfig: sdk.SFTPFsConfig{
+		BaseSFTPFsConfig: sdk.BaseSFTPFsConfig{
 			Endpoint: sftpServerAddr,
 			Username: user1.Username,
-			Password: kms.NewPlainSecret(defaultPassword),
 		},
+		Password: kms.NewPlainSecret(defaultPassword),
 	}
 	user1, resp, err := httpdtest.AddUser(user1, http.StatusCreated)
 	assert.NoError(t, err, string(resp))
@@ -4487,11 +4488,11 @@ func TestSFTPLoopVirtualFolders(t *testing.T) {
 			FsConfig: vfs.Filesystem{
 				Provider: sdk.SFTPFilesystemProvider,
 				SFTPConfig: vfs.SFTPFsConfig{
-					SFTPFsConfig: sdk.SFTPFsConfig{
+					BaseSFTPFsConfig: sdk.BaseSFTPFsConfig{
 						Endpoint: sftpServerAddr,
 						Username: user2.Username,
-						Password: kms.NewPlainSecret(defaultPassword),
 					},
+					Password: kms.NewPlainSecret(defaultPassword),
 				},
 			},
 		},
@@ -4500,19 +4501,19 @@ func TestSFTPLoopVirtualFolders(t *testing.T) {
 
 	user2.FsConfig.Provider = sdk.SFTPFilesystemProvider
 	user2.FsConfig.SFTPConfig = vfs.SFTPFsConfig{
-		SFTPFsConfig: sdk.SFTPFsConfig{
+		BaseSFTPFsConfig: sdk.BaseSFTPFsConfig{
 			Endpoint: sftpServerAddr,
 			Username: user1.Username,
-			Password: kms.NewPlainSecret(defaultPassword),
 		},
+		Password: kms.NewPlainSecret(defaultPassword),
 	}
 	user3.FsConfig.Provider = sdk.SFTPFilesystemProvider
 	user3.FsConfig.SFTPConfig = vfs.SFTPFsConfig{
-		SFTPFsConfig: sdk.SFTPFsConfig{
+		BaseSFTPFsConfig: sdk.BaseSFTPFsConfig{
 			Endpoint: sftpServerAddr,
 			Username: user1.Username,
-			Password: kms.NewPlainSecret(defaultPassword),
 		},
+		Password: kms.NewPlainSecret(defaultPassword),
 	}
 
 	user1, resp, err := httpdtest.AddUser(user1, http.StatusCreated)
@@ -4546,11 +4547,11 @@ func TestSFTPLoopVirtualFolders(t *testing.T) {
 			FsConfig: vfs.Filesystem{
 				Provider: sdk.SFTPFilesystemProvider,
 				SFTPConfig: vfs.SFTPFsConfig{
-					SFTPFsConfig: sdk.SFTPFsConfig{
+					BaseSFTPFsConfig: sdk.BaseSFTPFsConfig{
 						Endpoint: sftpServerAddr,
 						Username: user3.Username,
-						Password: kms.NewPlainSecret(defaultPassword),
 					},
+					Password: kms.NewPlainSecret(defaultPassword),
 				},
 			},
 		},
@@ -4602,9 +4603,7 @@ func TestNestedVirtualFolders(t *testing.T) {
 			FsConfig: vfs.Filesystem{
 				Provider: sdk.CryptedFilesystemProvider,
 				CryptConfig: vfs.CryptFsConfig{
-					CryptFsConfig: sdk.CryptFsConfig{
-						Passphrase: kms.NewPlainSecret(defaultPassword),
-					},
+					Passphrase: kms.NewPlainSecret(defaultPassword),
 				},
 			},
 			MappedPath: mappedPathCrypt,
@@ -7222,24 +7221,24 @@ func TestRelativePaths(t *testing.T) {
 	filesystems := []vfs.Fs{vfs.NewOsFs("", user.GetHomeDir(), "")}
 	keyPrefix := strings.TrimPrefix(user.GetHomeDir(), "/") + "/"
 	s3config := vfs.S3FsConfig{
-		S3FsConfig: sdk.S3FsConfig{
+		BaseS3FsConfig: sdk.BaseS3FsConfig{
 			KeyPrefix: keyPrefix,
 		},
 	}
 	s3fs, _ := vfs.NewS3Fs("", user.GetHomeDir(), "", s3config)
 	gcsConfig := vfs.GCSFsConfig{
-		GCSFsConfig: sdk.GCSFsConfig{
+		BaseGCSFsConfig: sdk.BaseGCSFsConfig{
 			KeyPrefix: keyPrefix,
 		},
 	}
 	gcsfs, _ := vfs.NewGCSFs("", user.GetHomeDir(), "", gcsConfig)
 	sftpconfig := vfs.SFTPFsConfig{
-		SFTPFsConfig: sdk.SFTPFsConfig{
+		BaseSFTPFsConfig: sdk.BaseSFTPFsConfig{
 			Endpoint: sftpServerAddr,
 			Username: defaultUsername,
-			Password: kms.NewPlainSecret(defaultPassword),
 			Prefix:   keyPrefix,
 		},
+		Password: kms.NewPlainSecret(defaultPassword),
 	}
 	sftpfs, _ := vfs.NewSFTPFs("", "", os.TempDir(), []string{user.Username}, sftpconfig)
 	if runtime.GOOS != osWindows {
@@ -7287,7 +7286,7 @@ func TestResolvePaths(t *testing.T) {
 	filesystems := []vfs.Fs{vfs.NewOsFs("", user.GetHomeDir(), "")}
 	keyPrefix := strings.TrimPrefix(user.GetHomeDir(), "/") + "/"
 	s3config := vfs.S3FsConfig{
-		S3FsConfig: sdk.S3FsConfig{
+		BaseS3FsConfig: sdk.BaseS3FsConfig{
 			KeyPrefix: keyPrefix,
 			Bucket:    "bucket",
 			Region:    "us-east-1",
@@ -7298,7 +7297,7 @@ func TestResolvePaths(t *testing.T) {
 	s3fs, err := vfs.NewS3Fs("", user.GetHomeDir(), "", s3config)
 	assert.NoError(t, err)
 	gcsConfig := vfs.GCSFsConfig{
-		GCSFsConfig: sdk.GCSFsConfig{
+		BaseGCSFsConfig: sdk.BaseGCSFsConfig{
 			KeyPrefix: keyPrefix,
 		},
 	}
@@ -7401,8 +7400,10 @@ func TestFilterFilePatterns(t *testing.T) {
 		AllowedPatterns: []string{"*.jpg", "*.png"},
 		DeniedPatterns:  []string{"*.pdf"},
 	}
-	filters := sdk.UserFilters{
-		FilePatterns: []sdk.PatternsFilter{pattern},
+	filters := dataprovider.UserFilters{
+		BaseUserFilters: sdk.BaseUserFilters{
+			FilePatterns: []sdk.PatternsFilter{pattern},
+		},
 	}
 	user.Filters = filters
 	assert.True(t, user.IsFileAllowed("/test/test.jPg"))
@@ -8482,9 +8483,7 @@ func TestSSHRemoveCryptFs(t *testing.T) {
 			FsConfig: vfs.Filesystem{
 				Provider: sdk.CryptedFilesystemProvider,
 				CryptConfig: vfs.CryptFsConfig{
-					CryptFsConfig: sdk.CryptFsConfig{
-						Passphrase: kms.NewPlainSecret(defaultPassword),
-					},
+					Passphrase: kms.NewPlainSecret(defaultPassword),
 				},
 			},
 		},
@@ -9070,11 +9069,11 @@ func TestSCPNestedFolders(t *testing.T) {
 			FsConfig: vfs.Filesystem{
 				Provider: sdk.SFTPFilesystemProvider,
 				SFTPConfig: vfs.SFTPFsConfig{
-					SFTPFsConfig: sdk.SFTPFsConfig{
+					BaseSFTPFsConfig: sdk.BaseSFTPFsConfig{
 						Endpoint: sftpServerAddr,
 						Username: baseUser.Username,
-						Password: kms.NewPlainSecret(defaultPassword),
 					},
+					Password: kms.NewPlainSecret(defaultPassword),
 				},
 			},
 		},
@@ -9089,9 +9088,7 @@ func TestSCPNestedFolders(t *testing.T) {
 			FsConfig: vfs.Filesystem{
 				Provider: sdk.CryptedFilesystemProvider,
 				CryptConfig: vfs.CryptFsConfig{
-					CryptFsConfig: sdk.CryptFsConfig{
-						Passphrase: kms.NewPlainSecret(defaultPassword),
-					},
+					Passphrase: kms.NewPlainSecret(defaultPassword),
 				},
 			},
 			MappedPath: mappedPathCrypt,

+ 12 - 14
vfs/filesystem.go

@@ -3,8 +3,8 @@ package vfs
 import (
 	"fmt"
 
+	"github.com/drakkan/sftpgo/v2/kms"
 	"github.com/drakkan/sftpgo/v2/sdk"
-	"github.com/drakkan/sftpgo/v2/sdk/kms"
 	"github.com/drakkan/sftpgo/v2/util"
 )
 
@@ -228,11 +228,10 @@ func (f *Filesystem) GetACopy() Filesystem {
 	fs := Filesystem{
 		Provider: f.Provider,
 		S3Config: S3FsConfig{
-			S3FsConfig: sdk.S3FsConfig{
+			BaseS3FsConfig: sdk.BaseS3FsConfig{
 				Bucket:              f.S3Config.Bucket,
 				Region:              f.S3Config.Region,
 				AccessKey:           f.S3Config.AccessKey,
-				AccessSecret:        f.S3Config.AccessSecret.Clone(),
 				Endpoint:            f.S3Config.Endpoint,
 				StorageClass:        f.S3Config.StorageClass,
 				ACL:                 f.S3Config.ACL,
@@ -244,47 +243,46 @@ func (f *Filesystem) GetACopy() Filesystem {
 				DownloadPartMaxTime: f.S3Config.DownloadPartMaxTime,
 				ForcePathStyle:      f.S3Config.ForcePathStyle,
 			},
+			AccessSecret: f.S3Config.AccessSecret.Clone(),
 		},
 		GCSConfig: GCSFsConfig{
-			GCSFsConfig: sdk.GCSFsConfig{
+			BaseGCSFsConfig: sdk.BaseGCSFsConfig{
 				Bucket:               f.GCSConfig.Bucket,
 				CredentialFile:       f.GCSConfig.CredentialFile,
-				Credentials:          f.GCSConfig.Credentials.Clone(),
 				AutomaticCredentials: f.GCSConfig.AutomaticCredentials,
 				StorageClass:         f.GCSConfig.StorageClass,
 				ACL:                  f.GCSConfig.ACL,
 				KeyPrefix:            f.GCSConfig.KeyPrefix,
 			},
+			Credentials: f.GCSConfig.Credentials.Clone(),
 		},
 		AzBlobConfig: AzBlobFsConfig{
-			AzBlobFsConfig: sdk.AzBlobFsConfig{
+			BaseAzBlobFsConfig: sdk.BaseAzBlobFsConfig{
 				Container:         f.AzBlobConfig.Container,
 				AccountName:       f.AzBlobConfig.AccountName,
-				AccountKey:        f.AzBlobConfig.AccountKey.Clone(),
 				Endpoint:          f.AzBlobConfig.Endpoint,
-				SASURL:            f.AzBlobConfig.SASURL.Clone(),
 				KeyPrefix:         f.AzBlobConfig.KeyPrefix,
 				UploadPartSize:    f.AzBlobConfig.UploadPartSize,
 				UploadConcurrency: f.AzBlobConfig.UploadConcurrency,
 				UseEmulator:       f.AzBlobConfig.UseEmulator,
 				AccessTier:        f.AzBlobConfig.AccessTier,
 			},
+			AccountKey: f.AzBlobConfig.AccountKey.Clone(),
+			SASURL:     f.AzBlobConfig.SASURL.Clone(),
 		},
 		CryptConfig: CryptFsConfig{
-			CryptFsConfig: sdk.CryptFsConfig{
-				Passphrase: f.CryptConfig.Passphrase.Clone(),
-			},
+			Passphrase: f.CryptConfig.Passphrase.Clone(),
 		},
 		SFTPConfig: SFTPFsConfig{
-			SFTPFsConfig: sdk.SFTPFsConfig{
+			BaseSFTPFsConfig: sdk.BaseSFTPFsConfig{
 				Endpoint:                f.SFTPConfig.Endpoint,
 				Username:                f.SFTPConfig.Username,
-				Password:                f.SFTPConfig.Password.Clone(),
-				PrivateKey:              f.SFTPConfig.PrivateKey.Clone(),
 				Prefix:                  f.SFTPConfig.Prefix,
 				DisableCouncurrentReads: f.SFTPConfig.DisableCouncurrentReads,
 				BufferSize:              f.SFTPConfig.BufferSize,
 			},
+			Password:   f.SFTPConfig.Password.Clone(),
+			PrivateKey: f.SFTPConfig.PrivateKey.Clone(),
 		},
 	}
 	if len(f.SFTPConfig.Fingerprints) > 0 {

+ 1 - 1
vfs/gcsfs.go

@@ -23,10 +23,10 @@ import (
 	"google.golang.org/api/iterator"
 	"google.golang.org/api/option"
 
+	"github.com/drakkan/sftpgo/v2/kms"
 	"github.com/drakkan/sftpgo/v2/logger"
 	"github.com/drakkan/sftpgo/v2/metric"
 	"github.com/drakkan/sftpgo/v2/plugin"
-	"github.com/drakkan/sftpgo/v2/sdk/kms"
 	"github.com/drakkan/sftpgo/v2/util"
 	"github.com/drakkan/sftpgo/v2/version"
 )

+ 5 - 3
vfs/sftpfs.go

@@ -19,9 +19,9 @@ import (
 	"github.com/rs/xid"
 	"golang.org/x/crypto/ssh"
 
+	"github.com/drakkan/sftpgo/v2/kms"
 	"github.com/drakkan/sftpgo/v2/logger"
 	"github.com/drakkan/sftpgo/v2/sdk"
-	"github.com/drakkan/sftpgo/v2/sdk/kms"
 	"github.com/drakkan/sftpgo/v2/util"
 	"github.com/drakkan/sftpgo/v2/version"
 )
@@ -36,8 +36,10 @@ var ErrSFTPLoop = errors.New("SFTP loop or nested local SFTP folders detected")
 
 // SFTPFsConfig defines the configuration for SFTP based filesystem
 type SFTPFsConfig struct {
-	sdk.SFTPFsConfig
-	forbiddenSelfUsernames []string `json:"-"`
+	sdk.BaseSFTPFsConfig
+	Password               *kms.Secret `json:"password,omitempty"`
+	PrivateKey             *kms.Secret `json:"private_key,omitempty"`
+	forbiddenSelfUsernames []string    `json:"-"`
 }
 
 // HideConfidentialData hides confidential data

+ 12 - 5
vfs/vfs.go

@@ -16,10 +16,10 @@ import (
 	"github.com/eikenb/pipeat"
 	"github.com/pkg/sftp"
 
+	"github.com/drakkan/sftpgo/v2/kms"
 	"github.com/drakkan/sftpgo/v2/logger"
 	"github.com/drakkan/sftpgo/v2/plugin"
 	"github.com/drakkan/sftpgo/v2/sdk"
-	"github.com/drakkan/sftpgo/v2/sdk/kms"
 	"github.com/drakkan/sftpgo/v2/sdk/plugin/metadata"
 	"github.com/drakkan/sftpgo/v2/util"
 )
@@ -150,7 +150,8 @@ func (q *QuotaCheckResult) GetRemainingFiles() int {
 
 // S3FsConfig defines the configuration for S3 based filesystem
 type S3FsConfig struct {
-	sdk.S3FsConfig
+	sdk.BaseS3FsConfig
+	AccessSecret *kms.Secret `json:"access_secret,omitempty"`
 }
 
 // HideConfidentialData hides confidential data
@@ -287,7 +288,8 @@ func (c *S3FsConfig) Validate() error {
 
 // GCSFsConfig defines the configuration for Google Cloud Storage based filesystem
 type GCSFsConfig struct {
-	sdk.GCSFsConfig
+	sdk.BaseGCSFsConfig
+	Credentials *kms.Secret `json:"credentials,omitempty"`
 }
 
 // HideConfidentialData hides confidential data
@@ -358,7 +360,12 @@ func (c *GCSFsConfig) Validate(credentialsFilePath string) error {
 
 // AzBlobFsConfig defines the configuration for Azure Blob Storage based filesystem
 type AzBlobFsConfig struct {
-	sdk.AzBlobFsConfig
+	sdk.BaseAzBlobFsConfig
+	// Storage Account Key leave blank to use SAS URL.
+	// The access key is stored encrypted based on the kms configuration
+	AccountKey *kms.Secret `json:"account_key,omitempty"`
+	// Shared access signature URL, leave blank if using account/key
+	SASURL *kms.Secret `json:"sas_url,omitempty"`
 }
 
 // HideConfidentialData hides confidential data
@@ -489,7 +496,7 @@ func (c *AzBlobFsConfig) Validate() error {
 
 // CryptFsConfig defines the configuration to store local files as encrypted
 type CryptFsConfig struct {
-	sdk.CryptFsConfig
+	Passphrase *kms.Secret `json:"passphrase,omitempty"`
 }
 
 // HideConfidentialData hides confidential data

+ 1 - 1
webdavd/internal_test.go

@@ -22,8 +22,8 @@ import (
 
 	"github.com/drakkan/sftpgo/v2/common"
 	"github.com/drakkan/sftpgo/v2/dataprovider"
+	"github.com/drakkan/sftpgo/v2/kms"
 	"github.com/drakkan/sftpgo/v2/sdk"
-	"github.com/drakkan/sftpgo/v2/sdk/kms"
 	"github.com/drakkan/sftpgo/v2/util"
 	"github.com/drakkan/sftpgo/v2/vfs"
 )

+ 8 - 9
webdavd/webdavd_test.go

@@ -31,9 +31,10 @@ import (
 	"github.com/drakkan/sftpgo/v2/dataprovider"
 	"github.com/drakkan/sftpgo/v2/httpclient"
 	"github.com/drakkan/sftpgo/v2/httpdtest"
+	"github.com/drakkan/sftpgo/v2/kms"
 	"github.com/drakkan/sftpgo/v2/logger"
 	"github.com/drakkan/sftpgo/v2/sdk"
-	"github.com/drakkan/sftpgo/v2/sdk/kms"
+	sdkkms "github.com/drakkan/sftpgo/v2/sdk/kms"
 	"github.com/drakkan/sftpgo/v2/sftpd"
 	"github.com/drakkan/sftpgo/v2/vfs"
 	"github.com/drakkan/sftpgo/v2/webdavd"
@@ -1621,7 +1622,7 @@ func TestLoginWithDatabaseCredentials(t *testing.T) {
 
 	user, _, err := httpdtest.AddUser(u, http.StatusCreated)
 	assert.NoError(t, err)
-	assert.Equal(t, kms.SecretStatusSecretBox, user.FsConfig.GCSConfig.Credentials.GetStatus())
+	assert.Equal(t, sdkkms.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())
@@ -2443,11 +2444,11 @@ func TestSFTPLoopVirtualFolders(t *testing.T) {
 			FsConfig: vfs.Filesystem{
 				Provider: sdk.SFTPFilesystemProvider,
 				SFTPConfig: vfs.SFTPFsConfig{
-					SFTPFsConfig: sdk.SFTPFsConfig{
+					BaseSFTPFsConfig: sdk.BaseSFTPFsConfig{
 						Endpoint: sftpServerAddr,
 						Username: user2.Username,
-						Password: kms.NewPlainSecret(defaultPassword),
 					},
+					Password: kms.NewPlainSecret(defaultPassword),
 				},
 			},
 		},
@@ -2455,11 +2456,11 @@ func TestSFTPLoopVirtualFolders(t *testing.T) {
 	})
 	user2.FsConfig.Provider = sdk.SFTPFilesystemProvider
 	user2.FsConfig.SFTPConfig = vfs.SFTPFsConfig{
-		SFTPFsConfig: sdk.SFTPFsConfig{
+		BaseSFTPFsConfig: sdk.BaseSFTPFsConfig{
 			Endpoint: sftpServerAddr,
 			Username: user1.Username,
-			Password: kms.NewPlainSecret(defaultPassword),
 		},
+		Password: kms.NewPlainSecret(defaultPassword),
 	}
 
 	user1, resp, err := httpdtest.AddUser(user1, http.StatusCreated)
@@ -2506,9 +2507,7 @@ func TestNestedVirtualFolders(t *testing.T) {
 			FsConfig: vfs.Filesystem{
 				Provider: sdk.CryptedFilesystemProvider,
 				CryptConfig: vfs.CryptFsConfig{
-					CryptFsConfig: sdk.CryptFsConfig{
-						Passphrase: kms.NewPlainSecret(defaultPassword),
-					},
+					Passphrase: kms.NewPlainSecret(defaultPassword),
 				},
 			},
 			MappedPath: mappedPathCrypt,

Some files were not shown because too many files changed in this diff