mirror of
https://github.com/kopia/kopia.git
synced 2026-03-30 12:03:39 -04:00
repo: moved password persistence to repository layer
This commit is contained in:
@@ -32,8 +32,9 @@ func setupConnectOptions(cmd *kingpin.CmdClause) {
|
||||
cmd.Flag("max-list-cache-duration", "Duration of index cache").Default("600s").Hidden().DurationVar(&connectMaxListCacheDuration)
|
||||
}
|
||||
|
||||
func connectOptions() repo.ConnectOptions {
|
||||
return repo.ConnectOptions{
|
||||
func connectOptions() *repo.ConnectOptions {
|
||||
return &repo.ConnectOptions{
|
||||
PersistCredentials: connectPersistCredentials,
|
||||
CachingOptions: content.CachingOptions{
|
||||
CacheDirectory: connectCacheDirectory,
|
||||
MaxCacheSizeBytes: connectMaxCacheSizeMB << 20, //nolint:gomnd
|
||||
@@ -61,14 +62,6 @@ func runConnectCommandWithStorageAndPassword(ctx context.Context, st blob.Storag
|
||||
return err
|
||||
}
|
||||
|
||||
if connectPersistCredentials {
|
||||
if err := persistPassword(configFile, getUserName(), password); err != nil {
|
||||
return errors.Wrap(err, "unable to persist password")
|
||||
}
|
||||
} else {
|
||||
deletePassword(configFile, getUserName())
|
||||
}
|
||||
|
||||
printStderr("Connected to repository.\n")
|
||||
|
||||
return nil
|
||||
|
||||
@@ -15,6 +15,5 @@ func init() {
|
||||
}
|
||||
|
||||
func runDisconnectCommand(ctx context.Context) error {
|
||||
deletePassword(repositoryConfigFileName(), getUserName())
|
||||
return repo.Disconnect(repositoryConfigFileName())
|
||||
}
|
||||
|
||||
@@ -1,18 +1,13 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/bgentry/speakeasy"
|
||||
"github.com/pkg/errors"
|
||||
keyring "github.com/zalando/go-keyring"
|
||||
|
||||
"github.com/kopia/kopia/repo"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -59,7 +54,7 @@ func getPasswordFromFlags(isNew, allowPersistent bool) (string, error) {
|
||||
}
|
||||
|
||||
if !isNew && allowPersistent {
|
||||
pass, ok := getPersistedPassword(repositoryConfigFileName(), getUserName())
|
||||
pass, ok := repo.GetPersistedPassword(repositoryConfigFileName())
|
||||
if ok {
|
||||
return pass, nil
|
||||
}
|
||||
@@ -92,70 +87,3 @@ func askPass(prompt string) (string, error) {
|
||||
|
||||
return "", errors.New("can't get password")
|
||||
}
|
||||
|
||||
func getPersistedPassword(configFile, username string) (string, bool) {
|
||||
if *keyringEnabled {
|
||||
kr, err := keyring.Get(getKeyringItemID(configFile), username)
|
||||
if err == nil {
|
||||
log.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.Debugf("password for %v retrieved from password file", configFile)
|
||||
return string(s), true
|
||||
}
|
||||
}
|
||||
|
||||
log.Debugf("could not find persisted password")
|
||||
|
||||
return "", false
|
||||
}
|
||||
|
||||
func persistPassword(configFile, username, password string) error {
|
||||
if *keyringEnabled {
|
||||
log.Debugf("saving password to OS keyring...")
|
||||
|
||||
err := keyring.Set(getKeyringItemID(configFile), username, password)
|
||||
if err == nil {
|
||||
log.Infof("Saved password")
|
||||
return nil
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
fn := passwordFileName(configFile)
|
||||
log.Infof("Saving password to file %v.", fn)
|
||||
|
||||
return ioutil.WriteFile(fn, []byte(base64.StdEncoding.EncodeToString([]byte(password))), 0600)
|
||||
}
|
||||
|
||||
func deletePassword(configFile, username string) {
|
||||
// delete from both keyring and a file
|
||||
if *keyringEnabled {
|
||||
err := keyring.Delete(getKeyringItemID(configFile), username)
|
||||
if err == nil {
|
||||
log.Infof("deleted repository password for %v.", configFile)
|
||||
} else if err != keyring.ErrNotFound {
|
||||
log.Warningf("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"
|
||||
}
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
package cli
|
||||
|
||||
var keyringEnabled = app.Flag("use-keychain", "Use macOS Keychain for storing repository password.").Default("true").Bool()
|
||||
import "github.com/kopia/kopia/repo"
|
||||
|
||||
func init() {
|
||||
app.Flag("use-keychain", "Use macOS Keychain for storing repository password.").Default("true").BoolVar(&repo.KeyRingEnabled)
|
||||
}
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
package cli
|
||||
|
||||
var keyringEnabled = app.Flag("use-keyring", "Use Gnome Keyring for storing repository password.").Default("false").Bool()
|
||||
import "github.com/kopia/kopia/repo"
|
||||
|
||||
func init() {
|
||||
app.Flag("use-keyring", "Use Gnome Keyring for storing repository password.").Default("false").BoolVar(&repo.KeyRingEnabled)
|
||||
}
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
package cli
|
||||
|
||||
var keyringEnabled = app.Flag("use-credential-manager", "Use Windows Credential Manager for storing repository password.").Default("true").Bool()
|
||||
import "github.com/kopia/kopia/repo"
|
||||
|
||||
func init() {
|
||||
app.Flag("use-credential-manager", "Use Windows Credential Manager for storing repository password.").Default("true").BoolVar(&repo.KeyRingEnabled)
|
||||
}
|
||||
|
||||
@@ -43,7 +43,7 @@ func setupRepositoryAndConnect(ctx context.Context, password string) error {
|
||||
}
|
||||
|
||||
// now establish connection to repository and create configuration file.
|
||||
if err := repo.Connect(ctx, configFile, st, password, repo.ConnectOptions{
|
||||
if err := repo.Connect(ctx, configFile, st, password, &repo.ConnectOptions{
|
||||
CachingOptions: content.CachingOptions{
|
||||
CacheDirectory: cacheDirectory,
|
||||
MaxCacheSizeBytes: 100000000,
|
||||
|
||||
@@ -68,11 +68,7 @@ func (e *Environment) Setup(t *testing.T, opts ...func(*repo.NewRepositoryOption
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
|
||||
connOpts := repo.ConnectOptions{
|
||||
//TraceStorage: log.Printf,
|
||||
}
|
||||
|
||||
if err = repo.Connect(ctx, e.configFile(), st, masterPassword, connOpts); err != nil {
|
||||
if err = repo.Connect(ctx, e.configFile(), st, masterPassword, nil); err != nil {
|
||||
t.Fatalf("can't connect: %v", err)
|
||||
}
|
||||
|
||||
|
||||
@@ -17,11 +17,17 @@
|
||||
|
||||
// ConnectOptions specifies options when persisting configuration to connect to a repository.
|
||||
type ConnectOptions struct {
|
||||
PersistCredentials bool `json:"persistCredentials"`
|
||||
|
||||
content.CachingOptions
|
||||
}
|
||||
|
||||
// Connect connects to the repository in the specified storage and persists the configuration and credentials in the file provided.
|
||||
func Connect(ctx context.Context, configFile string, st blob.Storage, password string, opt ConnectOptions) error {
|
||||
func Connect(ctx context.Context, configFile string, st blob.Storage, password string, opt *ConnectOptions) error {
|
||||
if opt == nil {
|
||||
opt = &ConnectOptions{}
|
||||
}
|
||||
|
||||
formatBytes, err := st.GetBlob(ctx, FormatBlobID, 0, -1)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "unable to read format blob")
|
||||
@@ -58,6 +64,14 @@ func Connect(ctx context.Context, configFile string, st blob.Storage, password s
|
||||
return err
|
||||
}
|
||||
|
||||
if opt.PersistCredentials {
|
||||
if err := persistPassword(configFile, password); err != nil {
|
||||
return errors.Wrap(err, "unable to persist password")
|
||||
}
|
||||
} else {
|
||||
deletePassword(configFile)
|
||||
}
|
||||
|
||||
return r.Close(ctx)
|
||||
}
|
||||
|
||||
@@ -106,6 +120,8 @@ func Disconnect(configFile string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
deletePassword(configFile)
|
||||
|
||||
if cfg.Caching.CacheDirectory != "" {
|
||||
if err = os.RemoveAll(cfg.Caching.CacheDirectory); err != nil {
|
||||
log.Warningf("unable to to remove cache directory: %v", err)
|
||||
|
||||
108
repo/password.go
Normal file
108
repo/password.go
Normal file
@@ -0,0 +1,108 @@
|
||||
package repo
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"os/user"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
"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(configFile string) (string, bool) {
|
||||
if KeyRingEnabled {
|
||||
kr, err := keyring.Get(getKeyringItemID(configFile), keyringUsername())
|
||||
if err == nil {
|
||||
log.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.Debugf("password for %v retrieved from password file", configFile)
|
||||
return string(s), true
|
||||
}
|
||||
}
|
||||
|
||||
log.Debugf("could not find persisted password")
|
||||
|
||||
return "", false
|
||||
}
|
||||
|
||||
// persistPassword stores password for a given repository config.
|
||||
func persistPassword(configFile, password string) error {
|
||||
if KeyRingEnabled {
|
||||
log.Debugf("saving password to OS keyring...")
|
||||
|
||||
err := keyring.Set(getKeyringItemID(configFile), keyringUsername(), password)
|
||||
if err == nil {
|
||||
log.Infof("Saved password")
|
||||
return nil
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
fn := passwordFileName(configFile)
|
||||
log.Infof("Saving password to file %v.", fn)
|
||||
|
||||
return ioutil.WriteFile(fn, []byte(base64.StdEncoding.EncodeToString([]byte(password))), 0600)
|
||||
}
|
||||
|
||||
// deletePassword removes stored repository password.
|
||||
func deletePassword(configFile string) {
|
||||
// delete from both keyring and a file
|
||||
if KeyRingEnabled {
|
||||
err := keyring.Delete(getKeyringItemID(configFile), keyringUsername())
|
||||
if err == nil {
|
||||
log.Infof("deleted repository password for %v.", configFile)
|
||||
} else if err != keyring.ErrNotFound {
|
||||
log.Warningf("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() string {
|
||||
currentUser, err := user.Current()
|
||||
if err != nil {
|
||||
log.Warningf("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
|
||||
}
|
||||
@@ -58,7 +58,7 @@ func newUploadTestHarness() *uploadTestHarness {
|
||||
log.Debugf("repo dir: %v", repoDir)
|
||||
|
||||
configFile := filepath.Join(repoDir, ".kopia.config")
|
||||
if conerr := repo.Connect(ctx, configFile, storage, masterPassword, repo.ConnectOptions{}); conerr != nil {
|
||||
if conerr := repo.Connect(ctx, configFile, storage, masterPassword, nil); conerr != nil {
|
||||
panic("unable to connect to repository: " + conerr.Error())
|
||||
}
|
||||
|
||||
|
||||
@@ -69,7 +69,7 @@ func TestStressRepository(t *testing.T) {
|
||||
}
|
||||
|
||||
// set up two parallel kopia connections, each with its own config file and cache.
|
||||
if err := repo.Connect(ctx, configFile1, st, masterPassword, repo.ConnectOptions{
|
||||
if err := repo.Connect(ctx, configFile1, st, masterPassword, &repo.ConnectOptions{
|
||||
CachingOptions: content.CachingOptions{
|
||||
CacheDirectory: filepath.Join(tmpPath, "cache1"),
|
||||
MaxCacheSizeBytes: 2000000000,
|
||||
@@ -78,7 +78,7 @@ func TestStressRepository(t *testing.T) {
|
||||
t.Fatalf("unable to connect 1: %v", err)
|
||||
}
|
||||
|
||||
if err := repo.Connect(ctx, configFile2, st, masterPassword, repo.ConnectOptions{
|
||||
if err := repo.Connect(ctx, configFile2, st, masterPassword, &repo.ConnectOptions{
|
||||
CachingOptions: content.CachingOptions{
|
||||
CacheDirectory: filepath.Join(tmpPath, "cache2"),
|
||||
MaxCacheSizeBytes: 2000000000,
|
||||
|
||||
Reference in New Issue
Block a user