From 1a239b00a1d8dcb29108d6554a7abed51011a847 Mon Sep 17 00:00:00 2001 From: Dominik Schulz Date: Thu, 15 Mar 2018 14:00:43 +0100 Subject: [PATCH] Add secret config store (#708) --- action/action.go | 3 + action/action_test.go | 9 +- action/clihelper.go | 2 +- action/history_test.go | 5 + action/init.go | 46 +++++-- action/init_test.go | 5 +- app.go | 1 + backend/crypto/xc/decrypt.go | 12 +- backend/crypto/xc/encrypt.go | 10 +- backend/crypto/xc/encrypt_test.go | 6 +- backend/crypto/xc/keyring/private_key.go | 32 +++-- backend/crypto/xc/keyring/secring.go | 5 +- backend/crypto/xc/utils.go | 4 +- backend/crypto/xc/utils_test.go | 2 +- backend/crypto/xc/xc.go | 8 +- backend/storage.go | 1 + backend/storage/fs/store.go | 5 + backend/storage/kv/consul/store.go | 5 + backend/storage/kv/inmem/store.go | 8 ++ commands.go | 5 +- config/config.go | 8 ++ config/io.go | 1 + config/secrets/config.go | 153 +++++++++++++++++++++++ config/secrets/config_test.go | 29 +++++ store/root/git.go | 3 + store/root/gpg.go | 3 + store/root/init.go | 86 +++++++++++-- store/root/mount.go | 2 +- store/root/store.go | 57 ++++----- store/root/store_test.go | 7 +- store/sub/crypto.go | 47 +++++++ store/sub/init.go | 3 + store/sub/rcs.go | 41 ++++++ store/sub/recipients_test.go | 1 + store/sub/storage.go | 105 ++++++++++++++++ store/sub/store.go | 114 ++--------------- store/sub/store_test.go | 3 +- store/sub/templates_test.go | 1 + tests/jsonapi_test.go | 1 - tests/tester.go | 2 - utils/agent/agent.go | 41 ++++-- utils/agent/client/client.go | 48 ++++--- utils/agent/client/client_others.go | 9 +- utils/agent/client/client_windows.go | 3 +- utils/jsonapi/api_test.go | 5 +- 45 files changed, 713 insertions(+), 234 deletions(-) create mode 100644 config/secrets/config.go create mode 100644 config/secrets/config_test.go create mode 100644 store/sub/crypto.go create mode 100644 store/sub/rcs.go create mode 100644 store/sub/storage.go diff --git a/action/action.go b/action/action.go index 05ba5be5b7..0890e5bb52 100644 --- a/action/action.go +++ b/action/action.go @@ -9,6 +9,7 @@ import ( "github.com/blang/semver" "github.com/justwatchcom/gopass/config" "github.com/justwatchcom/gopass/store/root" + "github.com/justwatchcom/gopass/utils/out" ) var ( @@ -42,6 +43,8 @@ func newAction(ctx context.Context, cfg *config.Config, sv semver.Version) (*Act version: sv, } + ctx = out.AddPrefix(ctx, "[action] ") + store, err := root.New(ctx, cfg) if err != nil { return nil, exitError(ctx, ExitUnknown, err, "failed to init root store: %s", err) diff --git a/action/action_test.go b/action/action_test.go index 99ce6b4442..8602bef8fb 100644 --- a/action/action_test.go +++ b/action/action_test.go @@ -21,7 +21,14 @@ func newMock(ctx context.Context, u *gptest.Unit) (*Action, error) { ctx = backend.WithRCSBackend(ctx, backend.Noop) ctx = backend.WithCryptoBackend(ctx, backend.Plain) ctx = backend.WithStorageBackend(ctx, backend.FS) - return newAction(ctx, cfg, semver.Version{}) + act, err := newAction(ctx, cfg, semver.Version{}) + if err != nil { + return nil, err + } + if err := act.Initialized(ctx, nil); err != nil { + return nil, err + } + return act, nil } func TestAction(t *testing.T) { diff --git a/action/clihelper.go b/action/clihelper.go index 9b55601f0f..038f9c9057 100644 --- a/action/clihelper.go +++ b/action/clihelper.go @@ -196,7 +196,7 @@ func (s *Action) askForPrivateKey(ctx context.Context, name, prompt string) (str fmt.Fprintln(stdout, prompt) for i, k := range kl { - fmt.Fprintf(stdout, "[%d] %s\n", i, crypto.FormatKey(ctx, k)) + fmt.Fprintf(stdout, "[%d] %s - %s\n", i, crypto.Name(), crypto.FormatKey(ctx, k)) } iv, err := termio.AskForInt(ctx, fmt.Sprintf("Please enter the number of a key (0-%d, [q]uit)", len(kl)-1), 0) if err != nil { diff --git a/action/history_test.go b/action/history_test.go index 047747d24e..7d0a27cf67 100644 --- a/action/history_test.go +++ b/action/history_test.go @@ -22,6 +22,7 @@ func TestHistory(t *testing.T) { defer u.Remove() ctx := context.Background() + ctx = ctxutil.WithDebug(ctx, true) ctx = ctxutil.WithAlwaysYes(ctx, true) ctx = backend.WithRCSBackend(ctx, backend.GitCLI) ctx = backend.WithCryptoBackend(ctx, backend.Plain) @@ -30,6 +31,7 @@ func TestHistory(t *testing.T) { cfg.Root.Path = backend.FromPath(u.StoreDir("")) act, err := newAction(ctx, cfg, semver.Version{}) assert.NoError(t, err) + assert.NoError(t, act.Initialized(ctx, nil)) buf := &bytes.Buffer{} out.Stdout = buf @@ -41,6 +43,7 @@ func TestHistory(t *testing.T) { // init git assert.NoError(t, act.gitInit(ctx, "", "foo bar", "foo.bar@example.org")) + buf.Reset() // insert bar fs := flag.NewFlagSet("default", flag.ContinueOnError) @@ -48,6 +51,7 @@ func TestHistory(t *testing.T) { c := cli.NewContext(app, fs, nil) assert.NoError(t, act.Insert(ctx, c)) + buf.Reset() // history bar fs = flag.NewFlagSet("default", flag.ContinueOnError) @@ -55,4 +59,5 @@ func TestHistory(t *testing.T) { c = cli.NewContext(app, fs, nil) assert.NoError(t, act.History(ctx, c)) + buf.Reset() } diff --git a/action/init.go b/action/init.go index b86950cf01..3e1f9979e4 100644 --- a/action/init.go +++ b/action/init.go @@ -8,6 +8,8 @@ import ( "github.com/fatih/color" "github.com/justwatchcom/gopass/backend" "github.com/justwatchcom/gopass/config" + "github.com/justwatchcom/gopass/store/sub" + "github.com/justwatchcom/gopass/utils/agent/client" "github.com/justwatchcom/gopass/utils/ctxutil" "github.com/justwatchcom/gopass/utils/cui" "github.com/justwatchcom/gopass/utils/out" @@ -21,6 +23,7 @@ import ( // prepared. func (s *Action) Initialized(ctx context.Context, c *cli.Context) error { if !s.Store.Initialized(ctx) { + out.Debug(ctx, "Store needs to be initialized") if !ctxutil.IsInteractive(ctx) { return exitError(ctx, ExitNotInitialized, nil, "password-store is not initialized. Try '%s init'", s.Name) } @@ -31,6 +34,7 @@ func (s *Action) Initialized(ctx context.Context, c *cli.Context) error { return nil } } + out.Debug(ctx, "Store is already initialized") return nil } @@ -59,7 +63,9 @@ func (s *Action) init(ctx context.Context, alias, path string, nogit bool, keys path = s.Store.Path() } } + out.Debug(ctx, "init(%s, %s, %t, %+v)", alias, path, nogit, keys) + out.Debug(ctx, "Checking private keys ...") if len(keys) < 1 { nk, err := s.askForPrivateKey(ctx, alias, color.CyanString("Please select a private key for encrypting secrets:")) if err != nil { @@ -68,17 +74,20 @@ func (s *Action) init(ctx context.Context, alias, path string, nogit bool, keys keys = []string{nk} } + out.Debug(ctx, "Initializing sub store - Alias: %s - Path: %s - Keys: %+v", alias, path, keys) if err := s.Store.Init(ctx, alias, path, keys...); err != nil { return errors.Wrapf(err, "failed to init store '%s' at '%s'", alias, path) } if alias != "" && path != "" { + out.Debug(ctx, "Mounting sub store %s -> %s", alias, path) if err := s.Store.AddMount(ctx, alias, path); err != nil { return errors.Wrapf(err, "failed to add mount '%s'", alias) } } if !nogit { + out.Debug(ctx, "Initializing RCS ...") if err := s.gitInit(ctx, alias, "", ""); err != nil { out.Debug(ctx, "Stacktrace: %+v\n", err) out.Red(ctx, "Failed to init git: %s", err) @@ -116,19 +125,32 @@ func (s *Action) InitOnboarding(ctx context.Context, c *cli.Context) error { email := c.String("email") ctx = out.AddPrefix(ctx, "[init] ") - ctx = backend.WithRCSBackend(ctx, backend.GitCLI) + out.Debug(ctx, "Starting Onboarding Wizard - remote: %s - team: %s - create: %t - name: %s - email: %s", remote, team, create, name, email) + + crypto := s.Store.Crypto(ctx, name) + if crypto == nil { + c, err := sub.GetCryptoBackend(ctx, backend.GetCryptoBackend(ctx), config.Directory(), client.New(config.Directory())) + if err != nil { + return errors.Wrapf(err, "failed to init crypto backend") + } + crypto = c + } + + out.Debug(ctx, "Crypto Backend initialized as: %s", crypto.Name()) // check for existing GPG keypairs (private/secret keys). We need at least // one useable key pair. If none exists try to create one - if !s.initHasUseablePrivateKeys(ctx, team) { - out.Yellow(ctx, "No useable GPG keys. Generating new key pair") - ctx := out.AddPrefix(ctx, "[gpg] ") + if !s.initHasUseablePrivateKeys(ctx, crypto, team) { + out.Yellow(ctx, "No useable crypto keys. Generating new key pair") + ctx := out.AddPrefix(ctx, "[crypto] ") out.Print(ctx, "Key generation may take up to a few minutes") - if err := s.initCreatePrivateKey(ctx, team, name, email); err != nil { + if err := s.initCreatePrivateKey(ctx, crypto, team, name, email); err != nil { return errors.Wrapf(err, "failed to create new private key") } } + out.Debug(ctx, "Has useable private keys") + // if a git remote and a team name are given attempt unattended team setup if remote != "" && team != "" { if create { @@ -162,8 +184,7 @@ func (s *Action) InitOnboarding(ctx context.Context, c *cli.Context) error { return nil } -func (s *Action) initCreatePrivateKey(ctx context.Context, mount, name, email string) error { - crypto := s.Store.Crypto(ctx, mount) +func (s *Action) initCreatePrivateKey(ctx context.Context, crypto backend.Crypto, mount, name, email string) error { out.Green(ctx, "Creating key pair ...") out.Yellow(ctx, "WARNING: We are about to generate some GPG keys.") out.Print(ctx, `However, the GPG program can sometimes lock up, displaying the following: @@ -213,8 +234,8 @@ https://github.com/justwatchcom/gopass/blob/master/docs/entropy.md`) return nil } -func (s *Action) initHasUseablePrivateKeys(ctx context.Context, mount string) bool { - kl, err := s.Store.Crypto(ctx, mount).ListPrivateKeyIDs(ctx) +func (s *Action) initHasUseablePrivateKeys(ctx context.Context, crypto backend.Crypto, mount string) bool { + kl, err := crypto.ListPrivateKeyIDs(ctx) if err != nil { return false } @@ -248,8 +269,13 @@ func (s *Action) initSetupGitRemote(ctx context.Context, team, remote string) er func (s *Action) initLocal(ctx context.Context, c *cli.Context) error { ctx = out.AddPrefix(ctx, "[local] ") + path := "" + if s.Store != nil { + path = s.Store.URL() + } + out.Print(ctx, "Initializing your local store ...") - if err := s.init(out.WithHidden(ctx, true), "", "", false); err != nil { + if err := s.init(out.WithHidden(ctx, true), "", path, false); err != nil { return errors.Wrapf(err, "failed to init local store") } out.Green(ctx, " -> OK") diff --git a/action/init_test.go b/action/init_test.go index 592428b584..6b5f497d21 100644 --- a/action/init_test.go +++ b/action/init_test.go @@ -39,8 +39,9 @@ func TestInit(t *testing.T) { assert.NoError(t, act.Initialized(ctx, c)) assert.Error(t, act.Init(ctx, c)) assert.Error(t, act.InitOnboarding(ctx, c)) - assert.Equal(t, true, act.initHasUseablePrivateKeys(ctx, "")) - assert.Error(t, act.initCreatePrivateKey(ctx, "", "foo bar", "foo.bar@example.org")) + crypto := act.Store.Crypto(ctx, "") + assert.Equal(t, true, act.initHasUseablePrivateKeys(ctx, crypto, "")) + assert.Error(t, act.initCreatePrivateKey(ctx, crypto, "", "foo bar", "foo.bar@example.org")) buf.Reset() // un-initialize the store diff --git a/app.go b/app.go index 99a9b1d056..85157f979b 100644 --- a/app.go +++ b/app.go @@ -29,6 +29,7 @@ func setupApp(ctx context.Context, sv semver.Version) *cli.App { } } + // initialize action handlers action, err := ap.New(ctx, cfg, sv) if err != nil { out.Red(ctx, "No gpg binary found: %s", err) diff --git a/backend/crypto/xc/decrypt.go b/backend/crypto/xc/decrypt.go index 0372b46e9d..e44070cf0b 100644 --- a/backend/crypto/xc/decrypt.go +++ b/backend/crypto/xc/decrypt.go @@ -29,7 +29,7 @@ func (x *XC) Decrypt(ctx context.Context, buf []byte) ([]byte, error) { } // try to find a suiteable decryption key in the header - sk, err := x.decryptSessionKey(msg.Header) + sk, err := x.decryptSessionKey(ctx, msg.Header) if err != nil { return nil, err } @@ -87,12 +87,12 @@ func (x *XC) findPublicKey(needle string) (*keyring.PublicKey, error) { } // decryptPrivateKey will ask the agent to unlock the private key -func (x *XC) decryptPrivateKey(recp *keyring.PrivateKey) error { +func (x *XC) decryptPrivateKey(ctx context.Context, recp *keyring.PrivateKey) error { fp := recp.Fingerprint() for i := 0; i < maxUnlockAttempts; i++ { // retry asking for key in case it's wrong - passphrase, err := x.client.Passphrase(fp, fmt.Sprintf("Unlock private key %s", recp.Fingerprint())) + passphrase, err := x.client.Passphrase(ctx, fp, fmt.Sprintf("Unlock private key %s", recp.Fingerprint())) if err != nil { return errors.Wrapf(err, "failed to get passphrase from agent: %s", err) } @@ -103,7 +103,7 @@ func (x *XC) decryptPrivateKey(recp *keyring.PrivateKey) error { } // decryption failed, clear cache and wait a moment before trying again - if err := x.client.Remove(fp); err != nil { + if err := x.client.Remove(ctx, fp); err != nil { return errors.Wrapf(err, "failed to clear cache") } time.Sleep(10 * time.Millisecond) @@ -114,7 +114,7 @@ func (x *XC) decryptPrivateKey(recp *keyring.PrivateKey) error { // decryptSessionKey will attempt to find a readable recipient entry in the // header and decrypt it's session key -func (x *XC) decryptSessionKey(hdr *xcpb.Header) ([]byte, error) { +func (x *XC) decryptSessionKey(ctx context.Context, hdr *xcpb.Header) ([]byte, error) { // find a suiteable decryption key, i.e. a recipient entry which was encrypted // for one of our private keys recp, err := x.findDecryptionKey(hdr) @@ -130,7 +130,7 @@ func (x *XC) decryptSessionKey(hdr *xcpb.Header) ([]byte, error) { } // unlock recipient key - if err := x.decryptPrivateKey(recp); err != nil { + if err := x.decryptPrivateKey(ctx, recp); err != nil { return nil, err } diff --git a/backend/crypto/xc/encrypt.go b/backend/crypto/xc/encrypt.go index 90211dbfab..620b430cae 100644 --- a/backend/crypto/xc/encrypt.go +++ b/backend/crypto/xc/encrypt.go @@ -43,7 +43,7 @@ func (x *XC) Encrypt(ctx context.Context, plaintext []byte, recipients []string) } // encrypt the session key per recipient - header, err := x.encryptHeader(privKey, sk, recipients) + header, err := x.encryptHeader(ctx, privKey, sk, recipients) if err != nil { return nil, errors.Wrapf(err, "failed to encrypt header: %s", err) } @@ -60,7 +60,7 @@ func (x *XC) Encrypt(ctx context.Context, plaintext []byte, recipients []string) // encrypt header creates and populates a header struct with the nonce (plain) // and the session key encrypted per recipient -func (x *XC) encryptHeader(signKey *keyring.PrivateKey, sk []byte, recipients []string) (*xcpb.Header, error) { +func (x *XC) encryptHeader(ctx context.Context, signKey *keyring.PrivateKey, sk []byte, recipients []string) (*xcpb.Header, error) { hdr := &xcpb.Header{ Sender: signKey.Fingerprint(), Recipients: make(map[string][]byte, len(recipients)), @@ -75,7 +75,7 @@ func (x *XC) encryptHeader(signKey *keyring.PrivateKey, sk []byte, recipients [] continue } - r, err := x.encryptForRecipient(signKey, sk, recp) + r, err := x.encryptForRecipient(ctx, signKey, sk, recp) if err != nil { return nil, errors.Wrapf(err, "failed to encrypt session key for recipient %s: %s", recp, err) } @@ -87,7 +87,7 @@ func (x *XC) encryptHeader(signKey *keyring.PrivateKey, sk []byte, recipients [] } // encryptForRecipients encrypts the given session key for the given recipient -func (x *XC) encryptForRecipient(sender *keyring.PrivateKey, sk []byte, recipient string) ([]byte, error) { +func (x *XC) encryptForRecipient(ctx context.Context, sender *keyring.PrivateKey, sk []byte, recipient string) ([]byte, error) { recp := x.pubring.Get(recipient) if recp == nil { return nil, fmt.Errorf("recipient public key not available for %s", recipient) @@ -97,7 +97,7 @@ func (x *XC) encryptForRecipient(sender *keyring.PrivateKey, sk []byte, recipien copy(recipientPublicKey[:], recp.PublicKey[:]) // unlock sender key - if err := x.decryptPrivateKey(sender); err != nil { + if err := x.decryptPrivateKey(ctx, sender); err != nil { return nil, err } diff --git a/backend/crypto/xc/encrypt_test.go b/backend/crypto/xc/encrypt_test.go index 1813161ac7..543b2c9cfb 100644 --- a/backend/crypto/xc/encrypt_test.go +++ b/backend/crypto/xc/encrypt_test.go @@ -19,15 +19,15 @@ type fakeAgent struct { pw string } -func (f *fakeAgent) Ping() error { +func (f *fakeAgent) Ping(context.Context) error { return nil } -func (f *fakeAgent) Remove(string) error { +func (f *fakeAgent) Remove(context.Context, string) error { return nil } -func (f *fakeAgent) Passphrase(string, string) (string, error) { +func (f *fakeAgent) Passphrase(context.Context, string, string) (string, error) { return f.pw, nil } diff --git a/backend/crypto/xc/keyring/private_key.go b/backend/crypto/xc/keyring/private_key.go index 135107ff31..82a7fa07f5 100644 --- a/backend/crypto/xc/keyring/private_key.go +++ b/backend/crypto/xc/keyring/private_key.go @@ -14,19 +14,27 @@ import ( "golang.org/x/crypto/nacl/secretbox" ) +// saltLength is chosen based on the recommendation in +// https://tools.ietf.org/html/draft-irtf-cfrg-argon2-03#section-3.1 +const ( + saltLength = 16 + nonceLength = 24 + keyLength = 32 +) + // PrivateKey is a private key part of a keypair type PrivateKey struct { PublicKey Encrypted bool EncryptedData []byte - privateKey [32]byte // only available after decryption - Nonce [24]byte // for private key encryption + privateKey [keyLength]byte // only available after decryption + Nonce [nonceLength]byte // for private key encryption Salt []byte // for KDF } // PrivateKey returns the decrypted private key material -func (p *PrivateKey) PrivateKey() [32]byte { +func (p *PrivateKey) PrivateKey() [keyLength]byte { return p.privateKey } @@ -52,18 +60,18 @@ func GenerateKeypair(passphrase string) (*PrivateKey, error) { // Encrypt encrypts the private key material with the given passphrase func (p *PrivateKey) Encrypt(passphrase string) error { - p.Salt = make([]byte, 12) + p.Salt = make([]byte, saltLength) if n, err := crypto_rand.Read(p.Salt); err != nil || n < len(p.Salt) { return err } secretKey := p.deriveKey(passphrase) - //fmt.Printf("[Encrypt] Passphrase: %s -> SecretKey: %x\n", passphrase, secretKey) - var nonce [24]byte + + var nonce [nonceLength]byte if _, err := io.ReadFull(crypto_rand.Reader, nonce[:]); err != nil { return err } - //fmt.Printf("[Encrypt] Plaintext: %x\n", p.privateKey) p.Nonce = nonce + p.EncryptedData = secretbox.Seal(nil, p.privateKey[:], &nonce, &secretKey) return nil } @@ -74,20 +82,20 @@ func (p *PrivateKey) Decrypt(passphrase string) error { return nil } secretKey := p.deriveKey(passphrase) - //fmt.Printf("[Decrypt] Passphrase: %s -> SecretKey: %x\n", passphrase, secretKey) + decrypted, ok := secretbox.Open(nil, p.EncryptedData, &p.Nonce, &secretKey) if !ok { return fmt.Errorf("decryption error") } copy(p.privateKey[:], decrypted) - //fmt.Printf("[Decrypt] Plaintext: %x\n", p.privateKey) + p.Encrypted = false return nil } -func (p *PrivateKey) deriveKey(passphrase string) [32]byte { - secretKeyBytes := argon2.Key([]byte(passphrase), p.Salt, 4, 32*1024, 4, 32) - var secretKey [32]byte +func (p *PrivateKey) deriveKey(passphrase string) [keyLength]byte { + secretKeyBytes := argon2.IDKey([]byte(passphrase), p.Salt, 4, 64*1024, 4, 32) + var secretKey [keyLength]byte copy(secretKey[:], secretKeyBytes) return secretKey } diff --git a/backend/crypto/xc/keyring/secring.go b/backend/crypto/xc/keyring/secring.go index 7aa968eb61..ffbab264d6 100644 --- a/backend/crypto/xc/keyring/secring.go +++ b/backend/crypto/xc/keyring/secring.go @@ -182,9 +182,10 @@ func secPBToKR(xpk *xcpb.PrivateKey) *PrivateKey { PublicKey: [32]byte{}, }, EncryptedData: make([]byte, len(xpk.Ciphertext)), - Nonce: [24]byte{}, + Nonce: [nonceLength]byte{}, Salt: make([]byte, len(xpk.Salt)), } + // public part pk.PublicKey.CreationTime = time.Unix(int64(xpk.PublicKey.CreationTime), 0) switch xpk.PublicKey.PubKeyAlgo { @@ -193,6 +194,7 @@ func secPBToKR(xpk *xcpb.PrivateKey) *PrivateKey { } copy(pk.PublicKey.PublicKey[:], xpk.PublicKey.PublicKey) pk.PublicKey.Identity = xpk.PublicKey.Identity + // private part pk.Encrypted = true copy(pk.EncryptedData, xpk.Ciphertext) @@ -214,6 +216,7 @@ func secKRToPB(pk *PrivateKey) *xcpb.PrivateKey { Nonce: make([]byte, len(pk.Nonce)), Salt: make([]byte, len(pk.Salt)), } + // public key switch pk.PubKeyAlgo { case PubKeyNaCl: diff --git a/backend/crypto/xc/utils.go b/backend/crypto/xc/utils.go index 23a8b7c8b6..dc2e756dd4 100644 --- a/backend/crypto/xc/utils.go +++ b/backend/crypto/xc/utils.go @@ -81,10 +81,10 @@ func (x *XC) FindPrivateKeys(ctx context.Context, search ...string) ([]string, e // FormatKey formats a key func (x *XC) FormatKey(ctx context.Context, id string) string { if key := x.pubring.Get(id); key != nil { - return key.Identity.ID() + return id + " - " + key.Identity.ID() } if key := x.secring.Get(id); key != nil { - return key.PublicKey.Identity.ID() + return id + " - " + key.PublicKey.Identity.ID() } return id } diff --git a/backend/crypto/xc/utils_test.go b/backend/crypto/xc/utils_test.go index d2614f75cc..377afeaf21 100644 --- a/backend/crypto/xc/utils_test.go +++ b/backend/crypto/xc/utils_test.go @@ -46,7 +46,7 @@ func TestCreatePrivateKeyBatch(t *testing.T) { assert.Equal(t, len(pubKeys), len(privKeys)) id := pubKeys[0] - assert.Equal(t, "foo ", xc.FormatKey(ctx, id)) + assert.Contains(t, xc.FormatKey(ctx, id), "foo ") assert.Equal(t, "foo", xc.NameFromKey(ctx, id)) assert.Equal(t, "bar@example.org", xc.EmailFromKey(ctx, id)) diff --git a/backend/crypto/xc/xc.go b/backend/crypto/xc/xc.go index 7956f76d1c..9fd761a67d 100644 --- a/backend/crypto/xc/xc.go +++ b/backend/crypto/xc/xc.go @@ -19,9 +19,9 @@ const ( ) type agentClient interface { - Ping() error - Passphrase(string, string) (string, error) - Remove(string) error + Ping(context.Context) error + Passphrase(context.Context, string, string) (string, error) + Remove(context.Context, string) error } // XC is an experimental crypto backend @@ -75,7 +75,7 @@ func (x *XC) Initialized(ctx context.Context) error { if x.client == nil { return fmt.Errorf("client not initialized") } - if err := x.client.Ping(); err != nil { + if err := x.client.Ping(ctx); err != nil { return fmt.Errorf("agent not running") } return nil diff --git a/backend/storage.go b/backend/storage.go index 8219f88134..cbdc4dc327 100644 --- a/backend/storage.go +++ b/backend/storage.go @@ -31,6 +31,7 @@ type Storage interface { List(ctx context.Context, prefix string) ([]string, error) IsDir(ctx context.Context, name string) bool Prune(ctx context.Context, prefix string) error + Available(ctx context.Context) error Name() string Version() semver.Version diff --git a/backend/storage/fs/store.go b/backend/storage/fs/store.go index 64507dd5c8..cd717c24d7 100644 --- a/backend/storage/fs/store.go +++ b/backend/storage/fs/store.go @@ -109,3 +109,8 @@ func (s *Store) Name() string { func (s *Store) Version() semver.Version { return semver.Version{Minor: 1} } + +// Available will check if this backend is useable +func (s *Store) Available(ctx context.Context) error { + return s.Set(ctx, ".test", []byte("test")) +} diff --git a/backend/storage/kv/consul/store.go b/backend/storage/kv/consul/store.go index 68b93d6710..1134d230ad 100644 --- a/backend/storage/kv/consul/store.go +++ b/backend/storage/kv/consul/store.go @@ -129,3 +129,8 @@ func (s *Store) Name() string { func (s *Store) Version() semver.Version { return semver.Version{Major: 1} } + +// Available will check if this backend is useable +func (s *Store) Available(ctx context.Context) error { + return s.Set(ctx, ".test", []byte("test")) +} diff --git a/backend/storage/kv/inmem/store.go b/backend/storage/kv/inmem/store.go index ed3620986b..d6ae2a4da0 100644 --- a/backend/storage/kv/inmem/store.go +++ b/backend/storage/kv/inmem/store.go @@ -115,3 +115,11 @@ func (m *InMem) Name() string { func (m *InMem) Version() semver.Version { return semver.Version{Major: 1} } + +// Available will check if this backend is useable +func (m *InMem) Available(ctx context.Context) error { + if m.data == nil { + return fmt.Errorf("not initialized") + } + return nil +} diff --git a/commands.go b/commands.go index f4e7701dac..f15bc19995 100644 --- a/commands.go +++ b/commands.go @@ -23,7 +23,7 @@ func getCommands(ctx context.Context, action *ap.Action, app *cli.App) []cli.Com Action: func(c *cli.Context) error { ec := make(chan error) go func() { - ec <- agent.New(config.Directory()).ListenAndServe() + ec <- agent.New(config.Directory()).ListenAndServe(ctx) }() select { case <-ctx.Done(): @@ -38,7 +38,7 @@ func getCommands(ctx context.Context, action *ap.Action, app *cli.App) []cli.Com Usage: "Start a simple agent test client", Hidden: true, Action: func(c *cli.Context) error { - pw, err := client.New(config.Directory()).Passphrase("test", "test") + pw, err := client.New(config.Directory()).Passphrase(ctx, "test", "test") if err != nil { return err } @@ -419,6 +419,7 @@ func getCommands(ctx context.Context, action *ap.Action, app *cli.App) []cli.Com Action: func(c *cli.Context) error { return action.JSONAPI(withGlobalFlags(ctx, c), c) }, + Before: func(c *cli.Context) error { return action.Initialized(withGlobalFlags(ctx, c), c) }, }, { Name: "configure", diff --git a/config/config.go b/config/config.go index 4e342b0ead..df11295975 100644 --- a/config/config.go +++ b/config/config.go @@ -3,6 +3,7 @@ package config import ( "fmt" "os" + "path/filepath" "sort" "github.com/pkg/errors" @@ -24,6 +25,7 @@ func init() { // Config is the current config struct type Config struct { + Path string `yaml:"-"` Root *StoreConfig `yaml:"root"` Mounts map[string]*StoreConfig `yaml:"mounts"` Version string `yaml:"version"` @@ -35,6 +37,7 @@ type Config struct { // New creates a new config with sane default values func New() *Config { return &Config{ + Path: configLocation(), Root: &StoreConfig{ AskForMore: false, AutoClip: true, @@ -113,3 +116,8 @@ func (c *Config) String() string { } return fmt.Sprintf("Config[Root:%s,Mounts(%s),Version:%s]", c.Root.String(), mounts, c.Version) } + +// Directory returns the directory this config is using +func (c *Config) Directory() string { + return filepath.Dir(c.Path) +} diff --git a/config/io.go b/config/io.go index 85a9bdb249..129d1c7207 100644 --- a/config/io.go +++ b/config/io.go @@ -57,6 +57,7 @@ func load(cf string) (*Config, error) { if cfg.Mounts == nil { cfg.Mounts = make(map[string]*StoreConfig) } + cfg.Path = cf return cfg, nil } diff --git a/config/secrets/config.go b/config/secrets/config.go new file mode 100644 index 0000000000..a6a07960f7 --- /dev/null +++ b/config/secrets/config.go @@ -0,0 +1,153 @@ +package secrets + +import ( + crypto_rand "crypto/rand" + "encoding/json" + "fmt" + "io" + "io/ioutil" + "path/filepath" + + "github.com/justwatchcom/gopass/utils/fsutil" + "github.com/pkg/errors" + + "golang.org/x/crypto/argon2" + "golang.org/x/crypto/nacl/secretbox" +) + +const ( + saltLength = 16 + nonceLength = 24 + keyLength = 32 + filename = "config.enc" +) + +// Config is an encrypted config store +type Config struct { + filename string + passphrase string +} + +// New will load the given file from disk and try to unseal it +func New(dir, passphrase string) (*Config, error) { + if dir == "" || dir == "." { + return nil, fmt.Errorf("dir must not be empty") + } + fn := filepath.Join(dir, filename) + + c := &Config{ + filename: fn, + passphrase: passphrase, + } + + if !fsutil.IsFile(fn) { + err := save(c.filename, c.passphrase, map[string]string{}) + return c, err + } + + _, err := c.Get("") + if err != nil { + return nil, errors.Wrapf(err, "failed to open existing secrects config %s: %s", fn, err) + } + return c, nil +} + +// Get loads the requested key from disk +func (c *Config) Get(key string) (string, error) { + data, err := load(c.filename, c.passphrase) + return data[key], err +} + +// Set writes the requested key to disk +func (c *Config) Set(key, value string) error { + data, err := load(c.filename, c.passphrase) + if err != nil { + return errors.Wrapf(err, "failed to read secrects config %s: %s", c.filename, err) + } + + old := data[key] + if value == old { + return nil + } + + data[key] = value + return save(c.filename, c.passphrase, data) +} + +// Unset removes the key from the storage +func (c *Config) Unset(key string) error { + data, err := load(c.filename, c.passphrase) + if err != nil { + return errors.Wrapf(err, "failed to read secrects config %s: %s", c.filename, err) + } + + _, found := data[key] + if !found { + return nil + } + + delete(data, key) + return save(c.filename, c.passphrase, data) +} + +func load(fn, passphrase string) (map[string]string, error) { + buf, err := ioutil.ReadFile(fn) + if err != nil { + return nil, err + } + return open(buf, passphrase) +} + +// open will try to unseal the given buffer +func open(buf []byte, passphrase string) (map[string]string, error) { + salt := make([]byte, saltLength) + copy(salt, buf[:saltLength]) + var nonce [nonceLength]byte + copy(nonce[:], buf[saltLength:nonceLength+saltLength]) + secretKey := deriveKey(passphrase, salt) + decrypted, ok := secretbox.Open(nil, buf[nonceLength+saltLength:], &nonce, &secretKey) + if !ok { + return nil, fmt.Errorf("failed to decrypt") + } + data := map[string]string{} + if err := json.Unmarshal(decrypted, &data); err != nil { + return nil, err + } + return data, nil +} + +// save will try to marshal, seal and write to disk +func save(filename, passphrase string, data map[string]string) error { + buf, err := seal(data, passphrase) + if err != nil { + return err + } + return ioutil.WriteFile(filename, buf, 0600) +} + +// seal will try to marshal and seal the given data +func seal(data map[string]string, passphrase string) ([]byte, error) { + jstr, err := json.Marshal(data) + if err != nil { + return nil, err + } + var nonce [nonceLength]byte + if _, err := io.ReadFull(crypto_rand.Reader, nonce[:]); err != nil { + return nil, err + } + salt := make([]byte, saltLength) + if _, err := crypto_rand.Read(salt); err != nil { + return nil, err + } + secretKey := deriveKey(passphrase, salt) + prefix := append(salt, nonce[:]...) + return secretbox.Seal(prefix, jstr, &nonce, &secretKey), nil +} + +// parameters chosen as per https://godoc.org/golang.org/x/crypto/argon2#IDKey +func deriveKey(passphrase string, salt []byte) [keyLength]byte { + secretKeyBytes := argon2.IDKey([]byte(passphrase), salt, 4, 64*1024, 4, 32) + var secretKey [keyLength]byte + copy(secretKey[:], secretKeyBytes) + return secretKey +} diff --git a/config/secrets/config_test.go b/config/secrets/config_test.go new file mode 100644 index 0000000000..e5feeaea50 --- /dev/null +++ b/config/secrets/config_test.go @@ -0,0 +1,29 @@ +package secrets + +import ( + "io/ioutil" + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestSaveLoad(t *testing.T) { + d := map[string]string{ + "foo": "bar", + } + + tempdir, err := ioutil.TempDir("", "gopass-") + assert.NoError(t, err) + defer func() { + _ = os.RemoveAll(tempdir) + }() + + fn := filepath.Join(tempdir, "config.sec") + + assert.NoError(t, save(fn, "foobar", d)) + data, err := load(fn, "foobar") + assert.NoError(t, err) + assert.Equal(t, d, data) +} diff --git a/store/root/git.go b/store/root/git.go index 83d2c6d4e1..69dff16870 100644 --- a/store/root/git.go +++ b/store/root/git.go @@ -11,6 +11,9 @@ import ( // Sync returns the sync backend func (r *Store) Sync(ctx context.Context, name string) backend.RCS { _, sub, _ := r.getStore(ctx, name) + if sub == nil { + return nil + } return sub.RCS() } diff --git a/store/root/gpg.go b/store/root/gpg.go index 93b2c56d7e..94b7fbe12a 100644 --- a/store/root/gpg.go +++ b/store/root/gpg.go @@ -9,5 +9,8 @@ import ( // Crypto returns the crypto backend func (r *Store) Crypto(ctx context.Context, name string) backend.Crypto { _, sub, _ := r.getStore(ctx, name) + if sub == nil { + return nil + } return sub.Crypto() } diff --git a/store/root/init.go b/store/root/init.go index 3fcf62905e..93b0b503dc 100644 --- a/store/root/init.go +++ b/store/root/init.go @@ -6,16 +6,27 @@ import ( "github.com/justwatchcom/gopass/backend" "github.com/justwatchcom/gopass/config" "github.com/justwatchcom/gopass/store/sub" + "github.com/justwatchcom/gopass/utils/agent/client" + "github.com/justwatchcom/gopass/utils/out" + "github.com/pkg/errors" ) // Initialized checks on disk if .gpg-id was generated and thus returns true. func (r *Store) Initialized(ctx context.Context) bool { + if r.store == nil { + out.Debug(ctx, "initializing store and possible sub-stores") + if err := r.initialize(ctx); err != nil { + out.Red(ctx, "Faild to initialize stores: %s", err) + return false + } + } return r.store.Initialized(ctx) } // Init tries to initialize a new password store location matching the object func (r *Store) Init(ctx context.Context, alias, path string, ids ...string) error { - sub, err := sub.New(ctx, alias, path, config.Directory()) + out.Debug(ctx, "Instantiating new sub store %s at %s for %+v", alias, path, ids) + sub, err := sub.New(ctx, alias, path, r.cfg.Directory(), r.agent) if err != nil { return err } @@ -23,6 +34,7 @@ func (r *Store) Init(ctx context.Context, alias, path string, ids ...string) err r.store = sub } + out.Debug(ctx, "Initializing sub store at %s for %+v", path, ids) if err := sub.Init(ctx, path, ids...); err != nil { return err } @@ -30,9 +42,15 @@ func (r *Store) Init(ctx context.Context, alias, path string, ids ...string) err if r.cfg.Root.Path == nil { r.cfg.Root.Path = backend.FromPath(path) } - r.cfg.Root.Path.Crypto = backend.GetCryptoBackend(ctx) - r.cfg.Root.Path.RCS = backend.GetRCSBackend(ctx) - r.cfg.Root.Path.Storage = backend.GetStorageBackend(ctx) + if backend.HasCryptoBackend(ctx) { + r.cfg.Root.Path.Crypto = backend.GetCryptoBackend(ctx) + } + if backend.HasRCSBackend(ctx) { + r.cfg.Root.Path.RCS = backend.GetRCSBackend(ctx) + } + if backend.HasStorageBackend(ctx) { + r.cfg.Root.Path.Storage = backend.GetStorageBackend(ctx) + } } else { if sc := r.cfg.Mounts[alias]; sc == nil { r.cfg.Mounts[alias] = &config.StoreConfig{} @@ -40,9 +58,63 @@ func (r *Store) Init(ctx context.Context, alias, path string, ids ...string) err if r.cfg.Mounts[alias].Path == nil { r.cfg.Mounts[alias].Path = backend.FromPath(path) } - r.cfg.Mounts[alias].Path.Crypto = backend.GetCryptoBackend(ctx) - r.cfg.Mounts[alias].Path.RCS = backend.GetRCSBackend(ctx) - r.cfg.Mounts[alias].Path.Storage = backend.GetStorageBackend(ctx) + if backend.HasCryptoBackend(ctx) { + r.cfg.Mounts[alias].Path.Crypto = backend.GetCryptoBackend(ctx) + } + if backend.HasRCSBackend(ctx) { + r.cfg.Mounts[alias].Path.RCS = backend.GetRCSBackend(ctx) + } + if backend.HasStorageBackend(ctx) { + r.cfg.Mounts[alias].Path.Storage = backend.GetStorageBackend(ctx) + } + } + return nil +} + +func (r *Store) initialize(ctx context.Context) error { + // already initialized? + if r.store != nil { + return nil } + + // init agent client + r.agent = client.New(config.Directory()) + + // create the base store + { + // capture ctx to limit effect on the next sub.New call and to not + // propagate it's effects to the mounts below + ctx := ctx + if !backend.HasCryptoBackend(ctx) { + ctx = backend.WithCryptoBackend(ctx, r.cfg.Root.Path.Crypto) + } + if !backend.HasRCSBackend(ctx) { + ctx = backend.WithRCSBackend(ctx, r.cfg.Root.Path.RCS) + } + if !backend.HasStorageBackend(ctx) { + ctx = backend.WithStorageBackend(ctx, r.cfg.Root.Path.Storage) + } + s, err := sub.New(ctx, "", r.url.String(), r.cfg.Directory(), r.agent) + if err != nil { + return errors.Wrapf(err, "failed to initialize the root store at '%s': %s", r.url.String(), err) + } + out.Debug(ctx, "Root Store initialized with URL %s", r.url.String()) + r.store = s + } + + // initialize all mounts + for alias, sc := range r.cfg.Mounts { + if err := r.addMount(ctx, alias, sc.Path.String(), sc); err != nil { + out.Red(ctx, "Failed to initialize mount %s (%s). Ignoring: %s", alias, sc.Path.String(), err) + continue + } + out.Debug(ctx, "Sub-Store mounted at %s from %s", alias, sc.Path.String()) + } + + // check for duplicate mounts + if err := r.checkMounts(); err != nil { + return errors.Errorf("checking mounts failed: %s", err) + } + return nil } diff --git a/store/root/mount.go b/store/root/mount.go index ec34312c97..64d438ca70 100644 --- a/store/root/mount.go +++ b/store/root/mount.go @@ -47,7 +47,7 @@ func (r *Store) addMount(ctx context.Context, alias, path string, sc *config.Sto out.Debug(ctx, "addMount - Using RCS backend %s", backend.RCSBackendName(sc.Path.RCS)) } } - s, err := sub.New(ctx, alias, path, config.Directory()) + s, err := sub.New(ctx, alias, path, r.cfg.Directory(), r.agent) if err != nil { return errors.Wrapf(err, "failed to initialize store '%s' at '%s': %s", alias, path, err) } diff --git a/store/root/store.go b/store/root/store.go index 2bef19337c..fe4ee5ffe9 100644 --- a/store/root/store.go +++ b/store/root/store.go @@ -8,7 +8,7 @@ import ( "github.com/justwatchcom/gopass/backend" "github.com/justwatchcom/gopass/config" "github.com/justwatchcom/gopass/store/sub" - "github.com/justwatchcom/gopass/utils/out" + "github.com/justwatchcom/gopass/utils/agent/client" "github.com/pkg/errors" ) @@ -19,6 +19,7 @@ type Store struct { url *backend.URL // url of the root store store *sub.Store version string + agent *client.Client } // New creates a new store @@ -36,40 +37,6 @@ func New(ctx context.Context, cfg *config.Config) (*Store, error) { version: cfg.Version, } - // create the base store - { - // capture ctx to limit effect on the next sub.New call and to not - // propagate it's effects to the mounts below - ctx := ctx - if !backend.HasCryptoBackend(ctx) { - ctx = backend.WithCryptoBackend(ctx, cfg.Root.Path.Crypto) - } - if !backend.HasRCSBackend(ctx) { - ctx = backend.WithRCSBackend(ctx, cfg.Root.Path.RCS) - } - if !backend.HasStorageBackend(ctx) { - ctx = backend.WithStorageBackend(ctx, cfg.Root.Path.Storage) - } - s, err := sub.New(ctx, "", r.url.String(), config.Directory()) - if err != nil { - return nil, errors.Wrapf(err, "failed to initialize the root store at '%s': %s", r.Path(), err) - } - r.store = s - } - - // initialize all mounts - for alias, sc := range cfg.Mounts { - if err := r.addMount(ctx, alias, sc.Path.String(), sc); err != nil { - out.Red(ctx, "Failed to initialize mount %s (%s). Ignoring: %s", alias, sc.Path.String(), err) - continue - } - } - - // check for duplicate mounts - if err := r.checkMounts(); err != nil { - return nil, errors.Errorf("checking mounts failed: %s", err) - } - return r, nil } @@ -90,14 +57,29 @@ func (r *Store) String() string { for alias, sub := range r.mounts { ms = append(ms, alias+"="+sub.String()) } - return fmt.Sprintf("Store(Path: %s, Mounts: %+v)", r.store.Path(), strings.Join(ms, ",")) + path := "" + if r.store != nil { + path = r.store.Path() + } + return fmt.Sprintf("Store(Path: %s, Mounts: %+v)", path, strings.Join(ms, ",")) } // Path returns the store path func (r *Store) Path() string { + if r.url == nil { + return "" + } return r.url.Path } +// URL returns the store URL +func (r *Store) URL() string { + if r.url == nil { + return "" + } + return r.url.String() +} + // Alias always returns an empty string func (r *Store) Alias() string { return "" @@ -106,5 +88,8 @@ func (r *Store) Alias() string { // Store returns the storage backend for the given mount point func (r *Store) Store(ctx context.Context, name string) backend.Storage { _, sub, _ := r.getStore(ctx, name) + if sub == nil { + return nil + } return sub.Storage() } diff --git a/store/root/store_test.go b/store/root/store_test.go index a340275816..cdc9d56522 100644 --- a/store/root/store_test.go +++ b/store/root/store_test.go @@ -120,7 +120,7 @@ func TestListNested(t *testing.T) { func createRootStore(ctx context.Context, u *gptest.Unit) (*Store, error) { ctx = backend.WithRCSBackendString(ctx, "noop") ctx = backend.WithCryptoBackendString(ctx, "plain") - return New( + s, err := New( ctx, &config.Config{ Root: &config.StoreConfig{ @@ -128,4 +128,9 @@ func createRootStore(ctx context.Context, u *gptest.Unit) (*Store, error) { }, }, ) + if err != nil { + return nil, err + } + s.Initialized(ctx) + return s, nil } diff --git a/store/sub/crypto.go b/store/sub/crypto.go new file mode 100644 index 0000000000..5df3c9e6ff --- /dev/null +++ b/store/sub/crypto.go @@ -0,0 +1,47 @@ +package sub + +import ( + "context" + "fmt" + + "github.com/justwatchcom/gopass/backend" + gpgcli "github.com/justwatchcom/gopass/backend/crypto/gpg/cli" + "github.com/justwatchcom/gopass/backend/crypto/gpg/openpgp" + "github.com/justwatchcom/gopass/backend/crypto/plain" + "github.com/justwatchcom/gopass/backend/crypto/xc" + "github.com/justwatchcom/gopass/utils/agent/client" + "github.com/justwatchcom/gopass/utils/fsutil" + "github.com/justwatchcom/gopass/utils/out" +) + +func (s *Store) initCryptoBackend(ctx context.Context) error { + cb, err := GetCryptoBackend(ctx, s.url.Crypto, s.cfgdir, s.agent) + if err != nil { + return err + } + s.crypto = cb + return nil +} + +// GetCryptoBackend initialized the correct crypto backend +func GetCryptoBackend(ctx context.Context, cb backend.CryptoBackend, cfgdir string, agent *client.Client) (backend.Crypto, error) { + switch cb { + case backend.GPGCLI: + out.Debug(ctx, "Using Crypto Backend: gpg-cli") + return gpgcli.New(ctx, gpgcli.Config{ + Umask: fsutil.Umask(), + Args: gpgcli.GPGOpts(), + }) + case backend.XC: + out.Debug(ctx, "Using Crypto Backend: xc (EXPERIMENTAL)") + return xc.New(cfgdir, agent) + case backend.Plain: + out.Debug(ctx, "Using Crypto Backend: plain (NO ENCRYPTION)") + return plain.New(), nil + case backend.OpenPGP: + out.Debug(ctx, "Using Crypto Backend: openpgp (ALPHA)") + return openpgp.New(ctx) + default: + return nil, fmt.Errorf("no valid crypto backend selected") + } +} diff --git a/store/sub/init.go b/store/sub/init.go index 78d5517b20..258e183b47 100644 --- a/store/sub/init.go +++ b/store/sub/init.go @@ -10,6 +10,9 @@ import ( // Initialized returns true if the store is properly initialized func (s *Store) Initialized(ctx context.Context) bool { + if s == nil || s.storage == nil { + return false + } return s.storage.Exists(ctx, s.idFile(ctx, "")) } diff --git a/store/sub/rcs.go b/store/sub/rcs.go new file mode 100644 index 0000000000..4e5c61cd9d --- /dev/null +++ b/store/sub/rcs.go @@ -0,0 +1,41 @@ +package sub + +import ( + "context" + "fmt" + + "github.com/justwatchcom/gopass/backend" + gpgcli "github.com/justwatchcom/gopass/backend/crypto/gpg/cli" + gitcli "github.com/justwatchcom/gopass/backend/rcs/git/cli" + "github.com/justwatchcom/gopass/backend/rcs/git/gogit" + "github.com/justwatchcom/gopass/utils/out" +) + +func (s *Store) initRCSBackend(ctx context.Context) error { + switch s.url.RCS { + case backend.GoGit: + out.Cyan(ctx, "WARNING: Using experimental RCS backend 'go-git'") + git, err := gogit.Open(s.url.Path) + if err != nil { + out.Debug(ctx, "Failed to initialize RCS backend 'gogit': %s", err) + } else { + s.rcs = git + out.Debug(ctx, "Using RCS Backend: go-git") + } + case backend.GitCLI: + gpgBin, _ := gpgcli.Binary(ctx, "") + git, err := gitcli.Open(s.url.Path, gpgBin) + if err != nil { + out.Debug(ctx, "Failed to initialize RCS backend 'gitcli': %s", err) + } else { + s.rcs = git + out.Debug(ctx, "Using RCS Backend: gitcli") + } + case backend.Noop: + // no-op + out.Debug(ctx, "Using RCS Backend: noop") + default: + return fmt.Errorf("Unknown RCS Backend") + } + return nil +} diff --git a/store/sub/recipients_test.go b/store/sub/recipients_test.go index 8a1b48e835..35a999fe51 100644 --- a/store/sub/recipients_test.go +++ b/store/sub/recipients_test.go @@ -241,6 +241,7 @@ func TestListRecipients(t *testing.T) { "", tempdir, tempdir, + nil, ) assert.NoError(t, err) diff --git a/store/sub/storage.go b/store/sub/storage.go new file mode 100644 index 0000000000..4ec2328f56 --- /dev/null +++ b/store/sub/storage.go @@ -0,0 +1,105 @@ +package sub + +import ( + "context" + "fmt" + + "github.com/justwatchcom/gopass/backend" + "github.com/justwatchcom/gopass/backend/storage/fs" + kvconsul "github.com/justwatchcom/gopass/backend/storage/kv/consul" + "github.com/justwatchcom/gopass/backend/storage/kv/inmem" + "github.com/justwatchcom/gopass/config/secrets" + "github.com/justwatchcom/gopass/utils/out" + "github.com/pkg/errors" +) + +func (s *Store) initStorageBackend(ctx context.Context) error { + switch s.url.Storage { + case backend.FS: + out.Debug(ctx, "Using Storage Backend: fs") + s.storage = fs.New(s.url.Path) + case backend.InMem: + out.Debug(ctx, "Using Storage Backend: inmem") + s.storage = inmem.New() + case backend.Consul: + out.Debug(ctx, "Using Storage Backend: consul") + token := s.url.Query.Get("token") + key := fmt.Sprintf("consul-%s-%s", s.alias, s.url.String()) + if token == "" { + out.Debug(ctx, "Requesting token from secrets config: %s", key) + t, err := s.loadSecret(ctx, key) + if err != nil { + return errors.Wrapf(err, "failed to load token from secrets config: %s", err) + } + out.Debug(ctx, "Got token from secrets config: '%s'", t) + token = t + } + out.Debug(ctx, "Consul-Token: '%s'", token) + store, err := kvconsul.New(s.url.Host+":"+s.url.Port, s.url.Path, s.url.Query.Get("datacenter"), token) + if err != nil { + _ = s.agent.Remove(ctx, key) + return err + } + // test connection and save token if it works + if err := store.Available(ctx); err != nil { + out.Debug(ctx, "Consul access not working. removing saved token") + _ = s.eraseSecret(ctx, key) + return err + } + out.Debug(ctx, "Consul access OK. saving token") + if err := s.storeSecret(ctx, key, token); err != nil { + return err + } + s.storage = store + default: + return fmt.Errorf("Unknown storage backend") + } + return nil +} + +func (s *Store) storeSecret(ctx context.Context, key, value string) error { + pw, err := s.agent.Passphrase(ctx, "config.sec", "Please enter passphrase to (un)lock config secrets") + if err != nil { + return err + } + seccfg, err := secrets.New(s.cfgdir, pw) + if err != nil { + return err + } + return seccfg.Set(key, value) +} + +func (s *Store) eraseSecret(ctx context.Context, key string) error { + pw, err := s.agent.Passphrase(ctx, "config.sec", "Please enter passphrase to (un)lock config secrets") + if err != nil { + return err + } + seccfg, err := secrets.New(s.cfgdir, pw) + if err != nil { + return err + } + _ = s.agent.Remove(ctx, key) + return seccfg.Unset(key) +} + +func (s *Store) loadSecret(ctx context.Context, key string) (string, error) { + pw, err := s.agent.Passphrase(ctx, "config.sec", "Please enter passphrase to (un)lock config secrets") + if err != nil { + return "", err + } + seccfg, err := secrets.New(s.cfgdir, pw) + if err != nil { + return "", err + } + t, err := seccfg.Get(key) + if err == nil && t != "" { + return t, nil + } + t, err = s.agent.Passphrase(ctx, key, "Please enter the secret "+key) + if err != nil { + return "", err + } + _ = s.agent.Remove(ctx, key) + err = seccfg.Set(key, t) + return t, err +} diff --git a/store/sub/store.go b/store/sub/store.go index 527debe437..5447274829 100644 --- a/store/sub/store.go +++ b/store/sub/store.go @@ -7,20 +7,10 @@ import ( "strings" "github.com/justwatchcom/gopass/backend" - gpgcli "github.com/justwatchcom/gopass/backend/crypto/gpg/cli" - "github.com/justwatchcom/gopass/backend/crypto/gpg/openpgp" - "github.com/justwatchcom/gopass/backend/crypto/plain" - "github.com/justwatchcom/gopass/backend/crypto/xc" - gitcli "github.com/justwatchcom/gopass/backend/rcs/git/cli" - "github.com/justwatchcom/gopass/backend/rcs/git/gogit" "github.com/justwatchcom/gopass/backend/rcs/noop" - "github.com/justwatchcom/gopass/backend/storage/fs" - kvconsul "github.com/justwatchcom/gopass/backend/storage/kv/consul" - "github.com/justwatchcom/gopass/backend/storage/kv/inmem" "github.com/justwatchcom/gopass/store" "github.com/justwatchcom/gopass/utils/agent/client" "github.com/justwatchcom/gopass/utils/ctxutil" - "github.com/justwatchcom/gopass/utils/fsutil" "github.com/justwatchcom/gopass/utils/out" "github.com/muesli/goprogressbar" "github.com/pkg/errors" @@ -34,23 +24,24 @@ type Store struct { rcs backend.RCS storage backend.Storage cfgdir string + agent *client.Client } // New creates a new store, copying settings from the given root store -func New(ctx context.Context, alias, path string, cfgdir string) (*Store, error) { - // TODO - out.Debug(ctx, "Path: %s", path) +func New(ctx context.Context, alias, path, cfgdir string, agent *client.Client) (*Store, error) { + out.Debug(ctx, "sub.New - Path: %s", path) u, err := backend.ParseURL(path) if err != nil { return nil, errors.Wrapf(err, "failed to parse path URL '%s': %s", path, err) } - out.Debug(ctx, "URL: %s", u.String()) + out.Debug(ctx, "sub.New - URL: %s", u.String()) s := &Store{ alias: alias, url: u, rcs: noop.New(), cfgdir: cfgdir, + agent: agent, } // init store backend @@ -59,7 +50,7 @@ func New(ctx context.Context, alias, path string, cfgdir string) (*Store, error) out.Debug(ctx, "sub.New - Using storage backend from ctx: %s", backend.StorageBackendName(s.url.Storage)) } if err := s.initStorageBackend(ctx); err != nil { - return nil, err + return nil, errors.Wrapf(err, "failed to init storage backend: %s", err) } // init sync backend @@ -68,7 +59,7 @@ func New(ctx context.Context, alias, path string, cfgdir string) (*Store, error) out.Debug(ctx, "sub.New - Using RCS backend from ctx: %s", backend.RCSBackendName(s.url.RCS)) } if err := s.initRCSBackend(ctx); err != nil { - return nil, err + return nil, errors.Wrapf(err, "failed to init RCS backend: %s", err) } // init crypto backend @@ -77,97 +68,13 @@ func New(ctx context.Context, alias, path string, cfgdir string) (*Store, error) out.Debug(ctx, "sub.New - Using Crypto backend from ctx: %s", backend.CryptoBackendName(s.url.Crypto)) } if err := s.initCryptoBackend(ctx); err != nil { - return nil, err + return nil, errors.Wrapf(err, "failed to init crypto backend: %s", err) } + out.Debug(ctx, "sub.New - initialized - storage: %p - rcs: %p - crypto: %p", s.storage, s.rcs, s.crypto) return s, nil } -func (s *Store) initStorageBackend(ctx context.Context) error { - switch s.url.Storage { - case backend.FS: - out.Debug(ctx, "Using Storage Backend: fs") - s.storage = fs.New(s.url.Path) - case backend.InMem: - out.Debug(ctx, "Using Storage Backend: inmem") - s.storage = inmem.New() - case backend.Consul: - out.Debug(ctx, "Using Storage Backend: consul") - store, err := kvconsul.New(s.url.Host+":"+s.url.Port, s.url.Path, s.url.Query.Get("datacenter"), s.url.Query.Get("token")) - if err != nil { - return err - } - s.storage = store - default: - return fmt.Errorf("Unknown storage backend") - } - return nil -} - -func (s *Store) initRCSBackend(ctx context.Context) error { - switch s.url.RCS { - case backend.GoGit: - out.Cyan(ctx, "WARNING: Using experimental RCS backend 'go-git'") - git, err := gogit.Open(s.url.Path) - if err != nil { - out.Debug(ctx, "Failed to initialize RCS backend 'gogit': %s", err) - } else { - s.rcs = git - out.Debug(ctx, "Using RCS Backend: go-git") - } - case backend.GitCLI: - gpgBin, _ := gpgcli.Binary(ctx, "") - git, err := gitcli.Open(s.url.Path, gpgBin) - if err != nil { - out.Debug(ctx, "Failed to initialize RCS backend 'gitcli': %s", err) - } else { - s.rcs = git - out.Debug(ctx, "Using RCS Backend: gitcli") - } - case backend.Noop: - // no-op - out.Debug(ctx, "Using RCS Backend: noop") - default: - return fmt.Errorf("Unknown RCS Backend") - } - return nil -} - -func (s *Store) initCryptoBackend(ctx context.Context) error { - switch s.url.Crypto { - case backend.GPGCLI: - out.Debug(ctx, "Using Crypto Backend: gpg-cli") - gpg, err := gpgcli.New(ctx, gpgcli.Config{ - Umask: fsutil.Umask(), - Args: gpgcli.GPGOpts(), - }) - if err != nil { - return err - } - s.crypto = gpg - case backend.XC: - out.Debug(ctx, "Using Crypto Backend: xc (EXPERIMENTAL)") - crypto, err := xc.New(s.cfgdir, client.New(s.cfgdir)) - if err != nil { - return err - } - s.crypto = crypto - case backend.Plain: - out.Debug(ctx, "Using Crypto Backend: plain (NO ENCRYPTION)") - s.crypto = plain.New() - case backend.OpenPGP: - out.Debug(ctx, "Using Crypto Backend: openpgp (ALPHA)") - crypto, err := openpgp.New(ctx) - if err != nil { - return err - } - s.crypto = crypto - default: - return fmt.Errorf("no valid crypto backend selected") - } - return nil -} - // idFile returns the path to the recipient list for this.storage // it walks up from the given filename until it finds a directory containing // a gpg id file or it leaves the scope of this.storage. @@ -317,6 +224,9 @@ func (s *Store) reencryptGitPush(ctx context.Context) error { // Path returns the value of path func (s *Store) Path() string { + if s.url == nil { + return "" + } return s.url.Path } diff --git a/store/sub/store_test.go b/store/sub/store_test.go index d10776b5aa..dc85bba55d 100644 --- a/store/sub/store_test.go +++ b/store/sub/store_test.go @@ -53,6 +53,7 @@ func createSubStore(dir string) (*Store, error) { "", sd, sd, + nil, ) } @@ -184,7 +185,7 @@ func TestNew(t *testing.T) { ok: false, }, } { - s, err := New(tc.ctx, "", tempdir, tempdir) + s, err := New(tc.ctx, "", tempdir, tempdir, nil) if tc.ok { assert.NoError(t, err) assert.NotNil(t, s) diff --git a/store/sub/templates_test.go b/store/sub/templates_test.go index ff6c43b00a..de2a782995 100644 --- a/store/sub/templates_test.go +++ b/store/sub/templates_test.go @@ -32,6 +32,7 @@ func TestTemplates(t *testing.T) { "", tempdir, tempdir, + nil, ) assert.NoError(t, err) diff --git a/tests/jsonapi_test.go b/tests/jsonapi_test.go index 38ee1474f7..9fe09ba1c4 100644 --- a/tests/jsonapi_test.go +++ b/tests/jsonapi_test.go @@ -67,5 +67,4 @@ func TestJSONAPI(t *testing.T) { // query for keys with matching one response = getMessageResponse(t, ts, "{\"type\":\"query\",\"query\":\"foo\"}") assert.Equal(t, "[\"awesomePrefix/foo/bar\"]", response) - } diff --git a/tests/tester.go b/tests/tester.go index fbfc771724..54a7a4adbf 100644 --- a/tests/tester.go +++ b/tests/tester.go @@ -27,9 +27,7 @@ const ( autoimport: true autosync: false cliptimeout: 45 - cryptobackend: gpg noconfirm: true - syncbackend: noop safecontent: true ` keyID = "BE73F104" diff --git a/utils/agent/agent.go b/utils/agent/agent.go index 6de2e4904a..d85e1b150e 100644 --- a/utils/agent/agent.go +++ b/utils/agent/agent.go @@ -1,15 +1,19 @@ package agent import ( + "context" "fmt" "net" "net/http" "os" "path/filepath" + "sync" "time" "github.com/justwatchcom/gopass/utils/agent/client" + "github.com/justwatchcom/gopass/utils/out" "github.com/justwatchcom/gopass/utils/pinentry" + "github.com/pkg/errors" ) type piner interface { @@ -21,6 +25,7 @@ type piner interface { // Agent is a gopass agent type Agent struct { + sync.Mutex socket string testing bool server *http.Server @@ -60,19 +65,24 @@ func NewForTesting(dir, key, pass string) *Agent { } // ListenAndServe starts listening and blocks -func (a *Agent) ListenAndServe() error { +func (a *Agent) ListenAndServe(ctx context.Context) error { + out.Debug(ctx, "Trying to listen on %s", a.socket) lis, err := net.Listen("unix", a.socket) + if err == nil { + return a.server.Serve(lis) + } + + out.Debug(ctx, "Failed to listen on %s: %s", a.socket, err) + if err := client.New(filepath.Dir(a.socket)).Ping(ctx); err == nil { + return fmt.Errorf("agent already running") + } + if err := os.Remove(a.socket); err != nil { + return errors.Wrapf(err, "failed to remove old agent socket %s: %s", a.socket, err) + } + out.Debug(ctx, "Trying to listen on %s after removing old socket", a.socket) + lis, err = net.Listen("unix", a.socket) if err != nil { - if err := client.New(filepath.Dir(a.socket)).Ping(); err == nil { - return fmt.Errorf("agent already running") - } - if err := os.Remove(a.socket); err != nil { - return err - } - lis, err = net.Listen("unix", a.socket) - if err != nil { - return err - } + return errors.Wrapf(err, "failed to listen on %s after cleanup: %s", a.socket, err) } return a.server.Serve(lis) } @@ -82,6 +92,9 @@ func (a *Agent) servePing(w http.ResponseWriter, r *http.Request) { } func (a *Agent) serveRemove(w http.ResponseWriter, r *http.Request) { + a.Lock() + defer a.Unlock() + key := r.URL.Query().Get("key") if !a.testing { a.cache.remove(key) @@ -90,6 +103,9 @@ func (a *Agent) serveRemove(w http.ResponseWriter, r *http.Request) { } func (a *Agent) servePurge(w http.ResponseWriter, r *http.Request) { + a.Lock() + defer a.Unlock() + if !a.testing { a.cache.purge() } @@ -97,6 +113,9 @@ func (a *Agent) servePurge(w http.ResponseWriter, r *http.Request) { } func (a *Agent) servePassphrase(w http.ResponseWriter, r *http.Request) { + a.Lock() + defer a.Unlock() + key := r.URL.Query().Get("key") reason := r.URL.Query().Get("reason") diff --git a/utils/agent/client/client.go b/utils/agent/client/client.go index e4e6290c87..9889cfcae5 100644 --- a/utils/agent/client/client.go +++ b/utils/agent/client/client.go @@ -17,27 +17,31 @@ import ( // Client is a agent client type Client struct { - http http.Client + http *http.Client } // New creates a new client func New(dir string) *Client { socket := filepath.Join(dir, ".gopass-agent.sock") return &Client{ - http: http.Client{ + http: &http.Client{ Transport: &http.Transport{ DialContext: func(context.Context, string, string) (net.Conn, error) { return net.Dial("unix", socket) }, }, - Timeout: 30 * time.Second, + Timeout: 10 * time.Minute, }, } } // Ping checks connectivity to the agent -func (c *Client) Ping() error { - resp, err := c.http.Get("http://unix/ping") +func (c *Client) Ping(ctx context.Context) error { + pc := &http.Client{ + Transport: c.http.Transport, + Timeout: 5 * time.Second, + } + resp, err := pc.Get("http://unix/ping") if err != nil { return err } @@ -45,14 +49,31 @@ func (c *Client) Ping() error { return nil } -func (c *Client) waitForAgent() error { +func (c *Client) waitForAgent(ctx context.Context) error { bo := backoff.NewExponentialBackOff() bo.MaxElapsedTime = 60 * time.Second - return backoff.Retry(c.Ping, bo) + return backoff.Retry(func() error { return c.Ping(ctx) }, bo) +} + +func (c *Client) checkAgent(ctx context.Context) error { + if err := c.Ping(ctx); err == nil { + return nil + } + if err := c.startAgent(ctx); err != nil { + return errors.Wrapf(err, "failed to start agent") + } + if err := c.waitForAgent(ctx); err != nil { + return errors.Wrapf(err, "failed to start agent (expired)") + } + return nil } // Remove un-caches a single key -func (c *Client) Remove(key string) error { +func (c *Client) Remove(ctx context.Context, key string) error { + if err := c.checkAgent(ctx); err != nil { + return errors.Wrapf(err, "agent not available: %s", err) + } + u, err := url.Parse("http://unix/cache/remove") if err != nil { return errors.Wrapf(err, "failed to build request url") @@ -76,14 +97,9 @@ func (c *Client) Remove(key string) error { } // Passphrase asks for a passphrase from the agent -func (c *Client) Passphrase(key, reason string) (string, error) { - if err := c.Ping(); err != nil { - if err := c.startAgent(); err != nil { - return "", errors.Wrapf(err, "failed to start agent") - } - if err := c.waitForAgent(); err != nil { - return "", errors.Wrapf(err, "failed to start agent (expired)") - } +func (c *Client) Passphrase(ctx context.Context, key, reason string) (string, error) { + if err := c.checkAgent(ctx); err != nil { + return "", errors.Wrapf(err, "no agent available: %s", err) } u, err := url.Parse("http://unix/passphrase") diff --git a/utils/agent/client/client_others.go b/utils/agent/client/client_others.go index 9bf194249b..cd2db7442b 100644 --- a/utils/agent/client/client_others.go +++ b/utils/agent/client/client_others.go @@ -3,17 +3,22 @@ package client import ( + "context" "os" "os/exec" "syscall" + + "github.com/justwatchcom/gopass/utils/out" + "github.com/pkg/errors" ) -func (c *Client) startAgent() error { +func (c *Client) startAgent(ctx context.Context) error { path, err := os.Executable() if err != nil { - return err + return errors.Wrapf(err, "unable to determine executable: %s", err) } + out.Debug(ctx, "Starting agent ...") cmd := exec.Command(path, "agent") cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr diff --git a/utils/agent/client/client_windows.go b/utils/agent/client/client_windows.go index d5828571e7..30d04d3bda 100644 --- a/utils/agent/client/client_windows.go +++ b/utils/agent/client/client_windows.go @@ -3,6 +3,7 @@ package client import ( + "context" "os" "os/exec" "syscall" @@ -16,7 +17,7 @@ const ( DETACHED_PROCESS = 0x00000008 ) -func (c *Client) startAgent() error { +func (c *Client) startAgent(ctx context.Context) error { path, err := os.Executable() if err != nil { return err diff --git a/utils/jsonapi/api_test.go b/utils/jsonapi/api_test.go index 2e12af0b7c..bcd4e586a5 100644 --- a/utils/jsonapi/api_test.go +++ b/utils/jsonapi/api_test.go @@ -198,9 +198,8 @@ func runRespondRawMessages(t *testing.T, requests []verifiedRequest, secrets []s }, ) assert.NoError(t, err) - - err = populateStore(tempdir, secrets) - assert.NoError(t, err) + assert.Equal(t, false, store.Initialized(ctx)) + assert.NoError(t, populateStore(tempdir, secrets)) for _, request := range requests { var inbuf bytes.Buffer