From 730ba7b94acb0b16a7b3da05066ed342b3f4a7c9 Mon Sep 17 00:00:00 2001 From: Jarek Kowalski Date: Sat, 17 Jul 2021 07:58:02 -0700 Subject: [PATCH] Repository password change support (#1197) * repo: added 'enable password change' flag (defaults to true for new repositories), which prevents embedding replicas of kopia.repository in pack blobs * cli: added 'repo change-password' which can change the password of a connected repository * repo: nit - renamed variables and functions dealing with key derivation * repo: fixed cache validation HMAC secret to use stored HMAC secret instead of password-derived one * cli: added test for repo change-password * repo: negative cases for attempting to change password in an old repository * Update cli/command_repository_change_password.go Co-authored-by: Julio Lopez Co-authored-by: Julio Lopez --- cli/command_repository.go | 18 ++++--- cli/command_repository_change_password.go | 50 +++++++++++++++++++ ...command_repository_change_password_test.go | 46 +++++++++++++++++ cli/command_repository_create.go | 3 ++ cli/command_repository_repair.go | 6 +-- cli/command_repository_status.go | 1 + cli/password.go | 20 ++++++++ internal/repotesting/repotesting.go | 29 +++++++---- internal/repotesting/repotesting_test.go | 2 +- repo/change_password.go | 47 +++++++++++++++++ repo/content/content_formatting_options.go | 2 + repo/crypto_key_derivation_nontest.go | 2 +- repo/crypto_key_derivation_testing.go | 2 +- repo/initialize.go | 7 +-- repo/open.go | 48 ++++++++++++------ repo/parameters.go | 4 +- repo/repository.go | 26 ++++++---- repo/repository_test.go | 10 ++++ .../end_to_end_test/repository_repair_test.go | 19 ++++++- tests/testenv/cli_inproc_runner.go | 10 ++-- 20 files changed, 292 insertions(+), 60 deletions(-) create mode 100644 cli/command_repository_change_password.go create mode 100644 cli/command_repository_change_password_test.go create mode 100644 repo/change_password.go diff --git a/cli/command_repository.go b/cli/command_repository.go index 1eb9aaa67..276e96763 100644 --- a/cli/command_repository.go +++ b/cli/command_repository.go @@ -1,14 +1,15 @@ package cli type commandRepository struct { - connect commandRepositoryConnect - create commandRepositoryCreate - disconnect commandRepositoryDisconnect - repair commandRepositoryRepair - setClient commandRepositorySetClient - setParameters commandRepositorySetParameters - status commandRepositoryStatus - syncTo commandRepositorySyncTo + connect commandRepositoryConnect + create commandRepositoryCreate + disconnect commandRepositoryDisconnect + repair commandRepositoryRepair + setClient commandRepositorySetClient + setParameters commandRepositorySetParameters + changePassword commandRepositoryChangePassword + status commandRepositoryStatus + syncTo commandRepositorySyncTo } func (c *commandRepository) setup(svc advancedAppServices, parent commandParent) { @@ -22,4 +23,5 @@ func (c *commandRepository) setup(svc advancedAppServices, parent commandParent) c.setParameters.setup(svc, cmd) c.status.setup(svc, cmd) c.syncTo.setup(svc, cmd) + c.changePassword.setup(svc, cmd) } diff --git a/cli/command_repository_change_password.go b/cli/command_repository_change_password.go new file mode 100644 index 000000000..5bb587ae0 --- /dev/null +++ b/cli/command_repository_change_password.go @@ -0,0 +1,50 @@ +package cli + +import ( + "context" + + "github.com/pkg/errors" + + "github.com/kopia/kopia/repo" +) + +type commandRepositoryChangePassword struct { + newPassword string + + svc advancedAppServices +} + +func (c *commandRepositoryChangePassword) setup(svc advancedAppServices, parent commandParent) { + cmd := parent.Command("change-password", "Change repository password") + cmd.Flag("new-password", "New password").Envar("KOPIA_NEW_PASSWORD").StringVar(&c.newPassword) + + c.svc = svc + cmd.Action(svc.directRepositoryWriteAction(c.run)) +} + +func (c *commandRepositoryChangePassword) run(ctx context.Context, rep repo.DirectRepositoryWriter) error { + var newPass string + + if c.newPassword == "" { + n, err := askForChangedRepositoryPassword(c.svc.stdout()) + if err != nil { + return err + } + + newPass = n + } else { + newPass = c.newPassword + } + + if err := rep.ChangePassword(ctx, newPass); err != nil { + return errors.Wrap(err, "unable to change password") + } + + log(ctx).Infof(`NOTE: Repository password has been changed.`) + + if err := c.svc.passwordPersistenceStrategy().PersistPassword(ctx, c.svc.repositoryConfigFileName(), newPass); err != nil { + return errors.Wrap(err, "unable to persist password") + } + + return nil +} diff --git a/cli/command_repository_change_password_test.go b/cli/command_repository_change_password_test.go new file mode 100644 index 000000000..09e5772cb --- /dev/null +++ b/cli/command_repository_change_password_test.go @@ -0,0 +1,46 @@ +package cli_test + +import ( + "testing" + + "github.com/kopia/kopia/tests/testenv" +) + +func TestRepositoryChangePassword(t *testing.T) { + r1 := testenv.NewInProcRunner(t) + r2 := testenv.NewInProcRunner(t) + env1 := testenv.NewCLITest(t, r1) + env2 := testenv.NewCLITest(t, r2) + + env1.RunAndExpectSuccess(t, "repo", "create", "filesystem", "--path", env1.RepoDir, "--disable-repository-format-cache") + env1.RunAndExpectSuccess(t, "snapshot", "ls") + + // connect to repo with --disable-repository-format-cache so that format blob is not cached + // this makes password changes immediate + env2.RunAndExpectSuccess(t, "repo", "connect", "filesystem", "--path", env1.RepoDir, "--disable-repository-format-cache") + env2.RunAndExpectSuccess(t, "snapshot", "ls") + + env1.RunAndExpectSuccess(t, "repo", "change-password", "--new-password", "newPass") + + // at this point env2 stops working + env2.RunAndExpectFailure(t, "snapshot", "ls") + + r3 := testenv.NewInProcRunner(t) + + // new connections will fail when using old (default) password + env3 := testenv.NewCLITest(t, r3) + env3.RunAndExpectFailure(t, "repo", "connect", "filesystem", "--path", env1.RepoDir, "--disable-repository-format-cache") + + // new connections will succeed when using new password + r3.RepoPassword = "newPass" + + env3.RunAndExpectSuccess(t, "repo", "connect", "filesystem", "--path", env1.RepoDir, "--disable-repository-format-cache") +} + +func TestRepositoryChangePassword_LegacDisallowed(t *testing.T) { + env1 := testenv.NewCLITest(t, testenv.NewInProcRunner(t)) + // pass --no-enable-password-change to create repository using old format that does + // not support password change. + env1.RunAndExpectSuccess(t, "repo", "create", "filesystem", "--path", env1.RepoDir, "--disable-repository-format-cache", "--no-enable-password-change") + env1.RunAndExpectFailure(t, "repo", "change-password", "--new-password", "newPass") +} diff --git a/cli/command_repository_create.go b/cli/command_repository_create.go index 8984f2258..ad371fe79 100644 --- a/cli/command_repository_create.go +++ b/cli/command_repository_create.go @@ -24,6 +24,7 @@ type commandRepositoryCreate struct { createOnly bool createIndexVersion int createIndexEpochs bool + enablePasswordChange bool co connectOptions svc advancedAppServices @@ -37,6 +38,7 @@ func (c *commandRepositoryCreate) setup(svc advancedAppServices, parent commandP cmd.Flag("encryption", "Content encryption algorithm.").PlaceHolder("ALGO").Default(encryption.DefaultAlgorithm).EnumVar(&c.createBlockEncryptionFormat, encryption.SupportedAlgorithms(false)...) cmd.Flag("object-splitter", "The splitter to use for new objects in the repository").Default(splitter.DefaultAlgorithm).EnumVar(&c.createSplitter, splitter.SupportedAlgorithms()...) cmd.Flag("create-only", "Create repository, but don't connect to it.").Short('c').BoolVar(&c.createOnly) + cmd.Flag("enable-password-change", "Enable password change").Hidden().Default("true").BoolVar(&c.enablePasswordChange) cmd.Flag("index-version", "Force particular index version").Hidden().Envar("KOPIA_CREATE_INDEX_VERSION").IntVar(&c.createIndexVersion) cmd.Flag("enable-index-epochs", "Enable index epochs").Hidden().BoolVar(&c.createIndexEpochs) @@ -82,6 +84,7 @@ func (c *commandRepositoryCreate) newRepositoryOptionsFromFlags() *repo.NewRepos IndexVersion: c.createIndexVersion, EpochParameters: c.epochParametersFromFlags(), }, + EnablePasswordChange: c.enablePasswordChange, }, ObjectFormat: object.Format{ diff --git a/cli/command_repository_repair.go b/cli/command_repository_repair.go index 68f502eb3..de572192a 100644 --- a/cli/command_repository_repair.go +++ b/cli/command_repository_repair.go @@ -15,7 +15,7 @@ type commandRepositoryRepair struct { repairCommandRecoverFormatBlob string repairCommandRecoverFormatBlobPrefixes []string - repairDryDrun bool + repairDryRun bool } func (c *commandRepositoryRepair) setup(svc advancedAppServices, parent commandParent) { @@ -23,7 +23,7 @@ func (c *commandRepositoryRepair) setup(svc advancedAppServices, parent commandP cmd.Flag("recover-format", "Recover format blob from a copy").Default("auto").EnumVar(&c.repairCommandRecoverFormatBlob, "auto", "yes", "no") cmd.Flag("recover-format-block-prefixes", "Prefixes of file names").StringsVar(&c.repairCommandRecoverFormatBlobPrefixes) - cmd.Flag("dry-run", "Do not modify repository").Short('n').BoolVar(&c.repairDryDrun) + cmd.Flag("dry-run", "Do not modify repository").Short('n').BoolVar(&c.repairDryRun) for _, prov := range storageProviders { f := prov.newFlags() @@ -80,7 +80,7 @@ func (c *commandRepositoryRepair) recoverFormatBlob(ctx context.Context, st blob err := st.ListBlobs(ctx, blob.ID(prefix), func(bi blob.Metadata) error { log(ctx).Infof("looking for replica of format blob in %v...", bi.BlobID) if b, err := repo.RecoverFormatBlob(ctx, st, bi.BlobID, bi.Length); err == nil { - if !c.repairDryDrun { + if !c.repairDryRun { if puterr := st.PutBlob(ctx, repo.FormatBlobID, gather.FromSlice(b)); puterr != nil { return errors.Wrap(puterr, "error writing format blob") } diff --git a/cli/command_repository_status.go b/cli/command_repository_status.go index 2c0a02fba..6f911a079 100644 --- a/cli/command_repository_status.go +++ b/cli/command_repository_status.go @@ -67,6 +67,7 @@ func (c *commandRepositoryStatus) run(ctx context.Context, rep repo.Repository) c.out.printStdout("Splitter: %v\n", dr.ObjectFormat().Splitter) c.out.printStdout("Format version: %v\n", dr.ContentReader().ContentFormat().Version) c.out.printStdout("Content compression: %v\n", dr.ContentReader().SupportsContentCompression()) + c.out.printStdout("Password changes: %v\n", dr.ContentReader().ContentFormat().EnablePasswordChange) c.out.printStdout("Max pack length: %v\n", units.BytesStringBase2(int64(dr.ContentReader().ContentFormat().MaxPackSize))) c.out.printStdout("Index Format: v%v\n", dr.ContentReader().ContentFormat().IndexVersion) diff --git a/cli/password.go b/cli/password.go index ec8a67310..46836e344 100644 --- a/cli/password.go +++ b/cli/password.go @@ -33,6 +33,26 @@ func askForNewRepositoryPassword(out io.Writer) (string, error) { } } +func askForChangedRepositoryPassword(out io.Writer) (string, error) { + for { + p1, err := askPass(out, "Enter new password: ") + if err != nil { + return "", errors.Wrap(err, "password entry") + } + + p2, err := askPass(out, "Re-enter password for verification: ") + if err != nil { + return "", errors.Wrap(err, "password verification") + } + + if p1 != p2 { + fmt.Println("Passwords don't match!") + } else { + return p1, nil + } + } +} + func askForExistingRepositoryPassword(out io.Writer) (string, error) { p1, err := askPass(out, "Enter password to open repository: ") if err != nil { diff --git a/internal/repotesting/repotesting.go b/internal/repotesting/repotesting.go index 879d60860..cfc371c54 100644 --- a/internal/repotesting/repotesting.go +++ b/internal/repotesting/repotesting.go @@ -17,13 +17,15 @@ "github.com/kopia/kopia/repo/object" ) -const masterPassword = "foobarbazfoobarbaz" +const defaultPassword = "foobarbazfoobarbaz" // Environment encapsulates details of a test environment. type Environment struct { Repository repo.Repository RepositoryWriter repo.DirectRepositoryWriter + Password string + configDir string storageDir string connected bool @@ -46,9 +48,10 @@ func (e *Environment) setup(t *testing.T, opts ...Options) *Environment { opt := &repo.NewRepositoryOptions{ BlockFormat: content.FormattingOptions{ - HMACSecret: []byte{}, - Hash: "HMAC-SHA256", - Encryption: encryption.DefaultAlgorithm, + HMACSecret: []byte{}, + Hash: "HMAC-SHA256", + Encryption: encryption.DefaultAlgorithm, + EnablePasswordChange: true, }, ObjectFormat: object.Format{ Splitter: "FIXED-1M", @@ -72,17 +75,21 @@ func (e *Environment) setup(t *testing.T, opts ...Options) *Environment { t.Fatalf("err: %v", err) } - if err = repo.Initialize(ctx, st, opt, masterPassword); err != nil { + if e.Password == "" { + e.Password = defaultPassword + } + + if err = repo.Initialize(ctx, st, opt, e.Password); err != nil { t.Fatalf("err: %v", err) } - if err = repo.Connect(ctx, e.ConfigFile(), st, masterPassword, nil); err != nil { + if err = repo.Connect(ctx, e.ConfigFile(), st, e.Password, nil); err != nil { t.Fatalf("can't connect: %v", err) } e.connected = true - rep, err := repo.Open(ctx, e.ConfigFile(), masterPassword, openOpt) + rep, err := repo.Open(ctx, e.ConfigFile(), e.Password, openOpt) if err != nil { t.Fatalf("can't open: %v", err) } @@ -135,7 +142,7 @@ func (e *Environment) MustReopen(t *testing.T, openOpts ...func(*repo.Options)) t.Fatalf("close error: %v", err) } - rep, err := repo.Open(ctx, e.ConfigFile(), masterPassword, repoOptions(openOpts)) + rep, err := repo.Open(ctx, e.ConfigFile(), e.Password, repoOptions(openOpts)) if err != nil { t.Fatalf("err: %v", err) } @@ -154,7 +161,7 @@ func (e *Environment) MustOpenAnother(t *testing.T) repo.RepositoryWriter { ctx := testlogging.Context(t) - rep2, err := repo.Open(ctx, e.ConfigFile(), masterPassword, &repo.Options{}) + rep2, err := repo.Open(ctx, e.ConfigFile(), e.Password, &repo.Options{}) if err != nil { t.Fatalf("err: %v", err) } @@ -192,11 +199,11 @@ func (e *Environment) MustConnectOpenAnother(t *testing.T, openOpts ...func(*rep }, } - if err = repo.Connect(ctx, config, st, masterPassword, connOpts); err != nil { + if err = repo.Connect(ctx, config, st, e.Password, connOpts); err != nil { t.Fatal("can't connect:", err) } - rep, err := repo.Open(ctx, e.ConfigFile(), masterPassword, repoOptions(openOpts)) + rep, err := repo.Open(ctx, e.ConfigFile(), e.Password, repoOptions(openOpts)) if err != nil { t.Fatal("can't open:", err) } diff --git a/internal/repotesting/repotesting_test.go b/internal/repotesting/repotesting_test.go index 2375038ce..cfb6796fe 100644 --- a/internal/repotesting/repotesting_test.go +++ b/internal/repotesting/repotesting_test.go @@ -19,7 +19,7 @@ func TestTimeFuncWiring(t *testing.T) { ft := faketime.NewTimeAdvance(time.Date(2018, time.February, 6, 0, 0, 0, 0, time.UTC), 0) // Re open with injected time - rep, err := repo.Open(ctx, env.RepositoryWriter.ConfigFilename(), masterPassword, &repo.Options{TimeNowFunc: ft.NowFunc()}) + rep, err := repo.Open(ctx, env.RepositoryWriter.ConfigFilename(), env.Password, &repo.Options{TimeNowFunc: ft.NowFunc()}) if err != nil { t.Fatal("Failed to open repo:", err) } diff --git a/repo/change_password.go b/repo/change_password.go new file mode 100644 index 000000000..bfc9b20d5 --- /dev/null +++ b/repo/change_password.go @@ -0,0 +1,47 @@ +package repo + +import ( + "context" + "os" + "path/filepath" + + "github.com/pkg/errors" +) + +// ChangePassword changes the repository password and rewrites `kopia.repository`. +func (r *directRepository) ChangePassword(ctx context.Context, newPassword string) error { + f := r.formatBlob + + repoConfig, err := f.decryptFormatBytes(r.formatEncryptionKey) + if err != nil { + return errors.Wrap(err, "unable to decrypt repository config") + } + + if !repoConfig.EnablePasswordChange { + return errors.Errorf("password changes are not supported for repositories created using Kopia v0.8 or older") + } + + newFormatEncryptionKey, err := f.deriveFormatEncryptionKeyFromPassword(newPassword) + if err != nil { + return errors.Wrap(err, "unable to derive master key") + } + + r.formatEncryptionKey = newFormatEncryptionKey + + if err := encryptFormatBytes(f, repoConfig, newFormatEncryptionKey, f.UniqueID); err != nil { + return errors.Wrap(err, "unable to encrypt format bytes") + } + + if err := writeFormatBlob(ctx, r.blobs, f); err != nil { + return errors.Wrap(err, "unable to write format blob") + } + + // remove cached kopia.repository blob. + if cd := r.cachingOptions.CacheDirectory; cd != "" { + if err := os.Remove(filepath.Join(r.cachingOptions.CacheDirectory, "kopia.repository")); err != nil { + log(ctx).Errorf("unable to remove kopia.repository: %v", err) + } + } + + return nil +} diff --git a/repo/content/content_formatting_options.go b/repo/content/content_formatting_options.go index 40a4414b2..df488e491 100644 --- a/repo/content/content_formatting_options.go +++ b/repo/content/content_formatting_options.go @@ -20,6 +20,8 @@ type FormattingOptions struct { HMACSecret []byte `json:"secret,omitempty"` // HMAC secret used to generate encryption keys MasterKey []byte `json:"masterKey,omitempty"` // master encryption key (SIV-mode encryption only) MutableParameters + + EnablePasswordChange bool `json:"enablePasswordChange"` // disables replication of kopia.repository blob in packs } // MutableParameters represents parameters of the content manager that can be mutated after the repository diff --git a/repo/crypto_key_derivation_nontest.go b/repo/crypto_key_derivation_nontest.go index ee143d422..e11cec37c 100644 --- a/repo/crypto_key_derivation_nontest.go +++ b/repo/crypto_key_derivation_nontest.go @@ -10,7 +10,7 @@ // defaultKeyDerivationAlgorithm is the key derivation algorithm for new configurations. const defaultKeyDerivationAlgorithm = "scrypt-65536-8-1" -func (f *formatBlob) deriveMasterKeyFromPassword(password string) ([]byte, error) { +func (f *formatBlob) deriveFormatEncryptionKeyFromPassword(password string) ([]byte, error) { const masterKeySize = 32 switch f.KeyDerivationAlgorithm { diff --git a/repo/crypto_key_derivation_testing.go b/repo/crypto_key_derivation_testing.go index dd2290544..93c3d2100 100644 --- a/repo/crypto_key_derivation_testing.go +++ b/repo/crypto_key_derivation_testing.go @@ -11,7 +11,7 @@ // defaultKeyDerivationAlgorithm is the key derivation algorithm for new configurations. const defaultKeyDerivationAlgorithm = "testing-only-insecure" -func (f *formatBlob) deriveMasterKeyFromPassword(password string) ([]byte, error) { +func (f *formatBlob) deriveFormatEncryptionKeyFromPassword(password string) ([]byte, error) { const masterKeySize = 32 switch f.KeyDerivationAlgorithm { diff --git a/repo/initialize.go b/repo/initialize.go index 32d87689a..24ac0966d 100644 --- a/repo/initialize.go +++ b/repo/initialize.go @@ -58,9 +58,9 @@ func Initialize(ctx context.Context, st blob.Storage, opt *NewRepositoryOptions, format := formatBlobFromOptions(opt) - masterKey, err := format.deriveMasterKeyFromPassword(password) + formatEncryptionKey, err := format.deriveFormatEncryptionKeyFromPassword(password) if err != nil { - return errors.Wrap(err, "unable to derive master key") + return errors.Wrap(err, "unable to derive format encryption key") } f := repositoryObjectFormatFromOptions(opt) @@ -68,7 +68,7 @@ func Initialize(ctx context.Context, st blob.Storage, opt *NewRepositoryOptions, return errors.Wrap(err, "invalid parameters") } - if err := encryptFormatBytes(format, f, masterKey, format.UniqueID); err != nil { + if err := encryptFormatBytes(format, f, formatEncryptionKey, format.UniqueID); err != nil { return errors.Wrap(err, "unable to encrypt format bytes") } @@ -104,6 +104,7 @@ func repositoryObjectFormatFromOptions(opt *NewRepositoryOptions) *repositoryObj IndexVersion: applyDefaultInt(opt.BlockFormat.IndexVersion, content.DefaultIndexVersion), EpochParameters: opt.BlockFormat.EpochParameters, }, + EnablePasswordChange: opt.BlockFormat.EnablePasswordChange, }, Format: object.Format{ Splitter: applyDefaultString(opt.ObjectFormat.Splitter, splitter.DefaultAlgorithm), diff --git a/repo/open.go b/repo/open.go index 0b80ceb53..e9957f02e 100644 --- a/repo/open.go +++ b/repo/open.go @@ -37,6 +37,11 @@ // as valid. const defaultFormatBlobCacheDuration = 15 * time.Minute +// localCacheIntegrityHMACSecretLength length of HMAC secret protecting local cache items. +const localCacheIntegrityHMACSecretLength = 16 + +var localCacheIntegrityPurpose = []byte("local-cache-integrity") + const cacheDirMarkerContents = CacheDirMarkerHeader + ` # # This file is a cache directory tag created by Kopia - Fast And Secure Open-Source Backup. @@ -186,18 +191,22 @@ func openWithConfig(ctx context.Context, st blob.Storage, lc *LocalConfig, passw return nil, errors.Errorf("unable to add checksum") } - masterKey, err := f.deriveMasterKeyFromPassword(password) + formatEncryptionKey, err := f.deriveFormatEncryptionKeyFromPassword(password) if err != nil { return nil, err } - repoConfig, err := f.decryptFormatBytes(masterKey) + repoConfig, err := f.decryptFormatBytes(formatEncryptionKey) if err != nil { return nil, ErrInvalidPassword } - // nolint:gomnd - caching.HMACSecret = deriveKeyFromMasterKey(masterKey, f.UniqueID, []byte("local-cache-integrity"), 16) + if repoConfig.FormattingOptions.EnablePasswordChange { + caching.HMACSecret = deriveKeyFromMasterKey(repoConfig.HMACSecret, f.UniqueID, localCacheIntegrityPurpose, localCacheIntegrityHMACSecretLength) + } else { + // deriving from formatEncryptionKey was actually a bug, that only matters will change when we change the password + caching.HMACSecret = deriveKeyFromMasterKey(formatEncryptionKey, f.UniqueID, localCacheIntegrityPurpose, localCacheIntegrityHMACSecretLength) + } fo := &repoConfig.FormattingOptions @@ -212,6 +221,11 @@ func openWithConfig(ctx context.Context, st blob.Storage, lc *LocalConfig, passw DisableInternalLog: options.DisableInternalLog, } + // do not embed repository format info in pack blobs when password change is enabled. + if fo.EnablePasswordChange { + cmOpts.RepositoryFormatBytes = nil + } + scm, err := content.NewSharedManager(ctx, st, fo, caching, cmOpts) if err != nil { return nil, errors.Wrap(err, "unable to create shared content manager") @@ -239,14 +253,14 @@ func openWithConfig(ctx context.Context, st blob.Storage, lc *LocalConfig, passw mmgr: manifests, sm: scm, directRepositoryParameters: directRepositoryParameters{ - uniqueID: f.UniqueID, - cachingOptions: *caching, - formatBlob: f, - masterKey: masterKey, - timeNow: cmOpts.TimeNow, - cliOpts: lc.ClientOptions.ApplyDefaults(ctx, "Repository in "+st.DisplayName()), - configFile: configFile, - nextWriterID: new(int32), + uniqueID: f.UniqueID, + cachingOptions: *caching, + formatBlob: f, + formatEncryptionKey: formatEncryptionKey, + timeNow: cmOpts.TimeNow, + cliOpts: lc.ClientOptions.ApplyDefaults(ctx, "Repository in "+st.DisplayName()), + configFile: configFile, + nextWriterID: new(int32), }, closed: make(chan struct{}), } @@ -294,10 +308,6 @@ func formatBytesCachingEnabled(cacheDirectory string, validDuration time.Duratio } func readFormatBlobBytesFromCache(ctx context.Context, cachedFile string, validDuration time.Duration) ([]byte, error) { - if err := os.MkdirAll(filepath.Dir(cachedFile), cache.DirMode); err != nil && !os.IsExist(err) { - log(ctx).Errorf("unable to create cache directory: %v", err) - } - cst, err := os.Stat(cachedFile) if err != nil { return nil, errors.Wrap(err, "unable to open cache file") @@ -322,6 +332,12 @@ func readAndCacheFormatBlobBytes(ctx context.Context, st blob.Storage, cacheDire validDuration = defaultFormatBlobCacheDuration } + if cacheDirectory != "" { + if err := os.MkdirAll(cacheDirectory, cache.DirMode); err != nil && !os.IsExist(err) { + log(ctx).Errorf("unable to create cache directory: %v", err) + } + } + cacheEnabled := formatBytesCachingEnabled(cacheDirectory, validDuration) if cacheEnabled { b, err := readFormatBlobBytesFromCache(ctx, cachedFile, validDuration) diff --git a/repo/parameters.go b/repo/parameters.go index d6e4e9e18..60ebaaf0f 100644 --- a/repo/parameters.go +++ b/repo/parameters.go @@ -14,7 +14,7 @@ func (r *directRepository) SetParameters(ctx context.Context, m content.MutableParameters) error { f := r.formatBlob - repoConfig, err := f.decryptFormatBytes(r.masterKey) + repoConfig, err := f.decryptFormatBytes(r.formatEncryptionKey) if err != nil { return errors.Wrap(err, "unable to decrypt repository config") } @@ -25,7 +25,7 @@ func (r *directRepository) SetParameters(ctx context.Context, m content.MutableP repoConfig.FormattingOptions.MutableParameters = m - if err := encryptFormatBytes(f, repoConfig, r.masterKey, f.UniqueID); err != nil { + if err := encryptFormatBytes(f, repoConfig, r.formatEncryptionKey, f.UniqueID); err != nil { return errors.Errorf("unable to encrypt format bytes") } diff --git a/repo/repository.go b/repo/repository.go index a2816c995..d352ee872 100644 --- a/repo/repository.go +++ b/repo/repository.go @@ -74,17 +74,18 @@ type DirectRepositoryWriter interface { BlobStorage() blob.Storage ContentManager() *content.WriteManager SetParameters(ctx context.Context, m content.MutableParameters) error + ChangePassword(ctx context.Context, newPassword string) error } type directRepositoryParameters struct { - uniqueID []byte - configFile string - cachingOptions content.CachingOptions - cliOpts ClientOptions - timeNow func() time.Time - formatBlob *formatBlob - masterKey []byte - nextWriterID *int32 + uniqueID []byte + configFile string + cachingOptions content.CachingOptions + cliOpts ClientOptions + timeNow func() time.Time + formatBlob *formatBlob + formatEncryptionKey []byte + nextWriterID *int32 } // directRepository is an implementation of repository that directly manipulates underlying storage. @@ -102,7 +103,14 @@ type directRepository struct { // DeriveKey derives encryption key of the provided length from the master key. func (r *directRepository) DeriveKey(purpose []byte, keyLength int) []byte { - return deriveKeyFromMasterKey(r.masterKey, r.uniqueID, purpose, keyLength) + if r.cmgr.ContentFormat().EnablePasswordChange { + return deriveKeyFromMasterKey(r.cmgr.ContentFormat().MasterKey, r.uniqueID, purpose, keyLength) + } + + // version of kopia