Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[FEATURE] Allow for non-interactive age setup #2970

Merged
merged 6 commits into from
Oct 14, 2024
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
2 changes: 1 addition & 1 deletion .github/workflows/autorelease.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ jobs:
name: Set up Go
uses: actions/setup-go@0a12ed9d6a96ab950c8f026ed9f722fe0da7ef32 # v5.0.2
with:
go-version: '1.22'
go-version: '1.23'
- uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2
with:
path: ~/go/pkg/mod
Expand Down
8 changes: 5 additions & 3 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ jobs:
raw.githubusercontent.com:443
storage.googleapis.com:443
sum.golang.org:443
golang.org:443
go.dev:443

- uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0
with:
Expand All @@ -38,7 +40,7 @@ jobs:
- name: Set up Go
uses: actions/setup-go@0a12ed9d6a96ab950c8f026ed9f722fe0da7ef32 # v5.0.2
with:
go-version: '1.22'
go-version: '1.23'
- uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2
with:
path: ~/go/pkg/mod
Expand Down Expand Up @@ -92,7 +94,7 @@ jobs:
- name: Set up Go
uses: actions/setup-go@0a12ed9d6a96ab950c8f026ed9f722fe0da7ef32 # v5.0.2
with:
go-version: '1.22'
go-version: '1.23'

- run: git config --global user.name nobody
- run: git config --global user.email [email protected]
Expand All @@ -115,7 +117,7 @@ jobs:
- name: Set up Go
uses: actions/setup-go@0a12ed9d6a96ab950c8f026ed9f722fe0da7ef32 # v5.0.2
with:
go-version: '1.22'
go-version: '1.23'

- run: git config --global user.name nobody
- run: git config --global user.email [email protected]
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/golangci-lint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ jobs:
- name: Set up Go
uses: actions/setup-go@0a12ed9d6a96ab950c8f026ed9f722fe0da7ef32 # v5.0.2
with:
go-version: '1.22'
go-version: '1.23'
- uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0
- name: golangci-lint
uses: golangci/golangci-lint-action@aaa42aa0628b4ae2578232a66b541047968fac86 # v6.1.0
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/grype.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ jobs:
- name: Set up Go
uses: actions/setup-go@0a12ed9d6a96ab950c8f026ed9f722fe0da7ef32 # v5.0.2
with:
go-version: '1.22'
go-version: '1.23'
- uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2
with:
path: ~/go/pkg/mod
Expand Down
10 changes: 10 additions & 0 deletions docs/backends/age.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,16 @@ This will automatically create a new age keypair and initialize the new store.

Existing stores can be migrated using `gopass convert --crypto age`.

N.B. for a fully scripted or **non-interactive setup**, you can use the `GOPASS_AGE_PASSWORD` env variable
to set your identity file secret passphrase, and specify the age identity and recipients
that should be used for encrypting/decrypting passwords as follows:
```
$ gopass age identity add <AGE-...> <age1...>
$ GOPASS_AGE_PASSWORD=mypassword gopass init --crypto age <age1...>
```
Notice the extra space in front of the command to skip most shell's history.
You'll need to set your name and username using `git` directly if you're using it as storage backend (the default one).

## Features

* Encryption using `age` library, can be decrypted using the `age` CLI
Expand Down
3 changes: 2 additions & 1 deletion docs/config.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ Some configuration options are only available through setting environment variab
| **Option** | **Type** | **Description** |
|------------------------------|----------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `CHECKPOINT_DISABLE` | `bool` | Set to any non-empty value to disable calling the GitHub API when running `gopass version`. |
| `GOPASS_AGE_PASSWORD` | `string` | Set to any value (including the empty string) to use as a password for the age identity file containing your secret age identities. |
| `GOPASS_AUTOSYNC_INTERVAL` | `int` | Set this to the number of days between autosync runs. |
| `GOPASS_CHARACTER_SET` | `bool` | Set to any non-empty value to restrict the characters used in generated passwords |
| `GOPASS_CLIPBOARD_CLEAR_CMD` | `string` | Use an external command to remove a password from the clipboard. See [GPaste](usecases/gpaste.md) for an example |
Expand All @@ -20,7 +21,7 @@ Some configuration options are only available through setting environment variab
| `GOPASS_DEBUG_LOG_SECRETS` | `bool` | Set to any non-empty value to enable logging of credentials |
| `GOPASS_DEBUG_LOG` | `string` | Set to a filename to enable debug logging (only set GOPASS_DEBUG to log to stderr) |
| `GOPASS_DEBUG` | `bool` | Set to any non-empty value to enable verbose debug output, by default on stderr, unless GOPASS_DEBUG_LOG is set |
| `GOPASS_DEBUG_VERBOSE` | `int` | Set to any integer value larger than zero to increase the verbosity of debug output |
| `GOPASS_DEBUG_VERBOSE` | `int` | Set to any integer value larger than zero to increase the verbosity of debug output |
| `GOPASS_EXTERNAL_PWGEN` | `string` | Use an external password generator. See [Features](features.md#using-custom-password-generators) for details |
| `GOPASS_FORCE_CHECK` | `string` | (internal) Force the updater to check for updates. Used for testing. |
| `GOPASS_FORCE_UPDATE` | `bool` | Set to any non-empty value to force an update (if available) |
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
module github.com/gopasspw/gopass

go 1.22.1
go 1.23.2

require (
filippo.io/age v1.2.1-0.20240618131852-7eedd929a6cf
Expand Down
3 changes: 2 additions & 1 deletion internal/action/action_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,8 @@ func newMock(ctx context.Context, path string) (*Action, error) {
c := cli.NewContext(cli.NewApp(), fs, nil)
c.Context = ctx
if err := act.IsInitialized(c); err != nil {
return nil, err
// we still return the action since this might be expected sometimes
return act, err
}

return act, nil
Expand Down
5 changes: 2 additions & 3 deletions internal/action/copy.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,14 @@ package action
import (
"context"
"fmt"
"github.com/gopasspw/gopass/internal/tree"
"path/filepath"
"strings"

"github.com/gopasspw/gopass/internal/action/exit"
"github.com/gopasspw/gopass/internal/tree"
"github.com/gopasspw/gopass/pkg/ctxutil"
"github.com/gopasspw/gopass/pkg/termio"

"github.com/urfave/cli/v2"
)

Expand Down Expand Up @@ -45,7 +46,6 @@ func (s *Action) copy(ctx context.Context, from, to string, force bool) error {

func (s *Action) copyFlattenDir(ctx context.Context, from, to string, force bool) error {
entries, err := s.Store.List(ctx, tree.INF)

if err != nil {
return exit.Error(exit.List, err, "failed to list entries in %q", from)
}
Expand All @@ -69,7 +69,6 @@ func (s *Action) copyFlattenDir(ctx context.Context, from, to string, force bool
}

func (s *Action) copyRegular(ctx context.Context, from, to string, force bool) error {

if !force {
if s.Store.Exists(ctx, to) && !termio.AskForConfirmation(ctx, fmt.Sprintf("%s already exists. Overwrite it?", to)) {
return exit.Error(exit.Aborted, nil, "not overwriting your current secret")
Expand Down
1 change: 0 additions & 1 deletion internal/action/copy_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -270,5 +270,4 @@ func TestCopyWithTrailingSlash(t *testing.T) {
require.NoError(t, act.show(ctx, c, "new/baz", false))
assert.Equal(t, "another\n", buf.String())
buf.Reset()

}
10 changes: 5 additions & 5 deletions internal/action/generate.go
Original file line number Diff line number Diff line change
Expand Up @@ -249,13 +249,13 @@ func getPwLengthFromEnvOrAskUser(ctx context.Context) (int, error) {
return pwlen, nil
}

func clamp(min, max, value int) int {
if value < min {
return min
func clamp(mi, ma, value int) int {
if value < mi {
return mi
}

if value > max && max > 0 {
return max
if value > ma && ma > 0 {
return ma
}

return value
Expand Down
29 changes: 15 additions & 14 deletions internal/action/setup.go
Original file line number Diff line number Diff line change
Expand Up @@ -144,26 +144,27 @@ func (s *Action) initGenerateIdentity(ctx context.Context, crypto backend.Crypto

passphrase := xkcdgen.Random()
pwGenerated := true
want, err := termio.AskForBool(ctx, "⚠ Do you want to enter a passphrase? (otherwise we generate one for you)", false)
if err != nil {
return err
}
if want {
pwGenerated = false
sv, err := termio.AskForPassword(ctx, "passphrase for your new keypair", true)
if err != nil {
return fmt.Errorf("failed to read passphrase: %w", err)
}
passphrase = sv
}

// support fully automated setup (e.g. for tests)
if !ctxutil.IsInteractive(ctx) && ctxutil.HasPasswordCallback(ctx) {
//nolint:nestif
if ctxutil.HasPasswordCallback(ctx) {
pw, err := ctxutil.GetPasswordCallback(ctx)("", true)
if err == nil {
passphrase = string(pw)
}
pwGenerated = false
} else {
want, err := termio.AskForBool(ctx, "⚠ Do you want to enter a passphrase? (otherwise we generate one for you)", false)
if err != nil {
return err
}
if want {
pwGenerated = false
sv, err := termio.AskForPassword(ctx, "passphrase for your new keypair", true)
if err != nil {
return fmt.Errorf("failed to read passphrase: %w", err)
}
passphrase = sv
}
}

if crypto.Name() == "gpgcli" {
Expand Down
10 changes: 3 additions & 7 deletions internal/action/setup_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,8 @@ func TestSetupAgeGitFS(t *testing.T) {
})
ctx = ctxutil.WithPasswordPurgeCallback(ctx, func(s string) {}) //nolint:staticcheck

t.Skip("TODO: fix setup test")

act, err := newMock(ctx, u.StoreDir(""))
require.NoError(t, err)
require.ErrorContains(t, err, "not initialized")
require.NotNil(t, act)

buf := &bytes.Buffer{}
Expand All @@ -56,11 +54,11 @@ func TestSetupAgeGitFS(t *testing.T) {
require.NotNil(t, crypto)
assert.Equal(t, "age", crypto.Name())
assert.True(t, act.initHasUseablePrivateKeys(ctx, crypto))
require.Error(t, act.initGenerateIdentity(ctx, crypto, "foo bar", "[email protected]"))
require.NoError(t, act.initGenerateIdentity(ctx, crypto, "foo bar", "[email protected]"))
buf.Reset()

act.printRecipients(ctx, "")
assert.Contains(t, buf.String(), "0xDEADBEEF")
assert.Contains(t, buf.String(), "age1")
buf.Reset()
}

Expand Down Expand Up @@ -89,8 +87,6 @@ func TestSetupPlainFS(t *testing.T) {
require.NoError(t, act.IsInitialized(c))
buf.Reset()

t.Skip("TODO: fix these tests")

require.Error(t, act.Init(c))
assert.Contains(t, buf.String(), "already initialized")
buf.Reset()
Expand Down
6 changes: 3 additions & 3 deletions internal/audit/audit.go
Original file line number Diff line number Diff line change
Expand Up @@ -138,9 +138,9 @@ func (a *Auditor) Batch(ctx context.Context, secrets []string) (*Report, error)
// https://github.com/gopasspw/gopass/pull/245
//
maxJobs := a.s.Concurrency()
if max := config.Int(ctx, "audit.concurrency"); max > 0 {
if maxJobs > max {
maxJobs = max
if maxVal := config.Int(ctx, "audit.concurrency"); maxVal > 0 {
if maxJobs > maxVal {
maxJobs = maxVal
}
}

Expand Down
4 changes: 4 additions & 0 deletions internal/backend/crypto/age/identities.go
Original file line number Diff line number Diff line change
Expand Up @@ -242,7 +242,11 @@ func IdentityToRecipient(id age.Identity) age.Recipient {

// GenerateIdentity creates a new identity.
func (a *Age) GenerateIdentity(ctx context.Context, _ string, _ string, pw string) error {
// we don't check if the password callback is set, since it could only be
// set through an env variable, and here pw can only be set through an
// actual user input.
if pw != "" {
debug.Log("age GenerateIdentity using provided pw")
ctx = ctxutil.WithPasswordCallback(ctx, func(prompt string, confirm bool) ([]byte, error) {
return []byte(pw), nil
})
Expand Down
4 changes: 3 additions & 1 deletion internal/backend/crypto/age/loader.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"github.com/gopasspw/gopass/internal/backend"
"github.com/gopasspw/gopass/internal/out"
"github.com/gopasspw/gopass/pkg/debug"
"github.com/gopasspw/gopass/pkg/fsutil"
)

const (
Expand All @@ -26,7 +27,8 @@ func (l loader) New(ctx context.Context) (backend.Crypto, error) {
}

func (l loader) Handles(ctx context.Context, s backend.Storage) error {
if s.Exists(ctx, OldIDFile) || s.Exists(ctx, OldKeyring) {
// OldKeyring is meant to be in the config folder, not necessarily in the store
if s.Exists(ctx, OldIDFile) || fsutil.IsNonEmptyFile(OldKeyring) {
if err := migrate(ctx, s); err != nil {
out.Errorf(ctx, "Failed to migrate age backend: %s", err)
}
Expand Down
8 changes: 0 additions & 8 deletions internal/store/root/store.go
Original file line number Diff line number Diff line change
Expand Up @@ -106,11 +106,3 @@ func (r *Store) Concurrency() int {

return min(c, runtime.NumCPU())
}

func min(a, b int) int {
if a < b {
return a
}

return b
}
9 changes: 9 additions & 0 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -330,6 +330,15 @@ func initContext(ctx context.Context, cfg *config.Config) context.Context {
}
}

// using a password callback for age identity file or not?
if pw, isSet := os.LookupEnv("GOPASS_AGE_PASSWORD"); isSet {
ctx = ctxutil.WithPasswordCallback(ctx, func(_ string, _ bool) ([]byte, error) {
debug.Log("using age password callback from env variable GOPASS_AGE_PASSWORD")

return []byte(pw), nil
})
}

return ctx
}

Expand Down
12 changes: 6 additions & 6 deletions pkg/clipboard/clipboard.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,22 +46,22 @@ func CopyTo(ctx context.Context, name string, content []byte, timeout int) error
}

if timeout < 1 {
debug.Log("Auto-clear of clipboard disabled.")
debug.Log("Auto-clearClip of clipboard disabled.")

out.Printf(ctx, "✔ Copied %s to clipboard.", color.YellowString(name))
_ = notify.Notify(ctx, "gopass - clipboard", fmt.Sprintf("✔ Copied %s to clipboard.", name))

return nil
}

if err := clear(ctx, name, content, timeout); err != nil {
_ = notify.Notify(ctx, "gopass - clipboard", "failed to clear clipboard")
if err := clearClip(ctx, name, content, timeout); err != nil {
_ = notify.Notify(ctx, "gopass - clipboard", "failed to clearClip clipboard")

return fmt.Errorf("failed to clear clipboard: %w", err)
return fmt.Errorf("failed to clearClip clipboard: %w", err)
}

out.Printf(ctx, "✔ Copied %s to clipboard. Will clear in %d seconds.", color.YellowString(name), timeout)
_ = notify.Notify(ctx, "gopass - clipboard", fmt.Sprintf("✔ Copied %s to clipboard. Will clear in %d seconds.", name, timeout))
out.Printf(ctx, "✔ Copied %s to clipboard. Will clearClip in %d seconds.", color.YellowString(name), timeout)
_ = notify.Notify(ctx, "gopass - clipboard", fmt.Sprintf("✔ Copied %s to clipboard. Will clearClip in %d seconds.", name, timeout))

return nil
}
Expand Down
4 changes: 2 additions & 2 deletions pkg/clipboard/clipboard_others.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,11 @@ import (
"github.com/gopasspw/gopass/internal/pwschemes/argon2id"
)

// clear will spawn a copy of gopass that waits in a detached background
// clearClip will spawn a copy of gopass that waits in a detached background
// process group until the timeout is expired. It will then compare the contents
// of the clipboard and erase it if it still contains the data gopass copied
// to it.
func clear(ctx context.Context, name string, content []byte, timeout int) error {
func clearClip(ctx context.Context, name string, content []byte, timeout int) error {
hash, err := argon2id.Generate(string(content), 0)
if err != nil {
return fmt.Errorf("failed to generate checksum: %w", err)
Expand Down
2 changes: 1 addition & 1 deletion pkg/clipboard/clipboard_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ func TestUnsupportedCopyToClipboard(t *testing.T) {

func TestClearClipboard(t *testing.T) {
ctx, cancel := context.WithCancel(config.NewContextInMemory())
require.NoError(t, clear(ctx, "foo", []byte("bar"), 0))
require.NoError(t, clearClip(ctx, "foo", []byte("bar"), 0))
cancel()
time.Sleep(50 * time.Millisecond)
}
Expand Down
4 changes: 2 additions & 2 deletions pkg/clipboard/clipboard_windows.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,11 @@ import (
"github.com/gopasspw/gopass/internal/pwschemes/argon2id"
)

// clear will spwan a copy of gopass that waits in a detached background
// clearClip will spwan a copy of gopass that waits in a detached background
// process group until the timeout is expired. It will then compare the contents
// of the clipboard and erase it if it still contains the data gopass copied
// to it.
func clear(ctx context.Context, name string, content []byte, timeout int) error {
func clearClip(ctx context.Context, name string, content []byte, timeout int) error {
hash, err := argon2id.Generate(string(content), 0)
if err != nil {
return err
Expand Down
2 changes: 1 addition & 1 deletion pkg/clipboard/copy_darwin.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ func copyViaOsascript(ctx context.Context, password string) error {
"-e", "set type to current application's NSPasteboardTypeString",
// pb = a reference to the system's pasteboard
"-e", "set pb to current application's NSPasteboard's generalPasteboard()",
// Must clear contents before adding a new item to pasteboard
// Must clearClip contents before adding a new item to pasteboard
"-e", "pb's clearContents()",
// Set the flag ConcealedType so clipboard history managers don't record the password.
// The first argument can by anything, but an empty string will do fine.
Expand Down
Loading
Loading