1
0
forked from sneak/secret
secret/internal/secret/secret.go

270 lines
8.0 KiB
Go

package secret
import (
"encoding/json"
"fmt"
"log/slog"
"os"
"path/filepath"
"strings"
"time"
"git.eeqj.de/sneak/secret/pkg/agehd"
"github.com/spf13/afero"
)
// Secret represents a secret in a vault
type Secret struct {
Name string
Directory string
Metadata SecretMetadata
vault *Vault
}
// NewSecret creates a new Secret instance
func NewSecret(vault *Vault, name string) *Secret {
DebugWith("Creating new secret instance",
slog.String("secret_name", name),
slog.String("vault_name", vault.Name),
)
// Convert slashes to percent signs for storage directory name
storageName := strings.ReplaceAll(name, "/", "%")
vaultDir, _ := vault.GetDirectory()
secretDir := filepath.Join(vaultDir, "secrets.d", storageName)
DebugWith("Secret storage details",
slog.String("secret_name", name),
slog.String("storage_name", storageName),
slog.String("secret_dir", secretDir),
)
return &Secret{
Name: name,
Directory: secretDir,
vault: vault,
Metadata: SecretMetadata{
Name: name,
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
},
}
}
// Save saves a secret value to the vault
func (s *Secret) Save(value []byte, force bool) error {
DebugWith("Saving secret",
slog.String("secret_name", s.Name),
slog.String("vault_name", s.vault.Name),
slog.Int("value_length", len(value)),
slog.Bool("force", force),
)
err := s.vault.AddSecret(s.Name, value, force)
if err != nil {
Debug("Failed to save secret", "error", err, "secret_name", s.Name)
return err
}
Debug("Successfully saved secret", "secret_name", s.Name)
return nil
}
// GetValue retrieves and decrypts the secret value using the provided unlock key
func (s *Secret) GetValue(unlockKey UnlockKey) ([]byte, error) {
DebugWith("Getting secret value",
slog.String("secret_name", s.Name),
slog.String("vault_name", s.vault.Name),
)
// Check if secret exists
exists, err := s.Exists()
if err != nil {
Debug("Failed to check if secret exists during GetValue", "error", err, "secret_name", s.Name)
return nil, fmt.Errorf("failed to check if secret exists: %w", err)
}
if !exists {
Debug("Secret not found during GetValue", "secret_name", s.Name, "vault_name", s.vault.Name)
return nil, fmt.Errorf("secret %s not found", s.Name)
}
Debug("Secret exists, proceeding with decryption", "secret_name", s.Name)
// Check if we have SB_SECRET_MNEMONIC environment variable for direct decryption
if envMnemonic := os.Getenv(EnvMnemonic); envMnemonic != "" {
Debug("Using mnemonic from environment for secret decryption", "secret_name", s.Name)
// Use mnemonic directly to derive long-term key
ltIdentity, err := agehd.DeriveIdentity(envMnemonic, 0)
if err != nil {
Debug("Failed to derive long-term key from mnemonic for secret", "error", err, "secret_name", s.Name)
return nil, fmt.Errorf("failed to derive long-term key from mnemonic: %w", err)
}
Debug("Successfully derived long-term key from mnemonic", "secret_name", s.Name)
// Read our own encrypted data
encryptedData, err := s.GetEncryptedData()
if err != nil {
Debug("Failed to get encrypted data for mnemonic decryption", "error", err, "secret_name", s.Name)
return nil, err
}
DebugWith("Retrieved encrypted data for mnemonic decryption",
slog.String("secret_name", s.Name),
slog.Int("encrypted_length", len(encryptedData)),
)
// Decrypt secret data
Debug("Decrypting secret with long-term key from mnemonic", "secret_name", s.Name)
decryptedData, err := decryptWithIdentity(encryptedData, ltIdentity)
if err != nil {
Debug("Failed to decrypt secret with mnemonic", "error", err, "secret_name", s.Name)
return nil, fmt.Errorf("failed to decrypt secret: %w", err)
}
DebugWith("Successfully decrypted secret with mnemonic",
slog.String("secret_name", s.Name),
slog.Int("decrypted_length", len(decryptedData)),
)
return decryptedData, nil
}
Debug("Using unlock key for secret decryption", "secret_name", s.Name)
// Use the provided unlock key to decrypt the secret
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")
}
DebugWith("Delegating secret decryption to unlock key",
slog.String("secret_name", s.Name),
slog.String("unlock_key_type", unlockKey.GetType()),
slog.String("unlock_key_id", unlockKey.GetID()),
)
// Delegate decryption to the unlock key implementation
decryptedData, err := unlockKey.DecryptSecret(s)
if err != nil {
Debug("Unlock key failed to decrypt secret", "error", err, "secret_name", s.Name, "unlock_key_type", unlockKey.GetType())
return nil, err
}
DebugWith("Successfully decrypted secret via unlock key",
slog.String("secret_name", s.Name),
slog.String("unlock_key_type", unlockKey.GetType()),
slog.Int("decrypted_length", len(decryptedData)),
)
return decryptedData, nil
}
// LoadMetadata loads the secret metadata from disk
func (s *Secret) LoadMetadata() error {
DebugWith("Loading secret metadata",
slog.String("secret_name", s.Name),
slog.String("vault_name", s.vault.Name),
)
vaultDir, err := s.vault.GetDirectory()
if err != nil {
Debug("Failed to get vault directory for metadata loading", "error", err, "secret_name", s.Name)
return err
}
// Convert slashes to percent signs for storage
storageName := strings.ReplaceAll(s.Name, "/", "%")
metadataPath := filepath.Join(vaultDir, "secrets.d", storageName, "secret-metadata.json")
DebugWith("Reading secret metadata",
slog.String("secret_name", s.Name),
slog.String("metadata_path", metadataPath),
)
// Read metadata file
metadataBytes, err := afero.ReadFile(s.vault.fs, metadataPath)
if err != nil {
Debug("Failed to read secret metadata file", "error", err, "metadata_path", metadataPath)
return fmt.Errorf("failed to read metadata: %w", err)
}
DebugWith("Read secret metadata file",
slog.String("secret_name", s.Name),
slog.Int("metadata_size", len(metadataBytes)),
)
var metadata SecretMetadata
if err := json.Unmarshal(metadataBytes, &metadata); err != nil {
Debug("Failed to parse secret metadata JSON", "error", err, "secret_name", s.Name)
return fmt.Errorf("failed to parse metadata: %w", err)
}
DebugWith("Parsed secret metadata",
slog.String("secret_name", metadata.Name),
slog.Time("created_at", metadata.CreatedAt),
slog.Time("updated_at", metadata.UpdatedAt),
)
s.Metadata = metadata
Debug("Successfully loaded secret metadata", "secret_name", s.Name)
return nil
}
// GetMetadata returns the secret metadata
func (s *Secret) GetMetadata() SecretMetadata {
Debug("Returning secret metadata", "secret_name", s.Name)
return s.Metadata
}
// GetEncryptedData reads and returns the encrypted secret data
func (s *Secret) GetEncryptedData() ([]byte, error) {
DebugWith("Getting encrypted secret data",
slog.String("secret_name", s.Name),
slog.String("vault_name", s.vault.Name),
)
secretPath := filepath.Join(s.Directory, "value.age")
Debug("Reading encrypted secret file", "secret_path", secretPath)
encryptedData, err := afero.ReadFile(s.vault.fs, secretPath)
if err != nil {
Debug("Failed to read encrypted secret file", "error", err, "secret_path", secretPath)
return nil, fmt.Errorf("failed to read encrypted secret: %w", err)
}
DebugWith("Successfully read encrypted secret data",
slog.String("secret_name", s.Name),
slog.Int("encrypted_length", len(encryptedData)),
)
return encryptedData, nil
}
// Exists checks if the secret exists on disk
func (s *Secret) Exists() (bool, error) {
DebugWith("Checking if secret exists",
slog.String("secret_name", s.Name),
slog.String("vault_name", s.vault.Name),
)
secretPath := filepath.Join(s.Directory, "value.age")
Debug("Checking secret file existence", "secret_path", secretPath)
exists, err := afero.Exists(s.vault.fs, secretPath)
if err != nil {
Debug("Failed to check secret file existence", "error", err, "secret_path", secretPath)
return false, err
}
DebugWith("Secret existence check result",
slog.String("secret_name", s.Name),
slog.Bool("exists", exists),
)
return exists, nil
}