Merge bb8c60a22293c7dfec12ade6ef609280132013bf into 0b9671313b14ffe839ecbd7dd2ae5ac7f6f05db8

This commit is contained in:
Takanori Hirano 2025-04-11 19:29:46 +05:30 committed by GitHub
commit 77024d107b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 120 additions and 42 deletions

View File

@ -70,13 +70,20 @@ NB If filename_encryption is "off" then this option will do nothing.`,
},
}, {
Name: "password",
Help: "Password or pass phrase for encryption.",
Help: "Password or pass phrase for encryption.\n\npassword or password command is required.",
IsPassword: true,
Required: true,
}, {
Name: "password_command",
Help: "Command to retrieve the password or pass phrase for encryption.\n\npassword or password command is required.",
IsPassword: false,
}, {
Name: "password2",
Help: "Password or pass phrase for salt.\n\nOptional but recommended.\nShould be different to the previous password.",
IsPassword: true,
}, {
Name: "password2_command",
Help: "Command to retrieve the password or pass phrase for salt.\n\nOptional but recommended.\nShould be different to the previous password.",
IsPassword: false,
}, {
Name: "server_side_across_configs",
Default: false,
@ -182,18 +189,30 @@ func newCipherForConfig(opt *Options) (*Cipher, error) {
if err != nil {
return nil, err
}
if opt.Password == "" {
return nil, errors.New("password not set in config file")
}
password, err := obscure.Reveal(opt.Password)
if err != nil {
return nil, fmt.Errorf("failed to decrypt password: %w", err)
var password string
if opt.Password != "" {
password, err = obscure.Reveal(opt.Password)
if err != nil {
return nil, fmt.Errorf("failed to decrypt password: %w", err)
}
} else if len(opt.PasswordCommand) != 0 {
password, err = fs.ExecCommand(opt.PasswordCommand)
if err != nil {
return nil, fmt.Errorf("--crypt-password-command failed: %w", err)
}
} else {
return nil, errors.New("--crypt-password or --crypt-password-command is required")
}
var salt string
if opt.Password2 != "" {
salt, err = obscure.Reveal(opt.Password2)
if err != nil {
return nil, fmt.Errorf("failed to decrypt password2: %w", err)
return nil, fmt.Errorf("failed to decrypt salt: %w", err)
}
} else if len(opt.Password2Command) != 0 {
salt, err = fs.ExecCommand(opt.Password2Command)
if err != nil {
return nil, fmt.Errorf("--crypt-password2-command failed: %w", err)
}
}
enc, err := NewNameEncoding(opt.FilenameEncoding)
@ -302,18 +321,20 @@ func NewFs(ctx context.Context, name, rpath string, m configmap.Mapper) (fs.Fs,
// Options defines the configuration for this backend
type Options struct {
Remote string `config:"remote"`
FilenameEncryption string `config:"filename_encryption"`
DirectoryNameEncryption bool `config:"directory_name_encryption"`
NoDataEncryption bool `config:"no_data_encryption"`
Password string `config:"password"`
Password2 string `config:"password2"`
ServerSideAcrossConfigs bool `config:"server_side_across_configs"`
ShowMapping bool `config:"show_mapping"`
PassBadBlocks bool `config:"pass_bad_blocks"`
FilenameEncoding string `config:"filename_encoding"`
Suffix string `config:"suffix"`
StrictNames bool `config:"strict_names"`
Remote string `config:"remote"`
FilenameEncryption string `config:"filename_encryption"`
DirectoryNameEncryption bool `config:"directory_name_encryption"`
NoDataEncryption bool `config:"no_data_encryption"`
Password string `config:"password"`
PasswordCommand fs.SpaceSepList `config:"password_command"`
Password2 string `config:"password2"`
Password2Command fs.SpaceSepList `config:"password2_command"`
ServerSideAcrossConfigs bool `config:"server_side_across_configs"`
ShowMapping bool `config:"show_mapping"`
PassBadBlocks bool `config:"pass_bad_blocks"`
FilenameEncoding string `config:"filename_encoding"`
Suffix string `config:"suffix"`
StrictNames bool `config:"strict_names"`
}
// Fs represents a wrapped fs.Fs

View File

@ -95,6 +95,49 @@ func TestStandardBase32768(t *testing.T) {
})
}
func TestPasswordCommand(t *testing.T) {
if *fstest.RemoteName != "" {
t.Skip("Skipping as -remote set")
}
tempdir := filepath.Join(os.TempDir(), "rclone-crypt-test-standard")
name := "TestCrypt"
fstests.Run(t, &fstests.Opt{
RemoteName: name + ":",
NilObject: (*crypt.Object)(nil),
ExtraConfig: []fstests.ExtraConfigItem{
{Name: name, Key: "type", Value: "crypt"},
{Name: name, Key: "remote", Value: tempdir},
{Name: name, Key: "password_command", Value: "echo potato"},
{Name: name, Key: "filename_encryption", Value: "standard"},
},
UnimplementableFsMethods: []string{"OpenWriterAt", "OpenChunkWriter"},
UnimplementableObjectMethods: []string{"MimeType"},
QuickTestOK: true,
})
}
func TestPassword2Command(t *testing.T) {
if *fstest.RemoteName != "" {
t.Skip("Skipping as -remote set")
}
tempdir := filepath.Join(os.TempDir(), "rclone-crypt-test-standard")
name := "TestCrypt"
fstests.Run(t, &fstests.Opt{
RemoteName: name + ":",
NilObject: (*crypt.Object)(nil),
ExtraConfig: []fstests.ExtraConfigItem{
{Name: name, Key: "type", Value: "crypt"},
{Name: name, Key: "remote", Value: tempdir},
{Name: name, Key: "password_command", Value: "echo potato"},
{Name: name, Key: "password2_command", Value: "echo potato"},
{Name: name, Key: "filename_encryption", Value: "standard"},
},
UnimplementableFsMethods: []string{"OpenWriterAt", "OpenChunkWriter"},
UnimplementableObjectMethods: []string{"MimeType"},
QuickTestOK: true,
})
}
// TestOff runs integration tests against the remote
func TestOff(t *testing.T) {
if *fstest.RemoteName != "" {

View File

@ -11,7 +11,6 @@ import (
"fmt"
"io"
"os"
"os/exec"
"strings"
"golang.org/x/crypto/nacl/secretbox"
@ -179,27 +178,9 @@ func GetPasswordCommand(ctx context.Context) (pass string, err error) {
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()
pass, err = fs.ExecCommand(ci.PasswordCommand)
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 "", fmt.Errorf("--password-command failed: %w", err)
}
return pass, nil
}

View File

@ -3,7 +3,11 @@ package fs
import (
"bytes"
"encoding/csv"
"errors"
"fmt"
"os"
"os/exec"
"strings"
)
// CommaSepList is a comma separated config value
@ -92,3 +96,32 @@ func (gl *genericList) scan(sep rune, s fmt.ScanState, ch rune) error {
}
return gl.set(sep, bytes.TrimSpace(token))
}
// ExecCommand executes a command and returns the output as a string
// It returns an error if the command fails or the output is empty
func ExecCommand(l SpaceSepList) (pass string, err error) {
var stdout bytes.Buffer
var stderr bytes.Buffer
cmd := exec.Command(l[0], l[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.
Errorf(nil, "Executing command %q failed: %v", l, err)
if ers := strings.TrimSpace(stderr.String()); ers != "" {
Errorf(nil, "stderr: %q", ers)
}
return pass, fmt.Errorf("executing command %q failed: %w", l, err)
}
pass = strings.Trim(stdout.String(), "\r\n")
if pass == "" {
return pass, errors.New("executing command %q failed: returned empty string")
}
return pass, nil
}