mirror of
https://github.com/rclone/rclone.git
synced 2025-01-11 08:49:37 +01:00
config: add --password-command to allow dynamic config password - fixes #3694
This commit is contained in:
parent
0ab2693da6
commit
06df133159
@ -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
|
If you are in an environment where that isn't possible, you can
|
||||||
add a password to your configuration. This means that you will
|
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`.
|
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
|
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
|
you will have to supply the password. See below for details.
|
||||||
change the password or completely remove encryption from your
|
In the same menu, you can change the password or completely remove
|
||||||
configuration.
|
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.
|
||||||
|
|
||||||
@ -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
|
would do `source set-rclone-password`. It will then ask you for the
|
||||||
password and set it in the environment variable.
|
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
|
password prompts. To do that, pass the parameter
|
||||||
`--ask-password=false` to rclone. This will make rclone fail instead
|
`--ask-password=false` to rclone. This will make rclone fail instead
|
||||||
of asking for a password if `RCLONE_CONFIG_PASS` doesn't contain
|
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
|
Developer options
|
||||||
|
@ -89,6 +89,7 @@ type ConfigInfo struct {
|
|||||||
StreamingUploadCutoff SizeSuffix
|
StreamingUploadCutoff SizeSuffix
|
||||||
StatsFileNameLength int
|
StatsFileNameLength int
|
||||||
AskPassword bool
|
AskPassword bool
|
||||||
|
PasswordCommand string
|
||||||
UseServerModTime bool
|
UseServerModTime bool
|
||||||
MaxTransfer SizeSuffix
|
MaxTransfer SizeSuffix
|
||||||
MaxBacklog int
|
MaxBacklog int
|
||||||
|
@ -14,6 +14,7 @@ import (
|
|||||||
"log"
|
"log"
|
||||||
mathrand "math/rand"
|
mathrand "math/rand"
|
||||||
"os"
|
"os"
|
||||||
|
"os/exec"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"regexp"
|
"regexp"
|
||||||
"runtime"
|
"runtime"
|
||||||
@ -239,15 +240,7 @@ var errorConfigFileNotFound = errors.New("config file not found")
|
|||||||
// loadConfigFile will load a config file, and
|
// loadConfigFile will load a config file, and
|
||||||
// automatically decrypt it.
|
// automatically decrypt it.
|
||||||
func loadConfigFile() (*goconfig.ConfigFile, error) {
|
func loadConfigFile() (*goconfig.ConfigFile, error) {
|
||||||
envpw := os.Getenv("RCLONE_CONFIG_PASS")
|
var usingPasswordCommand bool
|
||||||
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.")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
b, err := ioutil.ReadFile(ConfigPath)
|
b, err := ioutil.ReadFile(ConfigPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -280,6 +273,59 @@ func loadConfigFile() (*goconfig.ConfigFile, error) {
|
|||||||
return goconfig.LoadFromReader(bytes.NewBuffer(b))
|
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.
|
// Encrypted content is base64 encoded.
|
||||||
dec := base64.NewDecoder(base64.StdEncoding, r)
|
dec := base64.NewDecoder(base64.StdEncoding, r)
|
||||||
box, err := ioutil.ReadAll(dec)
|
box, err := ioutil.ReadAll(dec)
|
||||||
@ -310,6 +356,9 @@ func loadConfigFile() (*goconfig.ConfigFile, error) {
|
|||||||
fs.Debugf(nil, "using _RCLONE_CONFIG_KEY_FILE for configKey")
|
fs.Debugf(nil, "using _RCLONE_CONFIG_KEY_FILE for configKey")
|
||||||
} else {
|
} else {
|
||||||
if len(configKey) == 0 {
|
if len(configKey) == 0 {
|
||||||
|
if usingPasswordCommand {
|
||||||
|
return nil, errors.New("using password command derived password, unable to decrypt configuration")
|
||||||
|
}
|
||||||
if !fs.Config.AskPassword {
|
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")
|
return nil, errors.New("unable to decrypt configuration and not allowed to ask for password - set RCLONE_CONFIG_PASS to your configuration password")
|
||||||
}
|
}
|
||||||
|
@ -270,6 +270,84 @@ func TestConfigLoadEncrypted(t *testing.T) {
|
|||||||
assert.Equal(t, expect, keys)
|
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) {
|
func TestConfigLoadEncryptedFailures(t *testing.T) {
|
||||||
var err error
|
var err error
|
||||||
|
|
||||||
|
@ -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, &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.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.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, &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, &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)")
|
flags.BoolVarP(flagSet, &deleteAfter, "delete-after", "", false, "When synchronizing, delete files on destination after transferring (default)")
|
||||||
|
Loading…
Reference in New Issue
Block a user