Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dad77ab8d3 |
@@ -531,7 +531,11 @@ secret decrypt encryption/mykey --input document.txt.age --output document.txt
|
||||
### Vault Management
|
||||
|
||||
- **Derivation Index**: Each vault uses a unique derivation index from the
|
||||
mnemonic, and thus a unique key pair
|
||||
mnemonic, and thus a unique key pair; creating a vault or importing a mnemonic
|
||||
fails, with an error naming the file, when any vault's `vault-metadata.json`
|
||||
exists but cannot be read or parsed; a vault directory with no
|
||||
`vault-metadata.json` is skipped, so the index it uses can be given to a new
|
||||
vault
|
||||
- **Public Key Hash**: Double SHA-256 hash of the vault's public key; the same
|
||||
hash of the index-0 public key identifies vaults from the same mnemonic
|
||||
- **Automatic Key Derivation**: When creating vaults with a mnemonic, keys are
|
||||
|
||||
@@ -4,7 +4,9 @@ import (
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/spf13/afero"
|
||||
@@ -33,7 +35,10 @@ func ComputeDoubleSHA256(data []byte) string {
|
||||
}
|
||||
|
||||
// GetNextDerivationIndex finds the next available derivation index for a given mnemonic
|
||||
// by deriving the public key for index 0 and using its hash to identify related vaults
|
||||
// by deriving the public key for index 0 and using its hash to identify related vaults.
|
||||
// It fails when a vault's metadata file exists but cannot be read or parsed: the
|
||||
// index that vault uses is then unknown, and handing it out again would give a
|
||||
// second vault the same long-term key.
|
||||
func GetNextDerivationIndex(
|
||||
fs afero.Fs, stateDir string, mnemonic string,
|
||||
) (uint32, error) {
|
||||
@@ -72,21 +77,23 @@ func GetNextDerivationIndex(
|
||||
continue
|
||||
}
|
||||
|
||||
// Try to read vault metadata
|
||||
metadataPath := filepath.Join(vaultsDir, entry.Name(), "vault-metadata.json")
|
||||
|
||||
metadataBytes, err := afero.ReadFile(fs, metadataPath)
|
||||
if err != nil {
|
||||
// Skip vaults without metadata
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
// Skip directories without metadata
|
||||
continue
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("failed to read vault metadata %s: %w", metadataPath, err)
|
||||
}
|
||||
|
||||
var metadata Metadata
|
||||
|
||||
err = json.Unmarshal(metadataBytes, &metadata)
|
||||
if err != nil {
|
||||
// Skip vaults with invalid metadata
|
||||
continue
|
||||
return 0, fmt.Errorf("failed to parse vault metadata %s: %w", metadataPath, err)
|
||||
}
|
||||
|
||||
// Check if this vault uses the same mnemonic by comparing family hashes
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
package vault_test
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/spf13/afero"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"sneak.berlin/go/secret/internal/secret"
|
||||
"sneak.berlin/go/secret/internal/vault"
|
||||
"sneak.berlin/go/secret/pkg/agehd"
|
||||
)
|
||||
@@ -163,6 +167,88 @@ func testGetNextDerivationIndex(t *testing.T, fs afero.Fs) {
|
||||
}
|
||||
}
|
||||
|
||||
// errMetadataUnreadable is returned by the test filesystem in place of a
|
||||
// successful open of one vault's metadata file.
|
||||
var errMetadataUnreadable = errors.New("input/output error")
|
||||
|
||||
// metadataReadFailFs fails every open of the file at unreadablePath. The
|
||||
// file still exists, so checking for it succeeds and only reading it fails.
|
||||
type metadataReadFailFs struct {
|
||||
afero.Fs
|
||||
|
||||
unreadablePath string
|
||||
}
|
||||
|
||||
//nolint:ireturn // afero.File is the interface required by afero.Fs
|
||||
func (f *metadataReadFailFs) Open(name string) (afero.File, error) {
|
||||
if name == f.unreadablePath {
|
||||
return nil, errMetadataUnreadable
|
||||
}
|
||||
|
||||
return f.Fs.Open(name)
|
||||
}
|
||||
|
||||
// TestCreateVaultWithCorruptMetadataFails checks that creating a vault from
|
||||
// a mnemonic fails, naming the file and writing nothing, when another
|
||||
// vault's metadata file exists but cannot be parsed or read. That vault's
|
||||
// derivation index is then unknown, so the new vault could get the same one
|
||||
// and the same key.
|
||||
func TestCreateVaultWithCorruptMetadataFails(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
vaultsDir := filepath.Join(testStateDir, "vaults.d")
|
||||
metadataPath := filepath.Join(vaultsDir, "work", "vault-metadata.json")
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
// breakMetadata makes the work vault's metadata file unusable on
|
||||
// base and returns the filesystem to create the second vault on.
|
||||
breakMetadata func(t *testing.T, base afero.Fs) afero.Fs
|
||||
}{
|
||||
{
|
||||
name: "parsing the file fails",
|
||||
breakMetadata: func(t *testing.T, base afero.Fs) afero.Fs {
|
||||
t.Helper()
|
||||
|
||||
err := afero.WriteFile(base, metadataPath, []byte("{"),
|
||||
secret.FilePerms)
|
||||
require.NoError(t, err)
|
||||
|
||||
return base
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "reading the file fails",
|
||||
breakMetadata: func(_ *testing.T, base afero.Fs) afero.Fs {
|
||||
return &metadataReadFailFs{Fs: base, unreadablePath: metadataPath}
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
base := afero.NewMemMapFs()
|
||||
|
||||
_, err := vault.CreateVault(base, testStateDir, "work",
|
||||
testMnemonicBuffer(t), nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
fs := tt.breakMetadata(t, base)
|
||||
|
||||
_, err = vault.CreateVault(fs, testStateDir, "other",
|
||||
testMnemonicBuffer(t), nil)
|
||||
require.ErrorContains(t, err, metadataPath)
|
||||
|
||||
entries, err := afero.ReadDir(base, vaultsDir)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, entries, 1)
|
||||
assert.Equal(t, "work", entries[0].Name())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func testMetadataPersistence(t *testing.T, fs afero.Fs) {
|
||||
t.Helper()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user