'unlock keys' renamed to 'unlockers'
This commit is contained in:
@@ -15,19 +15,19 @@ import (
|
||||
"github.com/spf13/afero"
|
||||
)
|
||||
|
||||
// KeychainUnlockKeyMetadata extends UnlockKeyMetadata with keychain-specific data
|
||||
type KeychainUnlockKeyMetadata struct {
|
||||
UnlockKeyMetadata
|
||||
// KeychainUnlockerMetadata extends UnlockerMetadata with keychain-specific data
|
||||
type KeychainUnlockerMetadata struct {
|
||||
UnlockerMetadata
|
||||
// Age keypair information
|
||||
AgePublicKey string `json:"age_public_key"`
|
||||
// Keychain item name
|
||||
KeychainItemName string `json:"keychain_item_name"`
|
||||
}
|
||||
|
||||
// KeychainUnlockKey represents a macOS Keychain-protected unlock key
|
||||
type KeychainUnlockKey struct {
|
||||
// KeychainUnlocker represents a macOS Keychain-protected unlocker
|
||||
type KeychainUnlocker struct {
|
||||
Directory string
|
||||
Metadata UnlockKeyMetadata
|
||||
Metadata UnlockerMetadata
|
||||
fs afero.Fs
|
||||
}
|
||||
|
||||
@@ -38,17 +38,17 @@ type KeychainData struct {
|
||||
EncryptedLongtermKey string `json:"encrypted_longterm_key"`
|
||||
}
|
||||
|
||||
// GetIdentity implements UnlockKey interface for Keychain-based unlock keys
|
||||
func (k *KeychainUnlockKey) GetIdentity() (*age.X25519Identity, error) {
|
||||
DebugWith("Getting keychain unlock key identity",
|
||||
slog.String("key_id", k.GetID()),
|
||||
slog.String("key_type", k.GetType()),
|
||||
// GetIdentity implements Unlocker interface for Keychain-based unlockers
|
||||
func (k *KeychainUnlocker) GetIdentity() (*age.X25519Identity, error) {
|
||||
DebugWith("Getting keychain unlocker identity",
|
||||
slog.String("unlocker_id", k.GetID()),
|
||||
slog.String("unlocker_type", k.GetType()),
|
||||
)
|
||||
|
||||
// Step 1: Get keychain item name
|
||||
keychainItemName, err := k.GetKeychainItemName()
|
||||
if err != nil {
|
||||
Debug("Failed to get keychain item name", "error", err, "key_id", k.GetID())
|
||||
Debug("Failed to get keychain item name", "error", err, "unlocker_id", k.GetID())
|
||||
return nil, fmt.Errorf("failed to get keychain item name: %w", err)
|
||||
}
|
||||
|
||||
@@ -61,18 +61,18 @@ func (k *KeychainUnlockKey) GetIdentity() (*age.X25519Identity, error) {
|
||||
}
|
||||
|
||||
DebugWith("Retrieved data from keychain",
|
||||
slog.String("key_id", k.GetID()),
|
||||
slog.String("unlocker_id", k.GetID()),
|
||||
slog.Int("data_length", len(keychainDataBytes)),
|
||||
)
|
||||
|
||||
// Step 3: Parse keychain data
|
||||
var keychainData KeychainData
|
||||
if err := json.Unmarshal(keychainDataBytes, &keychainData); err != nil {
|
||||
Debug("Failed to parse keychain data", "error", err, "key_id", k.GetID())
|
||||
Debug("Failed to parse keychain data", "error", err, "unlocker_id", k.GetID())
|
||||
return nil, fmt.Errorf("failed to parse keychain data: %w", err)
|
||||
}
|
||||
|
||||
Debug("Parsed keychain data successfully", "key_id", k.GetID())
|
||||
Debug("Parsed keychain data successfully", "unlocker_id", k.GetID())
|
||||
|
||||
// Step 4: Read the encrypted age private key from filesystem
|
||||
agePrivKeyPath := filepath.Join(k.Directory, "priv.age")
|
||||
@@ -85,61 +85,61 @@ func (k *KeychainUnlockKey) GetIdentity() (*age.X25519Identity, error) {
|
||||
}
|
||||
|
||||
DebugWith("Read encrypted age private key",
|
||||
slog.String("key_id", k.GetID()),
|
||||
slog.String("unlocker_id", k.GetID()),
|
||||
slog.Int("encrypted_length", len(encryptedAgePrivKeyData)),
|
||||
)
|
||||
|
||||
// Step 5: Decrypt the age private key using the passphrase from keychain
|
||||
Debug("Decrypting age private key with keychain passphrase", "key_id", k.GetID())
|
||||
Debug("Decrypting age private key with keychain passphrase", "unlocker_id", k.GetID())
|
||||
agePrivKeyData, err := DecryptWithPassphrase(encryptedAgePrivKeyData, keychainData.AgePrivKeyPassphrase)
|
||||
if err != nil {
|
||||
Debug("Failed to decrypt age private key with keychain passphrase", "error", err, "key_id", k.GetID())
|
||||
Debug("Failed to decrypt age private key with keychain passphrase", "error", err, "unlocker_id", k.GetID())
|
||||
return nil, fmt.Errorf("failed to decrypt age private key with keychain passphrase: %w", err)
|
||||
}
|
||||
|
||||
DebugWith("Successfully decrypted age private key with keychain passphrase",
|
||||
slog.String("key_id", k.GetID()),
|
||||
slog.String("unlocker_id", k.GetID()),
|
||||
slog.Int("decrypted_length", len(agePrivKeyData)),
|
||||
)
|
||||
|
||||
// Step 6: Parse the decrypted age private key
|
||||
Debug("Parsing decrypted age private key", "key_id", k.GetID())
|
||||
Debug("Parsing decrypted age private key", "unlocker_id", k.GetID())
|
||||
ageIdentity, err := age.ParseX25519Identity(string(agePrivKeyData))
|
||||
if err != nil {
|
||||
Debug("Failed to parse age private key", "error", err, "key_id", k.GetID())
|
||||
Debug("Failed to parse age private key", "error", err, "unlocker_id", k.GetID())
|
||||
return nil, fmt.Errorf("failed to parse age private key: %w", err)
|
||||
}
|
||||
|
||||
DebugWith("Successfully parsed keychain age identity",
|
||||
slog.String("key_id", k.GetID()),
|
||||
slog.String("unlocker_id", k.GetID()),
|
||||
slog.String("public_key", ageIdentity.Recipient().String()),
|
||||
)
|
||||
|
||||
return ageIdentity, nil
|
||||
}
|
||||
|
||||
// GetType implements UnlockKey interface
|
||||
func (k *KeychainUnlockKey) GetType() string {
|
||||
// GetType implements Unlocker interface
|
||||
func (k *KeychainUnlocker) GetType() string {
|
||||
return "keychain"
|
||||
}
|
||||
|
||||
// GetMetadata implements UnlockKey interface
|
||||
func (k *KeychainUnlockKey) GetMetadata() UnlockKeyMetadata {
|
||||
// GetMetadata implements Unlocker interface
|
||||
func (k *KeychainUnlocker) GetMetadata() UnlockerMetadata {
|
||||
return k.Metadata
|
||||
}
|
||||
|
||||
// GetDirectory implements UnlockKey interface
|
||||
func (k *KeychainUnlockKey) GetDirectory() string {
|
||||
// GetDirectory implements Unlocker interface
|
||||
func (k *KeychainUnlocker) GetDirectory() string {
|
||||
return k.Directory
|
||||
}
|
||||
|
||||
// GetID implements UnlockKey interface
|
||||
func (k *KeychainUnlockKey) GetID() string {
|
||||
// GetID implements Unlocker interface
|
||||
func (k *KeychainUnlocker) GetID() string {
|
||||
return k.Metadata.ID
|
||||
}
|
||||
|
||||
// ID implements UnlockKey interface - generates ID from keychain item name
|
||||
func (k *KeychainUnlockKey) ID() string {
|
||||
// ID implements Unlocker interface - generates ID from keychain item name
|
||||
func (k *KeychainUnlocker) ID() string {
|
||||
// Generate ID using keychain item name
|
||||
keychainItemName, err := k.GetKeychainItemName()
|
||||
if err != nil {
|
||||
@@ -149,12 +149,12 @@ func (k *KeychainUnlockKey) ID() string {
|
||||
return fmt.Sprintf("%s-keychain", keychainItemName)
|
||||
}
|
||||
|
||||
// Remove implements UnlockKey interface - removes the keychain unlock key
|
||||
func (k *KeychainUnlockKey) Remove() error {
|
||||
// Remove implements Unlocker interface - removes the keychain unlocker
|
||||
func (k *KeychainUnlocker) Remove() error {
|
||||
// Step 1: Get keychain item name
|
||||
keychainItemName, err := k.GetKeychainItemName()
|
||||
if err != nil {
|
||||
Debug("Failed to get keychain item name during removal", "error", err, "key_id", k.GetID())
|
||||
Debug("Failed to get keychain item name during removal", "error", err, "unlocker_id", k.GetID())
|
||||
return fmt.Errorf("failed to get keychain item name: %w", err)
|
||||
}
|
||||
|
||||
@@ -166,19 +166,19 @@ func (k *KeychainUnlockKey) Remove() error {
|
||||
}
|
||||
|
||||
// Step 3: Remove directory
|
||||
Debug("Removing keychain unlock key directory", "directory", k.Directory)
|
||||
Debug("Removing keychain unlocker directory", "directory", k.Directory)
|
||||
if err := k.fs.RemoveAll(k.Directory); err != nil {
|
||||
Debug("Failed to remove keychain unlock key directory", "error", err, "directory", k.Directory)
|
||||
return fmt.Errorf("failed to remove keychain unlock key directory: %w", err)
|
||||
Debug("Failed to remove keychain unlocker directory", "error", err, "directory", k.Directory)
|
||||
return fmt.Errorf("failed to remove keychain unlocker directory: %w", err)
|
||||
}
|
||||
|
||||
Debug("Successfully removed keychain unlock key", "key_id", k.GetID(), "keychain_item", keychainItemName)
|
||||
Debug("Successfully removed keychain unlocker", "unlocker_id", k.GetID(), "keychain_item", keychainItemName)
|
||||
return nil
|
||||
}
|
||||
|
||||
// NewKeychainUnlockKey creates a new KeychainUnlockKey instance
|
||||
func NewKeychainUnlockKey(fs afero.Fs, directory string, metadata UnlockKeyMetadata) *KeychainUnlockKey {
|
||||
return &KeychainUnlockKey{
|
||||
// NewKeychainUnlocker creates a new KeychainUnlocker instance
|
||||
func NewKeychainUnlocker(fs afero.Fs, directory string, metadata UnlockerMetadata) *KeychainUnlocker {
|
||||
return &KeychainUnlocker{
|
||||
Directory: directory,
|
||||
Metadata: metadata,
|
||||
fs: fs,
|
||||
@@ -186,7 +186,7 @@ func NewKeychainUnlockKey(fs afero.Fs, directory string, metadata UnlockKeyMetad
|
||||
}
|
||||
|
||||
// GetKeychainItemName returns the keychain item name from metadata
|
||||
func (k *KeychainUnlockKey) GetKeychainItemName() (string, error) {
|
||||
func (k *KeychainUnlocker) GetKeychainItemName() (string, error) {
|
||||
// Load the metadata
|
||||
metadataPath := filepath.Join(k.Directory, "unlock-metadata.json")
|
||||
metadataData, err := afero.ReadFile(k.fs, metadataPath)
|
||||
@@ -194,7 +194,7 @@ func (k *KeychainUnlockKey) GetKeychainItemName() (string, error) {
|
||||
return "", fmt.Errorf("failed to read keychain metadata: %w", err)
|
||||
}
|
||||
|
||||
var keychainMetadata KeychainUnlockKeyMetadata
|
||||
var keychainMetadata KeychainUnlockerMetadata
|
||||
if err := json.Unmarshal(metadataData, &keychainMetadata); err != nil {
|
||||
return "", fmt.Errorf("failed to parse keychain metadata: %w", err)
|
||||
}
|
||||
@@ -202,8 +202,8 @@ func (k *KeychainUnlockKey) GetKeychainItemName() (string, error) {
|
||||
return keychainMetadata.KeychainItemName, nil
|
||||
}
|
||||
|
||||
// generateKeychainUnlockKeyName generates a unique name for the keychain unlock key
|
||||
func generateKeychainUnlockKeyName(vaultName string) (string, error) {
|
||||
// generateKeychainUnlockerName generates a unique name for the keychain unlocker
|
||||
func generateKeychainUnlockerName(vaultName string) (string, error) {
|
||||
hostname, err := os.Hostname()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to get hostname: %w", err)
|
||||
@@ -214,8 +214,8 @@ func generateKeychainUnlockKeyName(vaultName string) (string, error) {
|
||||
return fmt.Sprintf("secret-%s-%s-%s", vaultName, hostname, enrollmentDate), nil
|
||||
}
|
||||
|
||||
// CreateKeychainUnlockKey creates a new keychain unlock key and stores it in the vault
|
||||
func CreateKeychainUnlockKey(fs afero.Fs, stateDir string) (*KeychainUnlockKey, error) {
|
||||
// CreateKeychainUnlocker creates a new keychain unlocker and stores it in the vault
|
||||
func CreateKeychainUnlocker(fs afero.Fs, stateDir string) (*KeychainUnlocker, error) {
|
||||
// Check if we're on macOS
|
||||
if err := checkMacOSAvailable(); err != nil {
|
||||
return nil, err
|
||||
@@ -228,23 +228,23 @@ func CreateKeychainUnlockKey(fs afero.Fs, stateDir string) (*KeychainUnlockKey,
|
||||
}
|
||||
|
||||
// Generate the keychain item name
|
||||
keychainItemName, err := generateKeychainUnlockKeyName(vault.GetName())
|
||||
keychainItemName, err := generateKeychainUnlockerName(vault.GetName())
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to generate keychain item name: %w", err)
|
||||
}
|
||||
|
||||
// Create unlock key directory using the keychain item name as the directory name
|
||||
// Create unlocker directory using the keychain item name as the directory name
|
||||
vaultDir, err := vault.GetDirectory()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get vault directory: %w", err)
|
||||
}
|
||||
|
||||
unlockKeyDir := filepath.Join(vaultDir, "unlock.d", keychainItemName)
|
||||
if err := fs.MkdirAll(unlockKeyDir, DirPerms); err != nil {
|
||||
return nil, fmt.Errorf("failed to create unlock key directory: %w", err)
|
||||
unlockerDir := filepath.Join(vaultDir, "unlockers.d", keychainItemName)
|
||||
if err := fs.MkdirAll(unlockerDir, DirPerms); err != nil {
|
||||
return nil, fmt.Errorf("failed to create unlocker directory: %w", err)
|
||||
}
|
||||
|
||||
// Step 1: Generate a new age keypair for the keychain unlock key
|
||||
// Step 1: Generate a new age keypair for the keychain unlocker
|
||||
ageIdentity, err := age.GenerateX25519Identity()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to generate age keypair: %w", err)
|
||||
@@ -258,7 +258,7 @@ func CreateKeychainUnlockKey(fs afero.Fs, stateDir string) (*KeychainUnlockKey,
|
||||
|
||||
// Step 3: Store age public key as plaintext
|
||||
agePublicKeyString := ageIdentity.Recipient().String()
|
||||
agePubKeyPath := filepath.Join(unlockKeyDir, "pub.age")
|
||||
agePubKeyPath := filepath.Join(unlockerDir, "pub.age")
|
||||
if err := afero.WriteFile(fs, agePubKeyPath, []byte(agePublicKeyString), FilePerms); err != nil {
|
||||
return nil, fmt.Errorf("failed to write age public key: %w", err)
|
||||
}
|
||||
@@ -270,7 +270,7 @@ func CreateKeychainUnlockKey(fs afero.Fs, stateDir string) (*KeychainUnlockKey,
|
||||
return nil, fmt.Errorf("failed to encrypt age private key with passphrase: %w", err)
|
||||
}
|
||||
|
||||
agePrivKeyPath := filepath.Join(unlockKeyDir, "priv.age")
|
||||
agePrivKeyPath := filepath.Join(unlockerDir, "priv.age")
|
||||
if err := afero.WriteFile(fs, agePrivKeyPath, encryptedAgePrivKey, FilePerms); err != nil {
|
||||
return nil, fmt.Errorf("failed to write encrypted age private key: %w", err)
|
||||
}
|
||||
@@ -287,61 +287,61 @@ func CreateKeychainUnlockKey(fs afero.Fs, stateDir string) (*KeychainUnlockKey,
|
||||
}
|
||||
ltPrivKeyData = []byte(ltIdentity.String())
|
||||
} else {
|
||||
// Get the vault to access current unlock key
|
||||
currentUnlockKey, err := vault.GetCurrentUnlockKey()
|
||||
// Get the vault to access current unlocker
|
||||
currentUnlocker, err := vault.GetCurrentUnlocker()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get current unlock key: %w", err)
|
||||
return nil, fmt.Errorf("failed to get current unlocker: %w", err)
|
||||
}
|
||||
|
||||
// Get the current unlock key identity
|
||||
currentUnlockIdentity, err := currentUnlockKey.GetIdentity()
|
||||
// Get the current unlocker identity
|
||||
currentUnlockerIdentity, err := currentUnlocker.GetIdentity()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get current unlock key identity: %w", err)
|
||||
return nil, fmt.Errorf("failed to get current unlocker identity: %w", err)
|
||||
}
|
||||
|
||||
// Get encrypted long-term key from current unlock key, handling different types
|
||||
// Get encrypted long-term key from current unlocker, handling different types
|
||||
var encryptedLtPrivKey []byte
|
||||
switch currentUnlockKey := currentUnlockKey.(type) {
|
||||
case *PassphraseUnlockKey:
|
||||
// Read the encrypted long-term private key from passphrase unlock key
|
||||
encryptedLtPrivKey, err = afero.ReadFile(fs, filepath.Join(currentUnlockKey.GetDirectory(), "longterm.age"))
|
||||
switch currentUnlocker := currentUnlocker.(type) {
|
||||
case *PassphraseUnlocker:
|
||||
// Read the encrypted long-term private key from passphrase unlocker
|
||||
encryptedLtPrivKey, err = afero.ReadFile(fs, filepath.Join(currentUnlocker.GetDirectory(), "longterm.age"))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read encrypted long-term key from current passphrase unlock key: %w", err)
|
||||
return nil, fmt.Errorf("failed to read encrypted long-term key from current passphrase unlocker: %w", err)
|
||||
}
|
||||
|
||||
case *PGPUnlockKey:
|
||||
// Read the encrypted long-term private key from PGP unlock key
|
||||
encryptedLtPrivKey, err = afero.ReadFile(fs, filepath.Join(currentUnlockKey.GetDirectory(), "longterm.age"))
|
||||
case *PGPUnlocker:
|
||||
// Read the encrypted long-term private key from PGP unlocker
|
||||
encryptedLtPrivKey, err = afero.ReadFile(fs, filepath.Join(currentUnlocker.GetDirectory(), "longterm.age"))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read encrypted long-term key from current PGP unlock key: %w", err)
|
||||
return nil, fmt.Errorf("failed to read encrypted long-term key from current PGP unlocker: %w", err)
|
||||
}
|
||||
|
||||
case *KeychainUnlockKey:
|
||||
// Read the encrypted long-term private key from another keychain unlock key
|
||||
encryptedLtPrivKey, err = afero.ReadFile(fs, filepath.Join(currentUnlockKey.GetDirectory(), "longterm.age"))
|
||||
case *KeychainUnlocker:
|
||||
// Read the encrypted long-term private key from another keychain unlocker
|
||||
encryptedLtPrivKey, err = afero.ReadFile(fs, filepath.Join(currentUnlocker.GetDirectory(), "longterm.age"))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read encrypted long-term key from current keychain unlock key: %w", err)
|
||||
return nil, fmt.Errorf("failed to read encrypted long-term key from current keychain unlocker: %w", err)
|
||||
}
|
||||
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported current unlock key type for keychain unlock key creation")
|
||||
return nil, fmt.Errorf("unsupported current unlocker type for keychain unlocker creation")
|
||||
}
|
||||
|
||||
// Decrypt long-term private key using current unlock key
|
||||
ltPrivKeyData, err = DecryptWithIdentity(encryptedLtPrivKey, currentUnlockIdentity)
|
||||
// Decrypt long-term private key using current unlocker
|
||||
ltPrivKeyData, err = DecryptWithIdentity(encryptedLtPrivKey, currentUnlockerIdentity)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to decrypt long-term private key: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Step 6: Encrypt long-term private key to the new age unlock key
|
||||
// Step 6: Encrypt long-term private key to the new age unlocker
|
||||
encryptedLtPrivKeyToAge, err := EncryptToRecipient(ltPrivKeyData, ageIdentity.Recipient())
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to encrypt long-term private key to age unlock key: %w", err)
|
||||
return nil, fmt.Errorf("failed to encrypt long-term private key to age unlocker: %w", err)
|
||||
}
|
||||
|
||||
// Write encrypted long-term private key
|
||||
ltPrivKeyPath := filepath.Join(unlockKeyDir, "longterm.age")
|
||||
ltPrivKeyPath := filepath.Join(unlockerDir, "longterm.age")
|
||||
if err := afero.WriteFile(fs, ltPrivKeyPath, encryptedLtPrivKeyToAge, FilePerms); err != nil {
|
||||
return nil, fmt.Errorf("failed to write encrypted long-term private key: %w", err)
|
||||
}
|
||||
@@ -367,8 +367,8 @@ func CreateKeychainUnlockKey(fs afero.Fs, stateDir string) (*KeychainUnlockKey,
|
||||
// Generate the key ID directly using the keychain item name
|
||||
keyID := fmt.Sprintf("%s-keychain", keychainItemName)
|
||||
|
||||
keychainMetadata := KeychainUnlockKeyMetadata{
|
||||
UnlockKeyMetadata: UnlockKeyMetadata{
|
||||
keychainMetadata := KeychainUnlockerMetadata{
|
||||
UnlockerMetadata: UnlockerMetadata{
|
||||
ID: keyID,
|
||||
Type: "keychain",
|
||||
CreatedAt: time.Now(),
|
||||
@@ -380,16 +380,16 @@ func CreateKeychainUnlockKey(fs afero.Fs, stateDir string) (*KeychainUnlockKey,
|
||||
|
||||
metadataBytes, err := json.MarshalIndent(keychainMetadata, "", " ")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to marshal unlock key metadata: %w", err)
|
||||
return nil, fmt.Errorf("failed to marshal unlocker metadata: %w", err)
|
||||
}
|
||||
|
||||
if err := afero.WriteFile(fs, filepath.Join(unlockKeyDir, "unlock-metadata.json"), metadataBytes, FilePerms); err != nil {
|
||||
return nil, fmt.Errorf("failed to write unlock key metadata: %w", err)
|
||||
if err := afero.WriteFile(fs, filepath.Join(unlockerDir, "unlock-metadata.json"), metadataBytes, FilePerms); err != nil {
|
||||
return nil, fmt.Errorf("failed to write unlocker metadata: %w", err)
|
||||
}
|
||||
|
||||
return &KeychainUnlockKey{
|
||||
Directory: unlockKeyDir,
|
||||
Metadata: keychainMetadata.UnlockKeyMetadata,
|
||||
return &KeychainUnlocker{
|
||||
Directory: unlockerDir,
|
||||
Metadata: keychainMetadata.UnlockerMetadata,
|
||||
fs: fs,
|
||||
}, nil
|
||||
}
|
||||
@@ -398,7 +398,7 @@ func CreateKeychainUnlockKey(fs afero.Fs, stateDir string) (*KeychainUnlockKey,
|
||||
func checkMacOSAvailable() error {
|
||||
cmd := exec.Command("/usr/bin/security", "help")
|
||||
if err := cmd.Run(); err != nil {
|
||||
return fmt.Errorf("macOS security command not available: %w (keychain unlock keys are only supported on macOS)", err)
|
||||
return fmt.Errorf("macOS security command not available: %w (keychain unlockers are only supported on macOS)", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -14,8 +14,8 @@ type VaultMetadata struct {
|
||||
MnemonicHash string `json:"mnemonic_hash"` // Double SHA256 hash of mnemonic for index tracking
|
||||
}
|
||||
|
||||
// UnlockKeyMetadata contains information about an unlock key
|
||||
type UnlockKeyMetadata struct {
|
||||
// UnlockerMetadata contains information about an unlocker
|
||||
type UnlockerMetadata struct {
|
||||
ID string `json:"id"`
|
||||
Type string `json:"type"` // passphrase, pgp, keychain
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
|
||||
@@ -12,7 +12,7 @@ import (
|
||||
"github.com/spf13/afero"
|
||||
)
|
||||
|
||||
func TestPassphraseUnlockKeyWithRealFS(t *testing.T) {
|
||||
func TestPassphraseUnlockerWithRealFS(t *testing.T) {
|
||||
// Skip this test if CI=true is set, as it uses real filesystem
|
||||
if os.Getenv("CI") == "true" {
|
||||
t.Skip("Skipping test with real filesystem in CI environment")
|
||||
@@ -33,21 +33,21 @@ func TestPassphraseUnlockKeyWithRealFS(t *testing.T) {
|
||||
testPassphrase := "test-passphrase-123"
|
||||
|
||||
// Create the directory structure
|
||||
keyDir := filepath.Join(tempDir, "unlock-key")
|
||||
if err := os.MkdirAll(keyDir, secret.DirPerms); err != nil {
|
||||
t.Fatalf("Failed to create key directory: %v", err)
|
||||
unlockerDir := filepath.Join(tempDir, "unlocker")
|
||||
if err := os.MkdirAll(unlockerDir, secret.DirPerms); err != nil {
|
||||
t.Fatalf("Failed to create unlocker directory: %v", err)
|
||||
}
|
||||
|
||||
// Set up test metadata
|
||||
metadata := secret.UnlockKeyMetadata{
|
||||
metadata := secret.UnlockerMetadata{
|
||||
ID: "test-passphrase",
|
||||
Type: "passphrase",
|
||||
CreatedAt: time.Now(),
|
||||
Flags: []string{},
|
||||
}
|
||||
|
||||
// Create passphrase unlock key
|
||||
unlockKey := secret.NewPassphraseUnlockKey(fs, keyDir, metadata)
|
||||
// Create passphrase unlocker
|
||||
unlocker := secret.NewPassphraseUnlocker(fs, unlockerDir, metadata)
|
||||
|
||||
// Generate a test age identity
|
||||
ageIdentity, err := age.GenerateX25519Identity()
|
||||
@@ -59,7 +59,7 @@ func TestPassphraseUnlockKeyWithRealFS(t *testing.T) {
|
||||
|
||||
// Test writing public key
|
||||
t.Run("WritePublicKey", func(t *testing.T) {
|
||||
pubKeyPath := filepath.Join(keyDir, "pub.age")
|
||||
pubKeyPath := filepath.Join(unlockerDir, "pub.age")
|
||||
if err := afero.WriteFile(fs, pubKeyPath, []byte(agePublicKey), secret.FilePerms); err != nil {
|
||||
t.Fatalf("Failed to write public key: %v", err)
|
||||
}
|
||||
@@ -82,7 +82,7 @@ func TestPassphraseUnlockKeyWithRealFS(t *testing.T) {
|
||||
t.Fatalf("Failed to encrypt private key: %v", err)
|
||||
}
|
||||
|
||||
privKeyPath := filepath.Join(keyDir, "priv.age")
|
||||
privKeyPath := filepath.Join(unlockerDir, "priv.age")
|
||||
if err := afero.WriteFile(fs, privKeyPath, encryptedPrivKey, secret.FilePerms); err != nil {
|
||||
t.Fatalf("Failed to write encrypted private key: %v", err)
|
||||
}
|
||||
@@ -105,7 +105,7 @@ func TestPassphraseUnlockKeyWithRealFS(t *testing.T) {
|
||||
t.Fatalf("Failed to derive long-term identity: %v", err)
|
||||
}
|
||||
|
||||
// Encrypt long-term private key to the unlock key's recipient
|
||||
// Encrypt long-term private key to the unlocker's recipient
|
||||
recipient, err := age.ParseX25519Recipient(agePublicKey)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to parse recipient: %v", err)
|
||||
@@ -117,7 +117,7 @@ func TestPassphraseUnlockKeyWithRealFS(t *testing.T) {
|
||||
t.Fatalf("Failed to encrypt long-term private key: %v", err)
|
||||
}
|
||||
|
||||
ltPrivKeyPath := filepath.Join(keyDir, "longterm.age")
|
||||
ltPrivKeyPath := filepath.Join(unlockerDir, "longterm.age")
|
||||
if err := afero.WriteFile(fs, ltPrivKeyPath, encryptedLtPrivKey, secret.FilePerms); err != nil {
|
||||
t.Fatalf("Failed to write encrypted long-term private key: %v", err)
|
||||
}
|
||||
@@ -147,7 +147,7 @@ func TestPassphraseUnlockKeyWithRealFS(t *testing.T) {
|
||||
|
||||
// Test getting identity from environment variable
|
||||
t.Run("GetIdentityFromEnv", func(t *testing.T) {
|
||||
identity, err := unlockKey.GetIdentity()
|
||||
identity, err := unlocker.GetIdentity()
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to get identity from env: %v", err)
|
||||
}
|
||||
@@ -168,26 +168,26 @@ func TestPassphraseUnlockKeyWithRealFS(t *testing.T) {
|
||||
// Here we'll just verify the error is what we expect when no passphrase is available
|
||||
t.Run("GetIdentityWithoutEnv", func(t *testing.T) {
|
||||
// This should fail since we're not in an interactive terminal
|
||||
_, err := unlockKey.GetIdentity()
|
||||
_, err := unlocker.GetIdentity()
|
||||
if err == nil {
|
||||
t.Errorf("Should have failed to get identity without passphrase env var")
|
||||
}
|
||||
})
|
||||
|
||||
// Test removing the unlock key
|
||||
t.Run("RemoveUnlockKey", func(t *testing.T) {
|
||||
err := unlockKey.Remove()
|
||||
// Test removing the unlocker
|
||||
t.Run("RemoveUnlocker", func(t *testing.T) {
|
||||
err := unlocker.Remove()
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to remove unlock key: %v", err)
|
||||
t.Fatalf("Failed to remove unlocker: %v", err)
|
||||
}
|
||||
|
||||
// Verify the directory is gone
|
||||
exists, err := afero.DirExists(fs, keyDir)
|
||||
exists, err := afero.DirExists(fs, unlockerDir)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to check if key directory exists: %v", err)
|
||||
t.Fatalf("Failed to check if unlocker directory exists: %v", err)
|
||||
}
|
||||
if exists {
|
||||
t.Errorf("Key directory should not exist after removal")
|
||||
t.Errorf("Unlocker directory should not exist after removal")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,150 +0,0 @@
|
||||
package secret
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"filippo.io/age"
|
||||
"github.com/spf13/afero"
|
||||
)
|
||||
|
||||
// PassphraseUnlockKey represents a passphrase-protected unlock key
|
||||
type PassphraseUnlockKey struct {
|
||||
Directory string
|
||||
Metadata UnlockKeyMetadata
|
||||
fs afero.Fs
|
||||
Passphrase string
|
||||
}
|
||||
|
||||
// GetIdentity implements UnlockKey interface for passphrase-based unlock keys
|
||||
func (p *PassphraseUnlockKey) GetIdentity() (*age.X25519Identity, error) {
|
||||
DebugWith("Getting passphrase unlock key identity",
|
||||
slog.String("key_id", p.GetID()),
|
||||
slog.String("key_type", p.GetType()),
|
||||
)
|
||||
|
||||
// First check if we already have the passphrase
|
||||
passphraseStr := p.Passphrase
|
||||
if passphraseStr == "" {
|
||||
Debug("No passphrase in memory, checking environment")
|
||||
// Check environment variable for passphrase
|
||||
passphraseStr = os.Getenv(EnvUnlockPassphrase)
|
||||
if passphraseStr == "" {
|
||||
Debug("No passphrase in environment, prompting user")
|
||||
// Prompt for passphrase
|
||||
var err error
|
||||
passphraseStr, err = ReadPassphrase("Enter unlock passphrase: ")
|
||||
if err != nil {
|
||||
Debug("Failed to read passphrase", "error", err, "key_id", p.GetID())
|
||||
return nil, fmt.Errorf("failed to read passphrase: %w", err)
|
||||
}
|
||||
} else {
|
||||
Debug("Using passphrase from environment", "key_id", p.GetID())
|
||||
}
|
||||
} else {
|
||||
Debug("Using in-memory passphrase", "key_id", p.GetID())
|
||||
}
|
||||
|
||||
// Read encrypted private key of unlock key
|
||||
unlockKeyPrivPath := filepath.Join(p.Directory, "priv.age")
|
||||
Debug("Reading encrypted passphrase unlock key", "path", unlockKeyPrivPath)
|
||||
|
||||
encryptedPrivKeyData, err := afero.ReadFile(p.fs, unlockKeyPrivPath)
|
||||
if err != nil {
|
||||
Debug("Failed to read passphrase unlock key private key", "error", err, "path", unlockKeyPrivPath)
|
||||
return nil, fmt.Errorf("failed to read unlock key private key: %w", err)
|
||||
}
|
||||
|
||||
DebugWith("Read encrypted passphrase unlock key",
|
||||
slog.String("key_id", p.GetID()),
|
||||
slog.Int("encrypted_length", len(encryptedPrivKeyData)),
|
||||
)
|
||||
|
||||
Debug("Decrypting unlock key private key with passphrase", "key_id", p.GetID())
|
||||
|
||||
// Decrypt the unlock key private key with passphrase
|
||||
privKeyData, err := DecryptWithPassphrase(encryptedPrivKeyData, passphraseStr)
|
||||
if err != nil {
|
||||
Debug("Failed to decrypt unlock key private key", "error", err, "key_id", p.GetID())
|
||||
return nil, fmt.Errorf("failed to decrypt unlock key private key: %w", err)
|
||||
}
|
||||
|
||||
DebugWith("Successfully decrypted unlock key private key",
|
||||
slog.String("key_id", p.GetID()),
|
||||
slog.Int("decrypted_length", len(privKeyData)),
|
||||
)
|
||||
|
||||
// Parse the decrypted private key
|
||||
Debug("Parsing decrypted unlock key identity", "key_id", p.GetID())
|
||||
identity, err := age.ParseX25519Identity(string(privKeyData))
|
||||
if err != nil {
|
||||
Debug("Failed to parse unlock key private key", "error", err, "key_id", p.GetID())
|
||||
return nil, fmt.Errorf("failed to parse unlock key private key: %w", err)
|
||||
}
|
||||
|
||||
DebugWith("Successfully parsed passphrase unlock key identity",
|
||||
slog.String("key_id", p.GetID()),
|
||||
slog.String("public_key", identity.Recipient().String()),
|
||||
)
|
||||
|
||||
return identity, nil
|
||||
}
|
||||
|
||||
// GetType implements UnlockKey interface
|
||||
func (p *PassphraseUnlockKey) GetType() string {
|
||||
return "passphrase"
|
||||
}
|
||||
|
||||
// GetMetadata implements UnlockKey interface
|
||||
func (p *PassphraseUnlockKey) GetMetadata() UnlockKeyMetadata {
|
||||
return p.Metadata
|
||||
}
|
||||
|
||||
// GetDirectory implements UnlockKey interface
|
||||
func (p *PassphraseUnlockKey) GetDirectory() string {
|
||||
return p.Directory
|
||||
}
|
||||
|
||||
// GetID implements UnlockKey interface
|
||||
func (p *PassphraseUnlockKey) GetID() string {
|
||||
return p.Metadata.ID
|
||||
}
|
||||
|
||||
// ID implements UnlockKey interface - generates ID from creation timestamp
|
||||
func (p *PassphraseUnlockKey) ID() string {
|
||||
// Generate ID using creation timestamp: YYYY-MM-DD.HH.mm-passphrase
|
||||
createdAt := p.Metadata.CreatedAt
|
||||
return fmt.Sprintf("%s-passphrase", createdAt.Format("2006-01-02.15.04"))
|
||||
}
|
||||
|
||||
// Remove implements UnlockKey interface - removes the passphrase unlock key
|
||||
func (p *PassphraseUnlockKey) Remove() error {
|
||||
// For passphrase keys, we just need to remove the directory
|
||||
// No external resources (like keychain items) to clean up
|
||||
if err := p.fs.RemoveAll(p.Directory); err != nil {
|
||||
return fmt.Errorf("failed to remove passphrase unlock key directory: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// NewPassphraseUnlockKey creates a new PassphraseUnlockKey instance
|
||||
func NewPassphraseUnlockKey(fs afero.Fs, directory string, metadata UnlockKeyMetadata) *PassphraseUnlockKey {
|
||||
return &PassphraseUnlockKey{
|
||||
Directory: directory,
|
||||
Metadata: metadata,
|
||||
fs: fs,
|
||||
}
|
||||
}
|
||||
|
||||
// CreatePassphraseKey creates a new passphrase-protected unlock key
|
||||
func CreatePassphraseKey(fs afero.Fs, stateDir string, passphrase string) (*PassphraseUnlockKey, error) {
|
||||
// Get current vault
|
||||
currentVault, err := GetCurrentVault(fs, stateDir)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get current vault: %w", err)
|
||||
}
|
||||
|
||||
return currentVault.CreatePassphraseKey(passphrase)
|
||||
}
|
||||
150
internal/secret/passphraseunlocker.go
Normal file
150
internal/secret/passphraseunlocker.go
Normal file
@@ -0,0 +1,150 @@
|
||||
package secret
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"filippo.io/age"
|
||||
"github.com/spf13/afero"
|
||||
)
|
||||
|
||||
// PassphraseUnlocker represents a passphrase-protected unlocker
|
||||
type PassphraseUnlocker struct {
|
||||
Directory string
|
||||
Metadata UnlockerMetadata
|
||||
fs afero.Fs
|
||||
Passphrase string
|
||||
}
|
||||
|
||||
// GetIdentity implements Unlocker interface for passphrase-based unlockers
|
||||
func (p *PassphraseUnlocker) GetIdentity() (*age.X25519Identity, error) {
|
||||
DebugWith("Getting passphrase unlocker identity",
|
||||
slog.String("unlocker_id", p.GetID()),
|
||||
slog.String("unlocker_type", p.GetType()),
|
||||
)
|
||||
|
||||
// First check if we already have the passphrase
|
||||
passphraseStr := p.Passphrase
|
||||
if passphraseStr == "" {
|
||||
Debug("No passphrase in memory, checking environment")
|
||||
// Check environment variable for passphrase
|
||||
passphraseStr = os.Getenv(EnvUnlockPassphrase)
|
||||
if passphraseStr == "" {
|
||||
Debug("No passphrase in environment, prompting user")
|
||||
// Prompt for passphrase
|
||||
var err error
|
||||
passphraseStr, err = ReadPassphrase("Enter unlock passphrase: ")
|
||||
if err != nil {
|
||||
Debug("Failed to read passphrase", "error", err, "unlocker_id", p.GetID())
|
||||
return nil, fmt.Errorf("failed to read passphrase: %w", err)
|
||||
}
|
||||
} else {
|
||||
Debug("Using passphrase from environment", "unlocker_id", p.GetID())
|
||||
}
|
||||
} else {
|
||||
Debug("Using in-memory passphrase", "unlocker_id", p.GetID())
|
||||
}
|
||||
|
||||
// Read encrypted private key of unlocker
|
||||
unlockerPrivPath := filepath.Join(p.Directory, "priv.age")
|
||||
Debug("Reading encrypted passphrase unlocker", "path", unlockerPrivPath)
|
||||
|
||||
encryptedPrivKeyData, err := afero.ReadFile(p.fs, unlockerPrivPath)
|
||||
if err != nil {
|
||||
Debug("Failed to read passphrase unlocker private key", "error", err, "path", unlockerPrivPath)
|
||||
return nil, fmt.Errorf("failed to read unlocker private key: %w", err)
|
||||
}
|
||||
|
||||
DebugWith("Read encrypted passphrase unlocker",
|
||||
slog.String("unlocker_id", p.GetID()),
|
||||
slog.Int("encrypted_length", len(encryptedPrivKeyData)),
|
||||
)
|
||||
|
||||
Debug("Decrypting unlocker private key with passphrase", "unlocker_id", p.GetID())
|
||||
|
||||
// Decrypt the unlocker private key with passphrase
|
||||
privKeyData, err := DecryptWithPassphrase(encryptedPrivKeyData, passphraseStr)
|
||||
if err != nil {
|
||||
Debug("Failed to decrypt unlocker private key", "error", err, "unlocker_id", p.GetID())
|
||||
return nil, fmt.Errorf("failed to decrypt unlocker private key: %w", err)
|
||||
}
|
||||
|
||||
DebugWith("Successfully decrypted unlocker private key",
|
||||
slog.String("unlocker_id", p.GetID()),
|
||||
slog.Int("decrypted_length", len(privKeyData)),
|
||||
)
|
||||
|
||||
// Parse the decrypted private key
|
||||
Debug("Parsing decrypted unlocker identity", "unlocker_id", p.GetID())
|
||||
identity, err := age.ParseX25519Identity(string(privKeyData))
|
||||
if err != nil {
|
||||
Debug("Failed to parse unlocker private key", "error", err, "unlocker_id", p.GetID())
|
||||
return nil, fmt.Errorf("failed to parse unlocker private key: %w", err)
|
||||
}
|
||||
|
||||
DebugWith("Successfully parsed passphrase unlocker identity",
|
||||
slog.String("unlocker_id", p.GetID()),
|
||||
slog.String("public_key", identity.Recipient().String()),
|
||||
)
|
||||
|
||||
return identity, nil
|
||||
}
|
||||
|
||||
// GetType implements Unlocker interface
|
||||
func (p *PassphraseUnlocker) GetType() string {
|
||||
return "passphrase"
|
||||
}
|
||||
|
||||
// GetMetadata implements Unlocker interface
|
||||
func (p *PassphraseUnlocker) GetMetadata() UnlockerMetadata {
|
||||
return p.Metadata
|
||||
}
|
||||
|
||||
// GetDirectory implements Unlocker interface
|
||||
func (p *PassphraseUnlocker) GetDirectory() string {
|
||||
return p.Directory
|
||||
}
|
||||
|
||||
// GetID implements Unlocker interface
|
||||
func (p *PassphraseUnlocker) GetID() string {
|
||||
return p.Metadata.ID
|
||||
}
|
||||
|
||||
// ID implements Unlocker interface - generates ID from creation timestamp
|
||||
func (p *PassphraseUnlocker) ID() string {
|
||||
// Generate ID using creation timestamp: YYYY-MM-DD.HH.mm-passphrase
|
||||
createdAt := p.Metadata.CreatedAt
|
||||
return fmt.Sprintf("%s-passphrase", createdAt.Format("2006-01-02.15.04"))
|
||||
}
|
||||
|
||||
// Remove implements Unlocker interface - removes the passphrase unlocker
|
||||
func (p *PassphraseUnlocker) Remove() error {
|
||||
// For passphrase unlockers, we just need to remove the directory
|
||||
// No external resources (like keychain items) to clean up
|
||||
if err := p.fs.RemoveAll(p.Directory); err != nil {
|
||||
return fmt.Errorf("failed to remove passphrase unlocker directory: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// NewPassphraseUnlocker creates a new PassphraseUnlocker instance
|
||||
func NewPassphraseUnlocker(fs afero.Fs, directory string, metadata UnlockerMetadata) *PassphraseUnlocker {
|
||||
return &PassphraseUnlocker{
|
||||
Directory: directory,
|
||||
Metadata: metadata,
|
||||
fs: fs,
|
||||
}
|
||||
}
|
||||
|
||||
// CreatePassphraseUnlocker creates a new passphrase-protected unlocker
|
||||
func CreatePassphraseUnlocker(fs afero.Fs, stateDir string, passphrase string) (*PassphraseUnlocker, error) {
|
||||
// Get current vault
|
||||
currentVault, err := GetCurrentVault(fs, stateDir)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get current vault: %w", err)
|
||||
}
|
||||
|
||||
return currentVault.CreatePassphraseUnlocker(passphrase)
|
||||
}
|
||||
@@ -123,7 +123,7 @@ func runGPGWithPassphrase(gnupgHome, passphrase string, args []string, input io.
|
||||
return stdout.Bytes(), nil
|
||||
}
|
||||
|
||||
func TestPGPUnlockKeyWithRealFS(t *testing.T) {
|
||||
func TestPGPUnlockerWithRealFS(t *testing.T) {
|
||||
// Skip tests if gpg is not available
|
||||
if _, err := exec.LookPath("gpg"); err != nil {
|
||||
t.Skip("GPG not available, skipping PGP unlock key tests")
|
||||
@@ -258,7 +258,7 @@ Passphrase: ` + testPassphrase + `
|
||||
vaultName := "test-vault"
|
||||
|
||||
// Test creation of a PGP unlock key through a vault
|
||||
t.Run("CreatePGPUnlockKey", func(t *testing.T) {
|
||||
t.Run("CreatePGPUnlocker", func(t *testing.T) {
|
||||
// Set a limited test timeout to avoid hanging
|
||||
timer := time.AfterFunc(30*time.Second, func() {
|
||||
t.Fatalf("Test timed out after 30 seconds")
|
||||
@@ -298,50 +298,50 @@ Passphrase: ` + testPassphrase + `
|
||||
// Unlock the vault
|
||||
vlt.Unlock(ltIdentity)
|
||||
|
||||
// Create a passphrase unlock key first (to have current unlock key)
|
||||
passKey, err := vlt.CreatePassphraseKey("test-passphrase")
|
||||
// Create a passphrase unlocker first (to have current unlocker)
|
||||
passUnlocker, 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)
|
||||
}
|
||||
|
||||
// Verify passphrase key was created
|
||||
if passKey == nil {
|
||||
t.Fatal("Passphrase key is nil")
|
||||
// Verify passphrase unlocker was created
|
||||
if passUnlocker == nil {
|
||||
t.Fatal("Passphrase unlocker is nil")
|
||||
}
|
||||
|
||||
// Now create a PGP unlock key (this will use our custom GPGEncryptFunc)
|
||||
pgpKey, err := secret.CreatePGPUnlockKey(fs, stateDir, keyID)
|
||||
pgpUnlocker, err := secret.CreatePGPUnlocker(fs, stateDir, keyID)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create PGP unlock key: %v", err)
|
||||
}
|
||||
|
||||
// Verify the PGP unlock key was created
|
||||
if pgpKey == nil {
|
||||
if pgpUnlocker == nil {
|
||||
t.Fatal("PGP unlock key is nil")
|
||||
}
|
||||
|
||||
// Check if the key has the correct type
|
||||
if pgpKey.GetType() != "pgp" {
|
||||
t.Errorf("Expected PGP unlock key type 'pgp', got '%s'", pgpKey.GetType())
|
||||
if pgpUnlocker.GetType() != "pgp" {
|
||||
t.Errorf("Expected PGP unlock key type 'pgp', got '%s'", pgpUnlocker.GetType())
|
||||
}
|
||||
|
||||
// Check if the key ID includes the GPG key ID
|
||||
if !strings.Contains(pgpKey.GetID(), keyID) {
|
||||
t.Errorf("PGP unlock key ID '%s' does not contain GPG key ID '%s'", pgpKey.GetID(), keyID)
|
||||
if !strings.Contains(pgpUnlocker.GetID(), keyID) {
|
||||
t.Errorf("PGP unlock key ID '%s' does not contain GPG key ID '%s'", pgpUnlocker.GetID(), keyID)
|
||||
}
|
||||
|
||||
// Check if the key directory exists
|
||||
keyDir := pgpKey.GetDirectory()
|
||||
keyExists, err := afero.DirExists(fs, keyDir)
|
||||
unlockerDir := pgpUnlocker.GetDirectory()
|
||||
keyExists, err := afero.DirExists(fs, unlockerDir)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to check if PGP key directory exists: %v", err)
|
||||
}
|
||||
if !keyExists {
|
||||
t.Errorf("PGP unlock key directory does not exist: %s", keyDir)
|
||||
t.Errorf("PGP unlock key directory does not exist: %s", unlockerDir)
|
||||
}
|
||||
|
||||
// Check if required files exist
|
||||
pubKeyPath := filepath.Join(keyDir, "pub.age")
|
||||
pubKeyPath := filepath.Join(unlockerDir, "pub.age")
|
||||
pubKeyExists, err := afero.Exists(fs, pubKeyPath)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to check if public key file exists: %v", err)
|
||||
@@ -350,7 +350,7 @@ Passphrase: ` + testPassphrase + `
|
||||
t.Errorf("PGP unlock key public key file does not exist: %s", pubKeyPath)
|
||||
}
|
||||
|
||||
privKeyPath := filepath.Join(keyDir, "priv.age.gpg")
|
||||
privKeyPath := filepath.Join(unlockerDir, "priv.age.gpg")
|
||||
privKeyExists, err := afero.Exists(fs, privKeyPath)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to check if private key file exists: %v", err)
|
||||
@@ -359,7 +359,7 @@ Passphrase: ` + testPassphrase + `
|
||||
t.Errorf("PGP unlock key private key file does not exist: %s", privKeyPath)
|
||||
}
|
||||
|
||||
metadataPath := filepath.Join(keyDir, "unlock-metadata.json")
|
||||
metadataPath := filepath.Join(unlockerDir, "unlocker-metadata.json")
|
||||
metadataExists, err := afero.Exists(fs, metadataPath)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to check if metadata file exists: %v", err)
|
||||
@@ -368,7 +368,7 @@ Passphrase: ` + testPassphrase + `
|
||||
t.Errorf("PGP unlock key metadata file does not exist: %s", metadataPath)
|
||||
}
|
||||
|
||||
longtermPath := filepath.Join(keyDir, "longterm.age")
|
||||
longtermPath := filepath.Join(unlockerDir, "longterm.age")
|
||||
longtermExists, err := afero.Exists(fs, longtermPath)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to check if longterm key file exists: %v", err)
|
||||
@@ -405,37 +405,37 @@ Passphrase: ` + testPassphrase + `
|
||||
})
|
||||
|
||||
// Set up key directory for individual tests
|
||||
keyDir := filepath.Join(tempDir, "unlock-key")
|
||||
if err := os.MkdirAll(keyDir, secret.DirPerms); err != nil {
|
||||
t.Fatalf("Failed to create key directory: %v", err)
|
||||
unlockerDir := filepath.Join(tempDir, "unlocker")
|
||||
if err := os.MkdirAll(unlockerDir, secret.DirPerms); err != nil {
|
||||
t.Fatalf("Failed to create unlocker directory: %v", err)
|
||||
}
|
||||
|
||||
// Set up test metadata
|
||||
metadata := secret.UnlockKeyMetadata{
|
||||
metadata := secret.UnlockerMetadata{
|
||||
ID: fmt.Sprintf("%s-pgp", keyID),
|
||||
Type: "pgp",
|
||||
CreatedAt: time.Now(),
|
||||
Flags: []string{"gpg", "encrypted"},
|
||||
}
|
||||
|
||||
// Create a PGP unlock key for the remaining tests
|
||||
unlockKey := secret.NewPGPUnlockKey(fs, keyDir, metadata)
|
||||
// Create a PGP unlocker for the remaining tests
|
||||
unlocker := secret.NewPGPUnlocker(fs, unlockerDir, metadata)
|
||||
|
||||
// Test getting GPG key ID
|
||||
t.Run("GetGPGKeyID", func(t *testing.T) {
|
||||
// Create PGP metadata with GPG key ID
|
||||
type PGPUnlockKeyMetadata struct {
|
||||
secret.UnlockKeyMetadata
|
||||
type PGPUnlockerMetadata struct {
|
||||
secret.UnlockerMetadata
|
||||
GPGKeyID string `json:"gpg_key_id"`
|
||||
}
|
||||
|
||||
pgpMetadata := PGPUnlockKeyMetadata{
|
||||
UnlockKeyMetadata: metadata,
|
||||
GPGKeyID: keyID,
|
||||
pgpMetadata := PGPUnlockerMetadata{
|
||||
UnlockerMetadata: metadata,
|
||||
GPGKeyID: keyID,
|
||||
}
|
||||
|
||||
// Write metadata file
|
||||
metadataPath := filepath.Join(keyDir, "unlock-metadata.json")
|
||||
metadataPath := filepath.Join(unlockerDir, "unlocker-metadata.json")
|
||||
metadataBytes, err := json.MarshalIndent(pgpMetadata, "", " ")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to marshal metadata: %v", err)
|
||||
@@ -445,7 +445,7 @@ Passphrase: ` + testPassphrase + `
|
||||
}
|
||||
|
||||
// Get GPG key ID
|
||||
retrievedKeyID, err := unlockKey.GetGPGKeyID()
|
||||
retrievedKeyID, err := unlocker.GetGPGKeyID()
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to get GPG key ID: %v", err)
|
||||
}
|
||||
@@ -456,7 +456,7 @@ Passphrase: ` + testPassphrase + `
|
||||
}
|
||||
})
|
||||
|
||||
// Test getting identity from PGP unlock key
|
||||
// Test getting identity from PGP unlocker
|
||||
t.Run("GetIdentity", func(t *testing.T) {
|
||||
// Generate an age identity for testing
|
||||
ageIdentity, err := age.GenerateX25519Identity()
|
||||
@@ -465,7 +465,7 @@ Passphrase: ` + testPassphrase + `
|
||||
}
|
||||
|
||||
// Write the public key
|
||||
pubKeyPath := filepath.Join(keyDir, "pub.age")
|
||||
pubKeyPath := filepath.Join(unlockerDir, "pub.age")
|
||||
if err := afero.WriteFile(fs, pubKeyPath, []byte(ageIdentity.Recipient().String()), secret.FilePerms); err != nil {
|
||||
t.Fatalf("Failed to write public key: %v", err)
|
||||
}
|
||||
@@ -478,13 +478,13 @@ Passphrase: ` + testPassphrase + `
|
||||
}
|
||||
|
||||
// Write the encrypted data to a file
|
||||
encryptedPath := filepath.Join(keyDir, "priv.age.gpg")
|
||||
encryptedPath := filepath.Join(unlockerDir, "priv.age.gpg")
|
||||
if err := afero.WriteFile(fs, encryptedPath, encryptedOutput, secret.FilePerms); err != nil {
|
||||
t.Fatalf("Failed to write encrypted private key: %v", err)
|
||||
}
|
||||
|
||||
// Now try to get the identity - this will use our custom GPGDecryptFunc
|
||||
identity, err := unlockKey.GetIdentity()
|
||||
identity, err := unlocker.GetIdentity()
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to get identity: %v", err)
|
||||
}
|
||||
@@ -497,30 +497,30 @@ Passphrase: ` + testPassphrase + `
|
||||
}
|
||||
})
|
||||
|
||||
// Test removing the unlock key
|
||||
t.Run("RemoveUnlockKey", func(t *testing.T) {
|
||||
// Ensure key directory exists before removal
|
||||
keyExists, err := afero.DirExists(fs, keyDir)
|
||||
// Test removing the unlocker
|
||||
t.Run("RemoveUnlocker", func(t *testing.T) {
|
||||
// Ensure unlocker directory exists before removal
|
||||
keyExists, err := afero.DirExists(fs, unlockerDir)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to check if key directory exists: %v", err)
|
||||
t.Fatalf("Failed to check if unlocker directory exists: %v", err)
|
||||
}
|
||||
if !keyExists {
|
||||
t.Fatalf("Key directory does not exist: %s", keyDir)
|
||||
t.Fatalf("Unlocker directory does not exist: %s", unlockerDir)
|
||||
}
|
||||
|
||||
// Remove unlock key
|
||||
err = unlockKey.Remove()
|
||||
// Remove unlocker
|
||||
err = unlocker.Remove()
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to remove unlock key: %v", err)
|
||||
t.Fatalf("Failed to remove unlocker: %v", err)
|
||||
}
|
||||
|
||||
// Verify directory is gone
|
||||
keyExists, err = afero.DirExists(fs, keyDir)
|
||||
keyExists, err = afero.DirExists(fs, unlockerDir)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to check if key directory exists: %v", err)
|
||||
t.Fatalf("Failed to check if unlocker directory exists: %v", err)
|
||||
}
|
||||
if keyExists {
|
||||
t.Errorf("Key directory still exists after removal: %s", keyDir)
|
||||
t.Errorf("Unlocker directory still exists after removal: %s", unlockerDir)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -26,9 +26,9 @@ var (
|
||||
GPGDecryptFunc = gpgDecryptDefault
|
||||
)
|
||||
|
||||
// PGPUnlockKeyMetadata extends UnlockKeyMetadata with PGP-specific data
|
||||
type PGPUnlockKeyMetadata struct {
|
||||
UnlockKeyMetadata
|
||||
// PGPUnlockerMetadata extends UnlockerMetadata with PGP-specific data
|
||||
type PGPUnlockerMetadata struct {
|
||||
UnlockerMetadata
|
||||
// GPG key ID used for encryption
|
||||
GPGKeyID string `json:"gpg_key_id"`
|
||||
// Age keypair information
|
||||
@@ -36,18 +36,18 @@ type PGPUnlockKeyMetadata struct {
|
||||
AgeRecipient string `json:"age_recipient"`
|
||||
}
|
||||
|
||||
// PGPUnlockKey represents a PGP-protected unlock key
|
||||
type PGPUnlockKey struct {
|
||||
// PGPUnlocker represents a PGP-protected unlocker
|
||||
type PGPUnlocker struct {
|
||||
Directory string
|
||||
Metadata UnlockKeyMetadata
|
||||
Metadata UnlockerMetadata
|
||||
fs afero.Fs
|
||||
}
|
||||
|
||||
// GetIdentity implements UnlockKey interface for PGP-based unlock keys
|
||||
func (p *PGPUnlockKey) GetIdentity() (*age.X25519Identity, error) {
|
||||
DebugWith("Getting PGP unlock key identity",
|
||||
slog.String("key_id", p.GetID()),
|
||||
slog.String("key_type", p.GetType()),
|
||||
// GetIdentity implements Unlocker interface for PGP-based unlockers
|
||||
func (p *PGPUnlocker) GetIdentity() (*age.X25519Identity, error) {
|
||||
DebugWith("Getting PGP unlocker identity",
|
||||
slog.String("unlocker_id", p.GetID()),
|
||||
slog.String("unlocker_type", p.GetType()),
|
||||
)
|
||||
|
||||
// Step 1: Read the encrypted age private key from filesystem
|
||||
@@ -61,61 +61,61 @@ func (p *PGPUnlockKey) GetIdentity() (*age.X25519Identity, error) {
|
||||
}
|
||||
|
||||
DebugWith("Read PGP-encrypted age private key",
|
||||
slog.String("key_id", p.GetID()),
|
||||
slog.String("unlocker_id", p.GetID()),
|
||||
slog.Int("encrypted_length", len(encryptedAgePrivKeyData)),
|
||||
)
|
||||
|
||||
// Step 2: Decrypt the age private key using GPG
|
||||
Debug("Decrypting age private key with GPG", "key_id", p.GetID())
|
||||
Debug("Decrypting age private key with GPG", "unlocker_id", p.GetID())
|
||||
agePrivKeyData, err := GPGDecryptFunc(encryptedAgePrivKeyData)
|
||||
if err != nil {
|
||||
Debug("Failed to decrypt age private key with GPG", "error", err, "key_id", p.GetID())
|
||||
Debug("Failed to decrypt age private key with GPG", "error", err, "unlocker_id", p.GetID())
|
||||
return nil, fmt.Errorf("failed to decrypt age private key with GPG: %w", err)
|
||||
}
|
||||
|
||||
DebugWith("Successfully decrypted age private key with GPG",
|
||||
slog.String("key_id", p.GetID()),
|
||||
slog.String("unlocker_id", p.GetID()),
|
||||
slog.Int("decrypted_length", len(agePrivKeyData)),
|
||||
)
|
||||
|
||||
// Step 3: Parse the decrypted age private key
|
||||
Debug("Parsing decrypted age private key", "key_id", p.GetID())
|
||||
Debug("Parsing decrypted age private key", "unlocker_id", p.GetID())
|
||||
ageIdentity, err := age.ParseX25519Identity(string(agePrivKeyData))
|
||||
if err != nil {
|
||||
Debug("Failed to parse age private key", "error", err, "key_id", p.GetID())
|
||||
Debug("Failed to parse age private key", "error", err, "unlocker_id", p.GetID())
|
||||
return nil, fmt.Errorf("failed to parse age private key: %w", err)
|
||||
}
|
||||
|
||||
DebugWith("Successfully parsed PGP age identity",
|
||||
slog.String("key_id", p.GetID()),
|
||||
slog.String("unlocker_id", p.GetID()),
|
||||
slog.String("public_key", ageIdentity.Recipient().String()),
|
||||
)
|
||||
|
||||
return ageIdentity, nil
|
||||
}
|
||||
|
||||
// GetType implements UnlockKey interface
|
||||
func (p *PGPUnlockKey) GetType() string {
|
||||
// GetType implements Unlocker interface
|
||||
func (p *PGPUnlocker) GetType() string {
|
||||
return "pgp"
|
||||
}
|
||||
|
||||
// GetMetadata implements UnlockKey interface
|
||||
func (p *PGPUnlockKey) GetMetadata() UnlockKeyMetadata {
|
||||
// GetMetadata implements Unlocker interface
|
||||
func (p *PGPUnlocker) GetMetadata() UnlockerMetadata {
|
||||
return p.Metadata
|
||||
}
|
||||
|
||||
// GetDirectory implements UnlockKey interface
|
||||
func (p *PGPUnlockKey) GetDirectory() string {
|
||||
// GetDirectory implements Unlocker interface
|
||||
func (p *PGPUnlocker) GetDirectory() string {
|
||||
return p.Directory
|
||||
}
|
||||
|
||||
// GetID implements UnlockKey interface
|
||||
func (p *PGPUnlockKey) GetID() string {
|
||||
// GetID implements Unlocker interface
|
||||
func (p *PGPUnlocker) GetID() string {
|
||||
return p.Metadata.ID
|
||||
}
|
||||
|
||||
// ID implements UnlockKey interface - generates ID from GPG key ID
|
||||
func (p *PGPUnlockKey) ID() string {
|
||||
// ID implements Unlocker interface - generates ID from GPG key ID
|
||||
func (p *PGPUnlocker) ID() string {
|
||||
// Generate ID using GPG key ID: <keyid>-pgp
|
||||
gpgKeyID, err := p.GetGPGKeyID()
|
||||
if err != nil {
|
||||
@@ -125,19 +125,19 @@ func (p *PGPUnlockKey) ID() string {
|
||||
return fmt.Sprintf("%s-pgp", gpgKeyID)
|
||||
}
|
||||
|
||||
// Remove implements UnlockKey interface - removes the PGP unlock key
|
||||
func (p *PGPUnlockKey) Remove() error {
|
||||
// For PGP keys, we just need to remove the directory
|
||||
// Remove implements Unlocker interface - removes the PGP unlocker
|
||||
func (p *PGPUnlocker) Remove() error {
|
||||
// For PGP unlockers, we just need to remove the directory
|
||||
// No external resources (like keychain items) to clean up
|
||||
if err := p.fs.RemoveAll(p.Directory); err != nil {
|
||||
return fmt.Errorf("failed to remove PGP unlock key directory: %w", err)
|
||||
return fmt.Errorf("failed to remove PGP unlocker directory: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// NewPGPUnlockKey creates a new PGPUnlockKey instance
|
||||
func NewPGPUnlockKey(fs afero.Fs, directory string, metadata UnlockKeyMetadata) *PGPUnlockKey {
|
||||
return &PGPUnlockKey{
|
||||
// NewPGPUnlocker creates a new PGPUnlocker instance
|
||||
func NewPGPUnlocker(fs afero.Fs, directory string, metadata UnlockerMetadata) *PGPUnlocker {
|
||||
return &PGPUnlocker{
|
||||
Directory: directory,
|
||||
Metadata: metadata,
|
||||
fs: fs,
|
||||
@@ -145,7 +145,7 @@ func NewPGPUnlockKey(fs afero.Fs, directory string, metadata UnlockKeyMetadata)
|
||||
}
|
||||
|
||||
// GetGPGKeyID returns the GPG key ID from metadata
|
||||
func (p *PGPUnlockKey) GetGPGKeyID() (string, error) {
|
||||
func (p *PGPUnlocker) GetGPGKeyID() (string, error) {
|
||||
// Load the metadata
|
||||
metadataPath := filepath.Join(p.Directory, "unlock-metadata.json")
|
||||
metadataData, err := afero.ReadFile(p.fs, metadataPath)
|
||||
@@ -153,7 +153,7 @@ func (p *PGPUnlockKey) GetGPGKeyID() (string, error) {
|
||||
return "", fmt.Errorf("failed to read PGP metadata: %w", err)
|
||||
}
|
||||
|
||||
var pgpMetadata PGPUnlockKeyMetadata
|
||||
var pgpMetadata PGPUnlockerMetadata
|
||||
if err := json.Unmarshal(metadataData, &pgpMetadata); err != nil {
|
||||
return "", fmt.Errorf("failed to parse PGP metadata: %w", err)
|
||||
}
|
||||
@@ -161,8 +161,8 @@ func (p *PGPUnlockKey) GetGPGKeyID() (string, error) {
|
||||
return pgpMetadata.GPGKeyID, nil
|
||||
}
|
||||
|
||||
// generatePGPUnlockKeyName generates a unique name for the PGP unlock key based on hostname and date
|
||||
func generatePGPUnlockKeyName() (string, error) {
|
||||
// generatePGPUnlockerName generates a unique name for the PGP unlocker based on hostname and date
|
||||
func generatePGPUnlockerName() (string, error) {
|
||||
hostname, err := os.Hostname()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to get hostname: %w", err)
|
||||
@@ -173,8 +173,8 @@ func generatePGPUnlockKeyName() (string, error) {
|
||||
return fmt.Sprintf("%s-pgp-%s", hostname, enrollmentDate), nil
|
||||
}
|
||||
|
||||
// CreatePGPUnlockKey creates a new PGP unlock key and stores it in the vault
|
||||
func CreatePGPUnlockKey(fs afero.Fs, stateDir string, gpgKeyID string) (*PGPUnlockKey, error) {
|
||||
// CreatePGPUnlocker creates a new PGP unlocker and stores it in the vault
|
||||
func CreatePGPUnlocker(fs afero.Fs, stateDir string, gpgKeyID string) (*PGPUnlocker, error) {
|
||||
// Check if GPG is available
|
||||
if err := checkGPGAvailable(); err != nil {
|
||||
return nil, err
|
||||
@@ -186,24 +186,24 @@ func CreatePGPUnlockKey(fs afero.Fs, stateDir string, gpgKeyID string) (*PGPUnlo
|
||||
return nil, fmt.Errorf("failed to get current vault: %w", err)
|
||||
}
|
||||
|
||||
// Generate the unlock key name based on hostname and date
|
||||
unlockKeyName, err := generatePGPUnlockKeyName()
|
||||
// Generate the unlocker name based on hostname and date
|
||||
unlockerName, err := generatePGPUnlockerName()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to generate unlock key name: %w", err)
|
||||
return nil, fmt.Errorf("failed to generate unlocker name: %w", err)
|
||||
}
|
||||
|
||||
// Create unlock key directory using the generated name
|
||||
// Create unlocker directory using the generated name
|
||||
vaultDir, err := vault.GetDirectory()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get vault directory: %w", err)
|
||||
}
|
||||
|
||||
unlockKeyDir := filepath.Join(vaultDir, "unlock.d", unlockKeyName)
|
||||
if err := fs.MkdirAll(unlockKeyDir, DirPerms); err != nil {
|
||||
return nil, fmt.Errorf("failed to create unlock key directory: %w", err)
|
||||
unlockerDir := filepath.Join(vaultDir, "unlockers.d", unlockerName)
|
||||
if err := fs.MkdirAll(unlockerDir, DirPerms); err != nil {
|
||||
return nil, fmt.Errorf("failed to create unlocker directory: %w", err)
|
||||
}
|
||||
|
||||
// Step 1: Generate a new age keypair for the PGP unlock key
|
||||
// Step 1: Generate a new age keypair for the PGP unlocker
|
||||
ageIdentity, err := age.GenerateX25519Identity()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to generate age keypair: %w", err)
|
||||
@@ -211,7 +211,7 @@ func CreatePGPUnlockKey(fs afero.Fs, stateDir string, gpgKeyID string) (*PGPUnlo
|
||||
|
||||
// Step 2: Store age public key as plaintext
|
||||
agePublicKeyString := ageIdentity.Recipient().String()
|
||||
agePubKeyPath := filepath.Join(unlockKeyDir, "pub.age")
|
||||
agePubKeyPath := filepath.Join(unlockerDir, "pub.age")
|
||||
if err := afero.WriteFile(fs, agePubKeyPath, []byte(agePublicKeyString), FilePerms); err != nil {
|
||||
return nil, fmt.Errorf("failed to write age public key: %w", err)
|
||||
}
|
||||
@@ -228,54 +228,54 @@ func CreatePGPUnlockKey(fs afero.Fs, stateDir string, gpgKeyID string) (*PGPUnlo
|
||||
}
|
||||
ltPrivKeyData = []byte(ltIdentity.String())
|
||||
} else {
|
||||
// Get the vault to access current unlock key
|
||||
currentUnlockKey, err := vault.GetCurrentUnlockKey()
|
||||
// Get the vault to access current unlocker
|
||||
currentUnlocker, err := vault.GetCurrentUnlocker()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get current unlock key: %w", err)
|
||||
return nil, fmt.Errorf("failed to get current unlocker: %w", err)
|
||||
}
|
||||
|
||||
// Get the current unlock key identity
|
||||
currentUnlockIdentity, err := currentUnlockKey.GetIdentity()
|
||||
// Get the current unlocker identity
|
||||
currentUnlockerIdentity, err := currentUnlocker.GetIdentity()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get current unlock key identity: %w", err)
|
||||
return nil, fmt.Errorf("failed to get current unlocker identity: %w", err)
|
||||
}
|
||||
|
||||
// Get encrypted long-term key from current unlock key, handling different types
|
||||
// Get encrypted long-term key from current unlocker, handling different types
|
||||
var encryptedLtPrivKey []byte
|
||||
switch currentUnlockKey := currentUnlockKey.(type) {
|
||||
case *PassphraseUnlockKey:
|
||||
// Read the encrypted long-term private key from passphrase unlock key
|
||||
encryptedLtPrivKey, err = afero.ReadFile(fs, filepath.Join(currentUnlockKey.GetDirectory(), "longterm.age"))
|
||||
switch currentUnlocker := currentUnlocker.(type) {
|
||||
case *PassphraseUnlocker:
|
||||
// Read the encrypted long-term private key from passphrase unlocker
|
||||
encryptedLtPrivKey, err = afero.ReadFile(fs, filepath.Join(currentUnlocker.GetDirectory(), "longterm.age"))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read encrypted long-term key from current passphrase unlock key: %w", err)
|
||||
return nil, fmt.Errorf("failed to read encrypted long-term key from current passphrase unlocker: %w", err)
|
||||
}
|
||||
|
||||
case *PGPUnlockKey:
|
||||
// Read the encrypted long-term private key from PGP unlock key
|
||||
encryptedLtPrivKey, err = afero.ReadFile(fs, filepath.Join(currentUnlockKey.GetDirectory(), "longterm.age"))
|
||||
case *PGPUnlocker:
|
||||
// Read the encrypted long-term private key from PGP unlocker
|
||||
encryptedLtPrivKey, err = afero.ReadFile(fs, filepath.Join(currentUnlocker.GetDirectory(), "longterm.age"))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read encrypted long-term key from current PGP unlock key: %w", err)
|
||||
return nil, fmt.Errorf("failed to read encrypted long-term key from current PGP unlocker: %w", err)
|
||||
}
|
||||
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported current unlock key type for PGP unlock key creation")
|
||||
return nil, fmt.Errorf("unsupported current unlocker type for PGP unlocker creation")
|
||||
}
|
||||
|
||||
// Step 6: Decrypt long-term private key using current unlock key
|
||||
ltPrivKeyData, err = DecryptWithIdentity(encryptedLtPrivKey, currentUnlockIdentity)
|
||||
// Step 6: Decrypt long-term private key using current unlocker
|
||||
ltPrivKeyData, err = DecryptWithIdentity(encryptedLtPrivKey, currentUnlockerIdentity)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to decrypt long-term private key: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Step 7: Encrypt long-term private key to the new age unlock key
|
||||
// Step 7: Encrypt long-term private key to the new age unlocker
|
||||
encryptedLtPrivKeyToAge, err := EncryptToRecipient(ltPrivKeyData, ageIdentity.Recipient())
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to encrypt long-term private key to age unlock key: %w", err)
|
||||
return nil, fmt.Errorf("failed to encrypt long-term private key to age unlocker: %w", err)
|
||||
}
|
||||
|
||||
// Write encrypted long-term private key
|
||||
ltPrivKeyPath := filepath.Join(unlockKeyDir, "longterm.age")
|
||||
ltPrivKeyPath := filepath.Join(unlockerDir, "longterm.age")
|
||||
if err := afero.WriteFile(fs, ltPrivKeyPath, encryptedLtPrivKeyToAge, FilePerms); err != nil {
|
||||
return nil, fmt.Errorf("failed to write encrypted long-term private key: %w", err)
|
||||
}
|
||||
@@ -287,7 +287,7 @@ func CreatePGPUnlockKey(fs afero.Fs, stateDir string, gpgKeyID string) (*PGPUnlo
|
||||
return nil, fmt.Errorf("failed to encrypt age private key with GPG: %w", err)
|
||||
}
|
||||
|
||||
agePrivKeyPath := filepath.Join(unlockKeyDir, "priv.age.gpg")
|
||||
agePrivKeyPath := filepath.Join(unlockerDir, "priv.age.gpg")
|
||||
if err := afero.WriteFile(fs, agePrivKeyPath, encryptedAgePrivKey, FilePerms); err != nil {
|
||||
return nil, fmt.Errorf("failed to write encrypted age private key: %w", err)
|
||||
}
|
||||
@@ -296,8 +296,8 @@ func CreatePGPUnlockKey(fs afero.Fs, stateDir string, gpgKeyID string) (*PGPUnlo
|
||||
// Generate the key ID directly using the GPG key ID
|
||||
keyID := fmt.Sprintf("%s-pgp", gpgKeyID)
|
||||
|
||||
pgpMetadata := PGPUnlockKeyMetadata{
|
||||
UnlockKeyMetadata: UnlockKeyMetadata{
|
||||
pgpMetadata := PGPUnlockerMetadata{
|
||||
UnlockerMetadata: UnlockerMetadata{
|
||||
ID: keyID,
|
||||
Type: "pgp",
|
||||
CreatedAt: time.Now(),
|
||||
@@ -310,16 +310,16 @@ func CreatePGPUnlockKey(fs afero.Fs, stateDir string, gpgKeyID string) (*PGPUnlo
|
||||
|
||||
metadataBytes, err := json.MarshalIndent(pgpMetadata, "", " ")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to marshal unlock key metadata: %w", err)
|
||||
return nil, fmt.Errorf("failed to marshal unlocker metadata: %w", err)
|
||||
}
|
||||
|
||||
if err := afero.WriteFile(fs, filepath.Join(unlockKeyDir, "unlock-metadata.json"), metadataBytes, FilePerms); err != nil {
|
||||
return nil, fmt.Errorf("failed to write unlock key metadata: %w", err)
|
||||
if err := afero.WriteFile(fs, filepath.Join(unlockerDir, "unlock-metadata.json"), metadataBytes, FilePerms); err != nil {
|
||||
return nil, fmt.Errorf("failed to write unlocker metadata: %w", err)
|
||||
}
|
||||
|
||||
return &PGPUnlockKey{
|
||||
Directory: unlockKeyDir,
|
||||
Metadata: pgpMetadata.UnlockKeyMetadata,
|
||||
return &PGPUnlocker{
|
||||
Directory: unlockerDir,
|
||||
Metadata: pgpMetadata.UnlockerMetadata,
|
||||
fs: fs,
|
||||
}, nil
|
||||
}
|
||||
@@ -20,8 +20,8 @@ type VaultInterface interface {
|
||||
AddSecret(name string, value []byte, force bool) error
|
||||
GetName() string
|
||||
GetFilesystem() afero.Fs
|
||||
GetCurrentUnlockKey() (UnlockKey, error)
|
||||
CreatePassphraseKey(passphrase string) (*PassphraseUnlockKey, error)
|
||||
GetCurrentUnlocker() (Unlocker, error)
|
||||
CreatePassphraseUnlocker(passphrase string) (*PassphraseUnlocker, error)
|
||||
}
|
||||
|
||||
// Secret represents a secret in a vault
|
||||
@@ -81,8 +81,8 @@ func (s *Secret) Save(value []byte, force bool) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetValue retrieves and decrypts the secret value using the provided unlock key
|
||||
func (s *Secret) GetValue(unlockKey UnlockKey) ([]byte, error) {
|
||||
// GetValue retrieves and decrypts the secret value using the provided unlocker
|
||||
func (s *Secret) GetValue(unlocker Unlocker) ([]byte, error) {
|
||||
DebugWith("Getting secret value",
|
||||
slog.String("secret_name", s.Name),
|
||||
slog.String("vault_name", s.vault.GetName()),
|
||||
@@ -118,29 +118,29 @@ func (s *Secret) GetValue(unlockKey UnlockKey) ([]byte, error) {
|
||||
return s.decryptWithLongTermKey(ltIdentity)
|
||||
}
|
||||
|
||||
Debug("Using unlock key for vault access", "secret_name", s.Name)
|
||||
Debug("Using unlocker for vault access", "secret_name", s.Name)
|
||||
|
||||
// Use the provided unlock key to get the vault's long-term private key
|
||||
if unlockKey == nil {
|
||||
Debug("No unlock key provided for secret decryption", "secret_name", s.Name)
|
||||
return nil, fmt.Errorf("unlock key required to decrypt secret")
|
||||
// Use the provided unlocker to get the vault's long-term private key
|
||||
if unlocker == nil {
|
||||
Debug("No unlocker provided for secret decryption", "secret_name", s.Name)
|
||||
return nil, fmt.Errorf("unlocker required to decrypt secret")
|
||||
}
|
||||
|
||||
DebugWith("Getting vault's long-term key using unlock key",
|
||||
DebugWith("Getting vault's long-term key using unlocker",
|
||||
slog.String("secret_name", s.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()),
|
||||
)
|
||||
|
||||
// Step 1: Use the unlock key to get the vault's long-term private key
|
||||
unlockIdentity, err := unlockKey.GetIdentity()
|
||||
// Step 1: Use the unlocker to get the vault's long-term private key
|
||||
unlockIdentity, err := unlocker.GetIdentity()
|
||||
if err != nil {
|
||||
Debug("Failed to get unlock key identity", "error", err, "secret_name", s.Name, "unlock_key_type", unlockKey.GetType())
|
||||
return nil, fmt.Errorf("failed to get unlock key identity: %w", err)
|
||||
Debug("Failed to get unlocker identity", "error", err, "secret_name", s.Name, "unlocker_type", unlocker.GetType())
|
||||
return nil, fmt.Errorf("failed to get unlocker identity: %w", err)
|
||||
}
|
||||
|
||||
// Read the encrypted long-term private key from the unlock key directory
|
||||
encryptedLtPrivKeyPath := filepath.Join(unlockKey.GetDirectory(), "longterm.age")
|
||||
// Read the encrypted long-term private key from the unlocker directory
|
||||
encryptedLtPrivKeyPath := filepath.Join(unlocker.GetDirectory(), "longterm.age")
|
||||
Debug("Reading encrypted long-term private key", "path", encryptedLtPrivKeyPath)
|
||||
|
||||
encryptedLtPrivKey, err := afero.ReadFile(s.vault.GetFilesystem(), encryptedLtPrivKeyPath)
|
||||
@@ -149,8 +149,8 @@ func (s *Secret) GetValue(unlockKey UnlockKey) ([]byte, error) {
|
||||
return nil, fmt.Errorf("failed to read encrypted long-term private key: %w", err)
|
||||
}
|
||||
|
||||
// Decrypt the encrypted long-term private key using the unlock key
|
||||
Debug("Decrypting long-term private key using unlock key", "secret_name", s.Name)
|
||||
// Decrypt the encrypted long-term private key using the unlocker
|
||||
Debug("Decrypting long-term private key using unlocker", "secret_name", s.Name)
|
||||
ltPrivKeyData, err := DecryptWithIdentity(encryptedLtPrivKey, unlockIdentity)
|
||||
if err != nil {
|
||||
Debug("Failed to decrypt long-term private key", "error", err, "secret_name", s.Name)
|
||||
|
||||
@@ -39,11 +39,11 @@ func (m *MockVault) GetFilesystem() afero.Fs {
|
||||
return m.fs
|
||||
}
|
||||
|
||||
func (m *MockVault) GetCurrentUnlockKey() (UnlockKey, error) {
|
||||
func (m *MockVault) GetCurrentUnlocker() (Unlocker, error) {
|
||||
return nil, nil // Not needed for this test
|
||||
}
|
||||
|
||||
func (m *MockVault) CreatePassphraseKey(passphrase string) (*PassphraseUnlockKey, error) {
|
||||
func (m *MockVault) CreatePassphraseUnlocker(passphrase string) (*PassphraseUnlocker, error) {
|
||||
return nil, nil // Not needed for this test
|
||||
}
|
||||
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
package secret
|
||||
|
||||
import (
|
||||
"filippo.io/age"
|
||||
)
|
||||
|
||||
// UnlockKey interface defines the methods all unlock key types must implement
|
||||
type UnlockKey interface {
|
||||
GetIdentity() (*age.X25519Identity, error)
|
||||
GetType() string
|
||||
GetMetadata() UnlockKeyMetadata
|
||||
GetDirectory() string
|
||||
GetID() string
|
||||
ID() string // Generate ID from the key's public key
|
||||
Remove() error // Remove the unlock key and any associated resources
|
||||
}
|
||||
16
internal/secret/unlocker.go
Normal file
16
internal/secret/unlocker.go
Normal file
@@ -0,0 +1,16 @@
|
||||
package secret
|
||||
|
||||
import (
|
||||
"filippo.io/age"
|
||||
)
|
||||
|
||||
// Unlocker interface defines the methods all unlocker types must implement
|
||||
type Unlocker interface {
|
||||
GetIdentity() (*age.X25519Identity, error)
|
||||
GetType() string
|
||||
GetMetadata() UnlockerMetadata
|
||||
GetDirectory() string
|
||||
GetID() string
|
||||
ID() string // Generate ID from the unlocker's public key
|
||||
Remove() error // Remove the unlocker and any associated resources
|
||||
}
|
||||
Reference in New Issue
Block a user