1
mirror of https://github.com/rclone/rclone synced 2024-12-29 22:26:24 +01:00
rclone/fs/config/crypt.go
albertony 5d6b8141ec Replace deprecated ioutil
As of Go 1.16, the same functionality is now provided by package io or
package os, and those implementations should be preferred in new code.
2022-11-07 11:41:47 +00:00

306 lines
8.5 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
)
// 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, fmt.Errorf("password command failed: %w", err)
}
if pass := strings.Trim(stdout.String(), "\r\n"); pass != "" {
err := SetConfigPassword(pass)
if err != nil {
return nil, fmt.Errorf("incorrect password: %w", err)
}
} 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 := 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
}
// 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.
func changeConfigPassword() {
err := SetConfigPassword(ChangePassword("NEW configuration"))
if err != nil {
fmt.Printf("Failed to set config password: %v\n", err)
return
}
}