config: Wrap config library in an interface

This commit is contained in:
Fionera 2020-11-07 22:05:58 +01:00 committed by Nick Craig-Wood
parent 2be310cd6e
commit c95b580478
7 changed files with 652 additions and 523 deletions

View File

@ -3,19 +3,12 @@ package config
import ( import (
"bufio" "bufio"
"bytes"
"context" "context"
"crypto/rand"
"crypto/sha256"
"encoding/base64"
"encoding/json" "encoding/json"
"fmt" "fmt"
"io"
"io/ioutil"
"log" "log"
mathrand "math/rand" mathrand "math/rand"
"os" "os"
"os/exec"
"path/filepath" "path/filepath"
"regexp" "regexp"
"runtime" "runtime"
@ -23,11 +16,10 @@ import (
"strconv" "strconv"
"strings" "strings"
"time" "time"
"unicode/utf8"
"github.com/Unknwon/goconfig" "github.com/mitchellh/go-homedir"
homedir "github.com/mitchellh/go-homedir"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/rclone/rclone/fs" "github.com/rclone/rclone/fs"
"github.com/rclone/rclone/fs/accounting" "github.com/rclone/rclone/fs/accounting"
"github.com/rclone/rclone/fs/config/configmap" "github.com/rclone/rclone/fs/config/configmap"
@ -38,8 +30,6 @@ import (
"github.com/rclone/rclone/fs/rc" "github.com/rclone/rclone/fs/rc"
"github.com/rclone/rclone/lib/random" "github.com/rclone/rclone/lib/random"
"github.com/rclone/rclone/lib/terminal" "github.com/rclone/rclone/lib/terminal"
"golang.org/x/crypto/nacl/secretbox"
"golang.org/x/text/unicode/norm"
) )
const ( const (
@ -74,32 +64,56 @@ const (
ConfigAuthNoBrowser = "config_auth_no_browser" ConfigAuthNoBrowser = "config_auth_no_browser"
) )
// Storage defines an interface for loading and saving the config file.
type Storage interface {
// GetSectionList returns a slice of strings with names for all the
// sections
GetSectionList() []string
// HasSection returns true if section exists in the config file
HasSection(section string) bool
// DeleteSection removes the named section and all config from the
// config file
DeleteSection(section string)
// GetKeyList returns the keys in this section
GetKeyList(section string) []string
// GetValue returns the key in section or an error if not found
GetValue(section string, key string) (string, error)
// MustValue returns the key in section returning defaultValue if not set
MustValue(section string, key string, defaultValue ...string) string
// SetValue sets the value under key in section
SetValue(section string, key string, value string)
// DeleteKey removes the key under section
DeleteKey(section string, key string) bool
// Load the config from permanent storage
Load() error
// Save the config to permanent storage
Save() error
// Serialize the config into a string
Serialize() (string, error)
}
// Global // Global
var ( var (
// configFile is the global config data structure. Don't read it directly, use getConfigData() // configFile is the global config data structure. Don't read it directly, use Data
configFile *goconfig.ConfigFile Data Storage
// ConfigPath points to the config file
ConfigPath = makeConfigPath()
// CacheDir points to the cache directory. Users of this // CacheDir points to the cache directory. Users of this
// should make a subdirectory and use MkdirAll() to create it // should make a subdirectory and use MkdirAll() to create it
// and any parents. // and any parents.
CacheDir = makeCacheDir() CacheDir = makeCacheDir()
// Key to use for password en/decryption. // ConfigPath points to the config file
// When nil, no encryption will be used for saving. ConfigPath = makeConfigPath()
configKey []byte
// output of prompt for password
PasswordPromptOutput = os.Stderr
// If set to true, the configKey is obscured with obscure.Obscure and saved to a temp file when it is
// calculated from the password. The path of that temp file is then written to the environment variable
// `_RCLONE_CONFIG_KEY_FILE`. If `_RCLONE_CONFIG_KEY_FILE` is present, password prompt is skipped and `RCLONE_CONFIG_PASS` ignored.
// For security reasons, the temp file is deleted once the configKey is successfully loaded.
// This can be used to pass the configKey to a child process.
PassConfigKeyForDaemonization = false
// Password can be used to configure the random password generator // Password can be used to configure the random password generator
Password = random.Password Password = random.Password
@ -111,16 +125,8 @@ func init() {
fs.ConfigFileSet = SetValueAndSave fs.ConfigFileSet = SetValueAndSave
} }
func getConfigData() *goconfig.ConfigFile {
if configFile == nil {
LoadConfig(context.Background())
}
return configFile
}
// Return the path to the configuration file // Return the path to the configuration file
func makeConfigPath() string { func makeConfigPath() string {
// Use rclone.conf from rclone executable directory if already existing // Use rclone.conf from rclone executable directory if already existing
exe, err := os.Executable() exe, err := os.Executable()
if err == nil { if err == nil {
@ -217,11 +223,8 @@ func LoadConfig(ctx context.Context) {
_ = os.Setenv("RCLONE_CONFIG_DIR", filepath.Dir(ConfigPath)) _ = os.Setenv("RCLONE_CONFIG_DIR", filepath.Dir(ConfigPath))
// Load configuration file. // Load configuration file.
var err error if err := Data.Load(); err == ErrorConfigFileNotFound {
configFile, err = loadConfigFile()
if err == errorConfigFileNotFound {
fs.Logf(nil, "Config file %q not found - using defaults", ConfigPath) fs.Logf(nil, "Config file %q not found - using defaults", ConfigPath)
configFile, _ = goconfig.LoadFromReader(&bytes.Buffer{})
} else if err != nil { } else if err != nil {
log.Fatalf("Failed to load config file %q: %v", ConfigPath, err) log.Fatalf("Failed to load config file %q: %v", ConfigPath, err)
} else { } else {
@ -238,365 +241,8 @@ func LoadConfig(ctx context.Context) {
accounting.StartLimitTPS(ctx) accounting.StartLimitTPS(ctx)
} }
var errorConfigFileNotFound = errors.New("config file not found") // ErrorConfigFileNotFound is returned when the config file is not found
var ErrorConfigFileNotFound = errors.New("config file not found")
// loadConfigFile will load a config file, and
// automatically decrypt it.
func loadConfigFile() (*goconfig.ConfigFile, error) {
ctx := context.Background()
ci := fs.GetConfig(ctx)
var usingPasswordCommand bool
b, err := ioutil.ReadFile(ConfigPath)
if err != nil {
if os.IsNotExist(err) {
return nil, errorConfigFileNotFound
}
return nil, err
}
// Find first non-empty line
r := bufio.NewReader(bytes.NewBuffer(b))
for {
line, _, err := r.ReadLine()
if err != nil {
if err == io.EOF {
return goconfig.LoadFromReader(bytes.NewBuffer(b))
}
return nil, err
}
l := strings.TrimSpace(string(line))
if len(l) == 0 || strings.HasPrefix(l, ";") || strings.HasPrefix(l, "#") {
continue
}
// First non-empty or non-comment must be ENCRYPT_V0
if l == "RCLONE_ENCRYPT_V0:" {
break
}
if strings.HasPrefix(l, "RCLONE_ENCRYPT_V") {
return nil, errors.New("unsupported configuration encryption - update rclone for support")
}
return goconfig.LoadFromReader(bytes.NewBuffer(b))
}
if len(configKey) == 0 {
if len(ci.PasswordCommand) != 0 {
var stdout bytes.Buffer
var stderr bytes.Buffer
cmd := exec.Command(ci.PasswordCommand[0], ci.PasswordCommand[1:]...)
cmd.Stdout = &stdout
cmd.Stderr = &stderr
cmd.Stdin = os.Stdin
if err := cmd.Run(); err != nil {
// One does not always get the stderr returned in the wrapped error.
fs.Errorf(nil, "Using --password-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)
if err != nil {
return nil, errors.Wrap(err, "failed to load base64 encoded data")
}
if len(box) < 24+secretbox.Overhead {
return nil, errors.New("Configuration data too short")
}
var out []byte
for {
if envKeyFile := os.Getenv("_RCLONE_CONFIG_KEY_FILE"); len(envKeyFile) > 0 {
fs.Debugf(nil, "attempting to obtain configKey from temp file %s", envKeyFile)
obscuredKey, err := ioutil.ReadFile(envKeyFile)
if err != nil {
errRemove := os.Remove(envKeyFile)
if errRemove != nil {
log.Fatalf("unable to read obscured config key and unable to delete the temp file: %v", err)
}
log.Fatalf("unable to read obscured config key: %v", err)
}
errRemove := os.Remove(envKeyFile)
if errRemove != nil {
log.Fatalf("unable to delete temp file with configKey: %v", err)
}
configKey = []byte(obscure.MustReveal(string(obscuredKey)))
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 !ci.AskPassword {
return nil, errors.New("unable to decrypt configuration and not allowed to ask for password - set RCLONE_CONFIG_PASS to your configuration password")
}
getConfigPassword("Enter configuration password:")
}
}
// Nonce is first 24 bytes of the ciphertext
var nonce [24]byte
copy(nonce[:], box[:24])
var key [32]byte
copy(key[:], configKey[:32])
// Attempt to decrypt
var ok bool
out, ok = secretbox.Open(nil, box[24:], &nonce, &key)
if ok {
break
}
// Retry
fs.Errorf(nil, "Couldn't decrypt configuration, most likely wrong password.")
configKey = nil
}
return goconfig.LoadFromReader(bytes.NewBuffer(out))
}
// checkPassword normalises and validates the password
func checkPassword(password string) (string, error) {
if !utf8.ValidString(password) {
return "", errors.New("password contains invalid utf8 characters")
}
// Check for leading/trailing whitespace
trimmedPassword := strings.TrimSpace(password)
// Warn user if password has leading+trailing whitespace
if len(password) != len(trimmedPassword) {
_, _ = fmt.Fprintln(os.Stderr, "Your password contains leading/trailing whitespace - in previous versions of rclone this was stripped")
}
// Normalize to reduce weird variations.
password = norm.NFKC.String(password)
if len(password) == 0 || len(trimmedPassword) == 0 {
return "", errors.New("no characters in password")
}
return password, nil
}
// GetPassword asks the user for a password with the prompt given.
func GetPassword(prompt string) string {
_, _ = fmt.Fprintln(PasswordPromptOutput, prompt)
for {
_, _ = fmt.Fprint(PasswordPromptOutput, "password:")
password := ReadPassword()
password, err := checkPassword(password)
if err == nil {
return password
}
_, _ = fmt.Fprintf(os.Stderr, "Bad password: %v\n", err)
}
}
// ChangePassword will query the user twice for the named password. If
// the same password is entered it is returned.
func ChangePassword(name string) string {
for {
a := GetPassword(fmt.Sprintf("Enter %s password:", name))
b := GetPassword(fmt.Sprintf("Confirm %s password:", name))
if a == b {
return a
}
fmt.Println("Passwords do not match!")
}
}
// getConfigPassword will query the user for a password the
// first time it is required.
func getConfigPassword(q string) {
if len(configKey) != 0 {
return
}
for {
password := GetPassword(q)
err := setConfigPassword(password)
if err == nil {
return
}
_, _ = fmt.Fprintln(os.Stderr, "Error:", err)
}
}
// setConfigPassword will set the configKey to the hash of
// the password. If the length of the password is
// zero after trimming+normalization, an error is returned.
func setConfigPassword(password string) error {
password, err := checkPassword(password)
if err != nil {
return err
}
// Create SHA256 has of the password
sha := sha256.New()
_, err = sha.Write([]byte("[" + password + "][rclone-config]"))
if err != nil {
return err
}
configKey = sha.Sum(nil)
if PassConfigKeyForDaemonization {
tempFile, err := ioutil.TempFile("", "rclone")
if err != nil {
log.Fatalf("cannot create temp file to store configKey: %v", err)
}
_, err = tempFile.WriteString(obscure.MustObscure(string(configKey)))
if err != nil {
errRemove := os.Remove(tempFile.Name())
if errRemove != nil {
log.Fatalf("error writing configKey to temp file and also error deleting it: %v", err)
}
log.Fatalf("error writing configKey to temp file: %v", err)
}
err = tempFile.Close()
if err != nil {
errRemove := os.Remove(tempFile.Name())
if errRemove != nil {
log.Fatalf("error closing temp file with configKey and also error deleting it: %v", err)
}
log.Fatalf("error closing temp file with configKey: %v", err)
}
fs.Debugf(nil, "saving configKey to temp file")
err = os.Setenv("_RCLONE_CONFIG_KEY_FILE", tempFile.Name())
if err != nil {
errRemove := os.Remove(tempFile.Name())
if errRemove != nil {
log.Fatalf("unable to set environment variable _RCLONE_CONFIG_KEY_FILE and unable to delete the temp file: %v", err)
}
log.Fatalf("unable to set environment variable _RCLONE_CONFIG_KEY_FILE: %v", err)
}
}
return nil
}
// changeConfigPassword will query the user twice
// for a password. If the same password is entered
// twice the key is updated.
func changeConfigPassword() {
err := setConfigPassword(ChangePassword("NEW configuration"))
if err != nil {
fmt.Printf("Failed to set config password: %v\n", err)
return
}
}
// saveConfig saves configuration file.
// if configKey has been set, the file will be encrypted.
func saveConfig() error {
dir, name := filepath.Split(ConfigPath)
err := os.MkdirAll(dir, os.ModePerm)
if err != nil {
return errors.Wrap(err, "failed to create config directory")
}
f, err := ioutil.TempFile(dir, name)
if err != nil {
return errors.Errorf("Failed to create temp file for new config: %v", err)
}
defer func() {
if err := os.Remove(f.Name()); err != nil && !os.IsNotExist(err) {
fs.Errorf(nil, "Failed to remove temp config file: %v", err)
}
}()
var buf bytes.Buffer
err = goconfig.SaveConfigData(getConfigData(), &buf)
if err != nil {
return errors.Errorf("Failed to save config file: %v", err)
}
if len(configKey) == 0 {
if _, err := buf.WriteTo(f); err != nil {
return errors.Errorf("Failed to write temp config file: %v", err)
}
} else {
_, _ = fmt.Fprintln(f, "# Encrypted rclone configuration File")
_, _ = fmt.Fprintln(f, "")
_, _ = fmt.Fprintln(f, "RCLONE_ENCRYPT_V0:")
// Generate new nonce and write it to the start of the ciphertext
var nonce [24]byte
n, _ := rand.Read(nonce[:])
if n != 24 {
return errors.Errorf("nonce short read: %d", n)
}
enc := base64.NewEncoder(base64.StdEncoding, f)
_, err = enc.Write(nonce[:])
if err != nil {
return errors.Errorf("Failed to write temp config file: %v", err)
}
var key [32]byte
copy(key[:], configKey[:32])
b := secretbox.Seal(nil, buf.Bytes(), &nonce, &key)
_, err = enc.Write(b)
if err != nil {
return errors.Errorf("Failed to write temp config file: %v", err)
}
_ = enc.Close()
}
_ = f.Sync()
err = f.Close()
if err != nil {
return errors.Errorf("Failed to close config file: %v", err)
}
var fileMode os.FileMode = 0600
info, err := os.Stat(ConfigPath)
if err != nil {
fs.Debugf(nil, "Using default permissions for config file: %v", fileMode)
} else if info.Mode() != fileMode {
fs.Debugf(nil, "Keeping previous permissions for config file: %v", info.Mode())
fileMode = info.Mode()
}
attemptCopyGroup(ConfigPath, f.Name())
err = os.Chmod(f.Name(), fileMode)
if err != nil {
fs.Errorf(nil, "Failed to set permissions on config file: %v", err)
}
if err = os.Rename(ConfigPath, ConfigPath+".old"); err != nil && !os.IsNotExist(err) {
return errors.Errorf("Failed to move previous config to backup location: %v", err)
}
if err = os.Rename(f.Name(), ConfigPath); err != nil {
return errors.Errorf("Failed to move newly written config from %s to final location: %v", f.Name(), err)
}
if err := os.Remove(ConfigPath + ".old"); err != nil && !os.IsNotExist(err) {
fs.Errorf(nil, "Failed to remove backup config file: %v", err)
}
return nil
}
// SaveConfig calling function which saves configuration file. // SaveConfig calling function which saves configuration file.
// if saveConfig returns error trying again after sleep. // if saveConfig returns error trying again after sleep.
@ -605,7 +251,7 @@ func SaveConfig() {
ci := fs.GetConfig(ctx) ci := fs.GetConfig(ctx)
var err error var err error
for i := 0; i < ci.LowLevelRetries+1; i++ { for i := 0; i < ci.LowLevelRetries+1; i++ {
if err = saveConfig(); err == nil { if err = Data.Save(); err == nil {
return return
} }
waitingTimeMs := mathrand.Intn(1000) waitingTimeMs := mathrand.Intn(1000)
@ -619,26 +265,22 @@ func SaveConfig() {
// SetValueAndSave sets the key to the value and saves just that // SetValueAndSave sets the key to the value and saves just that
// value in the config file. It loads the old config file in from // value in the config file. It loads the old config file in from
// disk first and overwrites the given value only. // disk first and overwrites the given value only.
func SetValueAndSave(name, key, value string) (err error) { func SetValueAndSave(name, key, value string) error {
// Set the value in config in case we fail to reload it // Set the value in config in case we fail to reload it
getConfigData().SetValue(name, key, value) Data.SetValue(name, key, value)
// Reload the config file // Reload the config file
reloadedConfigFile, err := loadConfigFile() err := Data.Load()
if err == errorConfigFileNotFound { if err == ErrorConfigFileNotFound {
// Config file not written yet so ignore reload // Config file not written yet so ignore reload
return nil return nil
} else if err != nil { } else if err != nil {
return err return err
} }
_, err = reloadedConfigFile.GetSection(name) if !Data.HasSection(name) {
if err != nil {
// Section doesn't exist yet so ignore reload // Section doesn't exist yet so ignore reload
return nil return nil
} }
// Update the config file with the reloaded version
configFile = reloadedConfigFile
// Set the value in the reloaded version
reloadedConfigFile.SetValue(name, key, value)
// Save it again // Save it again
SaveConfig() SaveConfig()
return nil return nil
@ -648,16 +290,15 @@ func SetValueAndSave(name, key, value string) (err error) {
// an error if the config file was not found or that value couldn't be // an error if the config file was not found or that value couldn't be
// read. // read.
func FileGetFresh(section, key string) (value string, err error) { func FileGetFresh(section, key string) (value string, err error) {
reloadedConfigFile, err := loadConfigFile() if err := Data.Load(); err != nil {
if err != nil {
return "", err return "", err
} }
return reloadedConfigFile.GetValue(section, key) return Data.GetValue(section, key)
} }
// ShowRemotes shows an overview of the config file // ShowRemotes shows an overview of the config file
func ShowRemotes() { func ShowRemotes() {
remotes := getConfigData().GetSectionList() remotes := Data.GetSectionList()
if len(remotes) == 0 { if len(remotes) == 0 {
return return
} }
@ -671,7 +312,7 @@ func ShowRemotes() {
// ChooseRemote chooses a remote name // ChooseRemote chooses a remote name
func ChooseRemote() string { func ChooseRemote() string {
remotes := getConfigData().GetSectionList() remotes := Data.GetSectionList()
sort.Strings(remotes) sort.Strings(remotes)
return Choose("remote", remotes, nil, false) return Choose("remote", remotes, nil, false)
} }
@ -855,7 +496,7 @@ func ShowRemote(name string) {
fmt.Printf("--------------------\n") fmt.Printf("--------------------\n")
fmt.Printf("[%s]\n", name) fmt.Printf("[%s]\n", name)
fs := MustFindByName(name) fs := MustFindByName(name)
for _, key := range getConfigData().GetKeyList(name) { for _, key := range Data.GetKeyList(name) {
isPassword := false isPassword := false
for _, option := range fs.Options { for _, option := range fs.Options {
if option.Name == key && option.IsPassword { if option.Name == key && option.IsPassword {
@ -882,7 +523,7 @@ func OkRemote(name string) bool {
case 'e': case 'e':
return false return false
case 'd': case 'd':
getConfigData().DeleteSection(name) Data.DeleteSection(name)
return true return true
default: default:
fs.Errorf(nil, "Bad choice %c", i) fs.Errorf(nil, "Bad choice %c", i)
@ -942,7 +583,7 @@ func matchProvider(providerConfig, provider string) bool {
// ChooseOption asks the user to choose an option // ChooseOption asks the user to choose an option
func ChooseOption(o *fs.Option, name string) string { func ChooseOption(o *fs.Option, name string) string {
var subProvider = getConfigData().MustValue(name, fs.ConfigProvider, "") var subProvider = Data.MustValue(name, fs.ConfigProvider, "")
fmt.Println(o.Help) fmt.Println(o.Help)
if o.IsPassword { if o.IsPassword {
actions := []string{"yYes type in my own password", "gGenerate random password"} actions := []string{"yYes type in my own password", "gGenerate random password"}
@ -1079,7 +720,7 @@ func UpdateRemote(ctx context.Context, name string, keyValues rc.Params, doObscu
} }
} }
} }
getConfigData().SetValue(name, k, vStr) Data.SetValue(name, k, vStr)
} }
RemoteConfig(ctx, name) RemoteConfig(ctx, name)
SaveConfig() SaveConfig()
@ -1095,9 +736,9 @@ func CreateRemote(ctx context.Context, name string, provider string, keyValues r
return err return err
} }
// Delete the old config if it exists // Delete the old config if it exists
getConfigData().DeleteSection(name) Data.DeleteSection(name)
// Set the type // Set the type
getConfigData().SetValue(name, "type", provider) Data.SetValue(name, "type", provider)
// Set the remaining values // Set the remaining values
return UpdateRemote(ctx, name, keyValues, doObscure, noObscure) return UpdateRemote(ctx, name, keyValues, doObscure, noObscure)
} }
@ -1152,12 +793,11 @@ func NewRemoteName() (name string) {
for { for {
fmt.Printf("name> ") fmt.Printf("name> ")
name = ReadLine() name = ReadLine()
_, err := getConfigData().GetSection(name) if Data.HasSection(name) {
if err == nil {
fmt.Printf("Remote %q already exists.\n", name) fmt.Printf("Remote %q already exists.\n", name)
continue continue
} }
err = fspath.CheckConfigName(name) err := fspath.CheckConfigName(name)
switch { switch {
case name == "": case name == "":
fmt.Printf("Can't use empty name.\n") fmt.Printf("Can't use empty name.\n")
@ -1192,7 +832,7 @@ func editOptions(ri *fs.RegInfo, name string, isNew bool) {
if option.Advanced != advanced { if option.Advanced != advanced {
continue continue
} }
subProvider := getConfigData().MustValue(name, fs.ConfigProvider, "") subProvider := Data.MustValue(name, fs.ConfigProvider, "")
if matchProvider(option.Provider, subProvider) && isVisible { if matchProvider(option.Provider, subProvider) && isVisible {
if !isNew { if !isNew {
fmt.Printf("Value %q = %q\n", option.Name, FileGet(name, option.Name)) fmt.Printf("Value %q = %q\n", option.Name, FileGet(name, option.Name))
@ -1225,7 +865,7 @@ func NewRemote(ctx context.Context, name string) {
} }
break break
} }
getConfigData().SetValue(name, "type", newType) Data.SetValue(name, "type", newType)
editOptions(ri, name, true) editOptions(ri, name, true)
RemoteConfig(ctx, name) RemoteConfig(ctx, name)
@ -1252,7 +892,7 @@ func EditRemote(ctx context.Context, ri *fs.RegInfo, name string) {
// DeleteRemote gets the user to delete a remote // DeleteRemote gets the user to delete a remote
func DeleteRemote(name string) { func DeleteRemote(name string) {
getConfigData().DeleteSection(name) Data.DeleteSection(name)
SaveConfig() SaveConfig()
} }
@ -1261,9 +901,9 @@ func DeleteRemote(name string) {
func copyRemote(name string) string { func copyRemote(name string) string {
newName := NewRemoteName() newName := NewRemoteName()
// Copy the keys // Copy the keys
for _, key := range getConfigData().GetKeyList(name) { for _, key := range Data.GetKeyList(name) {
value := getConfigData().MustValue(name, key, "") value := Data.MustValue(name, key, "")
getConfigData().SetValue(newName, key, value) Data.SetValue(newName, key, value)
} }
return newName return newName
} }
@ -1273,7 +913,7 @@ func RenameRemote(name string) {
fmt.Printf("Enter new name for %q remote.\n", name) fmt.Printf("Enter new name for %q remote.\n", name)
newName := copyRemote(name) newName := copyRemote(name)
if name != newName { if name != newName {
getConfigData().DeleteSection(name) Data.DeleteSection(name)
SaveConfig() SaveConfig()
} }
} }
@ -1297,11 +937,10 @@ func ShowConfigLocation() {
// ShowConfig prints the (unencrypted) config options // ShowConfig prints the (unencrypted) config options
func ShowConfig() { func ShowConfig() {
var buf bytes.Buffer str, err := Data.Serialize()
if err := goconfig.SaveConfigData(getConfigData(), &buf); err != nil { if err != nil {
log.Fatalf("Failed to serialize config: %v", err) log.Fatalf("Failed to serialize config: %v", err)
} }
str := buf.String()
if str == "" { if str == "" {
str = "; empty config\n" str = "; empty config\n"
} }
@ -1311,7 +950,7 @@ func ShowConfig() {
// EditConfig edits the config file interactively // EditConfig edits the config file interactively
func EditConfig(ctx context.Context) { func EditConfig(ctx context.Context) {
for { for {
haveRemotes := len(getConfigData().GetSectionList()) != 0 haveRemotes := len(Data.GetSectionList()) != 0
what := []string{"eEdit existing remote", "nNew remote", "dDelete remote", "rRename remote", "cCopy remote", "sSet configuration password", "qQuit config"} what := []string{"eEdit existing remote", "nNew remote", "dDelete remote", "rRename remote", "cCopy remote", "sSet configuration password", "qQuit config"}
if haveRemotes { if haveRemotes {
fmt.Printf("Current remotes:\n\n") fmt.Printf("Current remotes:\n\n")
@ -1345,44 +984,6 @@ func EditConfig(ctx context.Context) {
} }
} }
// SetPassword will allow the user to modify the current
// configuration encryption settings.
func SetPassword() {
for {
if len(configKey) > 0 {
fmt.Println("Your configuration is encrypted.")
what := []string{"cChange Password", "uUnencrypt configuration", "qQuit to main menu"}
switch i := Command(what); i {
case 'c':
changeConfigPassword()
SaveConfig()
fmt.Println("Password changed")
continue
case 'u':
configKey = nil
SaveConfig()
continue
case 'q':
return
}
} else {
fmt.Println("Your configuration is not encrypted.")
fmt.Println("If you add a password, you will protect your login information to cloud services.")
what := []string{"aAdd Password", "qQuit to main menu"}
switch i := Command(what); i {
case 'a':
changeConfigPassword()
SaveConfig()
fmt.Println("Password set")
continue
case 'q':
return
}
}
}
}
// Authorize is for remote authorization of headless machines. // Authorize is for remote authorization of headless machines.
// //
// It expects 1 or 3 arguments // It expects 1 or 3 arguments
@ -1408,14 +1009,14 @@ func Authorize(ctx context.Context, args []string, noAutoBrowser bool) {
defer DeleteRemote(name) defer DeleteRemote(name)
// Indicate that we are running rclone authorize // Indicate that we are running rclone authorize
getConfigData().SetValue(name, ConfigAuthorize, "true") Data.SetValue(name, ConfigAuthorize, "true")
if noAutoBrowser { if noAutoBrowser {
getConfigData().SetValue(name, ConfigAuthNoBrowser, "true") Data.SetValue(name, ConfigAuthNoBrowser, "true")
} }
if len(args) == 3 { if len(args) == 3 {
getConfigData().SetValue(name, ConfigClientID, args[1]) Data.SetValue(name, ConfigClientID, args[1])
getConfigData().SetValue(name, ConfigClientSecret, args[2]) Data.SetValue(name, ConfigClientSecret, args[2])
} }
m := fs.ConfigMap(f, name) m := fs.ConfigMap(f, name)
@ -1425,7 +1026,7 @@ func Authorize(ctx context.Context, args []string, noAutoBrowser bool) {
// FileGetFlag gets the config key under section returning the // FileGetFlag gets the config key under section returning the
// the value and true if found and or ("", false) otherwise // the value and true if found and or ("", false) otherwise
func FileGetFlag(section, key string) (string, bool) { func FileGetFlag(section, key string) (string, bool) {
newValue, err := getConfigData().GetValue(section, key) newValue, err := Data.GetValue(section, key)
return newValue, err == nil return newValue, err == nil
} }
@ -1439,14 +1040,14 @@ func FileGet(section, key string, defaultVal ...string) string {
if found { if found {
defaultVal = []string{newValue} defaultVal = []string{newValue}
} }
return getConfigData().MustValue(section, key, defaultVal...) return Data.MustValue(section, key, defaultVal...)
} }
// FileSet sets the key in section to value. It doesn't save // FileSet sets the key in section to value. It doesn't save
// the config file. // the config file.
func FileSet(section, key, value string) { func FileSet(section, key, value string) {
if value != "" { if value != "" {
getConfigData().SetValue(section, key, value) Data.SetValue(section, key, value)
} else { } else {
FileDeleteKey(section, key) FileDeleteKey(section, key)
} }
@ -1456,25 +1057,20 @@ func FileSet(section, key, value string) {
// It returns true if the key was deleted, // It returns true if the key was deleted,
// or returns false if the section or key didn't exist. // or returns false if the section or key didn't exist.
func FileDeleteKey(section, key string) bool { func FileDeleteKey(section, key string) bool {
return getConfigData().DeleteKey(section, key) return Data.DeleteKey(section, key)
} }
var matchEnv = regexp.MustCompile(`^RCLONE_CONFIG_(.*?)_TYPE=.*$`) var matchEnv = regexp.MustCompile(`^RCLONE_CONFIG_(.*?)_TYPE=.*$`)
// FileRefresh ensures the latest configFile is loaded from disk // FileRefresh ensures the latest configFile is loaded from disk
func FileRefresh() error { func FileRefresh() error {
reloadedConfigFile, err := loadConfigFile() return Data.Load()
if err != nil {
return err
}
configFile = reloadedConfigFile
return nil
} }
// FileSections returns the sections in the config file // FileSections returns the sections in the config file
// including any defined by environment variables. // including any defined by environment variables.
func FileSections() []string { func FileSections() []string {
sections := getConfigData().GetSectionList() sections := Data.GetSectionList()
for _, item := range os.Environ() { for _, item := range os.Environ() {
matches := matchEnv.FindStringSubmatch(item) matches := matchEnv.FindStringSubmatch(item)
if len(matches) == 2 { if len(matches) == 2 {
@ -1487,7 +1083,7 @@ func FileSections() []string {
// DumpRcRemote dumps the config for a single remote // DumpRcRemote dumps the config for a single remote
func DumpRcRemote(name string) (dump rc.Params) { func DumpRcRemote(name string) (dump rc.Params) {
params := rc.Params{} params := rc.Params{}
for _, key := range getConfigData().GetKeyList(name) { for _, key := range Data.GetKeyList(name) {
params[key] = FileGet(name, key) params[key] = FileGet(name, key)
} }
return params return params
@ -1497,7 +1093,7 @@ func DumpRcRemote(name string) (dump rc.Params) {
// for the rc // for the rc
func DumpRcBlob() (dump rc.Params) { func DumpRcBlob() (dump rc.Params) {
dump = rc.Params{} dump = rc.Params{}
for _, name := range getConfigData().GetSectionList() { for _, name := range Data.GetSectionList() {
dump[name] = DumpRcRemote(name) dump[name] = DumpRcRemote(name)
} }
return dump return dump

View File

@ -31,16 +31,16 @@ func testConfigFile(t *testing.T, configFileName string) func() {
oldOsStdout := os.Stdout oldOsStdout := os.Stdout
oldConfigPath := ConfigPath oldConfigPath := ConfigPath
oldConfig := *ci oldConfig := *ci
oldConfigFile := configFile oldConfigFile := Data
oldReadLine := ReadLine oldReadLine := ReadLine
oldPassword := Password oldPassword := Password
os.Stdout = nil os.Stdout = nil
ConfigPath = path ConfigPath = path
ci = &fs.ConfigInfo{} ci = &fs.ConfigInfo{}
configFile = nil Data = nil
LoadConfig(ctx) LoadConfig(ctx)
assert.Equal(t, []string{}, getConfigData().GetSectionList()) assert.Equal(t, []string{}, Data.GetSectionList())
// Fake a remote // Fake a remote
fs.Register(&fs.RegInfo{ fs.Register(&fs.RegInfo{
@ -69,7 +69,7 @@ func testConfigFile(t *testing.T, configFileName string) func() {
ReadLine = oldReadLine ReadLine = oldReadLine
Password = oldPassword Password = oldPassword
*ci = oldConfig *ci = oldConfig
configFile = oldConfigFile Data = oldConfigFile
_ = os.Unsetenv("_RCLONE_CONFIG_KEY_FILE") _ = os.Unsetenv("_RCLONE_CONFIG_KEY_FILE")
_ = os.Unsetenv("RCLONE_CONFIG_PASS") _ = os.Unsetenv("RCLONE_CONFIG_PASS")
@ -101,7 +101,7 @@ func TestCRUD(t *testing.T) {
}) })
NewRemote(ctx, "test") NewRemote(ctx, "test")
assert.Equal(t, []string{"test"}, configFile.GetSectionList()) assert.Equal(t, []string{"test"}, Data.GetSectionList())
assert.Equal(t, "config_test_remote", FileGet("test", "type")) assert.Equal(t, "config_test_remote", FileGet("test", "type"))
assert.Equal(t, "true", FileGet("test", "bool")) assert.Equal(t, "true", FileGet("test", "bool"))
assert.Equal(t, "secret", obscure.MustReveal(FileGet("test", "pass"))) assert.Equal(t, "secret", obscure.MustReveal(FileGet("test", "pass")))
@ -114,14 +114,14 @@ func TestCRUD(t *testing.T) {
}) })
RenameRemote("test") RenameRemote("test")
assert.Equal(t, []string{"asdf"}, configFile.GetSectionList()) assert.Equal(t, []string{"asdf"}, Data.GetSectionList())
assert.Equal(t, "config_test_remote", FileGet("asdf", "type")) assert.Equal(t, "config_test_remote", FileGet("asdf", "type"))
assert.Equal(t, "true", FileGet("asdf", "bool")) assert.Equal(t, "true", FileGet("asdf", "bool"))
assert.Equal(t, "secret", obscure.MustReveal(FileGet("asdf", "pass"))) assert.Equal(t, "secret", obscure.MustReveal(FileGet("asdf", "pass")))
// delete remote // delete remote
DeleteRemote("asdf") DeleteRemote("asdf")
assert.Equal(t, []string{}, configFile.GetSectionList()) assert.Equal(t, []string{}, Data.GetSectionList())
} }
func TestChooseOption(t *testing.T) { func TestChooseOption(t *testing.T) {
@ -198,7 +198,7 @@ func TestCreateUpdatePasswordRemote(t *testing.T) {
"pass": "potato", "pass": "potato",
}, doObscure, noObscure)) }, doObscure, noObscure))
assert.Equal(t, []string{"test2"}, configFile.GetSectionList()) assert.Equal(t, []string{"test2"}, Data.GetSectionList())
assert.Equal(t, "config_test_remote", FileGet("test2", "type")) assert.Equal(t, "config_test_remote", FileGet("test2", "type"))
assert.Equal(t, "true", FileGet("test2", "bool")) assert.Equal(t, "true", FileGet("test2", "bool"))
gotPw := FileGet("test2", "pass") gotPw := FileGet("test2", "pass")
@ -214,7 +214,7 @@ func TestCreateUpdatePasswordRemote(t *testing.T) {
"spare": "spare", "spare": "spare",
}, doObscure, noObscure)) }, doObscure, noObscure))
assert.Equal(t, []string{"test2"}, configFile.GetSectionList()) assert.Equal(t, []string{"test2"}, Data.GetSectionList())
assert.Equal(t, "config_test_remote", FileGet("test2", "type")) assert.Equal(t, "config_test_remote", FileGet("test2", "type"))
assert.Equal(t, "false", FileGet("test2", "bool")) assert.Equal(t, "false", FileGet("test2", "bool"))
gotPw = FileGet("test2", "pass") gotPw = FileGet("test2", "pass")
@ -227,7 +227,7 @@ func TestCreateUpdatePasswordRemote(t *testing.T) {
"pass": "potato3", "pass": "potato3",
})) }))
assert.Equal(t, []string{"test2"}, configFile.GetSectionList()) assert.Equal(t, []string{"test2"}, Data.GetSectionList())
assert.Equal(t, "config_test_remote", FileGet("test2", "type")) assert.Equal(t, "config_test_remote", FileGet("test2", "type"))
assert.Equal(t, "false", FileGet("test2", "bool")) assert.Equal(t, "false", FileGet("test2", "bool"))
assert.Equal(t, "potato3", obscure.MustReveal(FileGet("test2", "pass"))) assert.Equal(t, "potato3", obscure.MustReveal(FileGet("test2", "pass")))
@ -260,15 +260,15 @@ func TestConfigLoad(t *testing.T) {
ConfigPath = oldConfigPath ConfigPath = oldConfigPath
}() }()
configKey = nil // reset password configKey = nil // reset password
c, err := loadConfigFile() err := Data.Load()
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
sections := c.GetSectionList() sections := Data.GetSectionList()
var expect = []string{"RCLONE_ENCRYPT_V0", "nounc", "unc"} var expect = []string{"RCLONE_ENCRYPT_V0", "nounc", "unc"}
assert.Equal(t, expect, sections) assert.Equal(t, expect, sections)
keys := c.GetKeyList("nounc") keys := Data.GetKeyList("nounc")
expect = []string{"type", "nounc"} expect = []string{"type", "nounc"}
assert.Equal(t, expect, keys) assert.Equal(t, expect, keys)
} }
@ -285,13 +285,13 @@ func TestConfigLoadEncrypted(t *testing.T) {
// Set correct password // Set correct password
err = setConfigPassword("asdf") err = setConfigPassword("asdf")
require.NoError(t, err) require.NoError(t, err)
c, err := loadConfigFile() err = Data.Load()
require.NoError(t, err) require.NoError(t, err)
sections := c.GetSectionList() sections := Data.GetSectionList()
var expect = []string{"nounc", "unc"} var expect = []string{"nounc", "unc"}
assert.Equal(t, expect, sections) assert.Equal(t, expect, sections)
keys := c.GetKeyList("nounc") keys := Data.GetKeyList("nounc")
expect = []string{"type", "nounc"} expect = []string{"type", "nounc"}
assert.Equal(t, expect, keys) assert.Equal(t, expect, keys)
} }
@ -313,14 +313,14 @@ func TestConfigLoadEncryptedWithValidPassCommand(t *testing.T) {
configKey = nil // reset password configKey = nil // reset password
c, err := loadConfigFile() err := Data.Load()
require.NoError(t, err) require.NoError(t, err)
sections := c.GetSectionList() sections := Data.GetSectionList()
var expect = []string{"nounc", "unc"} var expect = []string{"nounc", "unc"}
assert.Equal(t, expect, sections) assert.Equal(t, expect, sections)
keys := c.GetKeyList("nounc") keys := Data.GetKeyList("nounc")
expect = []string{"type", "nounc"} expect = []string{"type", "nounc"}
assert.Equal(t, expect, keys) assert.Equal(t, expect, keys)
} }
@ -342,7 +342,7 @@ func TestConfigLoadEncryptedWithInvalidPassCommand(t *testing.T) {
configKey = nil // reset password configKey = nil // reset password
_, err := loadConfigFile() err := Data.Load()
require.Error(t, err) require.Error(t, err)
assert.Contains(t, err.Error(), "using --password-command derived password") assert.Contains(t, err.Error(), "using --password-command derived password")
} }
@ -354,24 +354,23 @@ func TestConfigLoadEncryptedFailures(t *testing.T) {
oldConfigPath := ConfigPath oldConfigPath := ConfigPath
ConfigPath = "./testdata/enc-short.conf" ConfigPath = "./testdata/enc-short.conf"
defer func() { ConfigPath = oldConfigPath }() defer func() { ConfigPath = oldConfigPath }()
_, err = loadConfigFile() err = Data.Load()
require.Error(t, err) require.Error(t, err)
// This file contains invalid base64 characters. // This file contains invalid base64 characters.
ConfigPath = "./testdata/enc-invalid.conf" ConfigPath = "./testdata/enc-invalid.conf"
_, err = loadConfigFile() err = Data.Load()
require.Error(t, err) require.Error(t, err)
// This file contains invalid base64 characters. // This file contains invalid base64 characters.
ConfigPath = "./testdata/enc-too-new.conf" ConfigPath = "./testdata/enc-too-new.conf"
_, err = loadConfigFile() err = Data.Load()
require.Error(t, err) require.Error(t, err)
// This file does not exist. // This file does not exist.
ConfigPath = "./testdata/filenotfound.conf" ConfigPath = "./testdata/filenotfound.conf"
c, err := loadConfigFile() err = Data.Load()
assert.Equal(t, errorConfigFileNotFound, err) assert.Equal(t, ErrorConfigFileNotFound, err)
assert.Nil(t, c)
} }
func TestPassword(t *testing.T) { func TestPassword(t *testing.T) {
@ -448,8 +447,8 @@ func TestFileRefresh(t *testing.T) {
err = ioutil.WriteFile(ConfigPath, b, 0644) err = ioutil.WriteFile(ConfigPath, b, 0644)
assert.NoError(t, err) assert.NoError(t, err)
assert.NotEqual(t, []string{"refreshed_test"}, configFile.GetSectionList()) assert.NotEqual(t, []string{"refreshed_test"}, Data.GetSectionList())
err = FileRefresh() err = FileRefresh()
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, []string{"refreshed_test"}, configFile.GetSectionList()) assert.Equal(t, []string{"refreshed_test"}, Data.GetSectionList())
} }

View File

@ -0,0 +1,146 @@
package configfile
import (
"bytes"
"io/ioutil"
"os"
"path/filepath"
"github.com/Unknwon/goconfig"
"github.com/pkg/errors"
"github.com/rclone/rclone/fs"
"github.com/rclone/rclone/fs/config"
)
// GoConfig implements config file saving using a simple ini based
// format.
type GoConfig struct {
*goconfig.ConfigFile
}
// Load the config from permanent storage
func (gc *GoConfig) Load() error {
b, err := os.Open(config.ConfigPath)
if err != nil {
if os.IsNotExist(err) {
return config.ErrorConfigFileNotFound
}
return err
}
defer fs.CheckClose(b, &err)
cryptReader, err := config.Decrypt(b)
if err != nil {
return err
}
if gc.ConfigFile == nil {
c, err := goconfig.LoadFromReader(cryptReader)
if err != nil {
return err
}
gc.ConfigFile = c
} else {
return gc.ReloadData(cryptReader)
}
return nil
}
// Save the config to permanent storage
func (gc *GoConfig) Save() error {
dir, name := filepath.Split(config.ConfigPath)
err := os.MkdirAll(dir, os.ModePerm)
if err != nil {
return errors.Wrap(err, "failed to create config directory")
}
f, err := ioutil.TempFile(dir, name)
if err != nil {
return errors.Errorf("Failed to create temp file for new config: %v", err)
}
defer func() {
_ = f.Close()
if err := os.Remove(f.Name()); err != nil && !os.IsNotExist(err) {
fs.Errorf(nil, "Failed to remove temp config file: %v", err)
}
}()
var buf bytes.Buffer
if err := goconfig.SaveConfigData(gc.ConfigFile, &buf); err != nil {
return errors.Errorf("Failed to save config file: %v", err)
}
if err := config.Encrypt(&buf, f); err != nil {
return err
}
_ = f.Sync()
err = f.Close()
if err != nil {
return errors.Errorf("Failed to close config file: %v", err)
}
var fileMode os.FileMode = 0600
info, err := os.Stat(config.ConfigPath)
if err != nil {
fs.Debugf(nil, "Using default permissions for config file: %v", fileMode)
} else if info.Mode() != fileMode {
fs.Debugf(nil, "Keeping previous permissions for config file: %v", info.Mode())
fileMode = info.Mode()
}
attemptCopyGroup(config.ConfigPath, f.Name())
err = os.Chmod(f.Name(), fileMode)
if err != nil {
fs.Errorf(nil, "Failed to set permissions on config file: %v", err)
}
if err = os.Rename(config.ConfigPath, config.ConfigPath+".old"); err != nil && !os.IsNotExist(err) {
return errors.Errorf("Failed to move previous config to backup location: %v", err)
}
if err = os.Rename(f.Name(), config.ConfigPath); err != nil {
return errors.Errorf("Failed to move newly written config from %s to final location: %v", f.Name(), err)
}
if err := os.Remove(config.ConfigPath + ".old"); err != nil && !os.IsNotExist(err) {
fs.Errorf(nil, "Failed to remove backup config file: %v", err)
}
return nil
}
// Serialize the config into a string
func (gc *GoConfig) Serialize() (string, error) {
var buf bytes.Buffer
if err := goconfig.SaveConfigData(gc.ConfigFile, &buf); err != nil {
return "", errors.Errorf("Failed to save config file: %v", err)
}
return buf.String(), nil
}
// HasSection returns true if section exists in the config file
func (gc *GoConfig) HasSection(section string) bool {
_, err := gc.ConfigFile.GetSection(section)
if err != nil {
return false
}
return true
}
// DeleteSection removes the named section and all config from the
// config file
func (gc *GoConfig) DeleteSection(section string) {
gc.ConfigFile.DeleteSection(section)
}
// SetValue sets the value under key in section
func (gc *GoConfig) SetValue(section string, key string, value string) {
gc.ConfigFile.SetValue(section, key, value)
}
// Check the interfaces are satisfied
var (
_ config.File = (*GoConfig)(nil)
)

View File

@ -3,7 +3,7 @@
// +build !darwin,!dragonfly,!freebsd,!linux,!netbsd,!openbsd,!solaris // +build !darwin,!dragonfly,!freebsd,!linux,!netbsd,!openbsd,!solaris
package config package configfile
// attemptCopyGroups tries to keep the group the same, which only makes sense // attemptCopyGroups tries to keep the group the same, which only makes sense
// for system with user-group-world permission model. // for system with user-group-world permission model.

View File

@ -3,7 +3,7 @@
// +build darwin dragonfly freebsd linux netbsd openbsd solaris // +build darwin dragonfly freebsd linux netbsd openbsd solaris
package config package configfile
import ( import (
"os" "os"

388
fs/config/crypt.go Normal file
View File

@ -0,0 +1,388 @@
package config
import (
"bufio"
"bytes"
"context"
"crypto/rand"
"crypto/sha256"
"encoding/base64"
"fmt"
"io"
"io/ioutil"
"log"
"os"
"os/exec"
"strings"
"unicode/utf8"
"github.com/pkg/errors"
"golang.org/x/crypto/nacl/secretbox"
"golang.org/x/text/unicode/norm"
"github.com/rclone/rclone/fs"
"github.com/rclone/rclone/fs/config/obscure"
)
var (
// Key to use for password en/decryption.
// When nil, no encryption will be used for saving.
configKey []byte
// PasswordPromptOutput is output of prompt for password
PasswordPromptOutput = os.Stderr
// PassConfigKeyForDaemonization if set to true, the configKey
// is obscured with obscure.Obscure and saved to a temp file
// when it is calculated from the password. The path of that
// temp file is then written to the environment variable
// `_RCLONE_CONFIG_KEY_FILE`. If `_RCLONE_CONFIG_KEY_FILE` is
// present, password prompt is skipped and
// `RCLONE_CONFIG_PASS` ignored. For security reasons, the
// temp file is deleted once the configKey is successfully
// loaded. This can be used to pass the configKey to a child
// process.
PassConfigKeyForDaemonization = false
)
// Decrypt will automatically decrypt a reader
func Decrypt(b io.ReadSeeker) (io.Reader, error) {
ctx := context.Background()
ci := fs.GetConfig(ctx)
var usingPasswordCommand bool
// Find first non-empty line
r := bufio.NewReader(b)
for {
line, _, err := r.ReadLine()
if err != nil {
if err == io.EOF {
if _, err := b.Seek(0, io.SeekStart); err != nil {
return nil, err
}
return b, nil
}
return nil, err
}
l := strings.TrimSpace(string(line))
if len(l) == 0 || strings.HasPrefix(l, ";") || strings.HasPrefix(l, "#") {
continue
}
// First non-empty or non-comment must be ENCRYPT_V0
if l == "RCLONE_ENCRYPT_V0:" {
break
}
if strings.HasPrefix(l, "RCLONE_ENCRYPT_V") {
return nil, errors.New("unsupported configuration encryption - update rclone for support")
}
if _, err := b.Seek(0, io.SeekStart); err != nil {
return nil, err
}
return b, nil
}
if len(configKey) == 0 {
if len(ci.PasswordCommand) != 0 {
var stdout bytes.Buffer
var stderr bytes.Buffer
cmd := exec.Command(ci.PasswordCommand[0], ci.PasswordCommand[1:]...)
cmd.Stdout = &stdout
cmd.Stderr = &stderr
cmd.Stdin = os.Stdin
if err := cmd.Run(); err != nil {
// One does not always get the stderr returned in the wrapped error.
fs.Errorf(nil, "Using --password-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)
if err != nil {
return nil, errors.Wrap(err, "failed to load base64 encoded data")
}
if len(box) < 24+secretbox.Overhead {
return nil, errors.New("Configuration data too short")
}
var out []byte
for {
if envKeyFile := os.Getenv("_RCLONE_CONFIG_KEY_FILE"); len(envKeyFile) > 0 {
fs.Debugf(nil, "attempting to obtain configKey from temp file %s", envKeyFile)
obscuredKey, err := ioutil.ReadFile(envKeyFile)
if err != nil {
errRemove := os.Remove(envKeyFile)
if errRemove != nil {
log.Fatalf("unable to read obscured config key and unable to delete the temp file: %v", err)
}
log.Fatalf("unable to read obscured config key: %v", err)
}
errRemove := os.Remove(envKeyFile)
if errRemove != nil {
log.Fatalf("unable to delete temp file with configKey: %v", err)
}
configKey = []byte(obscure.MustReveal(string(obscuredKey)))
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 !ci.AskPassword {
return nil, errors.New("unable to decrypt configuration and not allowed to ask for password - set RCLONE_CONFIG_PASS to your configuration password")
}
getConfigPassword("Enter configuration password:")
}
}
// Nonce is first 24 bytes of the ciphertext
var nonce [24]byte
copy(nonce[:], box[:24])
var key [32]byte
copy(key[:], configKey[:32])
// Attempt to decrypt
var ok bool
out, ok = secretbox.Open(nil, box[24:], &nonce, &key)
if ok {
break
}
// Retry
fs.Errorf(nil, "Couldn't decrypt configuration, most likely wrong password.")
configKey = nil
}
return bytes.NewReader(out), nil
}
// Encrypt the config file
func Encrypt(src io.Reader, dst io.Writer) error {
if len(configKey) == 0 {
_, err := io.Copy(dst, src)
return err
}
_, _ = fmt.Fprintln(dst, "# Encrypted rclone configuration File")
_, _ = fmt.Fprintln(dst, "")
_, _ = fmt.Fprintln(dst, "RCLONE_ENCRYPT_V0:")
// Generate new nonce and write it to the start of the ciphertext
var nonce [24]byte
n, _ := rand.Read(nonce[:])
if n != 24 {
return errors.Errorf("nonce short read: %d", n)
}
enc := base64.NewEncoder(base64.StdEncoding, dst)
_, err := enc.Write(nonce[:])
if err != nil {
return errors.Errorf("Failed to write config file: %v", err)
}
var key [32]byte
copy(key[:], configKey[:32])
data, err := ioutil.ReadAll(src)
if err != nil {
return err
}
b := secretbox.Seal(nil, data, &nonce, &key)
_, err = enc.Write(b)
if err != nil {
return errors.Errorf("Failed to write config file: %v", err)
}
return enc.Close()
}
// checkPassword normalises and validates the password
func checkPassword(password string) (string, error) {
if !utf8.ValidString(password) {
return "", errors.New("password contains invalid utf8 characters")
}
// Check for leading/trailing whitespace
trimmedPassword := strings.TrimSpace(password)
// Warn user if password has leading+trailing whitespace
if len(password) != len(trimmedPassword) {
_, _ = fmt.Fprintln(os.Stderr, "Your password contains leading/trailing whitespace - in previous versions of rclone this was stripped")
}
// Normalize to reduce weird variations.
password = norm.NFKC.String(password)
if len(password) == 0 || len(trimmedPassword) == 0 {
return "", errors.New("no characters in password")
}
return password, nil
}
// GetPassword asks the user for a password with the prompt given.
func GetPassword(prompt string) string {
_, _ = fmt.Fprintln(PasswordPromptOutput, prompt)
for {
_, _ = fmt.Fprint(PasswordPromptOutput, "password:")
password := ReadPassword()
password, err := checkPassword(password)
if err == nil {
return password
}
_, _ = fmt.Fprintf(os.Stderr, "Bad password: %v\n", err)
}
}
// ChangePassword will query the user twice for the named password. If
// the same password is entered it is returned.
func ChangePassword(name string) string {
for {
a := GetPassword(fmt.Sprintf("Enter %s password:", name))
b := GetPassword(fmt.Sprintf("Confirm %s password:", name))
if a == b {
return a
}
fmt.Println("Passwords do not match!")
}
}
// getConfigPassword will query the user for a password the
// first time it is required.
func getConfigPassword(q string) {
if len(configKey) != 0 {
return
}
for {
password := GetPassword(q)
err := setConfigPassword(password)
if err == nil {
return
}
_, _ = fmt.Fprintln(os.Stderr, "Error:", err)
}
}
// setConfigPassword will set the configKey to the hash of
// the password. If the length of the password is
// zero after trimming+normalization, an error is returned.
func setConfigPassword(password string) error {
password, err := checkPassword(password)
if err != nil {
return err
}
// Create SHA256 has of the password
sha := sha256.New()
_, err = sha.Write([]byte("[" + password + "][rclone-config]"))
if err != nil {
return err
}
configKey = sha.Sum(nil)
if PassConfigKeyForDaemonization {
tempFile, err := ioutil.TempFile("", "rclone")
if err != nil {
log.Fatalf("cannot create temp file to store configKey: %v", err)
}
_, err = tempFile.WriteString(obscure.MustObscure(string(configKey)))
if err != nil {
errRemove := os.Remove(tempFile.Name())
if errRemove != nil {
log.Fatalf("error writing configKey to temp file and also error deleting it: %v", err)
}
log.Fatalf("error writing configKey to temp file: %v", err)
}
err = tempFile.Close()
if err != nil {
errRemove := os.Remove(tempFile.Name())
if errRemove != nil {
log.Fatalf("error closing temp file with configKey and also error deleting it: %v", err)
}
log.Fatalf("error closing temp file with configKey: %v", err)
}
fs.Debugf(nil, "saving configKey to temp file")
err = os.Setenv("_RCLONE_CONFIG_KEY_FILE", tempFile.Name())
if err != nil {
errRemove := os.Remove(tempFile.Name())
if errRemove != nil {
log.Fatalf("unable to set environment variable _RCLONE_CONFIG_KEY_FILE and unable to delete the temp file: %v", err)
}
log.Fatalf("unable to set environment variable _RCLONE_CONFIG_KEY_FILE: %v", err)
}
}
return nil
}
// changeConfigPassword will query the user twice
// for a password. If the same password is entered
// twice the key is updated.
func changeConfigPassword() {
err := setConfigPassword(ChangePassword("NEW configuration"))
if err != nil {
fmt.Printf("Failed to set config password: %v\n", err)
return
}
}
// SetPassword will allow the user to modify the current
// configuration encryption settings.
func SetPassword() {
for {
if len(configKey) > 0 {
fmt.Println("Your configuration is encrypted.")
what := []string{"cChange Password", "uUnencrypt configuration", "qQuit to main menu"}
switch i := Command(what); i {
case 'c':
changeConfigPassword()
SaveConfig()
fmt.Println("Password changed")
continue
case 'u':
configKey = nil
SaveConfig()
continue
case 'q':
return
}
} else {
fmt.Println("Your configuration is not encrypted.")
fmt.Println("If you add a password, you will protect your login information to cloud services.")
what := []string{"aAdd Password", "qQuit to main menu"}
switch i := Command(what); i {
case 'a':
changeConfigPassword()
SaveConfig()
fmt.Println("Password set")
continue
case 'q':
return
}
}
}
}

View File

@ -72,7 +72,7 @@ See the [listremotes command](/commands/rclone_listremotes/) command for more in
// Return the a list of remotes in the config file // Return the a list of remotes in the config file
func rcListRemotes(ctx context.Context, in rc.Params) (out rc.Params, err error) { func rcListRemotes(ctx context.Context, in rc.Params) (out rc.Params, err error) {
var remotes = []string{} var remotes = []string{}
for _, remote := range getConfigData().GetSectionList() { for _, remote := range Data.GetSectionList() {
remotes = append(remotes, remote) remotes = append(remotes, remote)
} }
out = rc.Params{ out = rc.Params{