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

This commit is contained in:
Damon Permezel
2020-01-23 15:39:15 +00:00
committed by Nick Craig-Wood
parent 0ab2693da6
commit 06df133159
5 changed files with 169 additions and 15 deletions
+58 -9
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")
}
+78
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
+1
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)")