From 84e3132ed19f000976643775336204b79ce1c270 Mon Sep 17 00:00:00 2001 From: sunilke <94823702+sunilke@users.noreply.github.com> Date: Mon, 30 May 2022 22:30:39 +0530 Subject: [PATCH] Feat private key passphrase for sftpfs (#855) Signed-off-by: Sunil Keswani --- httpd/api_folder.go | 3 ++- httpd/api_group.go | 3 ++- httpd/api_user.go | 8 +++++-- httpd/webadmin.go | 7 +++--- templates/webadmin/fsconfig.html | 11 +++++++++ vfs/filesystem.go | 11 +++++++++ vfs/folder.go | 3 +++ vfs/sftpfs.go | 39 ++++++++++++++++++++++++++++---- 8 files changed, 73 insertions(+), 12 deletions(-) diff --git a/httpd/api_folder.go b/httpd/api_folder.go index 273b876b..f2f25fa7 100644 --- a/httpd/api_folder.go +++ b/httpd/api_folder.go @@ -67,6 +67,7 @@ func updateFolder(w http.ResponseWriter, r *http.Request) { currentCryptoPassphrase := folder.FsConfig.CryptConfig.Passphrase currentSFTPPassword := folder.FsConfig.SFTPConfig.Password currentSFTPKey := folder.FsConfig.SFTPConfig.PrivateKey + currentSFTPKeyPassphrase := folder.FsConfig.SFTPConfig.Passphrase folder.FsConfig.S3Config = vfs.S3FsConfig{} folder.FsConfig.AzBlobConfig = vfs.AzBlobFsConfig{} @@ -82,7 +83,7 @@ func updateFolder(w http.ResponseWriter, r *http.Request) { folder.Name = name folder.FsConfig.SetEmptySecretsIfNil() updateEncryptedSecrets(&folder.FsConfig, currentS3AccessSecret, currentAzAccountKey, currentAzSASUrl, currentGCSCredentials, - currentCryptoPassphrase, currentSFTPPassword, currentSFTPKey) + currentCryptoPassphrase, currentSFTPPassword, currentSFTPKey, currentSFTPKeyPassphrase) err = dataprovider.UpdateFolder(&folder, users, groups, claims.Username, util.GetIPFromRemoteAddress(r.RemoteAddr)) if err != nil { sendAPIResponse(w, r, err, "", getRespStatus(err)) diff --git a/httpd/api_group.go b/httpd/api_group.go index ead160e6..c619f9b4 100644 --- a/httpd/api_group.go +++ b/httpd/api_group.go @@ -72,6 +72,7 @@ func updateGroup(w http.ResponseWriter, r *http.Request) { currentCryptoPassphrase := group.UserSettings.FsConfig.CryptConfig.Passphrase currentSFTPPassword := group.UserSettings.FsConfig.SFTPConfig.Password currentSFTPKey := group.UserSettings.FsConfig.SFTPConfig.PrivateKey + currentSFTPKeyPassphrase := group.UserSettings.FsConfig.SFTPConfig.Passphrase group.UserSettings.FsConfig.S3Config = vfs.S3FsConfig{} group.UserSettings.FsConfig.AzBlobConfig = vfs.AzBlobFsConfig{} @@ -87,7 +88,7 @@ func updateGroup(w http.ResponseWriter, r *http.Request) { group.Name = name group.UserSettings.FsConfig.SetEmptySecretsIfNil() updateEncryptedSecrets(&group.UserSettings.FsConfig, currentS3AccessSecret, currentAzAccountKey, currentAzSASUrl, - currentGCSCredentials, currentCryptoPassphrase, currentSFTPPassword, currentSFTPKey) + currentGCSCredentials, currentCryptoPassphrase, currentSFTPPassword, currentSFTPKey, currentSFTPKeyPassphrase) err = dataprovider.UpdateGroup(&group, users, claims.Username, util.GetIPFromRemoteAddress(r.RemoteAddr)) if err != nil { sendAPIResponse(w, r, err, "", getRespStatus(err)) diff --git a/httpd/api_user.go b/httpd/api_user.go index 8d9eee85..b278d7f1 100644 --- a/httpd/api_user.go +++ b/httpd/api_user.go @@ -134,6 +134,7 @@ func updateUser(w http.ResponseWriter, r *http.Request) { currentCryptoPassphrase := user.FsConfig.CryptConfig.Passphrase currentSFTPPassword := user.FsConfig.SFTPConfig.Password currentSFTPKey := user.FsConfig.SFTPConfig.PrivateKey + currentSFTPKeyPassphrase := user.FsConfig.SFTPConfig.Passphrase user.Permissions = make(map[string][]string) user.FsConfig.S3Config = vfs.S3FsConfig{} @@ -159,7 +160,7 @@ func updateUser(w http.ResponseWriter, r *http.Request) { user.Permissions = currentPermissions } updateEncryptedSecrets(&user.FsConfig, currentS3AccessSecret, currentAzAccountKey, currentAzSASUrl, - currentGCSCredentials, currentCryptoPassphrase, currentSFTPPassword, currentSFTPKey) + currentGCSCredentials, currentCryptoPassphrase, currentSFTPPassword, currentSFTPKey, currentSFTPKeyPassphrase) err = dataprovider.UpdateUser(&user, claims.Username, util.GetIPFromRemoteAddress(r.RemoteAddr)) if err != nil { sendAPIResponse(w, r, err, "", getRespStatus(err)) @@ -231,7 +232,7 @@ func disconnectUser(username string) { } func updateEncryptedSecrets(fsConfig *vfs.Filesystem, currentS3AccessSecret, currentAzAccountKey, currentAzSASUrl, - currentGCSCredentials, currentCryptoPassphrase, currentSFTPPassword, currentSFTPKey *kms.Secret) { + currentGCSCredentials, currentCryptoPassphrase, currentSFTPPassword, currentSFTPKey *kms.Secret, currentSFTPKeyPassphrase *kms.Secret) { // we use the new access secret if plain or empty, otherwise the old value switch fsConfig.Provider { case sdk.S3FilesystemProvider: @@ -262,5 +263,8 @@ func updateEncryptedSecrets(fsConfig *vfs.Filesystem, currentS3AccessSecret, cur if fsConfig.SFTPConfig.PrivateKey.IsNotPlainAndNotEmpty() { fsConfig.SFTPConfig.PrivateKey = currentSFTPKey } + if fsConfig.SFTPConfig.Passphrase.IsNotPlainAndNotEmpty() { + fsConfig.SFTPConfig.Passphrase = currentSFTPKeyPassphrase + } } } diff --git a/httpd/webadmin.go b/httpd/webadmin.go index f5172e98..6639ca71 100644 --- a/httpd/webadmin.go +++ b/httpd/webadmin.go @@ -1219,6 +1219,7 @@ func getSFTPConfig(r *http.Request) (vfs.SFTPFsConfig, error) { config.Username = r.Form.Get("sftp_username") config.Password = getSecretFromFormField(r, "sftp_password") config.PrivateKey = getSecretFromFormField(r, "sftp_private_key") + config.Passphrase = getSecretFromFormField(r, "sftp_passphrase") fingerprintsFormValue := r.Form.Get("sftp_fingerprints") config.Fingerprints = getSliceFromDelimitedValues(fingerprintsFormValue, "\n") config.Prefix = r.Form.Get("sftp_prefix") @@ -2202,7 +2203,7 @@ func (s *httpdServer) handleWebUpdateUserPost(w http.ResponseWriter, r *http.Req } updateEncryptedSecrets(&updatedUser.FsConfig, user.FsConfig.S3Config.AccessSecret, user.FsConfig.AzBlobConfig.AccountKey, user.FsConfig.AzBlobConfig.SASURL, user.FsConfig.GCSConfig.Credentials, user.FsConfig.CryptConfig.Passphrase, - user.FsConfig.SFTPConfig.Password, user.FsConfig.SFTPConfig.PrivateKey) + user.FsConfig.SFTPConfig.Password, user.FsConfig.SFTPConfig.PrivateKey, user.FsConfig.SFTPConfig.Passphrase) updatedUser = getUserFromTemplate(updatedUser, userTemplateFields{ Username: updatedUser.Username, @@ -2336,7 +2337,7 @@ func (s *httpdServer) handleWebUpdateFolderPost(w http.ResponseWriter, r *http.R updatedFolder.FsConfig.SetEmptySecretsIfNil() updateEncryptedSecrets(&updatedFolder.FsConfig, folder.FsConfig.S3Config.AccessSecret, folder.FsConfig.AzBlobConfig.AccountKey, folder.FsConfig.AzBlobConfig.SASURL, folder.FsConfig.GCSConfig.Credentials, folder.FsConfig.CryptConfig.Passphrase, - folder.FsConfig.SFTPConfig.Password, folder.FsConfig.SFTPConfig.PrivateKey) + folder.FsConfig.SFTPConfig.Password, folder.FsConfig.SFTPConfig.PrivateKey, folder.FsConfig.SFTPConfig.Passphrase) updatedFolder = getFolderFromTemplate(updatedFolder, updatedFolder.Name) @@ -2500,7 +2501,7 @@ func (s *httpdServer) handleWebUpdateGroupPost(w http.ResponseWriter, r *http.Re updateEncryptedSecrets(&updatedGroup.UserSettings.FsConfig, group.UserSettings.FsConfig.S3Config.AccessSecret, group.UserSettings.FsConfig.AzBlobConfig.AccountKey, group.UserSettings.FsConfig.AzBlobConfig.SASURL, group.UserSettings.FsConfig.GCSConfig.Credentials, group.UserSettings.FsConfig.CryptConfig.Passphrase, - group.UserSettings.FsConfig.SFTPConfig.Password, group.UserSettings.FsConfig.SFTPConfig.PrivateKey) + group.UserSettings.FsConfig.SFTPConfig.Password, group.UserSettings.FsConfig.SFTPConfig.PrivateKey, group.UserSettings.FsConfig.SFTPConfig.Passphrase) err = dataprovider.UpdateGroup(&updatedGroup, group.Users, claims.Username, ipAddr) if err != nil { diff --git a/templates/webadmin/fsconfig.html b/templates/webadmin/fsconfig.html index 67ab37df..879b4123 100644 --- a/templates/webadmin/fsconfig.html +++ b/templates/webadmin/fsconfig.html @@ -424,6 +424,17 @@ +
+ +
+ + + A passphrase is a word or phrase that protects private key files,its used in case of encrypted private keys. + +
+
+
diff --git a/vfs/filesystem.go b/vfs/filesystem.go index 2e3d5f77..6825e2d2 100644 --- a/vfs/filesystem.go +++ b/vfs/filesystem.go @@ -26,6 +26,7 @@ func (f *Filesystem) SetEmptySecrets() { f.CryptConfig.Passphrase = kms.NewEmptySecret() f.SFTPConfig.Password = kms.NewEmptySecret() f.SFTPConfig.PrivateKey = kms.NewEmptySecret() + f.SFTPConfig.Passphrase = kms.NewEmptySecret() } // SetEmptySecretsIfNil sets the secrets to empty if nil @@ -51,6 +52,9 @@ func (f *Filesystem) SetEmptySecretsIfNil() { if f.SFTPConfig.PrivateKey == nil { f.SFTPConfig.PrivateKey = kms.NewEmptySecret() } + if f.SFTPConfig.Passphrase == nil { + f.SFTPConfig.Passphrase = kms.NewEmptySecret() + } } // SetNilSecretsIfEmpty set the secrets to nil if empty. @@ -78,6 +82,9 @@ func (f *Filesystem) SetNilSecretsIfEmpty() { if f.SFTPConfig.PrivateKey != nil && f.SFTPConfig.PrivateKey.IsEmpty() { f.SFTPConfig.PrivateKey = nil } + if f.SFTPConfig.Passphrase != nil && f.SFTPConfig.Passphrase.IsEmpty() { + f.SFTPConfig.Passphrase = nil + } } // IsEqual returns true if the fs is equal to other @@ -191,6 +198,9 @@ func (f *Filesystem) HasRedactedSecret() bool { if f.SFTPConfig.PrivateKey.IsRedacted() { return true } + if f.SFTPConfig.Passphrase.IsRedacted() { + return true + } } return false @@ -276,6 +286,7 @@ func (f *Filesystem) GetACopy() Filesystem { }, Password: f.SFTPConfig.Password.Clone(), PrivateKey: f.SFTPConfig.PrivateKey.Clone(), + Passphrase: f.SFTPConfig.Passphrase.Clone(), }, } if len(f.SFTPConfig.Fingerprints) > 0 { diff --git a/vfs/folder.go b/vfs/folder.go index b595f5ec..4ab1cb73 100644 --- a/vfs/folder.go +++ b/vfs/folder.go @@ -168,6 +168,9 @@ func (v *BaseVirtualFolder) HasRedactedSecret() bool { if v.FsConfig.SFTPConfig.PrivateKey.IsRedacted() { return true } + if v.FsConfig.SFTPConfig.Passphrase.IsRedacted() { + return true + } } return false } diff --git a/vfs/sftpfs.go b/vfs/sftpfs.go index c50788eb..1ea1f6ac 100644 --- a/vfs/sftpfs.go +++ b/vfs/sftpfs.go @@ -41,6 +41,7 @@ type SFTPFsConfig struct { Password *kms.Secret `json:"password,omitempty"` PrivateKey *kms.Secret `json:"private_key,omitempty"` forbiddenSelfUsernames []string `json:"-"` + Passphrase *kms.Secret `json:"passphrase,omitempty"` } // HideConfidentialData hides confidential data @@ -51,6 +52,9 @@ func (c *SFTPFsConfig) HideConfidentialData() { if c.PrivateKey != nil { c.PrivateKey.Hide() } + if c.Passphrase != nil { + c.Passphrase.Hide() + } } func (c *SFTPFsConfig) isEqual(other *SFTPFsConfig) bool { @@ -82,6 +86,9 @@ func (c *SFTPFsConfig) isEqual(other *SFTPFsConfig) bool { if !c.Password.IsEqual(other.Password) { return false } + if !c.Passphrase.IsEqual(other.Passphrase) { + return false + } return c.PrivateKey.IsEqual(other.PrivateKey) } @@ -92,6 +99,9 @@ func (c *SFTPFsConfig) setEmptyCredentialsIfNil() { if c.PrivateKey == nil { c.PrivateKey = kms.NewEmptySecret() } + if c.Passphrase == nil { + c.Passphrase = kms.NewEmptySecret() + } } // validate returns an error if the configuration is not valid @@ -157,6 +167,12 @@ func (c *SFTPFsConfig) ValidateAndEncryptCredentials(additionalData string) erro return util.NewValidationError(fmt.Sprintf("could not encrypt SFTP fs private key: %v", err)) } } + if c.Passphrase.IsPlain() { + c.Passphrase.SetAdditionalData(additionalData) + if err := c.Passphrase.Encrypt(); err != nil { + return err + } + } return nil } @@ -195,6 +211,11 @@ func NewSFTPFs(connectionID, mountPath, localTempDir string, forbiddenSelfUserna return nil, err } } + if !config.Passphrase.IsEmpty() { + if err := config.Passphrase.TryDecrypt(); err != nil { + return nil, err + } + } config.forbiddenSelfUsernames = forbiddenSelfUsernames sftpFs := &SFTPFs{ connectionID: connectionID, @@ -778,11 +799,19 @@ func (fs *SFTPFs) createConnection() error { ClientVersion: fmt.Sprintf("SSH-2.0-SFTPGo_%v", version.Get().Version), } if fs.config.PrivateKey.GetPayload() != "" { - signer, err := ssh.ParsePrivateKey([]byte(fs.config.PrivateKey.GetPayload())) - if err != nil { - fsLog(fs, logger.LevelError, "unable to parse the provided private key: %v", err) - fs.err <- err - return err + var signer ssh.Signer + if fs.config.Passphrase.GetPayload() != "" { + signer, err = ssh.ParsePrivateKeyWithPassphrase([]byte(fs.config.PrivateKey.GetPayload()), []byte(fs.config.Passphrase.GetPayload())) + if err != nil { + fs.err <- err + return err + } + } else { + signer, err = ssh.ParsePrivateKey([]byte(fs.config.PrivateKey.GetPayload())) + if err != nil { + fs.err <- err + return err + } } clientConfig.Auth = append(clientConfig.Auth, ssh.PublicKeys(signer)) }