github.com/rclone/rclone@v1.66.1-0.20240517100346-7b89735ae726/fs/config/crypt.go (about)

     1  package config
     2  
     3  import (
     4  	"bufio"
     5  	"bytes"
     6  	"context"
     7  	"crypto/rand"
     8  	"crypto/sha256"
     9  	"encoding/base64"
    10  	"errors"
    11  	"fmt"
    12  	"io"
    13  	"os"
    14  	"os/exec"
    15  	"strings"
    16  
    17  	"golang.org/x/crypto/nacl/secretbox"
    18  
    19  	"github.com/rclone/rclone/fs"
    20  	"github.com/rclone/rclone/fs/config/obscure"
    21  )
    22  
    23  var (
    24  	// Key to use for password en/decryption.
    25  	// When nil, no encryption will be used for saving.
    26  	configKey []byte
    27  
    28  	// PasswordPromptOutput is output of prompt for password
    29  	PasswordPromptOutput = os.Stderr
    30  
    31  	// PassConfigKeyForDaemonization if set to true, the configKey
    32  	// is obscured with obscure.Obscure and saved to a temp file
    33  	// when it is calculated from the password. The path of that
    34  	// temp file is then written to the environment variable
    35  	// `_RCLONE_CONFIG_KEY_FILE`. If `_RCLONE_CONFIG_KEY_FILE` is
    36  	// present, password prompt is skipped and
    37  	// `RCLONE_CONFIG_PASS` ignored. For security reasons, the
    38  	// temp file is deleted once the configKey is successfully
    39  	// loaded. This can be used to pass the configKey to a child
    40  	// process.
    41  	PassConfigKeyForDaemonization = false
    42  )
    43  
    44  // Decrypt will automatically decrypt a reader
    45  func Decrypt(b io.ReadSeeker) (io.Reader, error) {
    46  	ctx := context.Background()
    47  	ci := fs.GetConfig(ctx)
    48  	var usingPasswordCommand bool
    49  
    50  	// Find first non-empty line
    51  	r := bufio.NewReader(b)
    52  	for {
    53  		line, _, err := r.ReadLine()
    54  		if err != nil {
    55  			if err == io.EOF {
    56  				if _, err := b.Seek(0, io.SeekStart); err != nil {
    57  					return nil, err
    58  				}
    59  				return b, nil
    60  			}
    61  			return nil, err
    62  		}
    63  		l := strings.TrimSpace(string(line))
    64  		if len(l) == 0 || strings.HasPrefix(l, ";") || strings.HasPrefix(l, "#") {
    65  			continue
    66  		}
    67  		// First non-empty or non-comment must be ENCRYPT_V0
    68  		if l == "RCLONE_ENCRYPT_V0:" {
    69  			break
    70  		}
    71  		if strings.HasPrefix(l, "RCLONE_ENCRYPT_V") {
    72  			return nil, errors.New("unsupported configuration encryption - update rclone for support")
    73  		}
    74  		if _, err := b.Seek(0, io.SeekStart); err != nil {
    75  			return nil, err
    76  		}
    77  		return b, nil
    78  	}
    79  
    80  	if len(configKey) == 0 {
    81  		if len(ci.PasswordCommand) != 0 {
    82  			var stdout bytes.Buffer
    83  			var stderr bytes.Buffer
    84  
    85  			cmd := exec.Command(ci.PasswordCommand[0], ci.PasswordCommand[1:]...)
    86  
    87  			cmd.Stdout = &stdout
    88  			cmd.Stderr = &stderr
    89  			cmd.Stdin = os.Stdin
    90  
    91  			if err := cmd.Run(); err != nil {
    92  				// One does not always get the stderr returned in the wrapped error.
    93  				fs.Errorf(nil, "Using --password-command returned: %v", err)
    94  				if ers := strings.TrimSpace(stderr.String()); ers != "" {
    95  					fs.Errorf(nil, "--password-command stderr: %s", ers)
    96  				}
    97  				return nil, fmt.Errorf("password command failed: %w", err)
    98  			}
    99  			if pass := strings.Trim(stdout.String(), "\r\n"); pass != "" {
   100  				err := SetConfigPassword(pass)
   101  				if err != nil {
   102  					return nil, fmt.Errorf("incorrect password: %w", err)
   103  				}
   104  			} else {
   105  				return nil, errors.New("password-command returned empty string")
   106  			}
   107  
   108  			if len(configKey) == 0 {
   109  				return nil, errors.New("unable to decrypt configuration: incorrect password")
   110  			}
   111  			usingPasswordCommand = true
   112  		} else {
   113  			usingPasswordCommand = false
   114  
   115  			envpw := os.Getenv("RCLONE_CONFIG_PASS")
   116  
   117  			if envpw != "" {
   118  				err := SetConfigPassword(envpw)
   119  				if err != nil {
   120  					fs.Errorf(nil, "Using RCLONE_CONFIG_PASS returned: %v", err)
   121  				} else {
   122  					fs.Debugf(nil, "Using RCLONE_CONFIG_PASS password.")
   123  				}
   124  			}
   125  		}
   126  	}
   127  
   128  	// Encrypted content is base64 encoded.
   129  	dec := base64.NewDecoder(base64.StdEncoding, r)
   130  	box, err := io.ReadAll(dec)
   131  	if err != nil {
   132  		return nil, fmt.Errorf("failed to load base64 encoded data: %w", err)
   133  	}
   134  	if len(box) < 24+secretbox.Overhead {
   135  		return nil, errors.New("configuration data too short")
   136  	}
   137  
   138  	var out []byte
   139  	for {
   140  		if envKeyFile := os.Getenv("_RCLONE_CONFIG_KEY_FILE"); len(envKeyFile) > 0 {
   141  			fs.Debugf(nil, "attempting to obtain configKey from temp file %s", envKeyFile)
   142  			obscuredKey, err := os.ReadFile(envKeyFile)
   143  			if err != nil {
   144  				errRemove := os.Remove(envKeyFile)
   145  				if errRemove != nil {
   146  					return nil, fmt.Errorf("unable to read obscured config key and unable to delete the temp file: %w", err)
   147  				}
   148  				return nil, fmt.Errorf("unable to read obscured config key: %w", err)
   149  			}
   150  			errRemove := os.Remove(envKeyFile)
   151  			if errRemove != nil {
   152  				return nil, fmt.Errorf("unable to delete temp file with configKey: %w", errRemove)
   153  			}
   154  			configKey = []byte(obscure.MustReveal(string(obscuredKey)))
   155  			fs.Debugf(nil, "using _RCLONE_CONFIG_KEY_FILE for configKey")
   156  		} else {
   157  			if len(configKey) == 0 {
   158  				if usingPasswordCommand {
   159  					return nil, errors.New("using --password-command derived password, unable to decrypt configuration")
   160  				}
   161  				if !ci.AskPassword {
   162  					return nil, errors.New("unable to decrypt configuration and not allowed to ask for password - set RCLONE_CONFIG_PASS to your configuration password")
   163  				}
   164  				getConfigPassword("Enter configuration password:")
   165  			}
   166  		}
   167  
   168  		// Nonce is first 24 bytes of the ciphertext
   169  		var nonce [24]byte
   170  		copy(nonce[:], box[:24])
   171  		var key [32]byte
   172  		copy(key[:], configKey[:32])
   173  
   174  		// Attempt to decrypt
   175  		var ok bool
   176  		out, ok = secretbox.Open(nil, box[24:], &nonce, &key)
   177  		if ok {
   178  			break
   179  		}
   180  
   181  		// Retry
   182  		fs.Errorf(nil, "Couldn't decrypt configuration, most likely wrong password.")
   183  		configKey = nil
   184  	}
   185  	return bytes.NewReader(out), nil
   186  }
   187  
   188  // Encrypt the config file
   189  func Encrypt(src io.Reader, dst io.Writer) error {
   190  	if len(configKey) == 0 {
   191  		_, err := io.Copy(dst, src)
   192  		return err
   193  	}
   194  
   195  	_, _ = fmt.Fprintln(dst, "# Encrypted rclone configuration File")
   196  	_, _ = fmt.Fprintln(dst, "")
   197  	_, _ = fmt.Fprintln(dst, "RCLONE_ENCRYPT_V0:")
   198  
   199  	// Generate new nonce and write it to the start of the ciphertext
   200  	var nonce [24]byte
   201  	n, _ := rand.Read(nonce[:])
   202  	if n != 24 {
   203  		return fmt.Errorf("nonce short read: %d", n)
   204  	}
   205  	enc := base64.NewEncoder(base64.StdEncoding, dst)
   206  	_, err := enc.Write(nonce[:])
   207  	if err != nil {
   208  		return fmt.Errorf("failed to write config file: %w", err)
   209  	}
   210  
   211  	var key [32]byte
   212  	copy(key[:], configKey[:32])
   213  
   214  	data, err := io.ReadAll(src)
   215  	if err != nil {
   216  		return err
   217  	}
   218  	b := secretbox.Seal(nil, data, &nonce, &key)
   219  	_, err = enc.Write(b)
   220  	if err != nil {
   221  		return fmt.Errorf("failed to write config file: %w", err)
   222  	}
   223  	return enc.Close()
   224  }
   225  
   226  // getConfigPassword will query the user for a password the
   227  // first time it is required.
   228  func getConfigPassword(q string) {
   229  	if len(configKey) != 0 {
   230  		return
   231  	}
   232  	for {
   233  		password := GetPassword(q)
   234  		err := SetConfigPassword(password)
   235  		if err == nil {
   236  			return
   237  		}
   238  		_, _ = fmt.Fprintln(os.Stderr, "Error:", err)
   239  	}
   240  }
   241  
   242  // SetConfigPassword will set the configKey to the hash of
   243  // the password. If the length of the password is
   244  // zero after trimming+normalization, an error is returned.
   245  func SetConfigPassword(password string) error {
   246  	password, err := checkPassword(password)
   247  	if err != nil {
   248  		return err
   249  	}
   250  	// Create SHA256 has of the password
   251  	sha := sha256.New()
   252  	_, err = sha.Write([]byte("[" + password + "][rclone-config]"))
   253  	if err != nil {
   254  		return err
   255  	}
   256  	configKey = sha.Sum(nil)
   257  	if PassConfigKeyForDaemonization {
   258  		tempFile, err := os.CreateTemp("", "rclone")
   259  		if err != nil {
   260  			return fmt.Errorf("cannot create temp file to store configKey: %w", err)
   261  		}
   262  		_, err = tempFile.WriteString(obscure.MustObscure(string(configKey)))
   263  		if err != nil {
   264  			errRemove := os.Remove(tempFile.Name())
   265  			if errRemove != nil {
   266  				return fmt.Errorf("error writing configKey to temp file and also error deleting it: %w", err)
   267  			}
   268  			return fmt.Errorf("error writing configKey to temp file: %w", err)
   269  		}
   270  		err = tempFile.Close()
   271  		if err != nil {
   272  			errRemove := os.Remove(tempFile.Name())
   273  			if errRemove != nil {
   274  				return fmt.Errorf("error closing temp file with configKey and also error deleting it: %w", err)
   275  			}
   276  			return fmt.Errorf("error closing temp file with configKey: %w", err)
   277  		}
   278  		fs.Debugf(nil, "saving configKey to temp file")
   279  		err = os.Setenv("_RCLONE_CONFIG_KEY_FILE", tempFile.Name())
   280  		if err != nil {
   281  			errRemove := os.Remove(tempFile.Name())
   282  			if errRemove != nil {
   283  				return fmt.Errorf("unable to set environment variable _RCLONE_CONFIG_KEY_FILE and unable to delete the temp file: %w", err)
   284  			}
   285  			return fmt.Errorf("unable to set environment variable _RCLONE_CONFIG_KEY_FILE: %w", err)
   286  		}
   287  	}
   288  	return nil
   289  }
   290  
   291  // ClearConfigPassword sets the current the password to empty
   292  func ClearConfigPassword() {
   293  	configKey = nil
   294  }
   295  
   296  // changeConfigPassword will query the user twice
   297  // for a password. If the same password is entered
   298  // twice the key is updated.
   299  func changeConfigPassword() {
   300  	err := SetConfigPassword(ChangePassword("NEW configuration"))
   301  	if err != nil {
   302  		fmt.Printf("Failed to set config password: %v\n", err)
   303  		return
   304  	}
   305  }