mirror of
https://github.com/gopasspw/gopass.git
synced 2026-05-30 11:18:48 +02:00
882d06e001
* 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>
195 lines
4.8 KiB
Go
195 lines
4.8 KiB
Go
package cryptfs
|
|
|
|
import (
|
|
"context"
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
|
|
"github.com/gopasspw/gopass/internal/backend"
|
|
"github.com/gopasspw/gopass/internal/backend/crypto/age"
|
|
_ "github.com/gopasspw/gopass/internal/backend/storage/fs"
|
|
_ "github.com/gopasspw/gopass/internal/backend/storage/gitfs"
|
|
"github.com/gopasspw/gopass/pkg/ctxutil"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
var password = "hunter2"
|
|
|
|
func newTestCryptFS(ctx context.Context, t *testing.T, td string) (*Crypt, string) {
|
|
t.Helper()
|
|
|
|
// setup age identity
|
|
a, err := age.New(ctx, "")
|
|
require.NoError(t, err)
|
|
|
|
recp, err := a.GenerateIdentity(ctx, "", "", password)
|
|
require.NoError(t, err)
|
|
|
|
// setup store
|
|
storePath := filepath.Join(td, ".store")
|
|
require.NoError(t, os.MkdirAll(storePath, 0o755))
|
|
|
|
// use fs backend for simplicity
|
|
sub, err := backend.InitStorage(ctx, backend.GitFS, storePath)
|
|
require.NoError(t, err)
|
|
|
|
// create .age-recipients file
|
|
err = os.WriteFile(filepath.Join(storePath, ".age-recipients"), []byte(recp), 0o644)
|
|
require.NoError(t, err)
|
|
|
|
// create cryptfs
|
|
crypt, err := newCrypt(ctx, sub)
|
|
require.NoError(t, err)
|
|
// save empty mapping
|
|
err = crypt.saveMappings(ctx)
|
|
require.NoError(t, err)
|
|
|
|
return crypt, storePath
|
|
}
|
|
|
|
func TestSetGet(t *testing.T) {
|
|
ctx := t.Context()
|
|
ctx = ctxutil.WithAlwaysYes(ctx, true)
|
|
ctx = ctxutil.WithPasswordCallback(ctx, func(prompt string, _ bool) ([]byte, error) {
|
|
return []byte(password), nil
|
|
})
|
|
|
|
td := t.TempDir()
|
|
t.Setenv("GOPASS_HOMEDIR", td)
|
|
|
|
crypt, _ := newTestCryptFS(ctx, t, td)
|
|
|
|
secret := []byte("my secret")
|
|
name := "foo/bar"
|
|
|
|
err := crypt.Set(ctx, name, secret)
|
|
require.NoError(t, err)
|
|
|
|
ret, err := crypt.Get(ctx, name)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, secret, ret)
|
|
}
|
|
|
|
func TestList(t *testing.T) {
|
|
ctx := t.Context()
|
|
ctx = ctxutil.WithAlwaysYes(ctx, true)
|
|
ctx = ctxutil.WithPasswordCallback(ctx, func(prompt string, _ bool) ([]byte, error) {
|
|
return []byte(password), nil
|
|
})
|
|
|
|
td := t.TempDir()
|
|
t.Setenv("GOPASS_HOMEDIR", td)
|
|
|
|
crypt, _ := newTestCryptFS(ctx, t, td)
|
|
|
|
err := crypt.Set(ctx, "foo/bar", []byte("1"))
|
|
require.NoError(t, err)
|
|
err = crypt.Set(ctx, "foo/baz", []byte("2"))
|
|
require.NoError(t, err)
|
|
err = crypt.Set(ctx, "qux/quux", []byte("3"))
|
|
require.NoError(t, err)
|
|
|
|
list, err := crypt.List(ctx, "")
|
|
require.NoError(t, err)
|
|
assert.Equal(t, []string{"foo/bar", "foo/baz", "qux/quux"}, list)
|
|
|
|
list, err = crypt.List(ctx, "foo")
|
|
require.NoError(t, err)
|
|
assert.Equal(t, []string{"foo/bar", "foo/baz"}, list)
|
|
}
|
|
|
|
func TestDelete(t *testing.T) {
|
|
ctx := t.Context()
|
|
ctx = ctxutil.WithAlwaysYes(ctx, true)
|
|
ctx = ctxutil.WithPasswordCallback(ctx, func(prompt string, _ bool) ([]byte, error) {
|
|
return []byte(password), nil
|
|
})
|
|
|
|
td := t.TempDir()
|
|
t.Setenv("GOPASS_HOMEDIR", td)
|
|
|
|
crypt, _ := newTestCryptFS(ctx, t, td)
|
|
|
|
err := crypt.Set(ctx, "foo/bar", []byte("1"))
|
|
require.NoError(t, err)
|
|
assert.True(t, crypt.Exists(ctx, "foo/bar"))
|
|
|
|
err = crypt.Delete(ctx, "foo/bar")
|
|
require.NoError(t, err)
|
|
assert.False(t, crypt.Exists(ctx, "foo/bar"))
|
|
}
|
|
|
|
func TestMove(t *testing.T) {
|
|
ctx := t.Context()
|
|
ctx = ctxutil.WithAlwaysYes(ctx, true)
|
|
ctx = ctxutil.WithPasswordCallback(ctx, func(prompt string, _ bool) ([]byte, error) {
|
|
return []byte(password), nil
|
|
})
|
|
|
|
td := t.TempDir()
|
|
t.Setenv("GOPASS_HOMEDIR", td)
|
|
|
|
crypt, _ := newTestCryptFS(ctx, t, td)
|
|
|
|
err := crypt.Set(ctx, "foo/bar", []byte("1"))
|
|
require.NoError(t, err)
|
|
assert.True(t, crypt.Exists(ctx, "foo/bar"))
|
|
|
|
err = crypt.Move(ctx, "foo/bar", "foo/baz", true)
|
|
require.NoError(t, err)
|
|
assert.False(t, crypt.Exists(ctx, "foo/bar"))
|
|
assert.True(t, crypt.Exists(ctx, "foo/baz"))
|
|
}
|
|
|
|
func TestIsDir(t *testing.T) {
|
|
ctx := t.Context()
|
|
ctx = ctxutil.WithAlwaysYes(ctx, true)
|
|
ctx = ctxutil.WithPasswordCallback(ctx, func(prompt string, _ bool) ([]byte, error) {
|
|
return []byte(password), nil
|
|
})
|
|
|
|
td := t.TempDir()
|
|
t.Setenv("GOPASS_HOMEDIR", td)
|
|
|
|
crypt, _ := newTestCryptFS(ctx, t, td)
|
|
|
|
err := crypt.Set(ctx, "foo/bar", []byte("1"))
|
|
require.NoError(t, err)
|
|
|
|
assert.True(t, crypt.IsDir(ctx, "foo"))
|
|
assert.False(t, crypt.IsDir(ctx, "foo/bar"))
|
|
}
|
|
|
|
func TestGit(t *testing.T) {
|
|
ctx := t.Context()
|
|
ctx = ctxutil.WithAlwaysYes(ctx, true)
|
|
ctx = ctxutil.WithGitInit(ctx, true)
|
|
ctx = ctxutil.WithUsername(ctx, "test")
|
|
ctx = ctxutil.WithEmail(ctx, "test@example.com")
|
|
ctx = ctxutil.WithPasswordCallback(ctx, func(prompt string, _ bool) ([]byte, error) {
|
|
return []byte(password), nil
|
|
})
|
|
|
|
td := t.TempDir()
|
|
t.Setenv("GOPASS_HOMEDIR", td)
|
|
|
|
crypt, _ := newTestCryptFS(ctx, t, td)
|
|
|
|
// Set a secret
|
|
err := crypt.Set(ctx, "foo/bar", []byte("1"))
|
|
require.NoError(t, err)
|
|
|
|
// Add and commit
|
|
err = crypt.Add(ctx, crypt.Path())
|
|
require.NoError(t, err)
|
|
err = crypt.Commit(ctx, "initial commit")
|
|
require.NoError(t, err)
|
|
|
|
// Check revisions
|
|
revs, err := crypt.Revisions(ctx, "foo/bar")
|
|
require.NoError(t, err)
|
|
assert.Len(t, revs, 1)
|
|
}
|