mirror of
https://github.com/rclone/rclone
synced 2024-11-18 18:46:07 +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
|
||||
|
||||
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
|
||||
-----------------
|
||||
|
||||
|
276
fs/config.go
276
fs/config.go
@ -4,8 +4,14 @@ package fs
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"crypto/rand"
|
||||
"crypto/sha256"
|
||||
"crypto/tls"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"math"
|
||||
"net/http"
|
||||
@ -16,12 +22,14 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
"unicode/utf8"
|
||||
|
||||
"crypto/tls"
|
||||
|
||||
"github.com/Unknwon/goconfig"
|
||||
"github.com/klauspost/goconfig"
|
||||
"github.com/mreiferson/go-httpclient"
|
||||
"github.com/spf13/pflag"
|
||||
"golang.org/x/crypto/nacl/secretbox"
|
||||
"golang.org/x/crypto/ssh/terminal"
|
||||
"golang.org/x/text/unicode/norm"
|
||||
)
|
||||
|
||||
const (
|
||||
@ -69,10 +77,15 @@ var (
|
||||
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")
|
||||
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")
|
||||
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")
|
||||
bwLimit SizeSuffix
|
||||
|
||||
// Key to use for password en/decryption.
|
||||
// When nil, no encryption will be used for saving.
|
||||
configKey []byte
|
||||
)
|
||||
|
||||
func init() {
|
||||
@ -292,13 +305,9 @@ func LoadConfig() {
|
||||
|
||||
// Load configuration file.
|
||||
var err error
|
||||
ConfigFile, err = goconfig.LoadConfigFile(ConfigPath)
|
||||
ConfigFile, err = loadConfigFile()
|
||||
if err != nil {
|
||||
log.Printf("Failed to load config file %v - using defaults: %v", ConfigPath, err)
|
||||
ConfigFile, err = goconfig.LoadConfigFile(os.DevNull)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to read null config file: %v", err)
|
||||
}
|
||||
log.Fatalf("Failed to config file \"%s\": %v", ConfigPath, err)
|
||||
}
|
||||
|
||||
// Load filters
|
||||
@ -311,12 +320,186 @@ func LoadConfig() {
|
||||
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.
|
||||
// if configKey has been set, the file will be encrypted.
|
||||
func SaveConfig() {
|
||||
err := goconfig.SaveConfigFile(ConfigFile, ConfigPath)
|
||||
if len(configKey) == 0 {
|
||||
err := goconfig.SaveConfigFile(ConfigFile, ConfigPath)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to save config file: %v", err)
|
||||
}
|
||||
err = os.Chmod(ConfigPath, 0600)
|
||||
if err != nil {
|
||||
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)
|
||||
@ -354,6 +537,17 @@ func ReadLine() string {
|
||||
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
|
||||
func Command(commands []string) byte {
|
||||
opts := []string{}
|
||||
@ -547,7 +741,7 @@ func DeleteRemote(name string) {
|
||||
func EditConfig() {
|
||||
for {
|
||||
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 {
|
||||
fmt.Printf("Current remotes:\n\n")
|
||||
ShowRemotes()
|
||||
@ -581,12 +775,72 @@ func EditConfig() {
|
||||
case 'd':
|
||||
name := ChooseRemote()
|
||||
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':
|
||||
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!")
|
||||
}
|
||||
}
|
||||
|
||||
// Authorize is for remote authorization of headless machines.
|
||||
//
|
||||
// It expects 1 or 3 arguments
|
||||
|
@ -1,6 +1,10 @@
|
||||
package fs
|
||||
|
||||
import "testing"
|
||||
import (
|
||||
"bytes"
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestSizeSuffixString(t *testing.T) {
|
||||
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),
|
||||
}
|
||||
|
||||
// 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.Config.Verbose = *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
|
||||
func TestInit(t *testing.T) {
|
||||
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.Config.Verbose = *verbose
|
||||
fs.Config.Quiet = !*verbose
|
||||
|
Loading…
Reference in New Issue
Block a user