Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,27 @@ git-credential-gopass configure --local --store=ci-team

This puts the value in front of the Gopass search path.

#### Option --erase

By default this helper never deletes your secrets. Git asks a credential helper to `erase` a credential
whenever authentication with it failed, e.g. because a token expired or was revoked. Since a temporarily
failing server, a wrong URL or a mistyped one-time password would otherwise silently wipe your secret,
`git-credential-gopass` ignores those `erase` requests unless you opt in:

```bash
git config credential.helper "gopass --erase"
```

With `--erase` the secret is removed from the store as soon as git reports it as invalid, so you will be
asked for new credentials on the next operation.

To delete a secret manually, use gopass directly:

```bash
gopass rm git/github.com/username # per host and user
gopass rm git/github.com/myrepo/username # per host, repository and user
```

#### Using with SMTP

If you want to use this with [`git-send-email`](https://git-scm.com/docs/git-send-email) you'll need to:
Expand Down
16 changes: 9 additions & 7 deletions git-credential.go
Original file line number Diff line number Diff line change
Expand Up @@ -277,14 +277,16 @@ func (s *gc) Store(ctx context.Context, cmd *cli.Command) error {

// Erase removes a credential got from git.
func (s *gc) Erase(ctx context.Context, cmd *cli.Command) error {
cred, err := parseGitCredentials(termio.Stdin)
if err != nil {
return fmt.Errorf("error: %w while parsing git-credential", err)
}
if cmd.Bool("erase") {
cred, err := parseGitCredentials(termio.Stdin)
if err != nil {
return fmt.Errorf("error: %w while parsing git-credential", err)
}

path := composePath(cmd, cred)
if err := s.gp.Remove(ctx, path); err != nil {
fmt.Fprintln(os.Stderr, "gopass error: error while writing to store")
path := composePath(cmd, cred)
if err := s.gp.Remove(ctx, path); err != nil {
fmt.Fprintln(os.Stderr, "gopass error: error while writing to store")
}
}

return nil
Expand Down
47 changes: 44 additions & 3 deletions git-credential_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ func testCmd(t *testing.T, ctx context.Context, flags map[string]string) *cli.Co
&cli.BoolFlag{Name: "global"},
&cli.BoolFlag{Name: "local"},
&cli.BoolFlag{Name: "system"},
&cli.BoolFlag{Name: "erase"},
},
Action: func(context.Context, *cli.Command) error { return nil },
}
Expand Down Expand Up @@ -185,10 +186,31 @@ func TestGitCredentialHelper(t *testing.T) { //nolint:paralleltest
assert.Equal(t, "bob", read.Username)
stdout.Reset()

// Erasing is a no-op unless the --erase flag is given, so the secret must still be there.
termio.Stdin = strings.NewReader(s)
require.NoError(t, act.Erase(ctx, cmd))
assert.Empty(t, stdout.String())

termio.Stdin = strings.NewReader(s)
require.NoError(t, act.Get(ctx, cmd))
read, err = parseGitCredentials(stdout)
require.NoError(t, err)
assert.Equal(t, "secr3=t", read.Password)
stdout.Reset()

// Invalid input is not even parsed without the --erase flag.
termio.Stdin = strings.NewReader("a")
require.NoError(t, act.Erase(ctx, cmd))

eraseCmd := testCmd(t, ctx, map[string]string{"erase": "true"})

termio.Stdin = strings.NewReader("a")
require.Error(t, act.Erase(ctx, eraseCmd))

termio.Stdin = strings.NewReader(s)
require.NoError(t, act.Erase(ctx, eraseCmd))
assert.Empty(t, stdout.String())

termio.Stdin = strings.NewReader(s)
require.NoError(t, act.Get(ctx, cmd))
assert.Empty(t, stdout.String())
Expand All @@ -197,8 +219,6 @@ func TestGitCredentialHelper(t *testing.T) { //nolint:paralleltest
require.Error(t, act.Get(ctx, cmd))
termio.Stdin = strings.NewReader("a")
require.Error(t, act.Store(ctx, cmd))
termio.Stdin = strings.NewReader("a")
require.Error(t, act.Erase(ctx, cmd))
}

func TestGitCredentialHelperWithStoreFlag(t *testing.T) { //nolint:paralleltest
Expand Down Expand Up @@ -555,9 +575,30 @@ func TestGitCredentialHelperMultipleCredentialsPerUser(t *testing.T) { //nolint:
assert.Equal(t, "token2", read.Password)
stdout.Reset()

// Erase first credential
// Erasing is a no-op by default, both credentials must still be available
termio.Stdin = strings.NewReader(s1)
require.NoError(t, act.Erase(ctx, cmd))
termio.Stdin = strings.NewReader(s2)
require.NoError(t, act.Erase(ctx, cmd))

termio.Stdin = strings.NewReader(s1)
require.NoError(t, act.Get(ctx, cmd))
read, err = parseGitCredentials(stdout)
require.NoError(t, err)
assert.Equal(t, "token1", read.Password)
stdout.Reset()

termio.Stdin = strings.NewReader(s2)
require.NoError(t, act.Get(ctx, cmd))
read, err = parseGitCredentials(stdout)
require.NoError(t, err)
assert.Equal(t, "token2", read.Password)
stdout.Reset()

// Erase first credential, only works with the --erase flag
eraseCmd := testCmd(t, ctx, map[string]string{"erase": "true"})
termio.Stdin = strings.NewReader(s1)
require.NoError(t, act.Erase(ctx, eraseCmd))
stdout.Reset()

// Try to retrieve first credential - should fail
Expand Down
6 changes: 5 additions & 1 deletion main.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,11 @@ func main() {
Flags: []cli.Flag{
&cli.StringFlag{
Name: "store",
Usage: "First part of path to find the secret.",
Usage: "First part of path to find the secret",
},
&cli.BoolFlag{
Name: "erase",
Usage: "Erase the secret when git requests",
},
},
Commands: []*cli.Command{
Expand Down
Loading