diff --git a/docs/content/docs.md b/docs/content/docs.md index 78aba9956..e3a15ab6d 100644 --- a/docs/content/docs.md +++ b/docs/content/docs.md @@ -1285,7 +1285,7 @@ your cloud services. This means that you should keep your If you are in an environment where that isn't possible, you can add a password to your configuration. This means that you will -have to enter the password every time you start rclone. +have to supply the password every time you start rclone. To add a password to your rclone configuration, execute `rclone config`. @@ -1322,9 +1322,9 @@ c/u/q> ``` Your configuration is now encrypted, and every time you start rclone -you will now be asked for the password. In the same menu, you can -change the password or completely remove encryption from your -configuration. +you will have to supply the password. See below for details. +In the same menu, you can change the password or completely remove +encryption from your configuration. There is no way to recover the configuration if you lose your password. @@ -1356,11 +1356,36 @@ Then source the file when you want to use it. From the shell you would do `source set-rclone-password`. It will then ask you for the password and set it in the environment variable. -If you are running rclone inside a script, you might want to disable +An alternate means of supplying the password is to provide a script +which will retrieve the password and print on standard output. This +script should have a fully specified path name and not rely on any +environment variables. The script is supplied either via +`--password-command="..."` command line argument or via the +`RCLONE_CONFIG_PASS_COMMAND` environment variable. + +One useful example of this is using the `passwordstore` application +to retrieve the password: + +``` +export RCLONE_CONFIG_PASS_COMMAND="pass rclone/config" +``` + +If the `passwordstore` password manager holds the password for the +rclone configuration, using the script method means the password +is primarily protected by the `passwordstore` system, and is never +embedded in the clear in scripts, nor available for examination +using the standard commands available. It is quite possible with +long running rclone sessions for copies of passwords to be innocently +captured in log files or terminal scroll buffers, etc. Using the +script method of supplying the password enhances the security of +the config password considerably. + +If you are running rclone inside a script, unless you are using the +`RCLONE_CONFIG_PASS_COMMAND` method, you might want to disable password prompts. To do that, pass the parameter `--ask-password=false` to rclone. This will make rclone fail instead of asking for a password if `RCLONE_CONFIG_PASS` doesn't contain -a valid password. +a valid password, and `RCLONE_CONFIG_PASS_COMMAND` has not been supplied. Developer options diff --git a/fs/config.go b/fs/config.go index 54675ffab..8b1b66349 100644 --- a/fs/config.go +++ b/fs/config.go @@ -89,6 +89,7 @@ type ConfigInfo struct { StreamingUploadCutoff SizeSuffix StatsFileNameLength int AskPassword bool + PasswordCommand string UseServerModTime bool MaxTransfer SizeSuffix MaxBacklog int diff --git a/fs/config/config.go b/fs/config/config.go index 086662b30..97c34f166 100644 --- a/fs/config/config.go +++ b/fs/config/config.go @@ -14,6 +14,7 @@ import ( "log" mathrand "math/rand" "os" + "os/exec" "path/filepath" "regexp" "runtime" @@ -239,15 +240,7 @@ var errorConfigFileNotFound = errors.New("config file not found") // loadConfigFile will load a config file, and // automatically decrypt it. func loadConfigFile() (*goconfig.ConfigFile, error) { - envpw := os.Getenv("RCLONE_CONFIG_PASS") - if len(configKey) == 0 && envpw != "" { - err := setConfigPassword(envpw) - if err != nil { - fs.Errorf(nil, "Using RCLONE_CONFIG_PASS returned: %v", err) - } else { - fs.Debugf(nil, "Using RCLONE_CONFIG_PASS password.") - } - } + var usingPasswordCommand bool b, err := ioutil.ReadFile(ConfigPath) if err != nil { @@ -280,6 +273,59 @@ func loadConfigFile() (*goconfig.ConfigFile, error) { return goconfig.LoadFromReader(bytes.NewBuffer(b)) } + if len(configKey) == 0 { + pwc := fs.Config.PasswordCommand + + if pwc == "" { + pwc = os.Getenv("RCLONE_CONFIG_PASS_COMMAND") + } + if pwc != "" { + var stdout bytes.Buffer + var stderr bytes.Buffer + + args := strings.Fields(pwc) + cmd := exec.Command(args[0], args[1:]...) + + cmd.Stdout = &stdout + cmd.Stderr = &stderr + + if err := cmd.Run(); err != nil { + // One does not always get the stderr returned in the wrapped error. + fs.Errorf(nil, "Using RCLONE_CONFIG_PASS_COMMAND returned: %v", err) + if ers := strings.TrimSpace(stderr.String()); ers != "" { + fs.Errorf(nil, "--password-command stderr: %s", ers) + } + return nil, errors.Wrap(err, "password command failed") + } + if pass := strings.Trim(stdout.String(), "\r\n"); pass != "" { + err := setConfigPassword(pass) + if err != nil { + return nil, errors.Wrap(err, "incorrect password") + } + } else { + return nil, errors.New("password-command returned empty string") + } + + if len(configKey) == 0 { + return nil, errors.New("unable to decrypt configuration: incorrect password") + } + usingPasswordCommand = true + } else { + usingPasswordCommand = false + + envpw := os.Getenv("RCLONE_CONFIG_PASS") + + if envpw != "" { + err := setConfigPassword(envpw) + if err != nil { + fs.Errorf(nil, "Using RCLONE_CONFIG_PASS returned: %v", err) + } else { + fs.Debugf(nil, "Using RCLONE_CONFIG_PASS password.") + } + } + } + } + // Encrypted content is base64 encoded. dec := base64.NewDecoder(base64.StdEncoding, r) box, err := ioutil.ReadAll(dec) @@ -310,6 +356,9 @@ func loadConfigFile() (*goconfig.ConfigFile, error) { fs.Debugf(nil, "using _RCLONE_CONFIG_KEY_FILE for configKey") } else { if len(configKey) == 0 { + if usingPasswordCommand { + return nil, errors.New("using password command derived password, unable to decrypt configuration") + } if !fs.Config.AskPassword { return nil, errors.New("unable to decrypt configuration and not allowed to ask for password - set RCLONE_CONFIG_PASS to your configuration password") } diff --git a/fs/config/config_test.go b/fs/config/config_test.go index c89b7f3b7..23642d1e4 100644 --- a/fs/config/config_test.go +++ b/fs/config/config_test.go @@ -270,6 +270,84 @@ func TestConfigLoadEncrypted(t *testing.T) { assert.Equal(t, expect, keys) } +func expectConfigValid(t *testing.T) { + var err error + + configKey = nil // reset password + + c, err := loadConfigFile() + require.NoError(t, err) + + // Not sure why there is no "RCLONE_ENCRYPT_V0" here... + sections := c.GetSectionList() + var expect = []string{"nounc", "unc"} + assert.Equal(t, expect, sections) + + keys := c.GetKeyList("nounc") + expect = []string{"type", "nounc"} + assert.Equal(t, expect, keys) +} + +func expectConfigInvalid(t *testing.T) { + var err error + + configKey = nil // reset password + + _, err = loadConfigFile() + require.Error(t, err) +} + +func TestConfigLoadEncryptedWithValidPassCommand(t *testing.T) { + oldConfigPath := ConfigPath + oldConfig := fs.Config + ConfigPath = "./testdata/encrypted.conf" + defer func() { + ConfigPath = oldConfigPath + configKey = nil // reset password + fs.Config = oldConfig + }() + + // using fs.Config.PasswordCommand, correct password + fs.Config.PasswordCommand = "echo asdf" + expectConfigValid(t) + + var err error + + // using "RCLONE_CONFIG_PASS_COMMAND" + + err = os.Setenv("RCLONE_CONFIG_PASS_COMMAND", "echo asdf") + require.NoError(t, err) + expectConfigValid(t) + + err = os.Unsetenv("RCLONE_CONFIG_PASS_COMMAND") + require.NoError(t, err) +} + +func TestConfigLoadEncryptedWithInvalidPassCommand(t *testing.T) { + oldConfigPath := ConfigPath + oldConfig := fs.Config + ConfigPath = "./testdata/encrypted.conf" + defer func() { + ConfigPath = oldConfigPath + configKey = nil // reset password + fs.Config = oldConfig + }() + + // using fs.Config.PasswordCommand, incorrect password + fs.Config.PasswordCommand = "echo asdf-blurfl" + expectConfigInvalid(t) + fs.Config.PasswordCommand = "" + + var err error + + err = os.Setenv("RCLONE_CONFIG_PASS_COMMAND", "echo asdf-blurfl") + require.NoError(t, err) + expectConfigInvalid(t) + + err = os.Unsetenv("RCLONE_CONFIG_PASS_COMMAND") + require.NoError(t, err) +} + func TestConfigLoadEncryptedFailures(t *testing.T) { var err error diff --git a/fs/config/configflags/configflags.go b/fs/config/configflags/configflags.go index 16ad272d9..2762ea929 100644 --- a/fs/config/configflags/configflags.go +++ b/fs/config/configflags/configflags.go @@ -55,6 +55,7 @@ func AddFlags(flagSet *pflag.FlagSet) { flags.BoolVarP(flagSet, &dumpBodies, "dump-bodies", "", false, "Dump HTTP headers and bodies - may contain sensitive info") flags.BoolVarP(flagSet, &fs.Config.InsecureSkipVerify, "no-check-certificate", "", fs.Config.InsecureSkipVerify, "Do not verify the server SSL certificate. Insecure.") flags.BoolVarP(flagSet, &fs.Config.AskPassword, "ask-password", "", fs.Config.AskPassword, "Allow prompt for password for encrypted configuration.") + flags.StringVarP(flagSet, &fs.Config.PasswordCommand, "password-command", "", fs.Config.PasswordCommand, "Command for supplying password for encrypted configuration.") flags.BoolVarP(flagSet, &deleteBefore, "delete-before", "", false, "When synchronizing, delete files on destination before transferring") flags.BoolVarP(flagSet, &deleteDuring, "delete-during", "", false, "When synchronizing, delete files during transfer") flags.BoolVarP(flagSet, &deleteAfter, "delete-after", "", false, "When synchronizing, delete files on destination after transferring (default)")