forked from sneak/secret
270 lines
8.0 KiB
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
|
|
}
|