mirror of
https://github.com/rclone/rclone.git
synced 2024-11-28 19:34:55 +01:00
361 lines
9.9 KiB
Go
361 lines
9.9 KiB
Go
package config
|
|
|
|
import (
|
|
"bufio"
|
|
"bytes"
|
|
"context"
|
|
"crypto/rand"
|
|
"crypto/sha256"
|
|
"encoding/base64"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"os/exec"
|
|
"strings"
|
|
|
|
"golang.org/x/crypto/nacl/secretbox"
|
|
|
|
"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
|
|
)
|
|
|
|
// IsEncrypted returns true if the config file is encrypted
|
|
func IsEncrypted() bool {
|
|
return len(configKey) > 0
|
|
}
|
|
|
|
// 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 {
|
|
pass, err := GetPasswordCommand(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if pass != "" {
|
|
usingPasswordCommand = true
|
|
err = SetConfigPassword(pass)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("incorrect password: %w", err)
|
|
}
|
|
if len(configKey) == 0 {
|
|
return nil, errors.New("unable to decrypt configuration: incorrect password")
|
|
}
|
|
} 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 := io.ReadAll(dec)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to load base64 encoded data: %w", err)
|
|
}
|
|
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 := os.ReadFile(envKeyFile)
|
|
if err != nil {
|
|
errRemove := os.Remove(envKeyFile)
|
|
if errRemove != nil {
|
|
return nil, fmt.Errorf("unable to read obscured config key and unable to delete the temp file: %w", err)
|
|
}
|
|
return nil, fmt.Errorf("unable to read obscured config key: %w", err)
|
|
}
|
|
errRemove := os.Remove(envKeyFile)
|
|
if errRemove != nil {
|
|
return nil, fmt.Errorf("unable to delete temp file with configKey: %w", errRemove)
|
|
}
|
|
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
|
|
}
|
|
|
|
// GetPasswordCommand gets the password using the --password-command setting
|
|
//
|
|
// If the the --password-command flag was not in use it returns "", nil
|
|
func GetPasswordCommand(ctx context.Context) (pass string, err error) {
|
|
ci := fs.GetConfig(ctx)
|
|
if len(ci.PasswordCommand) == 0 {
|
|
return "", nil
|
|
}
|
|
|
|
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
|
|
|
|
err = cmd.Run()
|
|
if 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 pass, fmt.Errorf("password command failed: %w", err)
|
|
}
|
|
pass = strings.Trim(stdout.String(), "\r\n")
|
|
if pass == "" {
|
|
return pass, errors.New("--password-command returned empty string")
|
|
}
|
|
return pass, 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 fmt.Errorf("nonce short read: %d", n)
|
|
}
|
|
enc := base64.NewEncoder(base64.StdEncoding, dst)
|
|
_, err := enc.Write(nonce[:])
|
|
if err != nil {
|
|
return fmt.Errorf("failed to write config file: %w", err)
|
|
}
|
|
|
|
var key [32]byte
|
|
copy(key[:], configKey[:32])
|
|
|
|
data, err := io.ReadAll(src)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
b := secretbox.Seal(nil, data, &nonce, &key)
|
|
_, err = enc.Write(b)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to write config file: %w", err)
|
|
}
|
|
return enc.Close()
|
|
}
|
|
|
|
// 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 := os.CreateTemp("", "rclone")
|
|
if err != nil {
|
|
return fmt.Errorf("cannot create temp file to store configKey: %w", err)
|
|
}
|
|
_, err = tempFile.WriteString(obscure.MustObscure(string(configKey)))
|
|
if err != nil {
|
|
errRemove := os.Remove(tempFile.Name())
|
|
if errRemove != nil {
|
|
return fmt.Errorf("error writing configKey to temp file and also error deleting it: %w", err)
|
|
}
|
|
return fmt.Errorf("error writing configKey to temp file: %w", err)
|
|
}
|
|
err = tempFile.Close()
|
|
if err != nil {
|
|
errRemove := os.Remove(tempFile.Name())
|
|
if errRemove != nil {
|
|
return fmt.Errorf("error closing temp file with configKey and also error deleting it: %w", err)
|
|
}
|
|
return fmt.Errorf("error closing temp file with configKey: %w", 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 {
|
|
return fmt.Errorf("unable to set environment variable _RCLONE_CONFIG_KEY_FILE and unable to delete the temp file: %w", err)
|
|
}
|
|
return fmt.Errorf("unable to set environment variable _RCLONE_CONFIG_KEY_FILE: %w", err)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// ClearConfigPassword sets the current the password to empty
|
|
func ClearConfigPassword() {
|
|
configKey = nil
|
|
}
|
|
|
|
// changeConfigPassword will query the user twice
|
|
// for a password. If the same password is entered
|
|
// twice the key is updated.
|
|
//
|
|
// This will use --password-command if configured to read the password.
|
|
func changeConfigPassword() {
|
|
// Set RCLONE_PASSWORD_CHANGE to "1" when calling the --password-command tool
|
|
_ = os.Setenv("RCLONE_PASSWORD_CHANGE", "1")
|
|
defer func() {
|
|
_ = os.Unsetenv("RCLONE_PASSWORD_CHANGE")
|
|
}()
|
|
pass, err := GetPasswordCommand(context.Background())
|
|
if err != nil {
|
|
fmt.Printf("Failed to read new password with --password-command: %v\n", err)
|
|
return
|
|
}
|
|
if pass == "" {
|
|
pass = ChangePassword("NEW configuration")
|
|
} else {
|
|
fmt.Printf("Read password using --password-command\n")
|
|
}
|
|
err = SetConfigPassword(pass)
|
|
if err != nil {
|
|
fmt.Printf("Failed to set config password: %v\n", err)
|
|
return
|
|
}
|
|
}
|
|
|
|
// ChangeConfigPasswordAndSave will query the user twice
|
|
// for a password. If the same password is entered
|
|
// twice the key is updated.
|
|
//
|
|
// This will use --password-command if configured to read the password.
|
|
//
|
|
// It will then save the config
|
|
func ChangeConfigPasswordAndSave() {
|
|
changeConfigPassword()
|
|
SaveConfig()
|
|
}
|
|
|
|
// RemoveConfigPasswordAndSave will clear the config password and save
|
|
// the unencrypted config file.
|
|
func RemoveConfigPasswordAndSave() {
|
|
configKey = nil
|
|
SaveConfig()
|
|
}
|