mirror of
https://github.com/gopasspw/gopass.git
synced 2026-10-17 01:49:38 +02:00
* Adding regression test Signed-off-by: Yolan Romailler <AnomalRoil@users.noreply.github.com> * fixing recipient logic to honor subkeys Signed-off-by: Yolan Romailler <AnomalRoil@users.noreply.github.com> * Revert "Adding regression test" This reverts commit fcb85c9d2ee4ce3b1d53f934338c6a33e18d7d9d. Signed-off-by: Yolan Romailler <AnomalRoil@users.noreply.github.com> * adding comment about noop Signed-off-by: Yolan Romailler <AnomalRoil@users.noreply.github.com> * Linting Signed-off-by: Yolan Romailler <AnomalRoil@users.noreply.github.com> * Addressing review comments Signed-off-by: Yolan Romailler <AnomalRoil@users.noreply.github.com> * avoid noise debug logs Signed-off-by: Yolan Romailler <AnomalRoil@users.noreply.github.com> --------- Signed-off-by: Yolan Romailler <AnomalRoil@users.noreply.github.com>
398 lines
10 KiB
Go
398 lines
10 KiB
Go
package leaf
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"sort"
|
|
"strings"
|
|
|
|
"github.com/gopasspw/gopass/internal/backend"
|
|
"github.com/gopasspw/gopass/internal/backend/crypto/age"
|
|
"github.com/gopasspw/gopass/internal/config"
|
|
"github.com/gopasspw/gopass/internal/diff"
|
|
"github.com/gopasspw/gopass/internal/out"
|
|
"github.com/gopasspw/gopass/internal/queue"
|
|
"github.com/gopasspw/gopass/internal/store"
|
|
"github.com/gopasspw/gopass/pkg/ctxutil"
|
|
"github.com/gopasspw/gopass/pkg/debug"
|
|
"github.com/gopasspw/gopass/pkg/gopass"
|
|
)
|
|
|
|
// ErrorSeverity provides a way for a function to specify how severe of an error it experienced.
|
|
type ErrorSeverity int
|
|
|
|
const (
|
|
errsNil ErrorSeverity = iota
|
|
errsNonFatal // an error that was recovered from, but still should be acknowledged
|
|
errsFatal // an error that terminated the function early
|
|
)
|
|
|
|
func (e ErrorSeverity) String() string {
|
|
switch e {
|
|
case errsNonFatal:
|
|
return "non-fatal"
|
|
case errsFatal:
|
|
return "fatal"
|
|
case errsNil:
|
|
return "nil"
|
|
default:
|
|
return "nil"
|
|
}
|
|
}
|
|
|
|
type fsckMultiError struct {
|
|
Severity ErrorSeverity
|
|
Errors []error
|
|
}
|
|
|
|
func (f *fsckMultiError) IsError() bool {
|
|
return len(f.Errors) > 0
|
|
}
|
|
|
|
func (f *fsckMultiError) Error() string {
|
|
if len(f.Errors) < 1 {
|
|
return ""
|
|
}
|
|
|
|
var sb strings.Builder
|
|
fmt.Fprintf(&sb, "[%s] ", f.Severity.String())
|
|
msgs := make([]string, 0, len(f.Errors))
|
|
for _, e := range f.Errors {
|
|
msgs = append(msgs, e.Error())
|
|
}
|
|
sb.WriteString(strings.Join(msgs, ", "))
|
|
|
|
return sb.String()
|
|
}
|
|
|
|
func (f *fsckMultiError) Append(s ErrorSeverity, e error) *fsckMultiError {
|
|
if e == nil {
|
|
return f
|
|
}
|
|
|
|
if f.Errors == nil {
|
|
f.Errors = make([]error, 0, 1)
|
|
}
|
|
|
|
f.Errors = append(f.Errors, e)
|
|
if s > f.Severity {
|
|
f.Severity = s
|
|
}
|
|
|
|
return f
|
|
}
|
|
|
|
func (f *fsckMultiError) ErrorOrNil() error {
|
|
if len(f.Errors) < 1 {
|
|
return nil
|
|
}
|
|
|
|
return f
|
|
}
|
|
|
|
// Fsck checks all entries matching the given prefix.
|
|
func (s *Store) Fsck(ctx context.Context, path string) error {
|
|
ctx = out.AddPrefix(ctx, "["+s.alias+"] ")
|
|
ctx = config.WithMount(ctx, s.alias)
|
|
debug.Log("Checking %s", path)
|
|
|
|
// first let the storage backend check itself
|
|
debug.Log("Checking storage backend")
|
|
if err := s.storage.Fsck(ctx); err != nil {
|
|
return fmt.Errorf("storage backend error: %w", err)
|
|
}
|
|
|
|
// then try to compact storage / rcs
|
|
debug.Log("Compacting storage")
|
|
if err := s.storage.Compact(ctx); err != nil {
|
|
return fmt.Errorf("storage backend compaction failed: %w", err)
|
|
}
|
|
|
|
// make sure all recipients are valid
|
|
debug.Log("Checking recipients")
|
|
if err := s.CheckRecipients(ctx); err != nil {
|
|
out.Errorf(ctx, "Invalid recipients found: %s", err)
|
|
}
|
|
|
|
// then we'll make sure all the secrets are readable by us and every
|
|
// valid recipient
|
|
if path != "" {
|
|
out.Printf(ctx, "Checking all secrets matching %s", path)
|
|
}
|
|
|
|
if err := s.fsckLoop(ctx, path); err != nil {
|
|
return err
|
|
}
|
|
|
|
if !config.Bool(ctx, "core.autopush") {
|
|
debug.Log("not pushing to git remote, core.autopush is false")
|
|
|
|
return nil
|
|
}
|
|
|
|
if err := s.storage.Push(ctx, "", ""); err != nil {
|
|
if !errors.Is(err, store.ErrGitNoRemote) {
|
|
out.Printf(ctx, "RCS Push failed: %s", err)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (s *Store) fsckLoop(ctx context.Context, path string) error {
|
|
pcb := ctxutil.GetProgressCallback(ctx)
|
|
|
|
// disable network ops, we will push at the end. pushing on possibly
|
|
// every single secret could be terribly slow.
|
|
ctx = ctxutil.WithNoNetwork(ctx, true)
|
|
|
|
// disable the queue, for batch operations this is not necessary / wanted
|
|
// since different git processes might step onto each other toes.
|
|
ctx = queue.WithQueue(ctx, nil)
|
|
|
|
names, err := s.List(ctx, path)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to list entries for %s: %w", path, err)
|
|
}
|
|
|
|
if IsFsckDecrypt(ctx) {
|
|
ctx = ctxutil.WithCommitMessage(ctx, "fsck --decrypt to fix recipients and format")
|
|
} else {
|
|
ctx = ctxutil.WithCommitMessage(ctx, "fsck to fix (a limited part of) recipients and format")
|
|
}
|
|
|
|
var warnings strings.Builder
|
|
|
|
if err := s.fsckUpdatePublicKeys(ctxutil.WithGitCommit(ctx, false)); err != nil {
|
|
out.Errorf(ctx, "Failed to update public keys: %s", err)
|
|
} else {
|
|
ctx = ctxutil.AddToCommitMessageBody(ctx, "- updated public keys")
|
|
}
|
|
|
|
sort.Strings(names)
|
|
|
|
debug.Log("names (%d): %q", len(names), names)
|
|
buf := &strings.Builder{}
|
|
for _, name := range names {
|
|
pcb()
|
|
if strings.HasPrefix(name, s.alias+"/") {
|
|
name = strings.TrimPrefix(name, s.alias+"/")
|
|
}
|
|
|
|
debug.Log("[%s] Checking %s", path, name)
|
|
|
|
msg, err := s.fsckCheckEntry(ctx, name)
|
|
if err != nil {
|
|
warnings.WriteString(fmt.Sprintf("failed to check %q:\n %s\n", name, err))
|
|
|
|
continue
|
|
}
|
|
|
|
buf.WriteString(msg)
|
|
buf.WriteString("\n")
|
|
}
|
|
if buf.Len() > 0 {
|
|
ctx = ctxutil.AddToCommitMessageBody(ctx, buf.String())
|
|
}
|
|
|
|
// print out any deferred warnings (if any)
|
|
if warnings.Len() > 0 {
|
|
out.Warning(ctx, warnings.String())
|
|
}
|
|
|
|
if ctxutil.GetCommitMessageBody(ctx) == "" {
|
|
if warnings.Len() > 0 {
|
|
out.Errorf(ctx, "Nothing to commit: all secrets that were not up to date failed to be updated")
|
|
|
|
return nil
|
|
}
|
|
out.Warningf(ctx, "Nothing to commit: all secrets up to date")
|
|
}
|
|
|
|
if err := s.storage.TryCommit(ctx, ctxutil.GetCommitMessageFull(ctx)); err != nil {
|
|
return fmt.Errorf("failed to commit changes to git: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (s *Store) fsckUpdatePublicKeys(ctx context.Context) error {
|
|
ctx = WithPubkeyUpdate(ctx, true)
|
|
rs := s.Recipients(ctx)
|
|
|
|
// first import possibly new/updated keys to merge any changes
|
|
// that might come from others.
|
|
if err := s.ImportMissingPublicKeys(ctx, rs...); err != nil {
|
|
return fmt.Errorf("failed to import new or updated pubkeys: %w", err)
|
|
}
|
|
|
|
// then export our (possibly updated) keys for consumption
|
|
// by others.
|
|
exported, err := s.UpdateExportedPublicKeys(ctx)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to update exported pubkeys: %w", err)
|
|
}
|
|
debug.Log("Updated exported public keys: %t", exported)
|
|
|
|
return nil
|
|
}
|
|
|
|
type convertedSecret interface {
|
|
gopass.Secret
|
|
FromMime() bool
|
|
}
|
|
|
|
func (s *Store) fsckCheckEntry(ctx context.Context, name string) (string, error) {
|
|
errs := &fsckMultiError{}
|
|
recpNeedFix := false
|
|
|
|
merr := s.fsckCheckRecipients(ctx, name)
|
|
if merr.ErrorOrNil() != nil {
|
|
if merr.Severity == errsFatal {
|
|
return "", errs.Append(errsFatal, fmt.Errorf("checking recipients for %s failed:\n %w", name, merr)).ErrorOrNil()
|
|
}
|
|
// the only errsNonFatal error from that function are missing/extra recipients, or unsupported recipient checks
|
|
// all of which aren't much of an issue since we have yet to correct that by re-encrypting.
|
|
recpNeedFix = true
|
|
_ = errs.Append(merr.Severity, merr)
|
|
}
|
|
|
|
// make sure we are actually allowed to decode this secret
|
|
// if this fails there is no way we could fix anything
|
|
if !IsFsckDecrypt(ctx) {
|
|
if !recpNeedFix {
|
|
return "", nil
|
|
}
|
|
|
|
return "", errs.Append(errsFatal, fmt.Errorf("secret %s needs re-encryption", name)).ErrorOrNil()
|
|
}
|
|
|
|
// we need to make sure Parsing is enabled in order to parse old Mime secrets
|
|
ctx = ctxutil.WithShowParsing(ctx, true)
|
|
sec, err := s.Get(ctx, name)
|
|
if err != nil {
|
|
return "", errs.Append(errsFatal, fmt.Errorf("failed to decode secret %s: %w", name, err)).ErrorOrNil()
|
|
}
|
|
|
|
// check if this is still an old MIME secret.
|
|
// Note: the secret was already converted when it was parsed during Get.
|
|
// This is just checking if it was converted from MIME or not.
|
|
// This branch is pretty much useless right now, but I'd like to add some
|
|
// reporting on how many secrets were converted from MIME to new format.
|
|
// TODO: report these stats
|
|
if cs, ok := sec.(convertedSecret); ok && cs.FromMime() {
|
|
debug.Log("leftover Mime secret: %s", name)
|
|
}
|
|
|
|
if recpNeedFix {
|
|
out.Printf(ctx, "Re-encrypting %s to fix recipients and storage format. [leaf store]", name)
|
|
} else {
|
|
out.Printf(ctx, "Re-encrypting %s to fix storage format. [leaf store]", name)
|
|
}
|
|
|
|
if err := s.Set(ctxutil.WithGitCommit(ctx, false), name, sec); err != nil {
|
|
return "", errs.Append(errsFatal, fmt.Errorf("failed to write secret %s: %w", name, err)).ErrorOrNil()
|
|
}
|
|
|
|
merr = s.fsckCheckRecipients(ctx, name)
|
|
if merr.ErrorOrNil() != nil {
|
|
if merr.Severity == errsFatal {
|
|
_ = errs.Append(merr.Severity, fmt.Errorf("checking recipients for %s failed:\n %w", name, merr))
|
|
} else {
|
|
_ = errs.Append(merr.Severity, merr)
|
|
}
|
|
}
|
|
|
|
if merr.IsError() {
|
|
return "", merr.ErrorOrNil()
|
|
}
|
|
|
|
return fmt.Sprintf("- re-encrypt secret %s", name), nil
|
|
}
|
|
|
|
func (s *Store) fsckCheckRecipients(ctx context.Context, name string) *fsckMultiError {
|
|
e := &fsckMultiError{}
|
|
|
|
// now compare the recipients this secret was encoded for and fix it if
|
|
// it doesn't match.
|
|
ciphertext, err := s.storage.Get(ctx, s.Passfile(name))
|
|
if err != nil {
|
|
return e.Append(errsFatal, fmt.Errorf("failed to get raw secret: %w", err))
|
|
}
|
|
|
|
if _, ok := s.crypto.(*age.Age); ok {
|
|
debug.Log("RecipientIDs not supported yet by age")
|
|
_ = e.Append(errsNonFatal, fmt.Errorf("recipients check not supported by age backend for now"))
|
|
|
|
return e
|
|
}
|
|
|
|
itemRecps, err := s.crypto.RecipientIDs(ctx, ciphertext)
|
|
if err != nil {
|
|
return e.Append(errsFatal, fmt.Errorf("failed to read recipient IDs from raw secret: %w", err))
|
|
}
|
|
|
|
itemRecps = fingerprints(ctx, s.crypto, itemRecps)
|
|
|
|
rs, err := s.GetRecipients(ctx, name)
|
|
if err != nil {
|
|
return e.Append(errsFatal, fmt.Errorf("failed to get recipients from store: %w", err))
|
|
}
|
|
|
|
perItemStoreRecps := fingerprints(ctx, s.crypto, rs.IDs())
|
|
|
|
// check itemRecps matches storeRecps
|
|
extra, missing := diff.List(perItemStoreRecps, itemRecps)
|
|
if len(missing) > 0 {
|
|
_ = e.Append(errsNonFatal, fmt.Errorf("missing recipients on %s: %+v", name, missing))
|
|
}
|
|
if len(extra) > 0 {
|
|
_ = e.Append(errsNonFatal, fmt.Errorf("extra recipients on %s: %+v", name, extra))
|
|
}
|
|
|
|
return e
|
|
}
|
|
|
|
func fingerprints(ctx context.Context, crypto backend.Crypto, in []string) []string {
|
|
out := make([]string, 0, len(in))
|
|
for _, r := range in {
|
|
out = append(out, crypto.Fingerprint(ctx, r))
|
|
}
|
|
|
|
return out
|
|
}
|
|
|
|
func compareStringSlices(want, have []string) ([]string, []string) {
|
|
missing := []string{}
|
|
extra := []string{}
|
|
|
|
wantMap := make(map[string]struct{}, len(want))
|
|
haveMap := make(map[string]struct{}, len(have))
|
|
|
|
for _, w := range want {
|
|
wantMap[w] = struct{}{}
|
|
}
|
|
|
|
for _, h := range have {
|
|
haveMap[h] = struct{}{}
|
|
}
|
|
|
|
for k := range wantMap {
|
|
if _, found := haveMap[k]; !found {
|
|
missing = append(missing, k)
|
|
}
|
|
}
|
|
|
|
for k := range haveMap {
|
|
if _, found := wantMap[k]; !found {
|
|
extra = append(extra, k)
|
|
}
|
|
}
|
|
|
|
sort.Strings(missing)
|
|
sort.Strings(extra)
|
|
|
|
return missing, extra
|
|
}
|