repo: moved password persistence to repository layer

This commit is contained in:
Jarek Kowalski
2020-02-09 17:07:14 -08:00
parent 29e5750686
commit edca1733b6
12 changed files with 151 additions and 99 deletions

View File

@@ -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

View File

@@ -15,6 +15,5 @@ func init() {
}
func runDisconnectCommand(ctx context.Context) error {
deletePassword(repositoryConfigFileName(), getUserName())
return repo.Disconnect(repositoryConfigFileName())
}

View File

@@ -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"
}

View File

@@ -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)
}

View File

@@ -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)
}

View File

@@ -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)
}

View File

@@ -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,

View File

@@ -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)
}

View File

@@ -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
View 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
}

View File

@@ -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())
}

View File

@@ -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,