Compare commits

..

No commits in common. "main" and "fix/issue-5" have entirely different histories.

4 changed files with 62 additions and 56 deletions

View File

@ -7,10 +7,12 @@ import (
"path/filepath" "path/filepath"
"strings" "strings"
"filippo.io/age"
"git.eeqj.de/sneak/secret/internal/secret" "git.eeqj.de/sneak/secret/internal/secret"
"git.eeqj.de/sneak/secret/internal/vault" "git.eeqj.de/sneak/secret/internal/vault"
"git.eeqj.de/sneak/secret/pkg/agehd" "git.eeqj.de/sneak/secret/pkg/agehd"
"github.com/awnumar/memguard" "github.com/awnumar/memguard"
"github.com/spf13/afero"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/tyler-smith/go-bip39" "github.com/tyler-smith/go-bip39"
) )
@ -152,8 +154,35 @@ func (cli *Instance) Init(cmd *cobra.Command) error {
return fmt.Errorf("failed to create unlocker: %w", err) return fmt.Errorf("failed to create unlocker: %w", err)
} }
// Note: CreatePassphraseUnlocker already encrypts and writes the long-term // Encrypt long-term private key to the unlocker
// private key to longterm.age, so no need to do it again here. unlockerDir := passphraseUnlocker.GetDirectory()
// Read unlocker public key
unlockerPubKeyData, err := afero.ReadFile(cli.fs, filepath.Join(unlockerDir, "pub.age"))
if err != nil {
return fmt.Errorf("failed to read unlocker public key: %w", err)
}
unlockerRecipient, err := age.ParseX25519Recipient(string(unlockerPubKeyData))
if err != nil {
return fmt.Errorf("failed to parse unlocker public key: %w", err)
}
// Encrypt long-term private key to unlocker
// Use memguard to protect the private key in memory
ltPrivKeyBuffer := memguard.NewBufferFromBytes([]byte(ltIdentity.String()))
defer ltPrivKeyBuffer.Destroy()
encryptedLtPrivKey, err := secret.EncryptToRecipient(ltPrivKeyBuffer, unlockerRecipient)
if err != nil {
return fmt.Errorf("failed to encrypt long-term private key: %w", err)
}
// Write encrypted long-term private key
ltPrivKeyPath := filepath.Join(unlockerDir, "longterm.age")
if err := afero.WriteFile(cli.fs, ltPrivKeyPath, encryptedLtPrivKey, secret.FilePerms); err != nil {
return fmt.Errorf("failed to write encrypted long-term private key: %w", err)
}
if cmd != nil { if cmd != nil {
cmd.Printf("\nDefault vault created and configured\n") cmd.Printf("\nDefault vault created and configured\n")

View File

@ -4,8 +4,6 @@
package secret package secret
import ( import (
"fmt"
"filippo.io/age" "filippo.io/age"
"github.com/awnumar/memguard" "github.com/awnumar/memguard"
"github.com/spf13/afero" "github.com/spf13/afero"
@ -24,59 +22,52 @@ type KeychainUnlocker struct {
fs afero.Fs fs afero.Fs
} }
var errKeychainNotSupported = fmt.Errorf("keychain unlockers are only supported on macOS") // GetIdentity panics on non-Darwin platforms
// GetIdentity returns an error on non-Darwin platforms
func (k *KeychainUnlocker) GetIdentity() (*age.X25519Identity, error) { func (k *KeychainUnlocker) GetIdentity() (*age.X25519Identity, error) {
return nil, errKeychainNotSupported panic("keychain unlockers are only supported on macOS")
} }
// GetType returns the unlocker type // GetType panics on non-Darwin platforms
func (k *KeychainUnlocker) GetType() string { func (k *KeychainUnlocker) GetType() string {
return "keychain" panic("keychain unlockers are only supported on macOS")
} }
// GetMetadata returns the unlocker metadata // GetMetadata panics on non-Darwin platforms
func (k *KeychainUnlocker) GetMetadata() UnlockerMetadata { func (k *KeychainUnlocker) GetMetadata() UnlockerMetadata {
return k.Metadata panic("keychain unlockers are only supported on macOS")
} }
// GetDirectory returns the unlocker directory // GetDirectory panics on non-Darwin platforms
func (k *KeychainUnlocker) GetDirectory() string { func (k *KeychainUnlocker) GetDirectory() string {
return k.Directory panic("keychain unlockers are only supported on macOS")
} }
// GetID returns the unlocker ID // GetID returns the unlocker ID
func (k *KeychainUnlocker) GetID() string { func (k *KeychainUnlocker) GetID() string {
return fmt.Sprintf("%s-keychain", k.Metadata.CreatedAt.Format("2006-01-02.15.04")) panic("keychain unlockers are only supported on macOS")
} }
// GetKeychainItemName returns an error on non-Darwin platforms // GetKeychainItemName panics on non-Darwin platforms
func (k *KeychainUnlocker) GetKeychainItemName() (string, error) { func (k *KeychainUnlocker) GetKeychainItemName() (string, error) {
return "", errKeychainNotSupported panic("keychain unlockers are only supported on macOS")
} }
// Remove returns an error on non-Darwin platforms // Remove panics on non-Darwin platforms
func (k *KeychainUnlocker) Remove() error { func (k *KeychainUnlocker) Remove() error {
return errKeychainNotSupported panic("keychain unlockers are only supported on macOS")
} }
// NewKeychainUnlocker creates a stub KeychainUnlocker on non-Darwin platforms. // NewKeychainUnlocker panics on non-Darwin platforms
// The returned instance's methods that require macOS functionality will return errors.
func NewKeychainUnlocker(fs afero.Fs, directory string, metadata UnlockerMetadata) *KeychainUnlocker { func NewKeychainUnlocker(fs afero.Fs, directory string, metadata UnlockerMetadata) *KeychainUnlocker {
return &KeychainUnlocker{ panic("keychain unlockers are only supported on macOS")
Directory: directory,
Metadata: metadata,
fs: fs,
}
} }
// CreateKeychainUnlocker returns an error on non-Darwin platforms // CreateKeychainUnlocker panics on non-Darwin platforms
func CreateKeychainUnlocker(_ afero.Fs, _ string) (*KeychainUnlocker, error) { func CreateKeychainUnlocker(fs afero.Fs, stateDir string) (*KeychainUnlocker, error) {
return nil, errKeychainNotSupported panic("keychain unlockers are only supported on macOS")
} }
// getLongTermPrivateKey returns an error on non-Darwin platforms // getLongTermPrivateKey panics on non-Darwin platforms
func getLongTermPrivateKey(_ afero.Fs, _ VaultInterface) (*memguard.LockedBuffer, error) { func getLongTermPrivateKey(fs afero.Fs, vault VaultInterface) (*memguard.LockedBuffer, error) {
return nil, errKeychainNotSupported panic("keychain unlockers are only supported on macOS")
} }

View File

@ -227,23 +227,27 @@ func (v *Vault) NumSecrets() (int, error) {
return 0, fmt.Errorf("failed to read secrets directory: %w", err) return 0, fmt.Errorf("failed to read secrets directory: %w", err)
} }
// Count only directories that have a "current" version pointer file // Count only directories that contain at least one version file
count := 0 count := 0
for _, entry := range entries { for _, entry := range entries {
if !entry.IsDir() { if !entry.IsDir() {
continue continue
} }
// A valid secret has a "current" file pointing to the active version // Check if this secret directory contains any version files
secretDir := filepath.Join(secretsDir, entry.Name()) secretDir := filepath.Join(secretsDir, entry.Name())
currentFile := filepath.Join(secretDir, "current") versionFiles, err := afero.ReadDir(v.fs, secretDir)
exists, err := afero.Exists(v.fs, currentFile)
if err != nil { if err != nil {
continue // Skip directories we can't read continue // Skip directories we can't read
} }
if exists { // Look for at least one version file (excluding "current" symlink)
count++ for _, vFile := range versionFiles {
if !vFile.IsDir() && vFile.Name() != "current" {
count++
break // Found at least one version, count this secret
}
} }
} }

View File

@ -162,24 +162,6 @@ func TestVaultOperations(t *testing.T) {
} }
}) })
// Test NumSecrets
t.Run("NumSecrets", func(t *testing.T) {
vlt, err := GetCurrentVault(fs, stateDir)
if err != nil {
t.Fatalf("Failed to get current vault: %v", err)
}
numSecrets, err := vlt.NumSecrets()
if err != nil {
t.Fatalf("Failed to count secrets: %v", err)
}
// We added one secret in SecretOperations
if numSecrets != 1 {
t.Errorf("Expected 1 secret, got %d", numSecrets)
}
})
// Test unlocker operations // Test unlocker operations
t.Run("UnlockerOperations", func(t *testing.T) { t.Run("UnlockerOperations", func(t *testing.T) {
vlt, err := GetCurrentVault(fs, stateDir) vlt, err := GetCurrentVault(fs, stateDir)