forked from TrueCloudLab/restic
Add --insecure-no-password option
This also includes two derived options `--from-insecure-no-password` used for commands that require specifying a source repository. And `--new-insecure-no-password` for the `key add` and `key passwd` commands. Specifying `--insecure-no-password` disabled the password prompt and immediately uses an empty password. Passing a password via CLI option or environment variable at the same time is an error.
This commit is contained in:
parent
d4b0d21199
commit
1d2277b4c3
9 changed files with 150 additions and 31 deletions
|
@ -257,7 +257,7 @@ func readFilenamesRaw(r io.Reader) (names []string, err error) {
|
|||
|
||||
// Check returns an error when an invalid combination of options was set.
|
||||
func (opts BackupOptions) Check(gopts GlobalOptions, args []string) error {
|
||||
if gopts.password == "" {
|
||||
if gopts.password == "" && !gopts.InsecureNoPassword {
|
||||
if opts.Stdin {
|
||||
return errors.Fatal("cannot read both password and data from stdin")
|
||||
}
|
||||
|
|
|
@ -627,3 +627,17 @@ func TestStdinFromCommandFailNoOutputAndExitCode(t *testing.T) {
|
|||
|
||||
testRunCheck(t, env.gopts)
|
||||
}
|
||||
|
||||
func TestBackupEmptyPassword(t *testing.T) {
|
||||
// basic sanity test that empty passwords work
|
||||
env, cleanup := withTestEnvironment(t)
|
||||
defer cleanup()
|
||||
|
||||
env.gopts.password = ""
|
||||
env.gopts.InsecureNoPassword = true
|
||||
|
||||
testSetupBackupData(t, env)
|
||||
testRunBackup(t, filepath.Dir(env.testdata), []string{"testdata"}, BackupOptions{}, env.gopts)
|
||||
testListSnapshots(t, env.gopts, 1)
|
||||
testRunCheck(t, env.gopts)
|
||||
}
|
||||
|
|
|
@ -13,10 +13,12 @@ func testRunCopy(t testing.TB, srcGopts GlobalOptions, dstGopts GlobalOptions) {
|
|||
gopts := srcGopts
|
||||
gopts.Repo = dstGopts.Repo
|
||||
gopts.password = dstGopts.password
|
||||
gopts.InsecureNoPassword = dstGopts.InsecureNoPassword
|
||||
copyOpts := CopyOptions{
|
||||
secondaryRepoOptions: secondaryRepoOptions{
|
||||
Repo: srcGopts.Repo,
|
||||
password: srcGopts.password,
|
||||
InsecureNoPassword: srcGopts.InsecureNoPassword,
|
||||
},
|
||||
}
|
||||
|
||||
|
@ -134,3 +136,22 @@ func TestCopyUnstableJSON(t *testing.T) {
|
|||
testRunCheck(t, env2.gopts)
|
||||
testListSnapshots(t, env2.gopts, 1)
|
||||
}
|
||||
|
||||
func TestCopyToEmptyPassword(t *testing.T) {
|
||||
env, cleanup := withTestEnvironment(t)
|
||||
defer cleanup()
|
||||
env2, cleanup2 := withTestEnvironment(t)
|
||||
defer cleanup2()
|
||||
env2.gopts.password = ""
|
||||
env2.gopts.InsecureNoPassword = true
|
||||
|
||||
testSetupBackupData(t, env)
|
||||
testRunBackup(t, "", []string{filepath.Join(env.testdata, "0", "0", "9")}, BackupOptions{}, env.gopts)
|
||||
|
||||
testRunInit(t, env2.gopts)
|
||||
testRunCopy(t, env.gopts, env2.gopts)
|
||||
|
||||
testListSnapshots(t, env.gopts, 1)
|
||||
testListSnapshots(t, env2.gopts, 1)
|
||||
testRunCheck(t, env2.gopts)
|
||||
}
|
||||
|
|
|
@ -28,12 +28,14 @@ Exit status is 0 if the command is successful, and non-zero if there was any err
|
|||
|
||||
type KeyAddOptions struct {
|
||||
NewPasswordFile string
|
||||
InsecureNoPassword bool
|
||||
Username string
|
||||
Hostname string
|
||||
}
|
||||
|
||||
func (opts *KeyAddOptions) Add(flags *pflag.FlagSet) {
|
||||
flags.StringVarP(&opts.NewPasswordFile, "new-password-file", "", "", "`file` from which to read the new password")
|
||||
flags.BoolVar(&opts.InsecureNoPassword, "new-insecure-no-password", false, "add an empty password for the repository (insecure)")
|
||||
flags.StringVarP(&opts.Username, "user", "", "", "the username for new key")
|
||||
flags.StringVarP(&opts.Hostname, "host", "", "", "the hostname for new key")
|
||||
}
|
||||
|
@ -63,7 +65,7 @@ func runKeyAdd(ctx context.Context, gopts GlobalOptions, opts KeyAddOptions, arg
|
|||
}
|
||||
|
||||
func addKey(ctx context.Context, repo *repository.Repository, gopts GlobalOptions, opts KeyAddOptions) error {
|
||||
pw, err := getNewPassword(ctx, gopts, opts.NewPasswordFile)
|
||||
pw, err := getNewPassword(ctx, gopts, opts.NewPasswordFile, opts.InsecureNoPassword)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -86,19 +88,35 @@ func addKey(ctx context.Context, repo *repository.Repository, gopts GlobalOption
|
|||
// testKeyNewPassword is used to set a new password during integration testing.
|
||||
var testKeyNewPassword string
|
||||
|
||||
func getNewPassword(ctx context.Context, gopts GlobalOptions, newPasswordFile string) (string, error) {
|
||||
func getNewPassword(ctx context.Context, gopts GlobalOptions, newPasswordFile string, insecureNoPassword bool) (string, error) {
|
||||
if testKeyNewPassword != "" {
|
||||
return testKeyNewPassword, nil
|
||||
}
|
||||
|
||||
if insecureNoPassword {
|
||||
if newPasswordFile != "" {
|
||||
return loadPasswordFromFile(newPasswordFile)
|
||||
return "", fmt.Errorf("only either --new-password-file or --new-insecure-no-password may be specified")
|
||||
}
|
||||
return "", nil
|
||||
}
|
||||
|
||||
if newPasswordFile != "" {
|
||||
password, err := loadPasswordFromFile(newPasswordFile)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if password == "" {
|
||||
return "", fmt.Errorf("an empty password is not allowed by default. Pass the flag `--new-insecure-no-password` to restic to disable this check")
|
||||
}
|
||||
return password, nil
|
||||
}
|
||||
|
||||
// Since we already have an open repository, temporary remove the password
|
||||
// to prompt the user for the passwd.
|
||||
newopts := gopts
|
||||
newopts.password = ""
|
||||
// empty passwords are already handled above
|
||||
newopts.InsecureNoPassword = false
|
||||
|
||||
return ReadPasswordTwice(ctx, newopts,
|
||||
"enter new password: ",
|
||||
|
|
|
@ -3,6 +3,8 @@ package main
|
|||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
"testing"
|
||||
|
@ -109,6 +111,43 @@ func TestKeyAddRemove(t *testing.T) {
|
|||
testRunKeyAddNewKeyUserHost(t, env.gopts)
|
||||
}
|
||||
|
||||
func TestKeyAddInvalid(t *testing.T) {
|
||||
env, cleanup := withTestEnvironment(t)
|
||||
defer cleanup()
|
||||
testRunInit(t, env.gopts)
|
||||
|
||||
err := runKeyAdd(context.TODO(), env.gopts, KeyAddOptions{
|
||||
NewPasswordFile: "some-file",
|
||||
InsecureNoPassword: true,
|
||||
}, []string{})
|
||||
rtest.Assert(t, strings.Contains(err.Error(), "only either"), "unexpected error message, got %q", err)
|
||||
|
||||
pwfile := filepath.Join(t.TempDir(), "pwfile")
|
||||
rtest.OK(t, os.WriteFile(pwfile, []byte{}, 0o666))
|
||||
|
||||
err = runKeyAdd(context.TODO(), env.gopts, KeyAddOptions{
|
||||
NewPasswordFile: pwfile,
|
||||
}, []string{})
|
||||
rtest.Assert(t, strings.Contains(err.Error(), "an empty password is not allowed by default"), "unexpected error message, got %q", err)
|
||||
}
|
||||
|
||||
func TestKeyAddEmpty(t *testing.T) {
|
||||
env, cleanup := withTestEnvironment(t)
|
||||
// must list keys more than once
|
||||
env.gopts.backendTestHook = nil
|
||||
defer cleanup()
|
||||
testRunInit(t, env.gopts)
|
||||
|
||||
rtest.OK(t, runKeyAdd(context.TODO(), env.gopts, KeyAddOptions{
|
||||
InsecureNoPassword: true,
|
||||
}, []string{}))
|
||||
|
||||
env.gopts.password = ""
|
||||
env.gopts.InsecureNoPassword = true
|
||||
|
||||
testRunCheck(t, env.gopts)
|
||||
}
|
||||
|
||||
type emptySaveBackend struct {
|
||||
backend.Backend
|
||||
}
|
||||
|
|
|
@ -53,7 +53,7 @@ func runKeyPasswd(ctx context.Context, gopts GlobalOptions, opts KeyPasswdOption
|
|||
}
|
||||
|
||||
func changePassword(ctx context.Context, repo *repository.Repository, gopts GlobalOptions, opts KeyPasswdOptions) error {
|
||||
pw, err := getNewPassword(ctx, gopts, opts.NewPasswordFile)
|
||||
pw, err := getNewPassword(ctx, gopts, opts.NewPasswordFile, opts.InsecureNoPassword)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
|
@ -68,6 +68,7 @@ type GlobalOptions struct {
|
|||
Compression repository.CompressionMode
|
||||
PackSize uint
|
||||
NoExtraVerify bool
|
||||
InsecureNoPassword bool
|
||||
|
||||
backend.TransportOptions
|
||||
limiter.Limits
|
||||
|
@ -125,6 +126,7 @@ func init() {
|
|||
f.BoolVar(&globalOptions.NoCache, "no-cache", false, "do not use a local cache")
|
||||
f.StringSliceVar(&globalOptions.RootCertFilenames, "cacert", nil, "`file` to load root certificates from (default: use system certificates or $RESTIC_CACERT)")
|
||||
f.StringVar(&globalOptions.TLSClientCertKeyFilename, "tls-client-cert", "", "path to a `file` containing PEM encoded TLS client certificate and private key (default: $RESTIC_TLS_CLIENT_CERT)")
|
||||
f.BoolVar(&globalOptions.InsecureNoPassword, "insecure-no-password", false, "use an empty password for the repository, must be passed to every restic command (insecure)")
|
||||
f.BoolVar(&globalOptions.InsecureTLS, "insecure-tls", false, "skip TLS certificate verification when connecting to the repository (insecure)")
|
||||
f.BoolVar(&globalOptions.CleanupCache, "cleanup-cache", false, "auto remove old cache directories")
|
||||
f.Var(&globalOptions.Compression, "compression", "compression mode (only available for repository format version 2), one of (auto|off|max) (default: $RESTIC_COMPRESSION)")
|
||||
|
@ -327,6 +329,13 @@ func readPasswordTerminal(ctx context.Context, in *os.File, out *os.File, prompt
|
|||
// variable RESTIC_PASSWORD or prompts the user. If the context is canceled,
|
||||
// the function leaks the password reading goroutine.
|
||||
func ReadPassword(ctx context.Context, opts GlobalOptions, prompt string) (string, error) {
|
||||
if opts.InsecureNoPassword {
|
||||
if opts.password != "" {
|
||||
return "", errors.Fatal("--insecure-no-password must not be specified together with providing a password via a cli option or environment variable")
|
||||
}
|
||||
return "", nil
|
||||
}
|
||||
|
||||
if opts.password != "" {
|
||||
return opts.password, nil
|
||||
}
|
||||
|
@ -348,7 +357,7 @@ func ReadPassword(ctx context.Context, opts GlobalOptions, prompt string) (strin
|
|||
}
|
||||
|
||||
if len(password) == 0 {
|
||||
return "", errors.Fatal("an empty password is not a password")
|
||||
return "", errors.Fatal("an empty password is not allowed by default. Pass the flag `--insecure-no-password` to restic to disable this check")
|
||||
}
|
||||
|
||||
return password, nil
|
||||
|
@ -445,7 +454,7 @@ func OpenRepository(ctx context.Context, opts GlobalOptions) (*repository.Reposi
|
|||
}
|
||||
|
||||
passwordTriesLeft := 1
|
||||
if stdinIsTerminal() && opts.password == "" {
|
||||
if stdinIsTerminal() && opts.password == "" && !opts.InsecureNoPassword {
|
||||
passwordTriesLeft = 3
|
||||
}
|
||||
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
rtest "github.com/restic/restic/internal/test"
|
||||
|
@ -50,3 +52,14 @@ func TestReadRepo(t *testing.T) {
|
|||
t.Fatal("must not read repository path from invalid file path")
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadEmptyPassword(t *testing.T) {
|
||||
opts := GlobalOptions{InsecureNoPassword: true}
|
||||
password, err := ReadPassword(context.TODO(), opts, "test")
|
||||
rtest.OK(t, err)
|
||||
rtest.Equals(t, "", password, "got unexpected password")
|
||||
|
||||
opts.password = "invalid"
|
||||
_, err = ReadPassword(context.TODO(), opts, "test")
|
||||
rtest.Assert(t, strings.Contains(err.Error(), "must not be specified together with providing a password via a cli option or environment variable"), "unexpected error message, got %v", err)
|
||||
}
|
||||
|
|
|
@ -16,6 +16,7 @@ type secondaryRepoOptions struct {
|
|||
PasswordFile string
|
||||
PasswordCommand string
|
||||
KeyHint string
|
||||
InsecureNoPassword bool
|
||||
// repo2 options
|
||||
LegacyRepo string
|
||||
LegacyRepositoryFile string
|
||||
|
@ -49,6 +50,7 @@ func initSecondaryRepoOptions(f *pflag.FlagSet, opts *secondaryRepoOptions, repo
|
|||
f.StringVarP(&opts.PasswordFile, "from-password-file", "", "", "`file` to read the source repository password from (default: $RESTIC_FROM_PASSWORD_FILE)")
|
||||
f.StringVarP(&opts.KeyHint, "from-key-hint", "", "", "key ID of key to try decrypting the source repository first (default: $RESTIC_FROM_KEY_HINT)")
|
||||
f.StringVarP(&opts.PasswordCommand, "from-password-command", "", "", "shell `command` to obtain the source repository password from (default: $RESTIC_FROM_PASSWORD_COMMAND)")
|
||||
f.BoolVar(&opts.InsecureNoPassword, "from-insecure-no-password", false, "use an empty password for the source repository, must be passed to every restic command (insecure)")
|
||||
|
||||
opts.Repo = os.Getenv("RESTIC_FROM_REPOSITORY")
|
||||
opts.RepositoryFile = os.Getenv("RESTIC_FROM_REPOSITORY_FILE")
|
||||
|
@ -63,7 +65,7 @@ func fillSecondaryGlobalOpts(ctx context.Context, opts secondaryRepoOptions, gop
|
|||
}
|
||||
|
||||
hasFromRepo := opts.Repo != "" || opts.RepositoryFile != "" || opts.PasswordFile != "" ||
|
||||
opts.KeyHint != "" || opts.PasswordCommand != ""
|
||||
opts.KeyHint != "" || opts.PasswordCommand != "" || opts.InsecureNoPassword
|
||||
hasRepo2 := opts.LegacyRepo != "" || opts.LegacyRepositoryFile != "" || opts.LegacyPasswordFile != "" ||
|
||||
opts.LegacyKeyHint != "" || opts.LegacyPasswordCommand != ""
|
||||
|
||||
|
@ -85,6 +87,7 @@ func fillSecondaryGlobalOpts(ctx context.Context, opts secondaryRepoOptions, gop
|
|||
dstGopts.PasswordFile = opts.PasswordFile
|
||||
dstGopts.PasswordCommand = opts.PasswordCommand
|
||||
dstGopts.KeyHint = opts.KeyHint
|
||||
dstGopts.InsecureNoPassword = opts.InsecureNoPassword
|
||||
|
||||
pwdEnv = "RESTIC_FROM_PASSWORD"
|
||||
repoPrefix = "source"
|
||||
|
@ -98,6 +101,8 @@ func fillSecondaryGlobalOpts(ctx context.Context, opts secondaryRepoOptions, gop
|
|||
dstGopts.PasswordFile = opts.LegacyPasswordFile
|
||||
dstGopts.PasswordCommand = opts.LegacyPasswordCommand
|
||||
dstGopts.KeyHint = opts.LegacyKeyHint
|
||||
// keep existing bevhaior for legacy options
|
||||
dstGopts.InsecureNoPassword = false
|
||||
|
||||
pwdEnv = "RESTIC_PASSWORD2"
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue