diff --git a/.gitignore b/.gitignore index d97c5ea..e69de29 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +0,0 @@ -secret diff --git a/README.md b/README.md index 928d3e3..a2e800e 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,7 @@ Build from source: ```bash git clone cd secret -make build +go build -o secret ./cmd/secret ``` ## Quick Start @@ -106,15 +106,15 @@ Lists all unlock keys in the current vault with their metadata. Creates a new unlock key of the specified type: **Types:** -- `passphrase`: Password-protected unlock key -- `keychain`: macOS Keychain unlock key (Touch ID/Face ID) -- `pgp`: GPG/PGP key unlock key +- `passphrase`: Traditional passphrase-protected unlock key +- `keychain`: macOS Keychain-protected unlock key (macOS only) +- `pgp`: Uses an existing GPG key for encryption/decryption **Options:** - `--keyid `: GPG key ID (required for PGP type) #### `secret keys rm ` -Removes an unlock key from the current vault. +Removes an unlock key. #### `secret key select ` Selects an unlock key as the current default for operations. @@ -127,9 +127,6 @@ Imports a secret from a file and stores it in the current vault under the given #### `secret vault import [vault-name]` Imports a mnemonic phrase into the specified vault (defaults to "default"). -#### `secret enroll` -Enrolls a macOS Keychain unlock key for biometric authentication. - ### Encryption Operations #### `secret encrypt [--input=file] [--output=file]` @@ -143,43 +140,23 @@ Decrypts data using an Age key stored as a secret. ### Directory Structure ``` -$BASE/ # ~/.config/berlin.sneak.pkg.secret (Linux) or ~/Library/Application Support/berlin.sneak.pkg.secret (macOS) -├── configuration.json # Global configuration -├── currentvault -> vaults.d/default # Symlink to current vault -└── vaults.d/ - ├── default/ # Default vault - │ ├── vault-metadata.json # Vault metadata - │ ├── pub.age # Long-term public key - │ ├── current-unlock-key -> unlock.d/passphrase # Current unlock key symlink - │ ├── unlock.d/ # Unlock keys directory - │ │ ├── passphrase/ # Passphrase unlock key - │ │ │ ├── unlock-metadata.json # Unlock key metadata - │ │ │ ├── pub.age # Unlock key public key - │ │ │ ├── priv.age # Unlock key private key (encrypted) - │ │ │ └── longterm.age # Long-term private key (encrypted to this unlock key) - │ │ ├── keychain/ # Keychain unlock key - │ │ │ ├── unlock-metadata.json - │ │ │ ├── pub.age - │ │ │ ├── priv.age - │ │ │ └── longterm.age - │ │ └── pgp/ # PGP unlock key - │ │ ├── unlock-metadata.json - │ │ ├── pub.age - │ │ ├── priv.asc # PGP-encrypted private key - │ │ └── longterm.age - │ └── secrets.d/ # Secrets directory - │ ├── my-service%password/ # Secret directory (slashes encoded as %) - │ │ ├── value.age # Encrypted secret value - │ │ ├── pub.age # Secret-specific public key - │ │ ├── priv.age # Secret-specific private key (encrypted to long-term key) - │ │ └── secret-metadata.json # Secret metadata - │ └── api%keys%production/ - │ ├── value.age - │ ├── pub.age - │ ├── priv.age - │ └── secret-metadata.json - └── work/ # Additional vault - └── ... (same structure as default) +~/.local/share/secret/ +├── vaults.d/ +│ ├── default/ +│ │ ├── unlock-keys.d/ +│ │ │ ├── passphrase/ # Passphrase unlock key +│ │ │ ├── keychain/ # Keychain unlock key (macOS) +│ │ │ └── pgp/ # PGP unlock key +│ │ ├── secrets.d/ +│ │ │ ├── api%key/ # Secret: api/key +│ │ │ └── database%password/ # Secret: database/password +│ │ └── current-unlock-key -> ../unlock-keys.d/passphrase +│ └── work/ +│ ├── unlock-keys.d/ +│ ├── secrets.d/ +│ └── current-unlock-key +├── currentvault -> vaults.d/default +└── configuration.json ``` ### Key Management and Encryption Flow @@ -192,19 +169,22 @@ $BASE/ # ~/.config/berlin.sneak.pkg.secret #### Unlock Keys Unlock keys provide different authentication methods to access the long-term keys: -1. **Passphrase Unlock Keys**: - - Private key encrypted using a user-provided passphrase - - Stored as encrypted Age identity in `priv.age` +1. **Passphrase Keys**: + - Encrypted with user-provided passphrase + - Stored as encrypted Age keys + - Cross-platform compatible -2. **macOS Keychain Keys**: - - Private key stored in the macOS Keychain - - Requires biometric authentication (Touch ID/Face ID) - - Provides hardware-backed security +2. **Keychain Keys** (macOS only): + - Uses macOS Keychain for secure storage + - Provides seamless authentication on macOS systems + - Age private key encrypted with random passphrase stored in Keychain -3. **PGP Unlock Keys**: - - Private key encrypted using an existing GPG key - - Compatible with hardware tokens (YubiKey, etc.) - - Stored as PGP-encrypted data in `priv.asc` +3. **PGP Keys**: + - Uses existing GPG key infrastructure + - Leverages existing key management workflows + - Strong authentication through GPG + +Each vault maintains its own set of unlock keys and one long-term key. The long-term key is encrypted to each unlock key, allowing any authorized unlock key to access vault secrets. #### Secret-specific Keys - Each secret has its own encryption key pair @@ -234,9 +214,9 @@ Unlock keys provide different authentication methods to access the long-term key - Per-secret encryption keys limit exposure if compromised - Long-term keys protected by multiple unlock key layers -### Hardware Integration -- macOS Keychain support for biometric authentication -- Hardware token support via PGP/GPG integration +### Platform Integration +- macOS Keychain integration for seamless authentication +- GPG integration for existing key management workflows ## Examples @@ -266,7 +246,7 @@ secret vault create personal # Work with work vault secret vault select work echo "work-db-pass" | secret add database/password -secret keys add keychain # Add Touch ID authentication +secret keys add passphrase # Add passphrase authentication # Switch to personal vault secret vault select personal @@ -280,7 +260,7 @@ secret vault list ```bash # Add multiple unlock methods secret keys add passphrase # Password-based -secret keys add keychain # Touch ID (macOS only) +secret keys add keychain # macOS Keychain (macOS only) secret keys add pgp --keyid ABCD1234 # GPG key # List unlock keys @@ -325,11 +305,11 @@ secret decrypt encryption/mykey --input document.txt.age --output document.txt ### Threat Model - Protects against unauthorized access to secret values - Provides defense against compromise of individual components -- Supports hardware-backed authentication where available +- Supports platform-specific authentication where available ### Best Practices 1. Use strong, unique passphrases for unlock keys -2. Enable hardware authentication (Keychain, hardware tokens) when available +2. Enable platform-specific authentication (Keychain) when available 3. Regularly audit unlock keys and remove unused ones 4. Keep mnemonic phrases securely backed up offline 5. Use separate vaults for different security contexts @@ -337,15 +317,15 @@ secret decrypt encryption/mykey --input document.txt.age --output document.txt ### Limitations - Requires access to unlock keys for secret retrieval - Mnemonic phrases must be securely stored and backed up -- Hardware features limited to supported platforms +- Platform-specific features limited to supported platforms ## Development ### Building ```bash -make build # Build binary -make test # Run tests -make lint # Run linter +go build -o secret ./cmd/secret # Build binary +go test ./... # Run tests +go vet ./... # Run static analysis ``` ### Testing @@ -355,3 +335,11 @@ The project includes comprehensive tests: go test ./... # Unit tests ``` +## Features + +- **Multiple Authentication Methods**: Supports passphrase-based, keychain-based (macOS), and PGP-based unlock keys +- **Vault Isolation**: Complete separation between different vaults +- **Per-Secret Encryption**: Each secret has its own encryption key +- **BIP39 Mnemonic Support**: Keyless operation using mnemonic phrases +- **Cross-Platform**: Works on macOS, Linux, and other Unix-like systems + diff --git a/cmd/secret/main.go b/cmd/secret/main.go new file mode 100644 index 0000000..615029c --- /dev/null +++ b/cmd/secret/main.go @@ -0,0 +1,5 @@ +package main + +func main() { + CLIEntry() +} diff --git a/internal/secret/cli.go b/internal/secret/cli.go index 0818e24..6533ea8 100644 --- a/internal/secret/cli.go +++ b/internal/secret/cli.go @@ -134,7 +134,6 @@ func newRootCmd() *cobra.Command { cmd.AddCommand(newKeysCmd()) cmd.AddCommand(newKeyCmd()) cmd.AddCommand(newImportCmd()) - cmd.AddCommand(newEnrollCmd()) cmd.AddCommand(newEncryptCmd()) cmd.AddCommand(newDecryptCmd()) @@ -431,19 +430,6 @@ func newImportCmd() *cobra.Command { return cmd } -func newEnrollCmd() *cobra.Command { - return &cobra.Command{ - Use: "enroll", - Short: "Enroll a macOS Keychain unlock key", - Long: `Enroll a macOS Keychain unlock key that uses Touch ID/Face ID for biometric authentication.`, - Args: cobra.NoArgs, - RunE: func(cmd *cobra.Command, args []string) error { - cli := NewCLIInstance() - return cli.Enroll() - }, - } -} - func newEncryptCmd() *cobra.Command { cmd := &cobra.Command{ Use: "encrypt ", @@ -1073,7 +1059,16 @@ func (cli *CLIInstance) KeysAdd(keyType string, cmd *cobra.Command) error { return nil case "keychain": - return fmt.Errorf("macOS Keychain unlock keys should be created using 'secret enroll' command") + keychainKey, err := CreateKeychainUnlockKey(cli.fs, cli.stateDir) + if err != nil { + return fmt.Errorf("failed to create macOS Keychain unlock key: %w", err) + } + + cmd.Printf("Created macOS Keychain unlock key: %s\n", keychainKey.GetMetadata().ID) + if keyName, err := keychainKey.GetKeychainItemName(); err == nil { + cmd.Printf("Keychain Item Name: %s\n", keyName) + } + return nil case "pgp": // Get GPG key ID from flag or environment variable @@ -1150,25 +1145,6 @@ func (cli *CLIInstance) Import(vaultName string) error { return cli.importMnemonic(vaultName, mnemonicStr) } -// Enroll enrolls a hardware security module -func (cli *CLIInstance) Enroll() error { - keychainKey, err := CreateKeychainUnlockKey(cli.fs, cli.stateDir) - if err != nil { - return fmt.Errorf("failed to enroll macOS Keychain unlock key: %w", err) - } - - fmt.Printf("macOS Keychain unlock key enrolled successfully!\n") - fmt.Printf("Key ID: %s\n", keychainKey.GetMetadata().ID) - fmt.Printf("Directory: %s\n", keychainKey.GetDirectory()) - - // Load the key name to show the keychain key name - if keyName, err := keychainKey.GetKeyName(); err == nil { - fmt.Printf("Keychain Key Name: %s\n", keyName) - } - - return nil -} - // Encrypt encrypts data using an age secret key stored in a secret func (cli *CLIInstance) Encrypt(secretName, inputFile, outputFile string) error { // Get current vault diff --git a/internal/secret/crypto.go b/internal/secret/crypto.go new file mode 100644 index 0000000..16f7321 --- /dev/null +++ b/internal/secret/crypto.go @@ -0,0 +1,112 @@ +package secret + +import ( + "bytes" + "fmt" + "io" + "syscall" + + "filippo.io/age" + "golang.org/x/term" +) + +// encryptToRecipient encrypts data to a recipient using age +func encryptToRecipient(data []byte, recipient age.Recipient) ([]byte, error) { + var buf bytes.Buffer + w, err := age.Encrypt(&buf, recipient) + if err != nil { + return nil, fmt.Errorf("failed to create encryptor: %w", err) + } + + if _, err := w.Write(data); err != nil { + return nil, fmt.Errorf("failed to write data: %w", err) + } + + if err := w.Close(); err != nil { + return nil, fmt.Errorf("failed to close encryptor: %w", err) + } + + return buf.Bytes(), nil +} + +// decryptWithIdentity decrypts data with an identity using age +func decryptWithIdentity(data []byte, identity age.Identity) ([]byte, error) { + r, err := age.Decrypt(bytes.NewReader(data), identity) + if err != nil { + return nil, fmt.Errorf("failed to create decryptor: %w", err) + } + + result, err := io.ReadAll(r) + if err != nil { + return nil, fmt.Errorf("failed to read decrypted data: %w", err) + } + + return result, nil +} + +// encryptWithPassphrase encrypts data using a passphrase with age's scrypt-based encryption +func encryptWithPassphrase(data []byte, passphrase string) ([]byte, error) { + recipient, err := age.NewScryptRecipient(passphrase) + if err != nil { + return nil, fmt.Errorf("failed to create scrypt recipient: %w", err) + } + + return encryptToRecipient(data, recipient) +} + +// decryptWithPassphrase decrypts data using a passphrase with age's scrypt-based decryption +func decryptWithPassphrase(encryptedData []byte, passphrase string) ([]byte, error) { + identity, err := age.NewScryptIdentity(passphrase) + if err != nil { + return nil, fmt.Errorf("failed to create scrypt identity: %w", err) + } + + return decryptWithIdentity(encryptedData, identity) +} + +// readPassphrase reads a passphrase securely from the terminal without echoing +// This version is for unlocking and doesn't require confirmation +func readPassphrase(prompt string) (string, error) { + // Check if stdin is a terminal + if !term.IsTerminal(int(syscall.Stdin)) { + // Not a terminal - fall back to regular input + fmt.Print(prompt) + var passphrase string + _, err := fmt.Scanln(&passphrase) + if err != nil { + return "", fmt.Errorf("failed to read passphrase: %w", err) + } + return passphrase, nil + } + + // Terminal input - use secure password reading + fmt.Print(prompt) + passphrase, err := term.ReadPassword(int(syscall.Stdin)) + if err != nil { + return "", fmt.Errorf("failed to read passphrase: %w", err) + } + fmt.Println() // Print newline since ReadPassword doesn't echo + + if len(passphrase) == 0 { + return "", fmt.Errorf("passphrase cannot be empty") + } + + return string(passphrase), nil +} + +// decryptSecretWithLongTermKey is a helper that parses a long-term private key and uses it to decrypt secret data +func decryptSecretWithLongTermKey(ltPrivKeyData []byte, encryptedData []byte) ([]byte, error) { + // Parse long-term private key + ltIdentity, err := age.ParseX25519Identity(string(ltPrivKeyData)) + if err != nil { + return nil, fmt.Errorf("failed to parse long-term private key: %w", err) + } + + // Decrypt secret data using long-term key + decryptedData, err := decryptWithIdentity(encryptedData, ltIdentity) + if err != nil { + return nil, fmt.Errorf("failed to decrypt secret: %w", err) + } + + return decryptedData, nil +} diff --git a/internal/secret/passphraseunlock.go b/internal/secret/passphraseunlock.go new file mode 100644 index 0000000..3799564 --- /dev/null +++ b/internal/secret/passphraseunlock.go @@ -0,0 +1,231 @@ +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 +} + +// 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()), + ) + + // 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)), + ) + + // Get passphrase for decrypting the unlock key + var passphraseStr string + if envPassphrase := os.Getenv(EnvUnlockPassphrase); envPassphrase != "" { + Debug("Using passphrase from environment variable", "key_id", p.GetID()) + passphraseStr = envPassphrase + } else { + Debug("Prompting for passphrase", "key_id", p.GetID()) + // Prompt for passphrase + passphraseStr, err = readPassphrase("Enter passphrase to unlock vault: ") + if err != nil { + Debug("Failed to read passphrase", "error", err, "key_id", p.GetID()) + return nil, fmt.Errorf("failed to read passphrase: %w", err) + } + } + + 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) +} + +// DecryptLongTermKey decrypts and returns the long-term private key for this vault +func (p *PassphraseUnlockKey) DecryptLongTermKey() ([]byte, error) { + DebugWith("Decrypting long-term key with passphrase unlock key", + slog.String("key_id", p.GetID()), + slog.String("key_type", p.GetType()), + ) + + // Get our unlock key identity + unlockIdentity, err := p.GetIdentity() + if err != nil { + Debug("Failed to get passphrase unlock identity for long-term decryption", "error", err, "key_id", p.GetID()) + return nil, fmt.Errorf("failed to get unlock identity: %w", err) + } + + // Read encrypted long-term private key + encryptedLtPrivKeyPath := filepath.Join(p.Directory, "longterm.age") + Debug("Reading encrypted long-term private key", "path", encryptedLtPrivKeyPath) + + encryptedLtPrivKey, err := afero.ReadFile(p.fs, encryptedLtPrivKeyPath) + if err != nil { + Debug("Failed to read encrypted long-term private key", "error", err, "path", encryptedLtPrivKeyPath) + return nil, fmt.Errorf("failed to read encrypted long-term private key: %w", err) + } + + DebugWith("Read encrypted long-term private key", + slog.String("key_id", p.GetID()), + slog.Int("encrypted_length", len(encryptedLtPrivKey)), + ) + + // Decrypt long-term private key using our unlock key + Debug("Decrypting long-term private key with passphrase unlock key", "key_id", p.GetID()) + ltPrivKeyData, err := decryptWithIdentity(encryptedLtPrivKey, unlockIdentity) + if err != nil { + Debug("Failed to decrypt long-term private key", "error", err, "key_id", p.GetID()) + return nil, fmt.Errorf("failed to decrypt long-term private key: %w", err) + } + + DebugWith("Successfully decrypted long-term private key", + slog.String("key_id", p.GetID()), + slog.Int("decrypted_length", len(ltPrivKeyData)), + ) + + return ltPrivKeyData, nil +} + +// DecryptSecret decrypts a secret using this passphrase unlock key's long-term key management +func (p *PassphraseUnlockKey) DecryptSecret(secret *Secret) ([]byte, error) { + DebugWith("Decrypting secret with passphrase unlock key", + slog.String("secret_name", secret.Name), + slog.String("key_id", p.GetID()), + slog.String("key_type", p.GetType()), + ) + + // Get encrypted secret data + encryptedData, err := secret.GetEncryptedData() + if err != nil { + Debug("Failed to get encrypted secret data for passphrase decryption", "error", err, "secret_name", secret.Name) + return nil, fmt.Errorf("failed to get encrypted secret data: %w", err) + } + + DebugWith("Retrieved encrypted secret data for passphrase decryption", + slog.String("secret_name", secret.Name), + slog.String("key_id", p.GetID()), + slog.Int("encrypted_length", len(encryptedData)), + ) + + // Decrypt long-term private key using our unlock key + ltPrivKeyData, err := p.DecryptLongTermKey() + if err != nil { + Debug("Failed to decrypt long-term private key for secret decryption", "error", err, "key_id", p.GetID()) + return nil, fmt.Errorf("failed to decrypt long-term private key: %w", err) + } + + // Use helper to parse long-term key and decrypt secret + decryptedData, err := decryptSecretWithLongTermKey(ltPrivKeyData, encryptedData) + if err != nil { + Debug("Failed to decrypt secret with long-term key", "error", err, "secret_name", secret.Name, "key_id", p.GetID()) + return nil, err + } + + DebugWith("Successfully decrypted secret with passphrase unlock key", + slog.String("secret_name", secret.Name), + slog.String("key_id", p.GetID()), + slog.Int("decrypted_length", len(decryptedData)), + ) + + return decryptedData, nil +} diff --git a/internal/secret/pgpunlock.go b/internal/secret/pgpunlock.go new file mode 100644 index 0000000..0519ecb --- /dev/null +++ b/internal/secret/pgpunlock.go @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/internal/secret/unlock.go b/internal/secret/unlock.go new file mode 100644 index 0000000..bd12048 --- /dev/null +++ b/internal/secret/unlock.go @@ -0,0 +1,22 @@ +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 + + // DecryptLongTermKey decrypts and returns the long-term private key for this vault + DecryptLongTermKey() ([]byte, error) + + // DecryptSecret decrypts a secret using this unlock key's long-term key management + DecryptSecret(secret *Secret) ([]byte, error) +} diff --git a/internal/secret/vault.go b/internal/secret/vault.go new file mode 100644 index 0000000..0519ecb --- /dev/null +++ b/internal/secret/vault.go @@ -0,0 +1 @@ + \ No newline at end of file