'unlock keys' renamed to 'unlockers'
This commit is contained in:
@@ -196,10 +196,10 @@ func CreateVault(fs afero.Fs, stateDir string, name string) (*Vault, error) {
|
||||
return nil, fmt.Errorf("failed to create secrets directory: %w", err)
|
||||
}
|
||||
|
||||
// Create unlock keys directory
|
||||
unlockKeysDir := filepath.Join(vaultDir, "unlock.d")
|
||||
if err := fs.MkdirAll(unlockKeysDir, secret.DirPerms); err != nil {
|
||||
return nil, fmt.Errorf("failed to create unlock keys directory: %w", err)
|
||||
// Create unlockers directory
|
||||
unlockersDir := filepath.Join(vaultDir, "unlockers.d")
|
||||
if err := fs.MkdirAll(unlockersDir, secret.DirPerms); err != nil {
|
||||
return nil, fmt.Errorf("failed to create unlockers directory: %w", err)
|
||||
}
|
||||
|
||||
// Save initial vault metadata (without derivation info until a mnemonic is imported)
|
||||
|
||||
@@ -13,7 +13,7 @@ import (
|
||||
|
||||
// Alias the metadata types from secret package for convenience
|
||||
type VaultMetadata = secret.VaultMetadata
|
||||
type UnlockKeyMetadata = secret.UnlockKeyMetadata
|
||||
type UnlockerMetadata = secret.UnlockerMetadata
|
||||
type SecretMetadata = secret.SecretMetadata
|
||||
type Configuration = secret.Configuration
|
||||
|
||||
|
||||
@@ -1,376 +0,0 @@
|
||||
package vault
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"filippo.io/age"
|
||||
"git.eeqj.de/sneak/secret/internal/secret"
|
||||
"github.com/spf13/afero"
|
||||
)
|
||||
|
||||
// GetCurrentUnlockKey returns the current unlock key for this vault
|
||||
func (v *Vault) GetCurrentUnlockKey() (secret.UnlockKey, error) {
|
||||
secret.DebugWith("Getting current unlock key", slog.String("vault_name", v.Name))
|
||||
|
||||
vaultDir, err := v.GetDirectory()
|
||||
if err != nil {
|
||||
secret.Debug("Failed to get vault directory for unlock key", "error", err, "vault_name", v.Name)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
currentUnlockKeyPath := filepath.Join(vaultDir, "current-unlock-key")
|
||||
|
||||
// Check if the symlink exists
|
||||
_, err = v.fs.Stat(currentUnlockKeyPath)
|
||||
if err != nil {
|
||||
secret.Debug("Failed to stat current unlock key symlink", "error", err, "path", currentUnlockKeyPath)
|
||||
return nil, fmt.Errorf("failed to read current unlock key: %w", err)
|
||||
}
|
||||
|
||||
// Resolve the symlink to get the target directory
|
||||
var unlockKeyDir string
|
||||
if _, ok := v.fs.(*afero.OsFs); ok {
|
||||
secret.Debug("Resolving unlock key symlink (real filesystem)")
|
||||
// For real filesystems, resolve the symlink properly
|
||||
unlockKeyDir, err = ResolveVaultSymlink(v.fs, currentUnlockKeyPath)
|
||||
if err != nil {
|
||||
secret.Debug("Failed to resolve unlock key symlink", "error", err, "symlink_path", currentUnlockKeyPath)
|
||||
return nil, fmt.Errorf("failed to resolve current unlock key symlink: %w", err)
|
||||
}
|
||||
} else {
|
||||
secret.Debug("Reading unlock key path (mock filesystem)")
|
||||
// Fallback for mock filesystems: read the path from file contents
|
||||
unlockKeyDirBytes, err := afero.ReadFile(v.fs, currentUnlockKeyPath)
|
||||
if err != nil {
|
||||
secret.Debug("Failed to read unlock key path file", "error", err, "path", currentUnlockKeyPath)
|
||||
return nil, fmt.Errorf("failed to read current unlock key: %w", err)
|
||||
}
|
||||
unlockKeyDir = strings.TrimSpace(string(unlockKeyDirBytes))
|
||||
}
|
||||
|
||||
secret.DebugWith("Resolved unlock key directory",
|
||||
slog.String("unlock_key_dir", unlockKeyDir),
|
||||
slog.String("vault_name", v.Name),
|
||||
)
|
||||
|
||||
// Read unlock key metadata
|
||||
metadataPath := filepath.Join(unlockKeyDir, "unlock-metadata.json")
|
||||
secret.Debug("Reading unlock key metadata", "path", metadataPath)
|
||||
|
||||
metadataBytes, err := afero.ReadFile(v.fs, metadataPath)
|
||||
if err != nil {
|
||||
secret.Debug("Failed to read unlock key metadata", "error", err, "path", metadataPath)
|
||||
return nil, fmt.Errorf("failed to read unlock key metadata: %w", err)
|
||||
}
|
||||
|
||||
var metadata UnlockKeyMetadata
|
||||
if err := json.Unmarshal(metadataBytes, &metadata); err != nil {
|
||||
secret.Debug("Failed to parse unlock key metadata", "error", err, "path", metadataPath)
|
||||
return nil, fmt.Errorf("failed to parse unlock key metadata: %w", err)
|
||||
}
|
||||
|
||||
secret.DebugWith("Parsed unlock key metadata",
|
||||
slog.String("key_id", metadata.ID),
|
||||
slog.String("key_type", metadata.Type),
|
||||
slog.Time("created_at", metadata.CreatedAt),
|
||||
slog.Any("flags", metadata.Flags),
|
||||
)
|
||||
|
||||
// Create unlock key instance using direct constructors with filesystem
|
||||
var unlockKey secret.UnlockKey
|
||||
// Convert our metadata to secret.UnlockKeyMetadata
|
||||
secretMetadata := secret.UnlockKeyMetadata(metadata)
|
||||
switch metadata.Type {
|
||||
case "passphrase":
|
||||
secret.Debug("Creating passphrase unlock key instance", "key_id", metadata.ID)
|
||||
unlockKey = secret.NewPassphraseUnlockKey(v.fs, unlockKeyDir, secretMetadata)
|
||||
case "pgp":
|
||||
secret.Debug("Creating PGP unlock key instance", "key_id", metadata.ID)
|
||||
unlockKey = secret.NewPGPUnlockKey(v.fs, unlockKeyDir, secretMetadata)
|
||||
case "keychain":
|
||||
secret.Debug("Creating keychain unlock key instance", "key_id", metadata.ID)
|
||||
unlockKey = secret.NewKeychainUnlockKey(v.fs, unlockKeyDir, secretMetadata)
|
||||
default:
|
||||
secret.Debug("Unsupported unlock key type", "type", metadata.Type, "key_id", metadata.ID)
|
||||
return nil, fmt.Errorf("unsupported unlock key type: %s", metadata.Type)
|
||||
}
|
||||
|
||||
secret.DebugWith("Successfully created unlock key instance",
|
||||
slog.String("key_type", unlockKey.GetType()),
|
||||
slog.String("key_id", unlockKey.GetID()),
|
||||
slog.String("vault_name", v.Name),
|
||||
)
|
||||
|
||||
return unlockKey, nil
|
||||
}
|
||||
|
||||
// ListUnlockKeys returns a list of available unlock keys for this vault
|
||||
func (v *Vault) ListUnlockKeys() ([]UnlockKeyMetadata, error) {
|
||||
vaultDir, err := v.GetDirectory()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
unlockKeysDir := filepath.Join(vaultDir, "unlock.d")
|
||||
|
||||
// Check if unlock keys directory exists
|
||||
exists, err := afero.DirExists(v.fs, unlockKeysDir)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to check if unlock keys directory exists: %w", err)
|
||||
}
|
||||
if !exists {
|
||||
return []UnlockKeyMetadata{}, nil
|
||||
}
|
||||
|
||||
// List directories in unlock.d
|
||||
files, err := afero.ReadDir(v.fs, unlockKeysDir)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read unlock keys directory: %w", err)
|
||||
}
|
||||
|
||||
var keys []UnlockKeyMetadata
|
||||
for _, file := range files {
|
||||
if file.IsDir() {
|
||||
// Read metadata file
|
||||
metadataPath := filepath.Join(unlockKeysDir, file.Name(), "unlock-metadata.json")
|
||||
exists, err := afero.Exists(v.fs, metadataPath)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if !exists {
|
||||
continue
|
||||
}
|
||||
|
||||
metadataBytes, err := afero.ReadFile(v.fs, metadataPath)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
var metadata UnlockKeyMetadata
|
||||
if err := json.Unmarshal(metadataBytes, &metadata); err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
keys = append(keys, metadata)
|
||||
}
|
||||
}
|
||||
|
||||
return keys, nil
|
||||
}
|
||||
|
||||
// RemoveUnlockKey removes an unlock key from this vault
|
||||
func (v *Vault) RemoveUnlockKey(keyID string) error {
|
||||
vaultDir, err := v.GetDirectory()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Find the key directory and create the unlock key instance
|
||||
unlockKeysDir := filepath.Join(vaultDir, "unlock.d")
|
||||
|
||||
// List directories in unlock.d
|
||||
files, err := afero.ReadDir(v.fs, unlockKeysDir)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read unlock keys directory: %w", err)
|
||||
}
|
||||
|
||||
var unlockKey secret.UnlockKey
|
||||
var keyDir string
|
||||
for _, file := range files {
|
||||
if file.IsDir() {
|
||||
// Read metadata file
|
||||
metadataPath := filepath.Join(unlockKeysDir, file.Name(), "unlock-metadata.json")
|
||||
exists, err := afero.Exists(v.fs, metadataPath)
|
||||
if err != nil || !exists {
|
||||
continue
|
||||
}
|
||||
|
||||
metadataBytes, err := afero.ReadFile(v.fs, metadataPath)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
var metadata UnlockKeyMetadata
|
||||
if err := json.Unmarshal(metadataBytes, &metadata); err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if metadata.ID == keyID {
|
||||
keyDir = filepath.Join(unlockKeysDir, file.Name())
|
||||
|
||||
// Convert our metadata to secret.UnlockKeyMetadata
|
||||
secretMetadata := secret.UnlockKeyMetadata(metadata)
|
||||
|
||||
// Create the appropriate unlock key instance
|
||||
switch metadata.Type {
|
||||
case "passphrase":
|
||||
unlockKey = secret.NewPassphraseUnlockKey(v.fs, keyDir, secretMetadata)
|
||||
case "pgp":
|
||||
unlockKey = secret.NewPGPUnlockKey(v.fs, keyDir, secretMetadata)
|
||||
case "keychain":
|
||||
unlockKey = secret.NewKeychainUnlockKey(v.fs, keyDir, secretMetadata)
|
||||
default:
|
||||
return fmt.Errorf("unsupported unlock key type: %s", metadata.Type)
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if unlockKey == nil {
|
||||
return fmt.Errorf("unlock key with ID %s not found", keyID)
|
||||
}
|
||||
|
||||
// Use the unlock key's Remove method
|
||||
return unlockKey.Remove()
|
||||
}
|
||||
|
||||
// SelectUnlockKey selects an unlock key as current for this vault
|
||||
func (v *Vault) SelectUnlockKey(keyID string) error {
|
||||
vaultDir, err := v.GetDirectory()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Find the unlock key directory by ID
|
||||
unlockKeysDir := filepath.Join(vaultDir, "unlock.d")
|
||||
|
||||
// List directories in unlock.d to find the key
|
||||
files, err := afero.ReadDir(v.fs, unlockKeysDir)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read unlock keys directory: %w", err)
|
||||
}
|
||||
|
||||
var targetKeyDir string
|
||||
for _, file := range files {
|
||||
if file.IsDir() {
|
||||
// Read metadata file
|
||||
metadataPath := filepath.Join(unlockKeysDir, file.Name(), "unlock-metadata.json")
|
||||
exists, err := afero.Exists(v.fs, metadataPath)
|
||||
if err != nil || !exists {
|
||||
continue
|
||||
}
|
||||
|
||||
metadataBytes, err := afero.ReadFile(v.fs, metadataPath)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
var metadata UnlockKeyMetadata
|
||||
if err := json.Unmarshal(metadataBytes, &metadata); err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if metadata.ID == keyID {
|
||||
targetKeyDir = filepath.Join(unlockKeysDir, file.Name())
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if targetKeyDir == "" {
|
||||
return fmt.Errorf("unlock key with ID %s not found", keyID)
|
||||
}
|
||||
|
||||
// Create/update current unlock key symlink
|
||||
currentUnlockKeyPath := filepath.Join(vaultDir, "current-unlock-key")
|
||||
|
||||
// Remove existing symlink if it exists
|
||||
if exists, _ := afero.Exists(v.fs, currentUnlockKeyPath); exists {
|
||||
if err := v.fs.Remove(currentUnlockKeyPath); err != nil {
|
||||
secret.Debug("Failed to remove existing unlock key symlink", "error", err, "path", currentUnlockKeyPath)
|
||||
}
|
||||
}
|
||||
|
||||
// Create new symlink
|
||||
return afero.WriteFile(v.fs, currentUnlockKeyPath, []byte(targetKeyDir), secret.FilePerms)
|
||||
}
|
||||
|
||||
// CreatePassphraseKey creates a new passphrase-protected unlock key
|
||||
func (v *Vault) CreatePassphraseKey(passphrase string) (*secret.PassphraseUnlockKey, error) {
|
||||
vaultDir, err := v.GetDirectory()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get vault directory: %w", err)
|
||||
}
|
||||
|
||||
// Create unlock key directory with timestamp
|
||||
timestamp := time.Now().Format("2006-01-02.15.04")
|
||||
unlockKeyDir := filepath.Join(vaultDir, "unlock.d", "passphrase")
|
||||
if err := v.fs.MkdirAll(unlockKeyDir, secret.DirPerms); err != nil {
|
||||
return nil, fmt.Errorf("failed to create unlock key directory: %w", err)
|
||||
}
|
||||
|
||||
// Generate new age keypair for unlock key
|
||||
unlockIdentity, err := age.GenerateX25519Identity()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to generate unlock key: %w", err)
|
||||
}
|
||||
|
||||
// Write public key
|
||||
pubKeyPath := filepath.Join(unlockKeyDir, "pub.age")
|
||||
if err := afero.WriteFile(v.fs, pubKeyPath, []byte(unlockIdentity.Recipient().String()), secret.FilePerms); err != nil {
|
||||
return nil, fmt.Errorf("failed to write unlock key public key: %w", err)
|
||||
}
|
||||
|
||||
// Encrypt private key with passphrase
|
||||
privKeyData := []byte(unlockIdentity.String())
|
||||
encryptedPrivKey, err := secret.EncryptWithPassphrase(privKeyData, passphrase)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to encrypt unlock key private key: %w", err)
|
||||
}
|
||||
|
||||
// Write encrypted private key
|
||||
privKeyPath := filepath.Join(unlockKeyDir, "priv.age")
|
||||
if err := afero.WriteFile(v.fs, privKeyPath, encryptedPrivKey, secret.FilePerms); err != nil {
|
||||
return nil, fmt.Errorf("failed to write encrypted unlock key private key: %w", err)
|
||||
}
|
||||
|
||||
// Create metadata
|
||||
keyID := fmt.Sprintf("%s-passphrase", timestamp)
|
||||
metadata := UnlockKeyMetadata{
|
||||
ID: keyID,
|
||||
Type: "passphrase",
|
||||
CreatedAt: time.Now(),
|
||||
Flags: []string{},
|
||||
}
|
||||
|
||||
// Write metadata
|
||||
metadataBytes, err := json.MarshalIndent(metadata, "", " ")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to marshal metadata: %w", err)
|
||||
}
|
||||
|
||||
metadataPath := filepath.Join(unlockKeyDir, "unlock-metadata.json")
|
||||
if err := afero.WriteFile(v.fs, metadataPath, metadataBytes, secret.FilePerms); err != nil {
|
||||
return nil, fmt.Errorf("failed to write unlock key metadata: %w", err)
|
||||
}
|
||||
|
||||
// Encrypt long-term private key to this unlock key if vault is unlocked
|
||||
if !v.Locked() {
|
||||
ltPrivKey := []byte(v.GetLongTermKey().String())
|
||||
encryptedLtPrivKey, err := secret.EncryptToRecipient(ltPrivKey, unlockIdentity.Recipient())
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to encrypt long-term private key: %w", err)
|
||||
}
|
||||
|
||||
ltPrivKeyPath := filepath.Join(unlockKeyDir, "longterm.age")
|
||||
if err := afero.WriteFile(v.fs, ltPrivKeyPath, encryptedLtPrivKey, secret.FilePerms); err != nil {
|
||||
return nil, fmt.Errorf("failed to write encrypted long-term private key: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Select this unlock key as current
|
||||
if err := v.SelectUnlockKey(keyID); err != nil {
|
||||
return nil, fmt.Errorf("failed to select new unlock key: %w", err)
|
||||
}
|
||||
|
||||
// Convert our metadata to secret.UnlockKeyMetadata for the constructor
|
||||
secretMetadata := secret.UnlockKeyMetadata(metadata)
|
||||
|
||||
return secret.NewPassphraseUnlockKey(v.fs, unlockKeyDir, secretMetadata), nil
|
||||
}
|
||||
376
internal/vault/unlockers.go
Normal file
376
internal/vault/unlockers.go
Normal file
@@ -0,0 +1,376 @@
|
||||
package vault
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"filippo.io/age"
|
||||
"git.eeqj.de/sneak/secret/internal/secret"
|
||||
"github.com/spf13/afero"
|
||||
)
|
||||
|
||||
// GetCurrentUnlocker returns the current unlocker for this vault
|
||||
func (v *Vault) GetCurrentUnlocker() (secret.Unlocker, error) {
|
||||
secret.DebugWith("Getting current unlocker", slog.String("vault_name", v.Name))
|
||||
|
||||
vaultDir, err := v.GetDirectory()
|
||||
if err != nil {
|
||||
secret.Debug("Failed to get vault directory for unlocker", "error", err, "vault_name", v.Name)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
currentUnlockerPath := filepath.Join(vaultDir, "current-unlocker")
|
||||
|
||||
// Check if the symlink exists
|
||||
_, err = v.fs.Stat(currentUnlockerPath)
|
||||
if err != nil {
|
||||
secret.Debug("Failed to stat current unlocker symlink", "error", err, "path", currentUnlockerPath)
|
||||
return nil, fmt.Errorf("failed to read current unlocker: %w", err)
|
||||
}
|
||||
|
||||
// Resolve the symlink to get the target directory
|
||||
var unlockerDir string
|
||||
if _, ok := v.fs.(*afero.OsFs); ok {
|
||||
secret.Debug("Resolving unlocker symlink (real filesystem)")
|
||||
// For real filesystems, resolve the symlink properly
|
||||
unlockerDir, err = ResolveVaultSymlink(v.fs, currentUnlockerPath)
|
||||
if err != nil {
|
||||
secret.Debug("Failed to resolve unlocker symlink", "error", err, "symlink_path", currentUnlockerPath)
|
||||
return nil, fmt.Errorf("failed to resolve current unlocker symlink: %w", err)
|
||||
}
|
||||
} else {
|
||||
secret.Debug("Reading unlocker path (mock filesystem)")
|
||||
// Fallback for mock filesystems: read the path from file contents
|
||||
unlockerDirBytes, err := afero.ReadFile(v.fs, currentUnlockerPath)
|
||||
if err != nil {
|
||||
secret.Debug("Failed to read unlocker path file", "error", err, "path", currentUnlockerPath)
|
||||
return nil, fmt.Errorf("failed to read current unlocker: %w", err)
|
||||
}
|
||||
unlockerDir = strings.TrimSpace(string(unlockerDirBytes))
|
||||
}
|
||||
|
||||
secret.DebugWith("Resolved unlocker directory",
|
||||
slog.String("unlocker_dir", unlockerDir),
|
||||
slog.String("vault_name", v.Name),
|
||||
)
|
||||
|
||||
// Read unlocker metadata
|
||||
metadataPath := filepath.Join(unlockerDir, "unlock-metadata.json")
|
||||
secret.Debug("Reading unlocker metadata", "path", metadataPath)
|
||||
|
||||
metadataBytes, err := afero.ReadFile(v.fs, metadataPath)
|
||||
if err != nil {
|
||||
secret.Debug("Failed to read unlocker metadata", "error", err, "path", metadataPath)
|
||||
return nil, fmt.Errorf("failed to read unlocker metadata: %w", err)
|
||||
}
|
||||
|
||||
var metadata UnlockerMetadata
|
||||
if err := json.Unmarshal(metadataBytes, &metadata); err != nil {
|
||||
secret.Debug("Failed to parse unlocker metadata", "error", err, "path", metadataPath)
|
||||
return nil, fmt.Errorf("failed to parse unlocker metadata: %w", err)
|
||||
}
|
||||
|
||||
secret.DebugWith("Parsed unlocker metadata",
|
||||
slog.String("unlocker_id", metadata.ID),
|
||||
slog.String("unlocker_type", metadata.Type),
|
||||
slog.Time("created_at", metadata.CreatedAt),
|
||||
slog.Any("flags", metadata.Flags),
|
||||
)
|
||||
|
||||
// Create unlocker instance using direct constructors with filesystem
|
||||
var unlocker secret.Unlocker
|
||||
// Convert our metadata to secret.UnlockerMetadata
|
||||
secretMetadata := secret.UnlockerMetadata(metadata)
|
||||
switch metadata.Type {
|
||||
case "passphrase":
|
||||
secret.Debug("Creating passphrase unlocker instance", "unlocker_id", metadata.ID)
|
||||
unlocker = secret.NewPassphraseUnlocker(v.fs, unlockerDir, secretMetadata)
|
||||
case "pgp":
|
||||
secret.Debug("Creating PGP unlocker instance", "unlocker_id", metadata.ID)
|
||||
unlocker = secret.NewPGPUnlocker(v.fs, unlockerDir, secretMetadata)
|
||||
case "keychain":
|
||||
secret.Debug("Creating keychain unlocker instance", "unlocker_id", metadata.ID)
|
||||
unlocker = secret.NewKeychainUnlocker(v.fs, unlockerDir, secretMetadata)
|
||||
default:
|
||||
secret.Debug("Unsupported unlocker type", "type", metadata.Type, "unlocker_id", metadata.ID)
|
||||
return nil, fmt.Errorf("unsupported unlocker type: %s", metadata.Type)
|
||||
}
|
||||
|
||||
secret.DebugWith("Successfully created unlocker instance",
|
||||
slog.String("unlocker_type", unlocker.GetType()),
|
||||
slog.String("unlocker_id", unlocker.GetID()),
|
||||
slog.String("vault_name", v.Name),
|
||||
)
|
||||
|
||||
return unlocker, nil
|
||||
}
|
||||
|
||||
// ListUnlockers returns a list of available unlockers for this vault
|
||||
func (v *Vault) ListUnlockers() ([]UnlockerMetadata, error) {
|
||||
vaultDir, err := v.GetDirectory()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
unlockersDir := filepath.Join(vaultDir, "unlockers.d")
|
||||
|
||||
// Check if unlockers directory exists
|
||||
exists, err := afero.DirExists(v.fs, unlockersDir)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to check if unlockers directory exists: %w", err)
|
||||
}
|
||||
if !exists {
|
||||
return []UnlockerMetadata{}, nil
|
||||
}
|
||||
|
||||
// List directories in unlockers.d
|
||||
files, err := afero.ReadDir(v.fs, unlockersDir)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read unlockers directory: %w", err)
|
||||
}
|
||||
|
||||
var unlockers []UnlockerMetadata
|
||||
for _, file := range files {
|
||||
if file.IsDir() {
|
||||
// Read metadata file
|
||||
metadataPath := filepath.Join(unlockersDir, file.Name(), "unlock-metadata.json")
|
||||
exists, err := afero.Exists(v.fs, metadataPath)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if !exists {
|
||||
continue
|
||||
}
|
||||
|
||||
metadataBytes, err := afero.ReadFile(v.fs, metadataPath)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
var metadata UnlockerMetadata
|
||||
if err := json.Unmarshal(metadataBytes, &metadata); err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
unlockers = append(unlockers, metadata)
|
||||
}
|
||||
}
|
||||
|
||||
return unlockers, nil
|
||||
}
|
||||
|
||||
// RemoveUnlocker removes an unlocker from this vault
|
||||
func (v *Vault) RemoveUnlocker(unlockerID string) error {
|
||||
vaultDir, err := v.GetDirectory()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Find the unlocker directory and create the unlocker instance
|
||||
unlockersDir := filepath.Join(vaultDir, "unlockers.d")
|
||||
|
||||
// List directories in unlockers.d
|
||||
files, err := afero.ReadDir(v.fs, unlockersDir)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read unlockers directory: %w", err)
|
||||
}
|
||||
|
||||
var unlocker secret.Unlocker
|
||||
var unlockerDirPath string
|
||||
for _, file := range files {
|
||||
if file.IsDir() {
|
||||
// Read metadata file
|
||||
metadataPath := filepath.Join(unlockersDir, file.Name(), "unlock-metadata.json")
|
||||
exists, err := afero.Exists(v.fs, metadataPath)
|
||||
if err != nil || !exists {
|
||||
continue
|
||||
}
|
||||
|
||||
metadataBytes, err := afero.ReadFile(v.fs, metadataPath)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
var metadata UnlockerMetadata
|
||||
if err := json.Unmarshal(metadataBytes, &metadata); err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if metadata.ID == unlockerID {
|
||||
unlockerDirPath = filepath.Join(unlockersDir, file.Name())
|
||||
|
||||
// Convert our metadata to secret.UnlockerMetadata
|
||||
secretMetadata := secret.UnlockerMetadata(metadata)
|
||||
|
||||
// Create the appropriate unlocker instance
|
||||
switch metadata.Type {
|
||||
case "passphrase":
|
||||
unlocker = secret.NewPassphraseUnlocker(v.fs, unlockerDirPath, secretMetadata)
|
||||
case "pgp":
|
||||
unlocker = secret.NewPGPUnlocker(v.fs, unlockerDirPath, secretMetadata)
|
||||
case "keychain":
|
||||
unlocker = secret.NewKeychainUnlocker(v.fs, unlockerDirPath, secretMetadata)
|
||||
default:
|
||||
return fmt.Errorf("unsupported unlocker type: %s", metadata.Type)
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if unlocker == nil {
|
||||
return fmt.Errorf("unlocker with ID %s not found", unlockerID)
|
||||
}
|
||||
|
||||
// Use the unlocker's Remove method
|
||||
return unlocker.Remove()
|
||||
}
|
||||
|
||||
// SelectUnlocker selects an unlocker as current for this vault
|
||||
func (v *Vault) SelectUnlocker(unlockerID string) error {
|
||||
vaultDir, err := v.GetDirectory()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Find the unlocker directory by ID
|
||||
unlockersDir := filepath.Join(vaultDir, "unlockers.d")
|
||||
|
||||
// List directories in unlockers.d to find the unlocker
|
||||
files, err := afero.ReadDir(v.fs, unlockersDir)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read unlockers directory: %w", err)
|
||||
}
|
||||
|
||||
var targetUnlockerDir string
|
||||
for _, file := range files {
|
||||
if file.IsDir() {
|
||||
// Read metadata file
|
||||
metadataPath := filepath.Join(unlockersDir, file.Name(), "unlock-metadata.json")
|
||||
exists, err := afero.Exists(v.fs, metadataPath)
|
||||
if err != nil || !exists {
|
||||
continue
|
||||
}
|
||||
|
||||
metadataBytes, err := afero.ReadFile(v.fs, metadataPath)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
var metadata UnlockerMetadata
|
||||
if err := json.Unmarshal(metadataBytes, &metadata); err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if metadata.ID == unlockerID {
|
||||
targetUnlockerDir = filepath.Join(unlockersDir, file.Name())
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if targetUnlockerDir == "" {
|
||||
return fmt.Errorf("unlocker with ID %s not found", unlockerID)
|
||||
}
|
||||
|
||||
// Create/update current unlocker symlink
|
||||
currentUnlockerPath := filepath.Join(vaultDir, "current-unlocker")
|
||||
|
||||
// Remove existing symlink if it exists
|
||||
if exists, _ := afero.Exists(v.fs, currentUnlockerPath); exists {
|
||||
if err := v.fs.Remove(currentUnlockerPath); err != nil {
|
||||
secret.Debug("Failed to remove existing unlocker symlink", "error", err, "path", currentUnlockerPath)
|
||||
}
|
||||
}
|
||||
|
||||
// Create new symlink
|
||||
return afero.WriteFile(v.fs, currentUnlockerPath, []byte(targetUnlockerDir), secret.FilePerms)
|
||||
}
|
||||
|
||||
// CreatePassphraseUnlocker creates a new passphrase-protected unlocker
|
||||
func (v *Vault) CreatePassphraseUnlocker(passphrase string) (*secret.PassphraseUnlocker, error) {
|
||||
vaultDir, err := v.GetDirectory()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get vault directory: %w", err)
|
||||
}
|
||||
|
||||
// Create unlocker directory with timestamp
|
||||
timestamp := time.Now().Format("2006-01-02.15.04")
|
||||
unlockerDir := filepath.Join(vaultDir, "unlockers.d", "passphrase")
|
||||
if err := v.fs.MkdirAll(unlockerDir, secret.DirPerms); err != nil {
|
||||
return nil, fmt.Errorf("failed to create unlocker directory: %w", err)
|
||||
}
|
||||
|
||||
// Generate new age keypair for unlocker
|
||||
unlockerIdentity, err := age.GenerateX25519Identity()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to generate unlocker: %w", err)
|
||||
}
|
||||
|
||||
// Write public key
|
||||
pubKeyPath := filepath.Join(unlockerDir, "pub.age")
|
||||
if err := afero.WriteFile(v.fs, pubKeyPath, []byte(unlockerIdentity.Recipient().String()), secret.FilePerms); err != nil {
|
||||
return nil, fmt.Errorf("failed to write unlocker public key: %w", err)
|
||||
}
|
||||
|
||||
// Encrypt private key with passphrase
|
||||
privKeyData := []byte(unlockerIdentity.String())
|
||||
encryptedPrivKey, err := secret.EncryptWithPassphrase(privKeyData, passphrase)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to encrypt unlocker private key: %w", err)
|
||||
}
|
||||
|
||||
// Write encrypted private key
|
||||
privKeyPath := filepath.Join(unlockerDir, "priv.age")
|
||||
if err := afero.WriteFile(v.fs, privKeyPath, encryptedPrivKey, secret.FilePerms); err != nil {
|
||||
return nil, fmt.Errorf("failed to write encrypted unlocker private key: %w", err)
|
||||
}
|
||||
|
||||
// Create metadata
|
||||
unlockerID := fmt.Sprintf("%s-passphrase", timestamp)
|
||||
metadata := UnlockerMetadata{
|
||||
ID: unlockerID,
|
||||
Type: "passphrase",
|
||||
CreatedAt: time.Now(),
|
||||
Flags: []string{},
|
||||
}
|
||||
|
||||
// Write metadata
|
||||
metadataBytes, err := json.MarshalIndent(metadata, "", " ")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to marshal metadata: %w", err)
|
||||
}
|
||||
|
||||
metadataPath := filepath.Join(unlockerDir, "unlock-metadata.json")
|
||||
if err := afero.WriteFile(v.fs, metadataPath, metadataBytes, secret.FilePerms); err != nil {
|
||||
return nil, fmt.Errorf("failed to write unlocker metadata: %w", err)
|
||||
}
|
||||
|
||||
// Encrypt long-term private key to this unlocker if vault is unlocked
|
||||
if !v.Locked() {
|
||||
ltPrivKey := []byte(v.GetLongTermKey().String())
|
||||
encryptedLtPrivKey, err := secret.EncryptToRecipient(ltPrivKey, unlockerIdentity.Recipient())
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to encrypt long-term private key: %w", err)
|
||||
}
|
||||
|
||||
ltPrivKeyPath := filepath.Join(unlockerDir, "longterm.age")
|
||||
if err := afero.WriteFile(v.fs, ltPrivKeyPath, encryptedLtPrivKey, secret.FilePerms); err != nil {
|
||||
return nil, fmt.Errorf("failed to write encrypted long-term private key: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Select this unlocker as current
|
||||
if err := v.SelectUnlocker(unlockerID); err != nil {
|
||||
return nil, fmt.Errorf("failed to select new unlocker: %w", err)
|
||||
}
|
||||
|
||||
// Convert our metadata to secret.UnlockerMetadata for the constructor
|
||||
secretMetadata := secret.UnlockerMetadata(metadata)
|
||||
|
||||
return secret.NewPassphraseUnlocker(v.fs, unlockerDir, secretMetadata), nil
|
||||
}
|
||||
@@ -83,32 +83,32 @@ func (v *Vault) GetOrDeriveLongTermKey() (*age.X25519Identity, error) {
|
||||
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)
|
||||
// No mnemonic available, try to use current unlocker
|
||||
secret.Debug("No mnemonic available, using current unlocker to unlock vault", "vault_name", v.Name)
|
||||
|
||||
// Get current unlock key
|
||||
unlockKey, err := v.GetCurrentUnlockKey()
|
||||
// Get current unlocker
|
||||
unlocker, err := v.GetCurrentUnlocker()
|
||||
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.Debug("Failed to get current unlocker", "error", err, "vault_name", v.Name)
|
||||
return nil, fmt.Errorf("failed to get current unlocker: %w", err)
|
||||
}
|
||||
|
||||
secret.DebugWith("Retrieved current unlock key for vault unlock",
|
||||
secret.DebugWith("Retrieved current unlocker for vault unlock",
|
||||
slog.String("vault_name", v.Name),
|
||||
slog.String("unlock_key_type", unlockKey.GetType()),
|
||||
slog.String("unlock_key_id", unlockKey.GetID()),
|
||||
slog.String("unlocker_type", unlocker.GetType()),
|
||||
slog.String("unlocker_id", unlocker.GetID()),
|
||||
)
|
||||
|
||||
// Get unlock key identity
|
||||
unlockIdentity, err := unlockKey.GetIdentity()
|
||||
// Get unlocker identity
|
||||
unlockerIdentity, err := unlocker.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)
|
||||
secret.Debug("Failed to get unlocker identity", "error", err, "unlocker_type", unlocker.GetType())
|
||||
return nil, fmt.Errorf("failed to get unlocker identity: %w", err)
|
||||
}
|
||||
|
||||
// Read encrypted long-term private key from unlock key directory
|
||||
unlockKeyDir := unlockKey.GetDirectory()
|
||||
encryptedLtPrivKeyPath := filepath.Join(unlockKeyDir, "longterm.age")
|
||||
// Read encrypted long-term private key from unlocker directory
|
||||
unlockerDir := unlocker.GetDirectory()
|
||||
encryptedLtPrivKeyPath := filepath.Join(unlockerDir, "longterm.age")
|
||||
secret.Debug("Reading encrypted long-term private key", "path", encryptedLtPrivKeyPath)
|
||||
|
||||
encryptedLtPrivKey, err := afero.ReadFile(v.fs, encryptedLtPrivKeyPath)
|
||||
@@ -119,21 +119,21 @@ func (v *Vault) GetOrDeriveLongTermKey() (*age.X25519Identity, error) {
|
||||
|
||||
secret.DebugWith("Read encrypted long-term private key",
|
||||
slog.String("vault_name", v.Name),
|
||||
slog.String("unlock_key_type", unlockKey.GetType()),
|
||||
slog.String("unlocker_type", unlocker.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)
|
||||
// Decrypt long-term private key using unlocker
|
||||
secret.Debug("Decrypting long-term private key with unlocker", "unlocker_type", unlocker.GetType())
|
||||
ltPrivKeyData, err := secret.DecryptWithIdentity(encryptedLtPrivKey, unlockerIdentity)
|
||||
if err != nil {
|
||||
secret.Debug("Failed to decrypt long-term private key", "error", err, "unlock_key_type", unlockKey.GetType())
|
||||
secret.Debug("Failed to decrypt long-term private key", "error", err, "unlocker_type", unlocker.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.String("unlocker_type", unlocker.GetType()),
|
||||
slog.Int("decrypted_length", len(ltPrivKeyData)),
|
||||
)
|
||||
|
||||
@@ -145,15 +145,15 @@ func (v *Vault) GetOrDeriveLongTermKey() (*age.X25519Identity, error) {
|
||||
return nil, fmt.Errorf("failed to parse long-term private key: %w", err)
|
||||
}
|
||||
|
||||
secret.DebugWith("Successfully obtained long-term identity via unlock key",
|
||||
secret.DebugWith("Successfully obtained long-term identity via unlocker",
|
||||
slog.String("vault_name", v.Name),
|
||||
slog.String("unlock_key_type", unlockKey.GetType()),
|
||||
slog.String("unlocker_type", unlocker.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())
|
||||
secret.Debug("Vault is unlocked (lt key in memory) via unlocker", "vault_name", v.Name, "unlocker_type", unlocker.GetType())
|
||||
|
||||
return ltIdentity, nil
|
||||
}
|
||||
|
||||
@@ -174,8 +174,8 @@ func TestVaultOperations(t *testing.T) {
|
||||
}
|
||||
})
|
||||
|
||||
// Test unlock key operations
|
||||
t.Run("UnlockKeyOperations", func(t *testing.T) {
|
||||
// Test unlocker operations
|
||||
t.Run("UnlockerOperations", func(t *testing.T) {
|
||||
vlt, err := GetCurrentVault(fs, stateDir)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to get current vault: %v", err)
|
||||
@@ -189,25 +189,25 @@ func TestVaultOperations(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// Create a passphrase unlock key
|
||||
passphraseKey, err := vlt.CreatePassphraseKey("test-passphrase")
|
||||
// Create a passphrase unlocker
|
||||
passphraseUnlocker, err := vlt.CreatePassphraseUnlocker("test-passphrase")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create passphrase key: %v", err)
|
||||
t.Fatalf("Failed to create passphrase unlocker: %v", err)
|
||||
}
|
||||
|
||||
// List unlock keys
|
||||
keys, err := vlt.ListUnlockKeys()
|
||||
// List unlockers
|
||||
unlockers, err := vlt.ListUnlockers()
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to list unlock keys: %v", err)
|
||||
t.Fatalf("Failed to list unlockers: %v", err)
|
||||
}
|
||||
|
||||
if len(keys) == 0 {
|
||||
t.Errorf("Expected at least one unlock key")
|
||||
if len(unlockers) == 0 {
|
||||
t.Errorf("Expected at least one unlocker")
|
||||
}
|
||||
|
||||
// Check key type
|
||||
keyFound := false
|
||||
for _, key := range keys {
|
||||
for _, key := range unlockers {
|
||||
if key.Type == "passphrase" {
|
||||
keyFound = true
|
||||
break
|
||||
@@ -215,23 +215,23 @@ func TestVaultOperations(t *testing.T) {
|
||||
}
|
||||
|
||||
if !keyFound {
|
||||
t.Errorf("Expected to find passphrase unlock key")
|
||||
t.Errorf("Expected to find passphrase unlocker")
|
||||
}
|
||||
|
||||
// Test selecting unlock key
|
||||
err = vlt.SelectUnlockKey(passphraseKey.GetID())
|
||||
// Test selecting unlocker
|
||||
err = vlt.SelectUnlocker(passphraseUnlocker.GetID())
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to select unlock key: %v", err)
|
||||
t.Fatalf("Failed to select unlocker: %v", err)
|
||||
}
|
||||
|
||||
// Test getting current unlock key
|
||||
currentKey, err := vlt.GetCurrentUnlockKey()
|
||||
// Test getting current unlocker
|
||||
currentUnlocker, err := vlt.GetCurrentUnlocker()
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to get current unlock key: %v", err)
|
||||
t.Fatalf("Failed to get current unlocker: %v", err)
|
||||
}
|
||||
|
||||
if currentKey.GetID() != passphraseKey.GetID() {
|
||||
t.Errorf("Expected current unlock key ID '%s', got '%s'", passphraseKey.GetID(), currentKey.GetID())
|
||||
if currentUnlocker.GetID() != passphraseUnlocker.GetID() {
|
||||
t.Errorf("Expected current unlocker ID '%s', got '%s'", passphraseUnlocker.GetID(), currentUnlocker.GetID())
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user