Skip to content

Commit

Permalink
Add experimental crypto backend
Browse files Browse the repository at this point in the history
This commit adds an experimental crypto backend. It comes with it's own keyring
as well as an agent. The crypto is based on NaCl and Argon2.

The on-disk format uses protobuf version 3.

Fixes gopasspw#154
  • Loading branch information
Dominik Schulz authored and dominikschulz committed Feb 14, 2018
1 parent fc06496 commit 31ff239
Show file tree
Hide file tree
Showing 219 changed files with 24,332 additions and 1,888 deletions.
7 changes: 7 additions & 0 deletions .codecov.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,12 @@ coverage:
range: 40..90
round: nearest
precision: 2
status:
project:
default: on
patch:
default: off
changes:
default: off
ignore:
- "vendor/"
1 change: 0 additions & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ language: go
dist: trusty
os:
- linux
- osx

before_install:
- if [ $TRAVIS_OS_NAME = linux ]; then sudo apt-get install git gnupg2; else brew install git gnupg || true; fi
Expand Down
8 changes: 4 additions & 4 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
FIRST_GOPATH := $(firstword $(subst :, ,$(GOPATH)))
PKGS := $(shell go list ./... | grep -v /tests)
GOFILES_NOVENDOR := $(shell find . -type f -name '*.go' -not -path "./vendor/*")
GOFILES_NOTEST := $(shell find . -type f -name '*.go' -not -path "./vendor/*" -not -name "*_test.go")
PKGS := $(shell go list ./... | grep -v /tests | grep -v /xcpb)
GOFILES_NOVENDOR := $(shell find . -type f -name '*.go' -not -path "./vendor/*" -not -name "*.pb.go")
GOFILES_NOTEST := $(shell find . -type f -name '*.go' -not -path "./vendor/*" -not -name "*_test.go" -not -name "*.pb.go")
GOPASS_VERSION ?= $(shell cat VERSION)
GOPASS_OUTPUT ?= gopass
GOPASS_REVISION := $(shell cat COMMIT 2>/dev/null || git rev-parse --short=8 HEAD)
Expand Down Expand Up @@ -147,7 +147,7 @@ codequality:
$(GO) get -u github.com/fzipp/gocyclo; \
fi
@$(foreach gofile, $(GOFILES_NOVENDOR),\
gocyclo -over 15 $(gofile) || exit 1;)
gocyclo -over 20 $(gofile) || exit 1;)
@$(call ok)

@echo -n " LINT "
Expand Down
61 changes: 3 additions & 58 deletions action/action.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,10 @@ import (
"io"
"os"
"path/filepath"
"strconv"
"strings"

"github.com/blang/semver"
"github.com/justwatchcom/gopass/backend/crypto/gpg"
gpgcli "github.com/justwatchcom/gopass/backend/crypto/gpg/cli"
"github.com/justwatchcom/gopass/config"
"github.com/justwatchcom/gopass/store/root"
"github.com/justwatchcom/gopass/utils/out"
)

var (
Expand All @@ -22,44 +17,20 @@ var (
stderr io.Writer = os.Stderr
)

type gpger interface {
Binary() string
ListPublicKeys(context.Context) (gpg.KeyList, error)
FindPublicKeys(context.Context, ...string) (gpg.KeyList, error)
ListPrivateKeys(context.Context) (gpg.KeyList, error)
CreatePrivateKeyBatch(context.Context, string, string, string) error
CreatePrivateKey(context.Context) error
FindPrivateKeys(context.Context, ...string) (gpg.KeyList, error)
GetRecipients(context.Context, string) ([]string, error)
Encrypt(context.Context, string, []byte, []string) error
Decrypt(context.Context, string) ([]byte, error)
ExportPublicKey(context.Context, string, string) error
ImportPublicKey(context.Context, string) error
Version(context.Context) semver.Version
}

// Action knows everything to run gopass CLI actions
type Action struct {
Name string
Store *root.Store
cfg *config.Config
gpg gpger
version semver.Version
}

// New returns a new Action wrapper
func New(ctx context.Context, cfg *config.Config, sv semver.Version) (*Action, error) {
gpg, err := gpgcli.New(ctx, gpgcli.Config{
Umask: umask(),
Args: gpgOpts(),
})
if err != nil {
out.Red(ctx, "Warning: GPG not found: %s", err)
}
return newAction(ctx, cfg, sv, gpg)
return newAction(ctx, cfg, sv)
}

func newAction(ctx context.Context, cfg *config.Config, sv semver.Version, gpg gpger) (*Action, error) {
func newAction(ctx context.Context, cfg *config.Config, sv semver.Version) (*Action, error) {
name := "gopass"
if len(os.Args) > 0 {
name = filepath.Base(os.Args[0])
Expand All @@ -69,10 +40,9 @@ func newAction(ctx context.Context, cfg *config.Config, sv semver.Version, gpg g
Name: name,
cfg: cfg,
version: sv,
gpg: gpg,
}

store, err := root.New(ctx, cfg, act.gpg)
store, err := root.New(ctx, cfg)
if err != nil {
return nil, exitError(ctx, ExitUnknown, err, "failed to init root store: %s", err)
}
Expand All @@ -81,32 +51,7 @@ func newAction(ctx context.Context, cfg *config.Config, sv semver.Version, gpg g
return act, nil
}

func umask() int {
for _, en := range []string{"GOPASS_UMASK", "PASSWORD_STORE_UMASK"} {
if um := os.Getenv(en); um != "" {
if iv, err := strconv.ParseInt(um, 8, 32); err == nil && iv >= 0 && iv <= 0777 {
return int(iv)
}
}
}
return 077
}

func gpgOpts() []string {
for _, en := range []string{"GOPASS_GPG_OPTS", "PASSWORD_STORE_GPG_OPTS"} {
if opts := os.Getenv(en); opts != "" {
return strings.Fields(opts)
}
}
return nil
}

// String implement fmt.Stringer
func (s *Action) String() string {
return s.Store.String()
}

// HasGPG returns true if the GPG wrapper is initialized
func (s *Action) HasGPG() bool {
return s.gpg != nil
}
38 changes: 4 additions & 34 deletions action/action_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import (
"testing"

"github.com/blang/semver"
gpgmock "github.com/justwatchcom/gopass/backend/crypto/gpg/mock"
"github.com/justwatchcom/gopass/backend"
"github.com/justwatchcom/gopass/config"
"github.com/justwatchcom/gopass/tests/gptest"
"github.com/stretchr/testify/assert"
Expand All @@ -18,10 +18,9 @@ func newMock(ctx context.Context, u *gptest.Unit) (*Action, error) {
cfg := config.New()
cfg.Root.Path = u.StoreDir("")

sv := semver.Version{}
gpg := gpgmock.New()

return newAction(ctx, cfg, sv, gpg)
ctx = backend.WithSyncBackendString(ctx, "gitmock")
ctx = backend.WithCryptoBackendString(ctx, "gpgmock")
return newAction(ctx, cfg, semver.Version{})
}

func TestAction(t *testing.T) {
Expand All @@ -34,7 +33,6 @@ func TestAction(t *testing.T) {
assert.Equal(t, "action.test", act.Name)

assert.Contains(t, act.String(), u.StoreDir(""))
assert.Equal(t, true, act.HasGPG())
assert.Equal(t, 0, len(act.Store.Mounts()))
}

Expand All @@ -56,31 +54,3 @@ func TestNew(t *testing.T) {
_, err = New(ctx, cfg, sv)
assert.NoError(t, err)
}

func TestUmask(t *testing.T) {
for _, vn := range []string{"GOPASS_UMASK", "PASSWORD_STORE_UMASK"} {
for in, out := range map[string]int{
"002": 02,
"0777": 0777,
"000": 0,
"07557575": 077,
} {
assert.NoError(t, os.Setenv(vn, in))
assert.Equal(t, out, umask())
assert.NoError(t, os.Unsetenv(vn))
}
}
}

func TestGpgOpts(t *testing.T) {
for _, vn := range []string{"GOPASS_GPG_OPTS", "PASSWORD_STORE_GPG_OPTS"} {
for in, out := range map[string][]string{
"": nil,
"--decrypt --armor --recipient 0xDEADBEEF": {"--decrypt", "--armor", "--recipient", "0xDEADBEEF"},
} {
assert.NoError(t, os.Setenv(vn, in))
assert.Equal(t, out, gpgOpts())
assert.NoError(t, os.Unsetenv(vn))
}
}
}
63 changes: 32 additions & 31 deletions action/clihelper.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ func (s *Action) ConfirmRecipients(ctx context.Context, name string, recipients
return recipients, nil
}

crypto := s.Store.Crypto(ctx, name)
sort.Strings(recipients)

fmt.Fprintf(stdout, "gopass: Encrypting %s for these recipients:\n", name)
Expand All @@ -33,7 +34,7 @@ func (s *Action) ConfirmRecipients(ctx context.Context, name string, recipients
default:
}

kl, err := s.gpg.FindPublicKeys(ctx, r)
kl, err := crypto.FindPublicKeys(ctx, r)
if err != nil {
out.Red(ctx, "Failed to read public key for '%s': %s", name, err)
continue
Expand All @@ -42,7 +43,7 @@ func (s *Action) ConfirmRecipients(ctx context.Context, name string, recipients
fmt.Fprintln(stdout, "key not found", r)
continue
}
fmt.Fprintf(stdout, " - %s\n", kl[0].OneLine())
fmt.Fprintf(stdout, " - %s\n", crypto.FormatKey(ctx, kl[0]))
}
fmt.Fprintln(stdout, "")

Expand All @@ -58,23 +59,23 @@ func (s *Action) ConfirmRecipients(ctx context.Context, name string, recipients
}

// askforPrivateKey promts the user to select from a list of private keys
func (s *Action) askForPrivateKey(ctx context.Context, prompt string) (string, error) {
func (s *Action) askForPrivateKey(ctx context.Context, name, prompt string) (string, error) {
if !ctxutil.IsInteractive(ctx) {
return "", errors.New("no interaction without terminal")
}

kl, err := s.gpg.ListPrivateKeys(ctx)
crypto := s.Store.Crypto(ctx, name)
kl, err := crypto.ListPrivateKeyIDs(ctx)
if err != nil {
return "", err
}
kl = kl.UseableKeys()
if len(kl) < 1 {
return "", errors.New("No useable private keys found")
}

for i := 0; i < maxTries; i++ {
if !ctxutil.IsTerminal(ctx) {
return kl[0].Fingerprint, nil
return kl[0], nil
}
// check for context cancelation
select {
Expand All @@ -85,14 +86,14 @@ func (s *Action) askForPrivateKey(ctx context.Context, prompt string) (string, e

fmt.Fprintln(stdout, prompt)
for i, k := range kl {
fmt.Fprintf(stdout, "[%d] %s\n", i, k.OneLine())
fmt.Fprintf(stdout, "[%d] %s\n", i, crypto.FormatKey(ctx, k))
}
iv, err := termio.AskForInt(ctx, fmt.Sprintf("Please enter the number of a key (0-%d)", len(kl)-1), 0)
if err != nil {
continue
}
if iv >= 0 && iv < len(kl) {
return kl[iv].Fingerprint, nil
return kl[iv], nil
}
}
return "", errors.New("no valid user input")
Expand All @@ -104,39 +105,39 @@ func (s *Action) askForPrivateKey(ctx context.Context, prompt string) (string, e
// On error or no selection, name and email will be empty.
// If s.isTerm is false (i.e., the user cannot be prompted), however,
// the first identity's name/email pair found is returned.
func (s *Action) askForGitConfigUser(ctx context.Context) (string, string, error) {
func (s *Action) askForGitConfigUser(ctx context.Context, name string) (string, string, error) {
var useCurrent bool

keyList, err := s.gpg.ListPrivateKeys(ctx)
crypto := s.Store.Crypto(ctx, name)
keyList, err := crypto.ListPrivateKeyIDs(ctx)
if err != nil {
return "", "", err
}
keyList = keyList.UseableKeys()
if len(keyList) < 1 {
return "", "", errors.New("No usable private keys found")
}

for _, key := range keyList {
for _, identity := range key.Identities {
if !ctxutil.IsTerminal(ctx) {
return identity.Name, identity.Email, nil
}
// check for context cancelation
select {
case <-ctx.Done():
return "", "", errors.New("user aborted")
default:
}

useCurrent, err = termio.AskForBool(
ctx,
fmt.Sprintf("Use %s (%s) for password store git config?", identity.Name, identity.Email), true)
if err != nil {
return "", "", err
}
if useCurrent {
return identity.Name, identity.Email, nil
}
// check for context cancelation
select {
case <-ctx.Done():
return "", "", errors.New("user aborted")
default:
}

name := crypto.NameFromKey(ctx, key)
email := crypto.EmailFromKey(ctx, key)

useCurrent, err = termio.AskForBool(
ctx,
fmt.Sprintf("Use %s (%s) for password store git config?", name, email),
true,
)
if err != nil {
return "", "", err
}
if useCurrent {
return name, email, nil
}
}

Expand Down
33 changes: 3 additions & 30 deletions action/clihelper_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,9 +49,9 @@ func TestAskForPrivateKey(t *testing.T) {
assert.NoError(t, err)

ctx = ctxutil.WithAlwaysYes(ctx, true)
key, err := act.askForPrivateKey(ctx, "test")
key, err := act.askForPrivateKey(ctx, "test", "test")
assert.NoError(t, err)
assert.Equal(t, "000000000000000000000000DEADBEEF", key)
assert.Equal(t, "0xDEADBEEF", key)
buf.Reset()
}

Expand All @@ -66,37 +66,10 @@ func TestAskForGitConfigUser(t *testing.T) {
ctx = ctxutil.WithTerminal(ctx, true)
ctx = ctxutil.WithAlwaysYes(ctx, true)

_, _, err = act.askForGitConfigUser(ctx)
_, _, err = act.askForGitConfigUser(ctx, "test")
assert.NoError(t, err)
}

func TestAskForGitConfigUserNonInteractive(t *testing.T) {
u := gptest.NewUnitTester(t)
defer u.Remove()

ctx := context.Background()
act, err := newMock(ctx, u)
assert.NoError(t, err)

ctx = ctxutil.WithTerminal(ctx, false)

keyList, err := act.gpg.ListPrivateKeys(ctx)
assert.NoError(t, err)

name, email, _ := act.askForGitConfigUser(ctx)

// unit tests cannot know whether keyList returned empty or not.
// a better distinction would require mocking/patching
// calls to s.gpg.ListPrivateKeys()
if len(keyList) > 0 {
assert.NotEqual(t, "", name)
assert.NotEqual(t, "", email)
} else {
assert.Equal(t, "", name)
assert.Equal(t, "", email)
}
}

func TestAskForStore(t *testing.T) {
u := gptest.NewUnitTester(t)
defer u.Remove()
Expand Down
Loading

0 comments on commit 31ff239

Please sign in to comment.