package leaf import ( "context" "errors" "fmt" "log" "os" "strings" "sync" "github.com/gopasspw/gopass/internal/config" "github.com/gopasspw/gopass/internal/out" "github.com/gopasspw/gopass/internal/store" "github.com/gopasspw/gopass/pkg/ctxutil" "github.com/gopasspw/gopass/pkg/debug" "github.com/gopasspw/gopass/pkg/termio" ) // nolint:ifshort // reencrypt will re-encrypt all entries for the current recipients. func (s *Store) reencrypt(ctx context.Context) error { entries, err := s.List(ctx, "") if err != nil { return fmt.Errorf("failed to list store: %w", err) } // Most gnupg setups don't work well with concurrency > 1, but // for other backends - e.g. age - this could very well be > 1. conc := s.crypto.Concurrency() // save original value of auto push { // shadow ctx in this block only ctx := ctxutil.WithGitCommit(ctx, false) // progress bar bar := termio.NewProgressBar(int64(len(entries))) bar.Hidden = !ctxutil.IsTerminal(ctx) || ctxutil.IsHidden(ctx) var wg sync.WaitGroup jobs := make(chan string) // We use a logger to write without race condition on stdout logger := log.New(os.Stdout, "", 0) out.Print(ctx, "Starting reencrypt") for i := range conc { wg.Add(1) // we start a new job go func(workerId int) { // the workers are fed through an unbuffered channel for e := range jobs { content, err := s.Get(ctx, e) if err != nil { logger.Printf("Worker %d: Failed to get current value for %s: %s\n", workerId, e, err) continue } if err := s.Set(WithNoGitOps(ctx, conc > 1), e, content); err != nil { if !errors.Is(err, store.ErrMeaninglessWrite) { logger.Printf("Worker %d: Failed to write %s: %s\n", workerId, e, err) continue } logger.Printf("Worker %d: Writing secret %s is not needed\n", workerId, e) } } wg.Done() // report the job as finished }(i) } for _, e := range entries { // check for context cancellation select { case <-ctx.Done(): // We close the channel, so the worker will terminate close(jobs) // we wait for all workers to have finished wg.Wait() return fmt.Errorf("context canceled") default: } if bar != nil { bar.Inc() } e = strings.TrimPrefix(e, s.alias) jobs <- e } // We close the channel, so the workers will terminate close(jobs) // we wait for all workers to have finished wg.Wait() bar.Done() } // if we are working concurrently, we cannot git add during the process // to avoid a race condition on git .index.lock file, so we do it now. if conc > 1 { for _, name := range entries { p := s.Passfile(name) if err := s.storage.TryAdd(ctx, p); err != nil { return fmt.Errorf("failed to add %q to git: %w", p, err) } debug.Log("added %s to git", p) } } if err := s.storage.TryCommit(ctx, ctxutil.GetCommitMessage(ctx)); err != nil { return fmt.Errorf("failed to commit changes to git: %w", err) } return s.reencryptGitPush(ctx) } func (s *Store) reencryptGitPush(ctx context.Context) error { ctx = config.WithMount(ctx, s.alias) if !config.Bool(ctx, "core.autopush") { debug.Log("not pushing to git remote, core.autopush is false") return nil } if err := s.storage.TryPush(ctx, "", ""); err != nil { return fmt.Errorf("failed to push change to git remote: %w", err) } return nil }