config: add --password-command to allow dynamic config password - fixes #3694

This commit is contained in:
Damon Permezel 2019-11-18 19:55:27 +10:00 committed by Nick Craig-Wood
parent 0ab2693da6
commit 06df133159
5 changed files with 169 additions and 15 deletions

View File

@ -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

View File

@ -89,6 +89,7 @@ type ConfigInfo struct {
StreamingUploadCutoff SizeSuffix
StatsFileNameLength int
AskPassword bool
PasswordCommand string
UseServerModTime bool
MaxTransfer SizeSuffix
MaxBacklog int

View File

@ -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")
}

View File

@ -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

View File

@ -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)")