google-labs-jules[bot] 882d06e001
feat: Add cryptfs storage backend for filename encryption (#3249)
* feat: Add cryptfs storage backend for filename encryption

This commit introduces a new storage backend called `cryptfs`. This backend encrypts the filenames of secrets to enhance privacy while maintaining compatibility with existing VCS backends like Git.

Key features:
- For each secret, a cryptographically secure hash (SHA-256) of its name is generated and used as the filename for the underlying storage.
- A mapping from the original secret name to the hashed filename is maintained in an encrypted file (`.gopass-mapping.age`) within the repository.
- The mapping file is encrypted using the `age` encryption backend, with recipients read from the store's `.age-recipients` file.
- The `cryptfs` backend is implemented as a wrapper around any existing storage backend (e.g., `gitfs`, `fs`), which can be configured by the user.
- The backend is registered with gopass and can be enabled by setting `storage: cryptfs` in the store's configuration.

This implementation addresses issue #2634.

* [fix] Fix lint errors

Signed-off-by: Dominik Schulz <dominik.schulz@gauner.org>

* [chore] Fix the remaining tests and add some docs.

Signed-off-by: Dominik Schulz <dominik.schulz@gauner.org>

---------

Signed-off-by: Dominik Schulz <dominik.schulz@gauner.org>
Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>
Co-authored-by: Dominik Schulz <dominik.schulz@gauner.org>
2025-09-24 08:47:09 +02:00

59 lines
1.4 KiB
Go

package age
import (
"context"
"fmt"
"github.com/gopasspw/gopass/internal/backend"
"github.com/gopasspw/gopass/internal/config"
"github.com/gopasspw/gopass/internal/out"
"github.com/gopasspw/gopass/pkg/debug"
"github.com/gopasspw/gopass/pkg/fsutil"
)
const (
name = "age"
)
func init() {
backend.CryptoRegistry.Register(backend.Age, name, &loader{})
}
type loader struct{}
func (l loader) New(ctx context.Context) (backend.Crypto, error) {
debug.Log("Using Crypto Backend: %s", name)
return New(ctx, config.String(ctx, "age.ssh-key-path"))
}
func (l loader) Handles(ctx context.Context, s backend.Storage) error {
// OldKeyring is meant to be in the config folder, not necessarily in the store
oldKeyring := OldKeyringPath()
if s.Exists(ctx, OldIDFile) || fsutil.IsNonEmptyFile(oldKeyring) {
debug.Log("Starting migration of age backend. Found ID File at %s = %t. Migrating to %s. Found Keyring at %s = %t", OldIDFile, s.Exists(ctx, OldIDFile), IDFile, oldKeyring, fsutil.IsNonEmptyFile(oldKeyring))
if err := migrate(ctx, s); err != nil {
out.Errorf(ctx, "Failed to migrate age backend: %s", err)
return err
}
out.OKf(ctx, "Migrated age backend to new format")
}
if s.Exists(ctx, IDFile) {
return nil
}
debug.Log("No age ID file %q found in %s", IDFile, s.Path())
return fmt.Errorf("not supported")
}
func (l loader) Priority() int {
return 10
}
func (l loader) String() string {
return name
}