mirror of
https://github.com/rclone/rclone
synced 2024-11-27 05:23:40 +01:00
Add configuration file encryption
See #317 for details. Use `rclone config` to add/change/remove password. Tests that loads the default configuration will now fail with a better error message, and add a switch that makes it possible to disable password prompts and fail instead. Make it possible to use the "RCLONE_CONFIG_PASS" environment variable as password for configuration.
This commit is contained in:
parent
4676a89963
commit
bfd7601cf9
@ -436,6 +436,72 @@ Very useful for debugging.
|
|||||||
|
|
||||||
Prints the version number
|
Prints the version number
|
||||||
|
|
||||||
|
Configuration Encryption
|
||||||
|
------------------------
|
||||||
|
Your configuration file contains information for logging in to
|
||||||
|
your cloud services. This means that you should keep your
|
||||||
|
`.rclone.conf` file in a secure location.
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
To add a password to your rclone configuration, execute `rclone config`.
|
||||||
|
|
||||||
|
```
|
||||||
|
>rclone config
|
||||||
|
Current remotes:
|
||||||
|
|
||||||
|
e) Edit existing remote
|
||||||
|
n) New remote
|
||||||
|
d) Delete remote
|
||||||
|
s) Set configuration password
|
||||||
|
q) Quit config
|
||||||
|
e/n/d/s/q>
|
||||||
|
```
|
||||||
|
|
||||||
|
Go into `s`, Set configuration password:
|
||||||
|
```
|
||||||
|
e/n/d/s/q> s
|
||||||
|
Your configuration is not encrypted.
|
||||||
|
If you add a password, you will protect your login information to cloud services.
|
||||||
|
a) Add Password
|
||||||
|
q) Quit to main menu
|
||||||
|
a/q> a
|
||||||
|
Enter NEW configuration password:
|
||||||
|
password>
|
||||||
|
Confirm NEW password:
|
||||||
|
password>
|
||||||
|
Password set
|
||||||
|
Your configuration is encrypted.
|
||||||
|
c) Change Password
|
||||||
|
u) Unencrypt configuration
|
||||||
|
q) Quit to main menu
|
||||||
|
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.
|
||||||
|
|
||||||
|
There is no way to recover the configuration if you lose your password.
|
||||||
|
|
||||||
|
rclone uses [nacl secretbox](https://godoc.org/golang.org/x/crypto/nacl/secretbox)
|
||||||
|
which in term uses XSalsa20 and Poly1305 to encrypt and authenticate
|
||||||
|
your configuration with secret-key cryptography.
|
||||||
|
The password is SHA-256 hashed, which produces the key for secretbox.
|
||||||
|
The hashed password is not stored.
|
||||||
|
|
||||||
|
While this provides very good security, we do not recommend storing
|
||||||
|
your encrypted rclone configuration in public, if it contains sensitive
|
||||||
|
information, maybe except if you use a very strong password.
|
||||||
|
|
||||||
|
If it is safe in your environment, you can set the `RCLONE_CONFIG_PASS`
|
||||||
|
environment variable to contain your password, in which case it will be
|
||||||
|
used for decrypting the configuration.
|
||||||
|
|
||||||
|
|
||||||
Developer options
|
Developer options
|
||||||
-----------------
|
-----------------
|
||||||
|
|
||||||
|
274
fs/config.go
274
fs/config.go
@ -4,8 +4,14 @@ package fs
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
|
"bytes"
|
||||||
|
"crypto/rand"
|
||||||
|
"crypto/sha256"
|
||||||
|
"crypto/tls"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"io/ioutil"
|
||||||
"log"
|
"log"
|
||||||
"math"
|
"math"
|
||||||
"net/http"
|
"net/http"
|
||||||
@ -16,12 +22,14 @@ import (
|
|||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
"unicode/utf8"
|
||||||
|
|
||||||
"crypto/tls"
|
"github.com/klauspost/goconfig"
|
||||||
|
|
||||||
"github.com/Unknwon/goconfig"
|
|
||||||
"github.com/mreiferson/go-httpclient"
|
"github.com/mreiferson/go-httpclient"
|
||||||
"github.com/spf13/pflag"
|
"github.com/spf13/pflag"
|
||||||
|
"golang.org/x/crypto/nacl/secretbox"
|
||||||
|
"golang.org/x/crypto/ssh/terminal"
|
||||||
|
"golang.org/x/text/unicode/norm"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@ -69,10 +77,15 @@ var (
|
|||||||
dumpHeaders = pflag.BoolP("dump-headers", "", false, "Dump HTTP headers - may contain sensitive info")
|
dumpHeaders = pflag.BoolP("dump-headers", "", false, "Dump HTTP headers - may contain sensitive info")
|
||||||
dumpBodies = pflag.BoolP("dump-bodies", "", false, "Dump HTTP headers and bodies - may contain sensitive info")
|
dumpBodies = pflag.BoolP("dump-bodies", "", false, "Dump HTTP headers and bodies - may contain sensitive info")
|
||||||
skipVerify = pflag.BoolP("no-check-certificate", "", false, "Do not verify the server SSL certificate. Insecure.")
|
skipVerify = pflag.BoolP("no-check-certificate", "", false, "Do not verify the server SSL certificate. Insecure.")
|
||||||
|
AskPassword = pflag.BoolP("ask-password", "", true, "Allow prompt for password for encrypted configuration.")
|
||||||
deleteBefore = pflag.BoolP("delete-before", "", false, "When synchronizing, delete files on destination before transfering")
|
deleteBefore = pflag.BoolP("delete-before", "", false, "When synchronizing, delete files on destination before transfering")
|
||||||
deleteDuring = pflag.BoolP("delete-during", "", false, "When synchronizing, delete files during transfer (default)")
|
deleteDuring = pflag.BoolP("delete-during", "", false, "When synchronizing, delete files during transfer (default)")
|
||||||
deleteAfter = pflag.BoolP("delete-after", "", false, "When synchronizing, delete files on destination after transfering")
|
deleteAfter = pflag.BoolP("delete-after", "", false, "When synchronizing, delete files on destination after transfering")
|
||||||
bwLimit SizeSuffix
|
bwLimit SizeSuffix
|
||||||
|
|
||||||
|
// Key to use for password en/decryption.
|
||||||
|
// When nil, no encryption will be used for saving.
|
||||||
|
configKey []byte
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
@ -292,13 +305,9 @@ func LoadConfig() {
|
|||||||
|
|
||||||
// Load configuration file.
|
// Load configuration file.
|
||||||
var err error
|
var err error
|
||||||
ConfigFile, err = goconfig.LoadConfigFile(ConfigPath)
|
ConfigFile, err = loadConfigFile()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Failed to load config file %v - using defaults: %v", ConfigPath, err)
|
log.Fatalf("Failed to config file \"%s\": %v", ConfigPath, err)
|
||||||
ConfigFile, err = goconfig.LoadConfigFile(os.DevNull)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("Failed to read null config file: %v", err)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load filters
|
// Load filters
|
||||||
@ -311,8 +320,135 @@ func LoadConfig() {
|
|||||||
startTokenBucket()
|
startTokenBucket()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// loadConfigFile will load a config file, and
|
||||||
|
// automatically decrypt it.
|
||||||
|
func loadConfigFile() (*goconfig.ConfigFile, error) {
|
||||||
|
b, err := ioutil.ReadFile(ConfigPath)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Failed to load config file %v - using defaults: %v", ConfigPath, err)
|
||||||
|
return goconfig.LoadFromData(nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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.LoadFromData(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, fmt.Errorf("Unsupported configuration encryption. Update rclone for support.")
|
||||||
|
}
|
||||||
|
return goconfig.LoadFromData(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Encrypted content is base64 encoded.
|
||||||
|
dec := base64.NewDecoder(base64.StdEncoding, r)
|
||||||
|
box, err := ioutil.ReadAll(dec)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("Failed to load base64 encoded data: %v", err)
|
||||||
|
}
|
||||||
|
if len(box) < 24+secretbox.Overhead {
|
||||||
|
return nil, fmt.Errorf("Configuration data too short")
|
||||||
|
}
|
||||||
|
envpw := os.Getenv("RCLONE_CONFIG_PASS")
|
||||||
|
|
||||||
|
var out []byte
|
||||||
|
for {
|
||||||
|
if len(configKey) == 0 && envpw != "" {
|
||||||
|
err := setPassword(envpw)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("Using RCLONE_CONFIG_PASS returned:", err)
|
||||||
|
envpw = ""
|
||||||
|
} else {
|
||||||
|
Debug(nil, "Using RCLONE_CONFIG_PASS password.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(configKey) == 0 {
|
||||||
|
if !*AskPassword {
|
||||||
|
return nil, fmt.Errorf("Unable to decrypt configuration and not allowed to ask for password. Set RCLONE_CONFIG_PASS to your configuration password.")
|
||||||
|
}
|
||||||
|
getPassword("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
|
||||||
|
log.Println("Couldn't decrypt configuration, most likely wrong password.")
|
||||||
|
configKey = nil
|
||||||
|
envpw = ""
|
||||||
|
}
|
||||||
|
return goconfig.LoadFromData(out)
|
||||||
|
}
|
||||||
|
|
||||||
|
// getPassword will query the user for a password the
|
||||||
|
// first time it is required.
|
||||||
|
func getPassword(q string) {
|
||||||
|
if len(configKey) != 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for {
|
||||||
|
fmt.Println(q)
|
||||||
|
fmt.Print("password>")
|
||||||
|
err := setPassword(ReadPassword())
|
||||||
|
if err == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fmt.Println("Error:", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// setPassword 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 setPassword(password string) error {
|
||||||
|
if !utf8.ValidString(password) {
|
||||||
|
return fmt.Errorf("Password contains invalid utf8 characters")
|
||||||
|
}
|
||||||
|
// Remove leading+trailing whitespace
|
||||||
|
password = strings.TrimSpace(password)
|
||||||
|
|
||||||
|
// Normalize to reduce weird variations.
|
||||||
|
password = norm.NFKC.String(password)
|
||||||
|
if len(password) == 0 {
|
||||||
|
return fmt.Errorf("No characters in password")
|
||||||
|
}
|
||||||
|
// 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)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// SaveConfig saves configuration file.
|
// SaveConfig saves configuration file.
|
||||||
|
// if configKey has been set, the file will be encrypted.
|
||||||
func SaveConfig() {
|
func SaveConfig() {
|
||||||
|
if len(configKey) == 0 {
|
||||||
err := goconfig.SaveConfigFile(ConfigFile, ConfigPath)
|
err := goconfig.SaveConfigFile(ConfigFile, ConfigPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("Failed to save config file: %v", err)
|
log.Fatalf("Failed to save config file: %v", err)
|
||||||
@ -321,6 +457,53 @@ func SaveConfig() {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Failed to set permissions on config file: %v", err)
|
log.Printf("Failed to set permissions on config file: %v", err)
|
||||||
}
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var buf bytes.Buffer
|
||||||
|
err := goconfig.SaveConfigData(ConfigFile, &buf)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed to save config file: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
f, err := os.Create(ConfigPath)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed to save config file: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
||||||
|
log.Fatalf("nonce short read: %d", n)
|
||||||
|
}
|
||||||
|
enc := base64.NewEncoder(base64.StdEncoding, f)
|
||||||
|
_, err = enc.Write(nonce[:])
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed to write 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 {
|
||||||
|
log.Fatalf("Failed to write config file: %v", err)
|
||||||
|
}
|
||||||
|
_ = enc.Close()
|
||||||
|
err = f.Close()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed to close config file: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = os.Chmod(ConfigPath, 0600)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Failed to set permissions on config file: %v", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ShowRemotes shows an overview of the config file
|
// ShowRemotes shows an overview of the config file
|
||||||
@ -354,6 +537,17 @@ func ReadLine() string {
|
|||||||
return strings.TrimSpace(line)
|
return strings.TrimSpace(line)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ReadPassword reads a password
|
||||||
|
// without echoing it to the terminal.
|
||||||
|
func ReadPassword() string {
|
||||||
|
line, err := terminal.ReadPassword(int(os.Stdin.Fd()))
|
||||||
|
fmt.Println("")
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed to read password: %v", err)
|
||||||
|
}
|
||||||
|
return strings.TrimSpace(string(line))
|
||||||
|
}
|
||||||
|
|
||||||
// Command - choose one
|
// Command - choose one
|
||||||
func Command(commands []string) byte {
|
func Command(commands []string) byte {
|
||||||
opts := []string{}
|
opts := []string{}
|
||||||
@ -547,7 +741,7 @@ func DeleteRemote(name string) {
|
|||||||
func EditConfig() {
|
func EditConfig() {
|
||||||
for {
|
for {
|
||||||
haveRemotes := len(ConfigFile.GetSectionList()) != 0
|
haveRemotes := len(ConfigFile.GetSectionList()) != 0
|
||||||
what := []string{"eEdit existing remote", "nNew remote", "dDelete remote", "qQuit config"}
|
what := []string{"eEdit existing remote", "nNew remote", "dDelete remote", "sSet configuration password", "qQuit config"}
|
||||||
if haveRemotes {
|
if haveRemotes {
|
||||||
fmt.Printf("Current remotes:\n\n")
|
fmt.Printf("Current remotes:\n\n")
|
||||||
ShowRemotes()
|
ShowRemotes()
|
||||||
@ -581,9 +775,69 @@ func EditConfig() {
|
|||||||
case 'd':
|
case 'd':
|
||||||
name := ChooseRemote()
|
name := ChooseRemote()
|
||||||
DeleteRemote(name)
|
DeleteRemote(name)
|
||||||
|
case 's':
|
||||||
|
SetPassword()
|
||||||
|
case 'q':
|
||||||
|
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':
|
||||||
|
changePassword()
|
||||||
|
SaveConfig()
|
||||||
|
fmt.Println("Password changed")
|
||||||
|
continue
|
||||||
|
case 'u':
|
||||||
|
configKey = nil
|
||||||
|
SaveConfig()
|
||||||
|
continue
|
||||||
case 'q':
|
case 'q':
|
||||||
return
|
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':
|
||||||
|
changePassword()
|
||||||
|
SaveConfig()
|
||||||
|
fmt.Println("Password set")
|
||||||
|
continue
|
||||||
|
case 'q':
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// changePassword will query the user twice
|
||||||
|
// for a password. If the same password is entered
|
||||||
|
// twice the key is updated.
|
||||||
|
func changePassword() {
|
||||||
|
for {
|
||||||
|
configKey = nil
|
||||||
|
getPassword("Enter NEW configuration password:")
|
||||||
|
a := configKey
|
||||||
|
// re-enter password
|
||||||
|
configKey = nil
|
||||||
|
getPassword("Confirm NEW password:")
|
||||||
|
b := configKey
|
||||||
|
if bytes.Equal(a, b) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fmt.Println("Passwords does not match!")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,6 +1,10 @@
|
|||||||
package fs
|
package fs
|
||||||
|
|
||||||
import "testing"
|
import (
|
||||||
|
"bytes"
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
func TestSizeSuffixString(t *testing.T) {
|
func TestSizeSuffixString(t *testing.T) {
|
||||||
for _, test := range []struct {
|
for _, test := range []struct {
|
||||||
@ -73,3 +77,136 @@ func TestReveal(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestConfigLoad(t *testing.T) {
|
||||||
|
ConfigPath = "./testdata/plain.conf"
|
||||||
|
configKey = nil
|
||||||
|
c, err := loadConfigFile()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
sections := c.GetSectionList()
|
||||||
|
var expect = []string{"RCLONE_ENCRYPT_V0", "nounc", "unc"}
|
||||||
|
if !reflect.DeepEqual(sections, expect) {
|
||||||
|
t.Fatalf("%v != %v", sections, expect)
|
||||||
|
}
|
||||||
|
|
||||||
|
keys := c.GetKeyList("nounc")
|
||||||
|
expect = []string{"type", "nounc"}
|
||||||
|
if !reflect.DeepEqual(keys, expect) {
|
||||||
|
t.Fatalf("%v != %v", keys, expect)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConfigLoadEncrypted(t *testing.T) {
|
||||||
|
var err error
|
||||||
|
ConfigPath = "./testdata/encrypted.conf"
|
||||||
|
|
||||||
|
// Set correct password
|
||||||
|
err = setPassword("asdf")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
c, err := loadConfigFile()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
sections := c.GetSectionList()
|
||||||
|
var expect = []string{"nounc", "unc"}
|
||||||
|
if !reflect.DeepEqual(sections, expect) {
|
||||||
|
t.Fatalf("%v != %v", sections, expect)
|
||||||
|
}
|
||||||
|
|
||||||
|
keys := c.GetKeyList("nounc")
|
||||||
|
expect = []string{"type", "nounc"}
|
||||||
|
if !reflect.DeepEqual(keys, expect) {
|
||||||
|
t.Fatalf("%v != %v", keys, expect)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConfigLoadEncryptedFailures(t *testing.T) {
|
||||||
|
var err error
|
||||||
|
|
||||||
|
// This file should be too short to be decoded.
|
||||||
|
ConfigPath = "./testdata/enc-short.conf"
|
||||||
|
_, err = loadConfigFile()
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected error")
|
||||||
|
}
|
||||||
|
t.Log("Correctly got:", err)
|
||||||
|
|
||||||
|
// This file contains invalid base64 characters.
|
||||||
|
ConfigPath = "./testdata/enc-invalid.conf"
|
||||||
|
_, err = loadConfigFile()
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected error")
|
||||||
|
}
|
||||||
|
t.Log("Correctly got:", err)
|
||||||
|
|
||||||
|
// This file contains invalid base64 characters.
|
||||||
|
ConfigPath = "./testdata/enc-too-new.conf"
|
||||||
|
_, err = loadConfigFile()
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected error")
|
||||||
|
}
|
||||||
|
t.Log("Correctly got:", err)
|
||||||
|
|
||||||
|
// This file contains invalid base64 characters.
|
||||||
|
ConfigPath = "./testdata/filenotfound.conf"
|
||||||
|
c, err := loadConfigFile()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if len(c.GetSectionList()) != 0 {
|
||||||
|
t.Fatalf("Expected 0-length section, got %d entries", len(c.GetSectionList()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPassword(t *testing.T) {
|
||||||
|
var err error
|
||||||
|
// Empty password should give error
|
||||||
|
err = setPassword(" \t ")
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected error")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test invalid utf8 sequence
|
||||||
|
err = setPassword(string([]byte{0xff, 0xfe, 0xfd}) + "abc")
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected error")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Simple check of wrong passwords
|
||||||
|
hashedKeyCompare(t, "mis", "match", false)
|
||||||
|
|
||||||
|
// Check that passwords match with trimmed whitespace
|
||||||
|
hashedKeyCompare(t, " abcdef \t", "abcdef", true)
|
||||||
|
|
||||||
|
// Check that passwords match after unicode normalization
|
||||||
|
hashedKeyCompare(t, "ff\u0041\u030A", "ffÅ", true)
|
||||||
|
|
||||||
|
// Check that passwords preserves case
|
||||||
|
hashedKeyCompare(t, "abcdef", "ABCDEF", false)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func hashedKeyCompare(t *testing.T, a, b string, shouldMatch bool) {
|
||||||
|
err := setPassword(a)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
k1 := configKey
|
||||||
|
|
||||||
|
err = setPassword(b)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
k2 := configKey
|
||||||
|
matches := bytes.Equal(k1, k2)
|
||||||
|
if shouldMatch && !matches {
|
||||||
|
t.Fatalf("%v != %v", k1, k2)
|
||||||
|
}
|
||||||
|
if !shouldMatch && matches {
|
||||||
|
t.Fatalf("%v == %v", k1, k2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -94,6 +94,10 @@ func newRun() *Run {
|
|||||||
mkdir: make(map[string]bool),
|
mkdir: make(map[string]bool),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Never ask for passwords, fail instead.
|
||||||
|
// If your local config is encrypted set environment variable
|
||||||
|
// "RCLONE_CONFIG_PASS=hunter2" (or your password)
|
||||||
|
*fs.AskPassword = false
|
||||||
fs.LoadConfig()
|
fs.LoadConfig()
|
||||||
fs.Config.Verbose = *Verbose
|
fs.Config.Verbose = *Verbose
|
||||||
fs.Config.Quiet = !*Verbose
|
fs.Config.Quiet = !*Verbose
|
||||||
|
4
fs/testdata/enc-invalid.conf
vendored
Normal file
4
fs/testdata/enc-invalid.conf
vendored
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
# Encrypted rclone configuration File
|
||||||
|
|
||||||
|
RCLONE_ENCRYPT_V0:
|
||||||
|
b5Uk6mE3cUn5Wb8xiWYnVBAxXUirAaEG1PO/GIDiO9274AOæøå+Yj790BwJA4d2y7lNkmHt4nJwIsoueFvUYmm7RDyzER8IA3XOCrjzl3OUcczZqcplk5JfBdhxMZpt1aGYWUdle1IgO/kAFne6sLD6IuxPySEb
|
4
fs/testdata/enc-short.conf
vendored
Normal file
4
fs/testdata/enc-short.conf
vendored
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
# Encrypted rclone configuration File
|
||||||
|
|
||||||
|
RCLONE_ENCRYPT_V0:
|
||||||
|
b5Uk6mE3cUn5Wb8xi
|
4
fs/testdata/enc-too-new.conf
vendored
Normal file
4
fs/testdata/enc-too-new.conf
vendored
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
# Encrypted rclone configuration File
|
||||||
|
|
||||||
|
RCLONE_ENCRYPT_V1:
|
||||||
|
b5Uk6mE3cUn5Wb8xiWYnVBAxXUirAaEG1PO/GIDiO9274AO+Yj790BwJA4d2y7lNkmHt4nJwIsoueFvUYmm7RDyzER8IA3XOCrjzl3OUcczZqcplk5JfBdhxMZpt1aGYWUdle1IgO/kAFne6sLD6IuxPySEb
|
4
fs/testdata/encrypted.conf
vendored
Normal file
4
fs/testdata/encrypted.conf
vendored
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
# Encrypted rclone configuration File
|
||||||
|
|
||||||
|
RCLONE_ENCRYPT_V0:
|
||||||
|
b5Uk6mE3cUn5Wb8xiWYnVBAxXUirAaEG1PO/GIDiO9274AO+Yj790BwJA4d2y7lNkmHt4nJwIsoueFvUYmm7RDyzER8IA3XOCrjzl3OUcczZqcplk5JfBdhxMZpt1aGYWUdle1IgO/kAFne6sLD6IuxPySEb
|
12
fs/testdata/plain.conf
vendored
Normal file
12
fs/testdata/plain.conf
vendored
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
[RCLONE_ENCRYPT_V0]
|
||||||
|
type = local
|
||||||
|
nounc = true
|
||||||
|
|
||||||
|
[nounc]
|
||||||
|
type = local
|
||||||
|
nounc = true
|
||||||
|
|
||||||
|
|
||||||
|
[unc]
|
||||||
|
type = local
|
||||||
|
nounc = false
|
@ -50,6 +50,11 @@ func init() {
|
|||||||
// TestInit tests basic intitialisation
|
// TestInit tests basic intitialisation
|
||||||
func TestInit(t *testing.T) {
|
func TestInit(t *testing.T) {
|
||||||
var err error
|
var err error
|
||||||
|
|
||||||
|
// Never ask for passwords, fail instead.
|
||||||
|
// If your local config is encrypted set environment variable
|
||||||
|
// "RCLONE_CONFIG_PASS=hunter2" (or your password)
|
||||||
|
*fs.AskPassword = false
|
||||||
fs.LoadConfig()
|
fs.LoadConfig()
|
||||||
fs.Config.Verbose = *verbose
|
fs.Config.Verbose = *verbose
|
||||||
fs.Config.Quiet = !*verbose
|
fs.Config.Quiet = !*verbose
|
||||||
|
Loading…
Reference in New Issue
Block a user