config encryption: set, remove and check to manage config file encryption #7859

This commit is contained in:
Nick Craig-Wood 2024-09-05 11:52:15 +01:00
parent ffb2e2a6de
commit 2d1c2b1f76
6 changed files with 194 additions and 10 deletions

View file

@ -36,6 +36,7 @@ func init() {
configCommand.AddCommand(configReconnectCommand) configCommand.AddCommand(configReconnectCommand)
configCommand.AddCommand(configDisconnectCommand) configCommand.AddCommand(configDisconnectCommand)
configCommand.AddCommand(configUserInfoCommand) configCommand.AddCommand(configUserInfoCommand)
configCommand.AddCommand(configEncryptionCommand)
} }
var configCommand = &cobra.Command{ var configCommand = &cobra.Command{
@ -518,3 +519,91 @@ system.
return nil return nil
}, },
} }
func init() {
configEncryptionCommand.AddCommand(configEncryptionSetCommand)
configEncryptionCommand.AddCommand(configEncryptionRemoveCommand)
configEncryptionCommand.AddCommand(configEncryptionCheckCommand)
}
var configEncryptionCommand = &cobra.Command{
Use: "encryption",
Short: `set, remove and check the encryption for the config file`,
Long: `This command sets, clears and checks the encryption for the config file using
the subcommands below.
`,
}
var configEncryptionSetCommand = &cobra.Command{
Use: "set",
Short: `Set or change the config file encryption password`,
Long: strings.ReplaceAll(`This command sets or changes the config file encryption password.
If there was no config password set then it sets a new one, otherwise
it changes the existing config password.
Note that if you are changing an encryption password using
|--password-command| then this will be called once to decrypt the
config using the old password and then again to read the new
password to re-encrypt the config.
When |--password-command| is called to change the password then the
environment variable |RCLONE_PASSWORD_CHANGE=1| will be set. So if
changing passwords programatically you can use the environment
variable to distinguish which password you must supply.
Alternatively you can remove the password first (with |rclone config
encryption remove|), then set it again with this command which may be
easier if you don't mind the unecrypted config file being on the disk
briefly.
`, "|", "`"),
RunE: func(command *cobra.Command, args []string) error {
cmd.CheckArgs(0, 0, command, args)
config.LoadedData()
config.ChangeConfigPasswordAndSave()
return nil
},
}
var configEncryptionRemoveCommand = &cobra.Command{
Use: "remove",
Short: `Remove the config file encryption password`,
Long: strings.ReplaceAll(`Remove the config file encryption password
This removes the config file encryption, returning it to un-encrypted.
If |--password-command| is in use, this will be called to supply the old config
password.
If the config was not encrypted then no error will be returned and
this command will do nothing.
`, "|", "`"),
RunE: func(command *cobra.Command, args []string) error {
cmd.CheckArgs(0, 0, command, args)
config.LoadedData()
config.RemoveConfigPasswordAndSave()
return nil
},
}
var configEncryptionCheckCommand = &cobra.Command{
Use: "check",
Short: `Check that the config file is encrypted`,
Long: strings.ReplaceAll(`This checks the config file is encrypted and that you can decrypt it.
It will attempt to decrypt the config using the password you supply.
If decryption fails it will return a non-zero exit code if using
|--password-command|, otherwise it will prompt again for the password.
If the config file is not encrypted it will return a non zero exit code.
`, "|", "`"),
RunE: func(command *cobra.Command, args []string) error {
cmd.CheckArgs(0, 0, command, args)
config.LoadedData()
if !config.IsEncrypted() {
return errors.New("config file is NOT encrypted")
}
return nil
},
}

View file

@ -1924,7 +1924,7 @@ Suffix length limit is 16 characters.
The default is `.partial`. The default is `.partial`.
### --password-command SpaceSepList ### ### --password-command SpaceSepList {#password-command}
This flag supplies a program which should supply the config password This flag supplies a program which should supply the config password
when run. This is an alternative to rclone prompting for the password when run. This is an alternative to rclone prompting for the password
@ -1943,6 +1943,11 @@ Eg
--password-command 'echo "hello with space"' --password-command 'echo "hello with space"'
--password-command 'echo "hello with ""quotes"" and space"' --password-command 'echo "hello with ""quotes"" and space"'
Note that when changing the configuration password the environment
variable `RCLONE_PASSWORD_CHANGE=1` will be set. This can be used to
distinguish initial decryption of the config file from the new
password.
See the [Configuration Encryption](#configuration-encryption) for more info. See the [Configuration Encryption](#configuration-encryption) for more info.
See a [Windows PowerShell example on the Wiki](https://github.com/rclone/rclone/wiki/Windows-Powershell-use-rclone-password-command-for-Config-file-password). See a [Windows PowerShell example on the Wiki](https://github.com/rclone/rclone/wiki/Windows-Powershell-use-rclone-password-command-for-Config-file-password).
@ -2546,6 +2551,12 @@ encryption from your configuration.
There is no way to recover the configuration if you lose your password. There is no way to recover the configuration if you lose your password.
You can also use
- [rclone config encryption set](/commands/rclone_config_encryption_set/) to set the config encryption directly
- [rclone config encryption remove](/commands/rclone_config_encryption_remove/) to remove it
- [rclone config encryption check](/commands/rclone_config_encryption_check/) to check that it is encrypted properly.
rclone uses [nacl secretbox](https://godoc.org/golang.org/x/crypto/nacl/secretbox) rclone uses [nacl secretbox](https://godoc.org/golang.org/x/crypto/nacl/secretbox)
which in turn uses XSalsa20 and Poly1305 to encrypt and authenticate which in turn uses XSalsa20 and Poly1305 to encrypt and authenticate
your configuration with secret-key cryptography. your configuration with secret-key cryptography.
@ -2578,7 +2589,7 @@ An alternate means of supplying the password is to provide a script
which will retrieve the password and print on standard output. This which will retrieve the password and print on standard output. This
script should have a fully specified path name and not rely on any script should have a fully specified path name and not rely on any
environment variables. The script is supplied either via environment variables. The script is supplied either via
`--password-command="..."` command line argument or via the [`--password-command="..."`](#password-command) command line argument or via the
`RCLONE_PASSWORD_COMMAND` environment variable. `RCLONE_PASSWORD_COMMAND` environment variable.
One useful example of this is using the `passwordstore` application One useful example of this is using the `passwordstore` application

View file

@ -41,6 +41,11 @@ var (
PassConfigKeyForDaemonization = false PassConfigKeyForDaemonization = false
) )
// IsEncrypted returns true if the config file is encrypted
func IsEncrypted() bool {
return len(configKey) > 0
}
// Decrypt will automatically decrypt a reader // Decrypt will automatically decrypt a reader
func Decrypt(b io.ReadSeeker) (io.Reader, error) { func Decrypt(b io.ReadSeeker) (io.Reader, error) {
ctx := context.Background() ctx := context.Background()
@ -313,6 +318,11 @@ func ClearConfigPassword() {
// //
// This will use --password-command if configured to read the password. // This will use --password-command if configured to read the password.
func changeConfigPassword() { func changeConfigPassword() {
// Set RCLONE_PASSWORD_CHANGE to "1" when calling the --password-command tool
_ = os.Setenv("RCLONE_PASSWORD_CHANGE", "1")
defer func() {
_ = os.Unsetenv("RCLONE_PASSWORD_CHANGE")
}()
pass, err := GetPasswordCommand(context.Background()) pass, err := GetPasswordCommand(context.Background())
if err != nil { if err != nil {
fmt.Printf("Failed to read new password with --password-command: %v\n", err) fmt.Printf("Failed to read new password with --password-command: %v\n", err)
@ -329,3 +339,22 @@ func changeConfigPassword() {
return return
} }
} }
// ChangeConfigPasswordAndSave will query the user twice
// for a password. If the same password is entered
// twice the key is updated.
//
// This will use --password-command if configured to read the password.
//
// It will then save the config
func ChangeConfigPasswordAndSave() {
changeConfigPassword()
SaveConfig()
}
// RemoveConfigPasswordAndSave will clear the config password and save
// the unencrypted config file.
func RemoveConfigPasswordAndSave() {
configKey = nil
SaveConfig()
}

View file

@ -2,6 +2,8 @@ package config
import ( import (
"context" "context"
"os"
"path/filepath"
"testing" "testing"
"github.com/rclone/rclone/fs" "github.com/rclone/rclone/fs"
@ -64,8 +66,33 @@ func TestChangeConfigPassword(t *testing.T) {
// Get rid of any config password // Get rid of any config password
ClearConfigPassword() ClearConfigPassword()
// Set correct password using --password command // Return the password, checking the state of the environment variable
ci.PasswordCommand = fs.SpaceSepList{"echo", "asdf"} checkCode := `
package main
import (
"fmt"
"os"
"log"
)
func main() {
v := os.Getenv("RCLONE_PASSWORD_CHANGE")
if v == "" {
log.Fatal("Env var not found")
} else if v != "1" {
log.Fatal("Env var wrong value")
} else {
fmt.Println("asdf")
}
}
`
dir := t.TempDir()
code := filepath.Join(dir, "file.go")
require.NoError(t, os.WriteFile(code, []byte(checkCode), 0777))
// Set correct password using --password-command
ci.PasswordCommand = fs.SpaceSepList{"go", "run", code}
changeConfigPassword() changeConfigPassword()
err = Data().Load() err = Data().Load()
require.NoError(t, err) require.NoError(t, err)

View file

@ -6,6 +6,8 @@ package config_test
import ( import (
"context" "context"
"os"
"path/filepath"
"testing" "testing"
"github.com/rclone/rclone/fs" "github.com/rclone/rclone/fs"
@ -24,8 +26,10 @@ func TestConfigLoadEncrypted(t *testing.T) {
}() }()
// Set correct password // Set correct password
assert.False(t, config.IsEncrypted())
err = config.SetConfigPassword("asdf") err = config.SetConfigPassword("asdf")
require.NoError(t, err) require.NoError(t, err)
assert.True(t, config.IsEncrypted())
err = config.Data().Load() err = config.Data().Load()
require.NoError(t, err) require.NoError(t, err)
sections := config.Data().GetSectionList() sections := config.Data().GetSectionList()
@ -138,4 +142,31 @@ func TestGetPasswordCommand(t *testing.T) {
ci.PasswordCommand = fs.SpaceSepList{"XXX non-existent command XXX", ""} ci.PasswordCommand = fs.SpaceSepList{"XXX non-existent command XXX", ""}
_, err = config.GetPasswordCommand(ctx) _, err = config.GetPasswordCommand(ctx)
assert.ErrorContains(t, err, "not found") assert.ErrorContains(t, err, "not found")
// Check the state of the environment variable in --password-command
checkCode := `
package main
import (
"fmt"
"os"
)
func main() {
if _, found := os.LookupEnv("RCLONE_PASSWORD_CHANGE"); found {
fmt.Println("Env var set")
} else {
fmt.Println("OK")
}
}
`
dir := t.TempDir()
code := filepath.Join(dir, "file.go")
require.NoError(t, os.WriteFile(code, []byte(checkCode), 0777))
// Check the environment variable unset when called directly
ci.PasswordCommand = fs.SpaceSepList{"go", "run", code}
pass, err = config.GetPasswordCommand(ctx)
require.NoError(t, err)
assert.Equal(t, "OK", pass)
} }

View file

@ -797,13 +797,11 @@ func SetPassword() {
what := []string{"cChange Password", "uUnencrypt configuration", "qQuit to main menu"} what := []string{"cChange Password", "uUnencrypt configuration", "qQuit to main menu"}
switch i := Command(what); i { switch i := Command(what); i {
case 'c': case 'c':
changeConfigPassword() ChangeConfigPasswordAndSave()
SaveConfig()
fmt.Println("Password changed") fmt.Println("Password changed")
continue continue
case 'u': case 'u':
configKey = nil RemoveConfigPasswordAndSave()
SaveConfig()
continue continue
case 'q': case 'q':
return return
@ -815,8 +813,7 @@ func SetPassword() {
what := []string{"aAdd Password", "qQuit to main menu"} what := []string{"aAdd Password", "qQuit to main menu"}
switch i := Command(what); i { switch i := Command(what); i {
case 'a': case 'a':
changeConfigPassword() ChangeConfigPasswordAndSave()
SaveConfig()
fmt.Println("Password set") fmt.Println("Password set")
continue continue
case 'q': case 'q':