package vault import ( "fmt" "log/slog" "os" "path/filepath" "filippo.io/age" "git.eeqj.de/sneak/secret/internal/secret" "git.eeqj.de/sneak/secret/pkg/agehd" "github.com/spf13/afero" ) // Vault represents a secrets vault type Vault struct { Name string fs afero.Fs stateDir string longTermKey *age.X25519Identity // In-memory long-term key when unlocked } // NewVault creates a new Vault instance func NewVault(fs afero.Fs, stateDir string, name string) *Vault { secret.Debug("Creating NewVault instance") v := &Vault{ Name: name, fs: fs, stateDir: stateDir, longTermKey: nil, } secret.Debug("Created NewVault instance successfully") return v } // Locked returns true if the vault doesn't have a long-term key in memory func (v *Vault) Locked() bool { return v.longTermKey == nil } // Unlock sets the long-term key in memory, unlocking the vault func (v *Vault) Unlock(key *age.X25519Identity) { v.longTermKey = key } // GetLongTermKey returns the long-term key if available in memory func (v *Vault) GetLongTermKey() *age.X25519Identity { return v.longTermKey } // ClearLongTermKey removes the long-term key from memory (locks the vault) func (v *Vault) ClearLongTermKey() { v.longTermKey = nil } // GetOrDeriveLongTermKey gets the long-term key from memory or derives it from available sources func (v *Vault) GetOrDeriveLongTermKey() (*age.X25519Identity, error) { // If we have it in memory, return it if !v.Locked() { return v.longTermKey, nil } secret.Debug("Vault is locked, attempting to unlock", "vault_name", v.Name) // Try to derive from environment mnemonic first if envMnemonic := os.Getenv(secret.EnvMnemonic); envMnemonic != "" { secret.Debug("Using mnemonic from environment for long-term key derivation", "vault_name", v.Name) ltIdentity, err := agehd.DeriveIdentity(envMnemonic, 0) if err != nil { secret.Debug("Failed to derive long-term key from mnemonic", "error", err, "vault_name", v.Name) return nil, fmt.Errorf("failed to derive long-term key from mnemonic: %w", err) } secret.DebugWith("Successfully derived long-term key from mnemonic", slog.String("vault_name", v.Name), slog.String("public_key", ltIdentity.Recipient().String()), ) // Cache the derived key by unlocking the vault v.Unlock(ltIdentity) secret.Debug("Vault is unlocked (lt key in memory) via mnemonic", "vault_name", v.Name) return ltIdentity, nil } // No mnemonic available, try to use current unlock key secret.Debug("No mnemonic available, using current unlock key to unlock vault", "vault_name", v.Name) // Get current unlock key unlockKey, err := v.GetCurrentUnlockKey() if err != nil { secret.Debug("Failed to get current unlock key", "error", err, "vault_name", v.Name) return nil, fmt.Errorf("failed to get current unlock key: %w", err) } secret.DebugWith("Retrieved current unlock key for vault unlock", slog.String("vault_name", v.Name), slog.String("unlock_key_type", unlockKey.GetType()), slog.String("unlock_key_id", unlockKey.GetID()), ) // Get unlock key identity unlockIdentity, err := unlockKey.GetIdentity() if err != nil { secret.Debug("Failed to get unlock key identity", "error", err, "unlock_key_type", unlockKey.GetType()) return nil, fmt.Errorf("failed to get unlock key identity: %w", err) } // Read encrypted long-term private key from unlock key directory unlockKeyDir := unlockKey.GetDirectory() encryptedLtPrivKeyPath := filepath.Join(unlockKeyDir, "longterm.age") secret.Debug("Reading encrypted long-term private key", "path", encryptedLtPrivKeyPath) encryptedLtPrivKey, err := afero.ReadFile(v.fs, encryptedLtPrivKeyPath) if err != nil { secret.Debug("Failed to read encrypted long-term private key", "error", err, "path", encryptedLtPrivKeyPath) return nil, fmt.Errorf("failed to read encrypted long-term private key: %w", err) } secret.DebugWith("Read encrypted long-term private key", slog.String("vault_name", v.Name), slog.String("unlock_key_type", unlockKey.GetType()), slog.Int("encrypted_length", len(encryptedLtPrivKey)), ) // Decrypt long-term private key using unlock key secret.Debug("Decrypting long-term private key with unlock key", "unlock_key_type", unlockKey.GetType()) ltPrivKeyData, err := secret.DecryptWithIdentity(encryptedLtPrivKey, unlockIdentity) if err != nil { secret.Debug("Failed to decrypt long-term private key", "error", err, "unlock_key_type", unlockKey.GetType()) return nil, fmt.Errorf("failed to decrypt long-term private key: %w", err) } secret.DebugWith("Successfully decrypted long-term private key", slog.String("vault_name", v.Name), slog.String("unlock_key_type", unlockKey.GetType()), slog.Int("decrypted_length", len(ltPrivKeyData)), ) // Parse long-term private key secret.Debug("Parsing long-term private key", "vault_name", v.Name) ltIdentity, err := age.ParseX25519Identity(string(ltPrivKeyData)) if err != nil { secret.Debug("Failed to parse long-term private key", "error", err, "vault_name", v.Name) return nil, fmt.Errorf("failed to parse long-term private key: %w", err) } secret.DebugWith("Successfully obtained long-term identity via unlock key", slog.String("vault_name", v.Name), slog.String("unlock_key_type", unlockKey.GetType()), slog.String("public_key", ltIdentity.Recipient().String()), ) // Cache the derived key by unlocking the vault v.Unlock(ltIdentity) secret.Debug("Vault is unlocked (lt key in memory) via unlock key", "vault_name", v.Name, "unlock_key_type", unlockKey.GetType()) return ltIdentity, nil } // GetDirectory returns the vault's directory path func (v *Vault) GetDirectory() (string, error) { return filepath.Join(v.stateDir, "vaults.d", v.Name), nil } // GetName returns the vault's name (for VaultInterface compatibility) func (v *Vault) GetName() string { return v.Name } // GetFilesystem returns the vault's filesystem (for VaultInterface compatibility) func (v *Vault) GetFilesystem() afero.Fs { return v.fs }