repo: refactored password persistence (#1065)

* introduced passwordpersist package which has password persistence
  strategies (keyring, file, none, multiple) with possibility of adding
  more in the future.
* moved all password persistence logic out of 'repo'
* removed global variable repo.EnableKeyRing
This commit is contained in:
Jarek Kowalski
2021-05-11 21:53:36 -07:00
committed by GitHub
parent b844ce6b50
commit 41931f21ce
22 changed files with 371 additions and 155 deletions

View File

@@ -16,6 +16,7 @@
"github.com/pkg/errors"
"github.com/kopia/kopia/internal/apiclient"
"github.com/kopia/kopia/internal/passwordpersist"
"github.com/kopia/kopia/repo"
"github.com/kopia/kopia/repo/blob"
"github.com/kopia/kopia/repo/logging"
@@ -95,6 +96,7 @@ type advancedAppServices interface {
maybeInitializeUpdateCheck(ctx context.Context, co *connectOptions)
removeUpdateState()
passwordPersistenceStrategy() passwordpersist.Strategy
getPasswordFromFlags(ctx context.Context, isNew, allowPersistent bool) (string, error)
optionsFromFlags(ctx context.Context) *repo.Options
}
@@ -112,6 +114,8 @@ type App struct {
configPath string
traceStorage bool
metricsListenAddr string
keyRingEnabled bool
persistCredentials bool
// subcommands
blob commandBlob
@@ -150,6 +154,21 @@ func (c *App) stderr() io.Writer {
return c.stderrWriter
}
func (c *App) passwordPersistenceStrategy() passwordpersist.Strategy {
if !c.persistCredentials {
return passwordpersist.None
}
if c.keyRingEnabled {
return passwordpersist.Multiple{
passwordpersist.Keyring,
passwordpersist.File,
}
}
return passwordpersist.File
}
func (c *App) setup(app *kingpin.Application) {
_ = app.Flag("help-full", "Show help for all commands, including hidden").Action(func(pc *kingpin.ParseContext) error {
_ = app.UsageForContextWithTemplate(pc, 0, kingpin.DefaultUsageTemplate)
@@ -168,6 +187,7 @@ func (c *App) setup(app *kingpin.Application) {
app.Flag("metrics-listen-addr", "Expose Prometheus metrics on a given host:port").Hidden().StringVar(&c.metricsListenAddr)
app.Flag("timezone", "Format time according to specified time zone (local, utc, original or time zone name)").Default("local").Hidden().StringVar(&timeZone)
app.Flag("password", "Repository password.").Envar("KOPIA_PASSWORD").Short('p').StringVar(&c.password)
app.Flag("persist-credentials", "Persist credentials").Default("true").Envar("KOPIA_PERSIST_CREDENTIALS_ON_CONNECT").BoolVar(&c.persistCredentials)
c.setupOSSpecificKeychainFlags(app)

View File

@@ -7,6 +7,7 @@
"github.com/alecthomas/kingpin"
"github.com/pkg/errors"
"github.com/kopia/kopia/internal/passwordpersist"
"github.com/kopia/kopia/repo"
"github.com/kopia/kopia/repo/blob"
"github.com/kopia/kopia/repo/content"
@@ -42,7 +43,6 @@ func (c *commandRepositoryConnect) setup(svc advancedAppServices, parent command
}
type connectOptions struct {
connectPersistCredentials bool
connectCacheDirectory string
connectMaxCacheSizeMB int64
connectMaxMetadataCacheSizeMB int64
@@ -58,7 +58,6 @@ type connectOptions struct {
func (c *connectOptions) setup(cmd *kingpin.CmdClause) {
// Set up flags shared between 'create' and 'connect'. Note that because those flags are used by both command
// we must use *Var() methods, otherwise one of the commands would always get default flag values.
cmd.Flag("persist-credentials", "Persist credentials").Default("true").Envar("KOPIA_PERSIST_CREDENTIALS_ON_CONNECT").BoolVar(&c.connectPersistCredentials)
cmd.Flag("cache-directory", "Cache directory").PlaceHolder("PATH").Envar("KOPIA_CACHE_DIRECTORY").StringVar(&c.connectCacheDirectory)
cmd.Flag("content-cache-size-mb", "Size of local content cache").PlaceHolder("MB").Default("5000").Int64Var(&c.connectMaxCacheSizeMB)
cmd.Flag("metadata-cache-size-mb", "Size of local metadata cache").PlaceHolder("MB").Default("5000").Int64Var(&c.connectMaxMetadataCacheSizeMB)
@@ -73,7 +72,6 @@ func (c *connectOptions) setup(cmd *kingpin.CmdClause) {
func (c *connectOptions) toRepoConnectOptions() *repo.ConnectOptions {
return &repo.ConnectOptions{
PersistCredentials: c.connectPersistCredentials,
CachingOptions: content.CachingOptions{
CacheDirectory: c.connectCacheDirectory,
MaxCacheSizeBytes: c.connectMaxCacheSizeMB << 20, //nolint:gomnd
@@ -101,7 +99,9 @@ func (c *App) runConnectCommandWithStorage(ctx context.Context, co *connectOptio
func (c *App) runConnectCommandWithStorageAndPassword(ctx context.Context, co *connectOptions, st blob.Storage, password string) error {
configFile := c.repositoryConfigFileName()
if err := repo.Connect(ctx, configFile, st, password, co.toRepoConnectOptions()); err != nil {
if err := passwordpersist.OnSuccess(
ctx, repo.Connect(ctx, configFile, st, password, co.toRepoConnectOptions()),
c.passwordPersistenceStrategy(), configFile, password); err != nil {
return errors.Wrap(err, "error connecting to repository")
}

View File

@@ -6,6 +6,7 @@
"github.com/pkg/errors"
"github.com/kopia/kopia/internal/passwordpersist"
"github.com/kopia/kopia/repo"
)
@@ -59,7 +60,9 @@ func (c *commandRepositoryConnectServer) run(ctx context.Context) error {
return errors.Wrap(err, "getting password")
}
if err := repo.ConnectAPIServer(ctx, configFile, as, pass, opt); err != nil {
if err := passwordpersist.OnSuccess(
ctx, repo.ConnectAPIServer(ctx, configFile, as, pass, opt),
c.svc.passwordPersistenceStrategy(), configFile, pass); err != nil {
return errors.Wrap(err, "error connecting to API server")
}

View File

@@ -3,6 +3,8 @@
import (
"context"
"github.com/pkg/errors"
"github.com/kopia/kopia/repo"
)
@@ -20,5 +22,13 @@ func (c *commandRepositoryDisconnect) setup(svc advancedAppServices, parent comm
func (c *commandRepositoryDisconnect) run(ctx context.Context) error {
c.svc.removeUpdateState()
return repo.Disconnect(ctx, c.svc.repositoryConfigFileName())
if err := repo.Disconnect(ctx, c.svc.repositoryConfigFileName()); err != nil {
return errors.Wrap(err, "unable to disconnect from repository")
}
if err := c.svc.passwordPersistenceStrategy().DeletePassword(ctx, c.svc.repositoryConfigFileName()); err != nil {
return errors.Wrap(err, "unable to remove persisted password")
}
return nil
}

View File

@@ -42,7 +42,7 @@ func (c *serverClientFlags) setup(cmd *kingpin.CmdClause) {
cmd.Flag("server-cert-fingerprint", "Server certificate fingerprint").StringVar(&c.serverCertFingerprint)
}
func (c *commandServer) setup(svc appServices, parent commandParent) {
func (c *commandServer) setup(svc advancedAppServices, parent commandParent) {
cmd := parent.Command("server", "Commands to control HTTP API server.")
c.cancel.setup(svc, cmd)

View File

@@ -56,11 +56,11 @@ type commandServerStart struct {
serverStartTLSPrintFullServerCert bool
sf serverFlags
svc appServices
svc advancedAppServices
out textOutput
}
func (c *commandServerStart) setup(svc appServices, parent commandParent) {
func (c *commandServerStart) setup(svc advancedAppServices, parent commandParent) {
cmd := parent.Command("start", "Start Kopia server").Default()
cmd.Flag("html", "Server the provided HTML at the root URL").ExistingDirVar(&c.serverStartHTMLPath)
cmd.Flag("ui", "Start the server with HTML UI").Default("true").BoolVar(&c.serverStartUI)
@@ -114,6 +114,7 @@ func (c *commandServerStart) run(ctx context.Context, rep repo.Repository) error
Authorizer: auth.DefaultAuthorizer(),
AuthCookieSigningKey: c.serverAuthCookieSingingKey,
UIUser: c.sf.serverUsername,
PasswordPersist: c.svc.passwordPersistenceStrategy(),
})
if err != nil {
return errors.Wrap(err, "unable to initialize server")

View File

@@ -130,13 +130,13 @@ func (c *commandSnapshotMigrate) run(ctx context.Context, destRepo repo.Reposito
}
func (c *commandSnapshotMigrate) openSourceRepo(ctx context.Context) (repo.Repository, error) {
pass, ok := repo.GetPersistedPassword(ctx, c.migrateSourceConfig)
if !ok {
var err error
pass, err := c.svc.passwordPersistenceStrategy().GetPassword(ctx, c.migrateSourceConfig)
if err != nil {
pass, err = c.svc.getPasswordFromFlags(ctx, false, false)
}
if pass, err = c.svc.getPasswordFromFlags(ctx, false, false); err != nil {
return nil, errors.Wrap(err, "source repository password")
}
if err != nil {
return nil, errors.Wrap(err, "source repository password")
}
sourceRepo, err := repo.Open(ctx, c.migrateSourceConfig, pass, c.svc.optionsFromFlags(ctx))

View File

@@ -10,7 +10,7 @@
"github.com/pkg/errors"
"golang.org/x/term"
"github.com/kopia/kopia/repo"
"github.com/kopia/kopia/internal/passwordpersist"
)
func askForNewRepositoryPassword(out io.Writer) (string, error) {
@@ -58,10 +58,14 @@ func (c *App) getPasswordFromFlags(ctx context.Context, isNew, allowPersistent b
return askForNewRepositoryPassword(c.stdoutWriter)
case allowPersistent:
// try fetching the password from persistent storage specific to the configuration file.
pass, ok := repo.GetPersistedPassword(ctx, c.repositoryConfigFileName())
if ok {
pass, err := c.passwordPersistenceStrategy().GetPassword(ctx, c.repositoryConfigFileName())
if err == nil {
return pass, nil
}
if !errors.Is(err, passwordpersist.ErrPasswordNotFound) {
return "", errors.Wrap(err, "error getting persistent password")
}
}
// fall back to asking for existing password

View File

@@ -2,10 +2,8 @@
import (
"github.com/alecthomas/kingpin"
"github.com/kopia/kopia/repo"
)
func (c *App) setupOSSpecificKeychainFlags(app *kingpin.Application) {
app.Flag("use-keychain", "Use macOS Keychain for storing repository password.").Default("true").BoolVar(&repo.KeyRingEnabled)
app.Flag("use-keychain", "Use macOS Keychain for storing repository password.").Default("true").BoolVar(&c.keyRingEnabled)
}

View File

@@ -2,10 +2,8 @@
import (
"github.com/alecthomas/kingpin"
"github.com/kopia/kopia/repo"
)
func (c *App) setupOSSpecificKeychainFlags(app *kingpin.Application) {
app.Flag("use-keyring", "Use Gnome Keyring for storing repository password.").Default("false").BoolVar(&repo.KeyRingEnabled)
app.Flag("use-keyring", "Use Gnome Keyring for storing repository password.").Default("false").BoolVar(&c.keyRingEnabled)
}

View File

@@ -2,10 +2,8 @@
import (
"github.com/alecthomas/kingpin"
"github.com/kopia/kopia/repo"
)
func (c *App) setupOSSpecificKeychainFlags(app *kingpin.Application) {
app.Flag("use-credential-manager", "Use Windows Credential Manager for storing repository password.").Default("true").BoolVar(&repo.KeyRingEnabled)
app.Flag("use-credential-manager", "Use Windows Credential Manager for storing repository password.").Default("true").BoolVar(&c.keyRingEnabled)
}

View File

@@ -0,0 +1,44 @@
// Package passwordpersist manages password persistence.
package passwordpersist
import (
"context"
"github.com/pkg/errors"
"github.com/kopia/kopia/repo/logging"
)
// ErrPasswordNotFound is returned when a password cannot be found in a persistent storage.
var ErrPasswordNotFound = errors.Errorf("password not found")
// ErrUnsupported is returned when a password storage is not supported.
var ErrUnsupported = errors.Errorf("password storage not supported")
var log = logging.GetContextLoggerFunc("passwordpersist")
// Strategy encapsulates persisting and fetching passwords.
type Strategy interface {
// GetPassword gets persisted password, returns ErrNotFound or fatal errors.
GetPassword(ctx context.Context, configFile string) (string, error)
// PersistPassword persists a password, returns ErrUnsupported or fatal errors.
PersistPassword(ctx context.Context, configFile, password string) error
// DeletePassword deletes any persisted password, returns fatal errors.
DeletePassword(ctx context.Context, configFile string) error
}
// OnSuccess is a helper that persists the given (configFile,password) if the provided err is nil
// and deletes any persisted password otherwise.
func OnSuccess(ctx context.Context, err error, s Strategy, configFile, password string) error {
if err != nil {
if err2 := s.DeletePassword(ctx, configFile); err2 != nil {
log(ctx).Infof("unable to delete persistent password: %v", err2)
}
return err
}
return errors.Wrap(s.PersistPassword(ctx, configFile, password), "unable to persist password")
}

View File

@@ -0,0 +1,55 @@
package passwordpersist
import (
"context"
"encoding/base64"
"io/ioutil"
"os"
"github.com/pkg/errors"
)
// File is a Strategy that persists the base64-encoded password in a file next to repository config file.
var File Strategy = filePasswordStorage{}
type filePasswordStorage struct{}
func (filePasswordStorage) GetPassword(ctx context.Context, configFile string) (string, error) {
b, err := ioutil.ReadFile(passwordFileName(configFile))
if os.IsNotExist(err) {
return "", ErrPasswordNotFound
}
if err != nil {
return "", errors.Wrap(err, "error reading persisted password")
}
s, err := base64.StdEncoding.DecodeString(string(b))
if err != nil {
return "", errors.Wrap(err, "error invalid persisted password")
}
log(ctx).Debugf("password for %v retrieved from password file", configFile)
return string(s), nil
}
func (filePasswordStorage) PersistPassword(ctx context.Context, configFile, password string) error {
fn := passwordFileName(configFile)
log(ctx).Debugf("Saving password to file %v.", fn)
return ioutil.WriteFile(fn, []byte(base64.StdEncoding.EncodeToString([]byte(password))), 0o600)
}
func (filePasswordStorage) DeletePassword(ctx context.Context, configFile string) error {
err := os.Remove(passwordFileName(configFile))
if err != nil && !os.IsNotExist(err) {
return errors.Wrap(err, "error deleting password file")
}
return nil
}
func passwordFileName(configFile string) string {
return configFile + ".kopia-password"
}

View File

@@ -0,0 +1,99 @@
package passwordpersist
import (
"context"
"crypto/sha256"
"fmt"
"io"
"os/user"
"path/filepath"
"runtime"
"strings"
"github.com/pkg/errors"
"github.com/zalando/go-keyring"
)
// Keyring is a Strategy that persists the password in OS-specific keyring.
var Keyring Strategy = keyringStrategy{}
type keyringStrategy struct{}
func (keyringStrategy) GetPassword(ctx context.Context, configFile string) (string, error) {
kr, err := keyring.Get(getKeyringItemID(configFile), keyringUsername(ctx))
switch {
case err == nil:
log(ctx).Debugf("password for %v retrieved from OS keyring", configFile)
return kr, nil
case errors.Is(err, keyring.ErrNotFound):
return "", ErrPasswordNotFound
case errors.Is(err, keyring.ErrUnsupportedPlatform):
return "", ErrPasswordNotFound
default:
return "", errors.Wrap(err, "error retrieving password from OS keyring")
}
}
func (keyringStrategy) PersistPassword(ctx context.Context, configFile, password string) error {
log(ctx).Debugf("saving password to OS keyring...")
err := keyring.Set(getKeyringItemID(configFile), keyringUsername(ctx), password)
switch {
case err == nil:
log(ctx).Debugf("Saved password in OS keyring")
return nil
case errors.Is(err, keyring.ErrUnsupportedPlatform):
return ErrUnsupported
default:
return errors.Wrap(err, "error saving password in OS keyring")
}
}
func (keyringStrategy) DeletePassword(ctx context.Context, configFile string) error {
err := keyring.Delete(getKeyringItemID(configFile), keyringUsername(ctx))
switch {
case err == nil:
log(ctx).Infof("deleted repository password for %v.", configFile)
return nil
case errors.Is(err, keyring.ErrUnsupportedPlatform):
return ErrUnsupported
case errors.Is(err, keyring.ErrNotFound):
return ErrPasswordNotFound
default:
return errors.Wrapf(err, "unable to delete keyring item %v", getKeyringItemID(configFile))
}
}
func getKeyringItemID(configFile string) string {
h := sha256.New()
io.WriteString(h, configFile) //nolint:errcheck
return fmt.Sprintf("%v-%x", filepath.Base(configFile), h.Sum(nil)[0:8])
}
func keyringUsername(ctx context.Context) string {
currentUser, err := user.Current()
if err != nil {
log(ctx).Errorf("Cannot determine keyring username: %s", err)
return "nobody"
}
u := currentUser.Username
if runtime.GOOS == "windows" {
if p := strings.Index(u, "\\"); p >= 0 {
// On Windows ignore domain name.
u = u[p+1:]
}
}
return u
}

View File

@@ -0,0 +1,67 @@
package passwordpersist
import (
"context"
"github.com/pkg/errors"
)
var _ Strategy = (Multiple{})
// Multiple is a Strategy that tries several underlying persistence strategies.
type Multiple []Strategy
// GetPassword retrieves the password form the first password storage that has it.
func (m Multiple) GetPassword(ctx context.Context, configFile string) (string, error) {
for _, s := range m {
pass, err := s.GetPassword(ctx, configFile)
if err == nil {
return pass, nil
}
if errors.Is(err, ErrPasswordNotFound) {
// try next strategy one.
continue
}
return "", errors.Wrap(err, "error getting persistent password")
}
return "", ErrPasswordNotFound
}
// PersistPassword persists the provided password using the first method that succeeds.
func (m Multiple) PersistPassword(ctx context.Context, configFile, password string) error {
for _, s := range m {
err := s.PersistPassword(ctx, configFile, password)
if err == nil {
return nil
}
if errors.Is(err, ErrUnsupported) {
continue
}
return errors.Wrap(err, "error persisting password")
}
return ErrUnsupported
}
// DeletePassword deletes the password from all persistent storages.
func (m Multiple) DeletePassword(ctx context.Context, configFile string) error {
for _, s := range m {
err := s.DeletePassword(ctx, configFile)
switch {
case err == nil: // good
case errors.Is(err, ErrPasswordNotFound): // ignore
case errors.Is(err, ErrUnsupported): // ignore
default:
return errors.Wrap(err, "error removing password from persistent storage")
}
}
return nil
}

View File

@@ -0,0 +1,21 @@
package passwordpersist
import "context"
// None is a strategy that does not persist the password at all.
var None Strategy = noneStrategy{}
type noneStrategy struct{}
func (noneStrategy) GetPassword(ctx context.Context, configFile string) (string, error) {
return "", ErrPasswordNotFound
}
func (noneStrategy) PersistPassword(ctx context.Context, configFile, password string) error {
// silently succeed
return nil
}
func (noneStrategy) DeletePassword(ctx context.Context, configFile string) error {
return nil
}

View File

@@ -8,6 +8,7 @@
"github.com/pkg/errors"
"github.com/kopia/kopia/internal/passwordpersist"
"github.com/kopia/kopia/internal/remoterepoapi"
"github.com/kopia/kopia/internal/serverapi"
"github.com/kopia/kopia/repo"
@@ -243,7 +244,9 @@ func (s *Server) getConnectOptions(cliOpts repo.ClientOptions) *repo.ConnectOpti
}
func (s *Server) connectAPIServerAndOpen(ctx context.Context, si *repo.APIServerInfo, password string, cliOpts repo.ClientOptions) *apiError {
if err := repo.ConnectAPIServer(ctx, s.options.ConfigFile, si, password, s.getConnectOptions(cliOpts)); err != nil {
if err := passwordpersist.OnSuccess(
ctx, repo.ConnectAPIServer(ctx, s.options.ConfigFile, si, password, s.getConnectOptions(cliOpts)),
s.options.PasswordPersist, s.options.ConfigFile, password); err != nil {
return repoErrorToAPIError(err)
}
@@ -257,7 +260,9 @@ func (s *Server) connectAndOpen(ctx context.Context, conn blob.ConnectionInfo, p
}
defer st.Close(ctx) //nolint:errcheck
if err = repo.Connect(ctx, s.options.ConfigFile, st, password, s.getConnectOptions(cliOpts)); err != nil {
if err = passwordpersist.OnSuccess(
ctx, repo.Connect(ctx, s.options.ConfigFile, st, password, s.getConnectOptions(cliOpts)),
s.options.PasswordPersist, s.options.ConfigFile, password); err != nil {
return repoErrorToAPIError(err)
}
@@ -297,6 +302,10 @@ func (s *Server) handleRepoDisconnect(ctx context.Context, r *http.Request, body
return nil, internalServerError(err)
}
if err := s.options.PasswordPersist.DeletePassword(ctx, s.options.ConfigFile); err != nil {
return nil, internalServerError(err)
}
return &serverapi.Empty{}, nil
}

View File

@@ -17,6 +17,7 @@
"github.com/kopia/kopia/internal/auth"
"github.com/kopia/kopia/internal/clock"
"github.com/kopia/kopia/internal/passwordpersist"
"github.com/kopia/kopia/internal/serverapi"
"github.com/kopia/kopia/internal/uitask"
"github.com/kopia/kopia/repo"
@@ -610,6 +611,7 @@ type Options struct {
MaxConcurrency int
Authenticator auth.Authenticator
Authorizer auth.Authorizer
PasswordPersist passwordpersist.Strategy
AuthCookieSigningKey string
UIUser string // name of the user allowed to access the UI
}
@@ -621,6 +623,10 @@ func New(ctx context.Context, options Options) (*Server, error) {
return nil, errors.Errorf("missing authorizer")
}
if options.PasswordPersist == nil {
return nil, errors.Errorf("missing password persistence")
}
if options.AuthCookieSigningKey == "" {
// generate random signing key
options.AuthCookieSigningKey = uuid.New().String()

View File

@@ -16,6 +16,7 @@
"github.com/kopia/kopia/internal/apiclient"
"github.com/kopia/kopia/internal/auth"
"github.com/kopia/kopia/internal/passwordpersist"
"github.com/kopia/kopia/internal/repotesting"
"github.com/kopia/kopia/internal/server"
"github.com/kopia/kopia/internal/testlogging"
@@ -44,8 +45,9 @@ func startServer(ctx context.Context, t *testing.T) *repo.APIServerInfo {
_, env := repotesting.NewEnvironment(t)
s, err := server.New(ctx, server.Options{
ConfigFile: env.ConfigFile(),
Authorizer: auth.LegacyAuthorizer(),
ConfigFile: env.ConfigFile(),
PasswordPersist: passwordpersist.File,
Authorizer: auth.LegacyAuthorizer(),
Authenticator: auth.CombineAuthenticators(
auth.AuthenticateSingleUser(testUsername+"@"+testHostname, testPassword),
auth.AuthenticateSingleUser(testUIUsername, testUIPassword),
@@ -54,6 +56,8 @@ func startServer(ctx context.Context, t *testing.T) *repo.APIServerInfo {
UIUser: testUIUsername,
})
require.NoError(t, err)
s.SetRepository(ctx, env.Repository)
// ensure we disconnect the repository before shutting down the server.

View File

@@ -289,5 +289,5 @@ func ConnectAPIServer(ctx context.Context, configFile string, si *APIServerInfo,
return errors.Wrap(err, "unable to write config file")
}
return verifyConnect(ctx, configFile, password, opt.PersistCredentials)
return verifyConnect(ctx, configFile, password)
}

View File

@@ -13,7 +13,6 @@
// ConnectOptions specifies options when persisting configuration to connect to a repository.
type ConnectOptions struct {
PersistCredentials bool `json:"persistCredentials"`
ClientOptions
content.CachingOptions
@@ -58,10 +57,10 @@ func Connect(ctx context.Context, configFile string, st blob.Storage, password s
return errors.Wrap(err, "unable to write config file")
}
return verifyConnect(ctx, configFile, password, opt.PersistCredentials)
return verifyConnect(ctx, configFile, password)
}
func verifyConnect(ctx context.Context, configFile, password string, persist bool) error {
func verifyConnect(ctx context.Context, configFile, password string) error {
// now verify that the repository can be opened with the provided config file.
r, err := Open(ctx, configFile, password, nil)
if err != nil {
@@ -74,14 +73,6 @@ func verifyConnect(ctx context.Context, configFile, password string, persist boo
return err
}
if persist {
if err := persistPassword(ctx, configFile, password); err != nil {
return errors.Wrap(err, "unable to persist password")
}
} else {
deletePassword(ctx, configFile)
}
return r.Close(ctx)
}
@@ -92,8 +83,6 @@ func Disconnect(ctx context.Context, configFile string) error {
return err
}
deletePassword(ctx, configFile)
if cfg.Caching != nil && cfg.Caching.CacheDirectory != "" {
if !filepath.IsAbs(cfg.Caching.CacheDirectory) {
return errors.Errorf("cache directory was not absolute, refusing to delete")

View File

@@ -1,110 +0,0 @@
package repo
import (
"context"
"crypto/sha256"
"encoding/base64"
"fmt"
"io"
"io/ioutil"
"os"
"os/user"
"path/filepath"
"runtime"
"strings"
"github.com/pkg/errors"
"github.com/zalando/go-keyring"
)
// KeyRingEnabled enables password persistence uses OS-specific keyring.
var KeyRingEnabled = false
// GetPersistedPassword retrieves persisted password for a given repository config.
func GetPersistedPassword(ctx context.Context, configFile string) (string, bool) {
if KeyRingEnabled {
kr, err := keyring.Get(getKeyringItemID(configFile), keyringUsername(ctx))
if err == nil {
log(ctx).Debugf("password for %v retrieved from OS keyring", configFile)
return kr, true
}
}
b, err := ioutil.ReadFile(passwordFileName(configFile))
if err == nil {
s, err := base64.StdEncoding.DecodeString(string(b))
if err == nil {
log(ctx).Debugf("password for %v retrieved from password file", configFile)
return string(s), true
}
}
log(ctx).Debugf("could not find persisted password")
return "", false
}
// persistPassword stores password for a given repository config.
func persistPassword(ctx context.Context, configFile, password string) error {
if KeyRingEnabled {
log(ctx).Debugf("saving password to OS keyring...")
err := keyring.Set(getKeyringItemID(configFile), keyringUsername(ctx), password)
if err == nil {
log(ctx).Debugf("Saved password in OS keyring")
return nil
}
return errors.Wrap(err, "error saving password in key ring")
}
fn := passwordFileName(configFile)
log(ctx).Debugf("Saving password to file %v.", fn)
return ioutil.WriteFile(fn, []byte(base64.StdEncoding.EncodeToString([]byte(password))), 0o600)
}
// deletePassword removes stored repository password.
func deletePassword(ctx context.Context, configFile string) {
// delete from both keyring and a file
if KeyRingEnabled {
err := keyring.Delete(getKeyringItemID(configFile), keyringUsername(ctx))
if err == nil {
log(ctx).Infof("deleted repository password for %v.", configFile)
} else if !errors.Is(err, keyring.ErrNotFound) {
log(ctx).Errorf("unable to delete keyring item %v: %v", getKeyringItemID(configFile), err)
}
}
_ = os.Remove(passwordFileName(configFile))
}
func getKeyringItemID(configFile string) string {
h := sha256.New()
io.WriteString(h, configFile) //nolint:errcheck
return fmt.Sprintf("%v-%x", filepath.Base(configFile), h.Sum(nil)[0:8])
}
func passwordFileName(configFile string) string {
return configFile + ".kopia-password"
}
func keyringUsername(ctx context.Context) string {
currentUser, err := user.Current()
if err != nil {
log(ctx).Errorf("Cannot determine keyring username: %s", err)
return "nobody"
}
u := currentUser.Username
if runtime.GOOS == "windows" {
if p := strings.Index(u, "\\"); p >= 0 {
// On Windows ignore domain name.
u = u[p+1:]
}
}
return u
}