Documentation
¶
Overview ¶
Package vault provides recipients.txt management for multi-user encryption support. Recipients can be added to enable multiple parties to decrypt vault entries.
Package vault provides encrypted storage and search for OpenPass entries.
============================================================================= DESIGN DOCUMENT: Scalable Search for Large Vaults =============================================================================
Current Bottleneck: ------------------- Find() uses a two-pass approach. The first pass (path matching) is fast O(n). The second pass (field search) requires decrypting ALL entries where the path didn't match. This sequential decryption is the primary bottleneck for large vaults. For a vault with 50k entries where no paths match, we must sequentially decrypt 50k entries before returning results.
Why Concurrent Decryption Helps: -------------------------------- Modern CPUs have multiple cores. Sequential decryption leaves cores idle. By decrypting entries in parallel using a bounded worker pool, we can utilize multiple cores simultaneously, proportionally reducing wall-clock time.
Bounded Parallelism Rationale: ----------------------------- We use a bounded worker pool (default: 4 concurrent decryptions) rather than unbounded parallelism for these reasons:
Memory Pressure: Each decrypted entry consumes memory. With unbounded parallelism on a 50k entry vault, we could spawn 50k goroutines all trying to decrypt simultaneously, causing memory exhaustion.
I/O Saturation: The underlying storage (SSD/HDD) has finite read throughput. Beyond a certain concurrency level, additional workers simply compete for the same I/O bandwidth without improvement.
Cryptographic Operations: Age decryption involves CPU-intensive operations (X25519 key exchange, ChaCha20-Poly1305). Too many concurrent operations can cause CPU cache thrashing.
Practical Results: Testing shows 4 workers provides near-optimal throughput for typical user machines while keeping memory bounded.
Performance Targets (4 workers): -------------------------------- - 50k entries, path-only: ~500ms (no decryption needed) - 50k entries, field-search: ~2-4s (bounded by decrypt parallelism) - 10k entries, field-search: ~500ms-1s - 1k entries, field-search: ~100-200ms
Security Tradeoffs of Persistent Encrypted Index: ------------------------------------------------ An alternative approach would be to build a persistent index that maps search terms to encrypted entry references. This would enable O(1) or O(log n) searches without decryption.
Security Considerations:
- A persistent index MUST be encrypted at rest to prevent leakage of entry relationships and search patterns
- The index encryption key must be derived from the user's passphrase (like the vault identity key) ensuring only authorized users can access it
- Even with encryption, an index reveals search patterns over time:
- Which terms are searched frequently
- Which entries are accessed together
- Temporal patterns of access
- If the index is stored alongside the vault (e.g., in the vault directory), it could be stolen alongside encrypted entries
Attack Scenarios:
- A passive observer who steals the vault but not the passphrase: encrypted index provides no additional advantage over encrypted entries
- An active observer with passphrase but no vault: reveals search history but not content
- A side-channel attack on a compromised machine: index access patterns could reveal search behavior even with encrypted index
For these reasons, we currently prefer the on-demand decrypt approach which provides no persistent search metadata to steal.
Future encrypted index implementation would need to: 1. Use a key derived from the vault's master key 2. Store only term->entry mapping, never plaintext content 3. Consider adding plausible deniability via fake entries
============================================================================= END DESIGN DOCUMENT =============================================================================
Performance Characteristics:
List is O(n) where n is the number of entries. It performs no decryption, only directory walking. For large vaults (1k+ entries), List is fast.
Find uses a two-pass approach:
- First pass: O(n) path-only comparison (no decryption)
- Second pass: O(k) decryption + field search where k = entries where path didn't match
The fast path optimization means:
- Queries matching paths (e.g., "github" matching "github.com/user") avoid decryption entirely
- Only field content searches require decrypting entries
FindConcurrent uses a bounded worker pool for parallel decryption:
- Same two-pass logic as Find, but decrypts entries concurrently
- Default 4 concurrent workers balances throughput vs memory pressure
- Best for field-search queries on large vaults (10k+ entries)
Limits:
- 100 entries: ~10ms (path-only when possible)
- 1,000 entries: ~100ms (path-only when possible)
- 10,000 entries: ~1s (path-only when possible)
- Field searches scale with number of non-path-matching entries
Index ¶
- Variables
- func CollectFieldMatches(matches map[string]struct{}, prefix string, value any, needle string)
- func DeleteEntry(vaultDir, path string) error
- func EnsureDir(v *Vault, path string) error
- func EntryPath(v *Vault, path string) string
- func ExtractTOTP(data map[string]any) (secret, algorithm string, digits, period int, hasTOTP bool)
- func Init(vaultDir string, identity *age.X25519Identity, cfg *vaultconfig.Config) error
- func InitWithPassphrase(vaultDir string, passphrase string, cfg *vaultconfig.Config) (*age.X25519Identity, error)
- func IsInitialized(vaultDir string) bool
- func List(vaultDir string, prefix string) ([]string, error)
- func ListFast(vaultDir string, prefix string) ([]string, error)
- func SafeMkdirAll(path string, perm os.FileMode) error
- func SafeRemove(path string) error
- func SafeWriteFile(path string, data []byte, perm os.FileMode) error
- func WriteEntry(vaultDir, path string, entry *Entry, identity *age.X25519Identity) error
- func WriteEntryWithRecipients(vaultDir, path string, entry *Entry, identity *age.X25519Identity) error
- type Entry
- type EntryMetadata
- type FindOptions
- type Match
- type RecipientInfo
- type RecipientsManager
- func (rm *RecipientsManager) AddRecipient(recipientStr string) error
- func (rm *RecipientsManager) ListRecipients() ([]RecipientInfo, error)
- func (rm *RecipientsManager) LoadRecipientStrings() ([]string, error)
- func (rm *RecipientsManager) LoadRecipients() ([]*age.X25519Recipient, error)
- func (rm *RecipientsManager) RecipientsFileExists() bool
- func (rm *RecipientsManager) RecipientsFilePath() string
- func (rm *RecipientsManager) RemoveRecipient(recipientStr string) error
- type Vault
Constants ¶
This section is empty.
Variables ¶
var ( ErrRecipientAlreadyExists = errors.New("recipient already exists") ErrRecipientNotFound = errors.New("recipient not found") ErrInvalidRecipient = errors.New("invalid recipient") ErrEmptyRecipientFile = errors.New("recipients file is empty") )
Common recipients errors
var ( ErrVaultDirEmpty = errors.New("vault directory is empty") ErrNilIdentity = errors.New("identity is nil") ErrNilConfig = errors.New("config is nil") ErrIdentityMismatch = errors.New("identity mismatch") ErrVaultNotInitialized = errors.New("vault not initialized") ErrVaultDirEscapes = errors.New("vault directory path escapes intended directory") )
Common vault errors
Functions ¶
func CollectFieldMatches ¶ added in v1.1.0
func DeleteEntry ¶
DeleteEntry removes an entry from the vault
func ExtractTOTP ¶ added in v1.1.1
ExtractTOTP extracts TOTP configuration from entry data. Returns the secret, algorithm, digits, period, and a boolean indicating whether a valid TOTP configuration was found.
func Init ¶
func Init(vaultDir string, identity *age.X25519Identity, cfg *vaultconfig.Config) error
Init initializes a new vault at the given directory with the provided identity and config. It creates the vault directory, config file, and encrypted identity file.
func InitWithPassphrase ¶
func InitWithPassphrase(vaultDir string, passphrase string, cfg *vaultconfig.Config) (*age.X25519Identity, error)
InitWithPassphrase initializes a new vault with a passphrase-protected identity.
func IsInitialized ¶
IsInitialized checks if a vault is initialized at the given directory
func ListFast ¶ added in v1.1.1
ListFast returns all entry paths in the vault, optionally filtered by prefix. It uses os.ReadDir for efficient directory traversal without stat calls.
func SafeRemove ¶ added in v1.1.0
func SafeWriteFile ¶ added in v1.1.0
func WriteEntry ¶
func WriteEntry(vaultDir, path string, entry *Entry, identity *age.X25519Identity) error
WriteEntry encrypts and writes an entry to the vault
func WriteEntryWithRecipients ¶
func WriteEntryWithRecipients(vaultDir, path string, entry *Entry, identity *age.X25519Identity) error
WriteEntryWithRecipients encrypts and writes an entry to the vault, encrypting for all recipients including those in recipients.txt
Types ¶
type Entry ¶
type Entry struct {
Data map[string]any `json:"data"`
Metadata EntryMetadata `json:"meta"`
}
Entry represents a vault entry with flexible data storage using map[string]any.
func MergeEntry ¶
func MergeEntry(vaultDir, path string, partialData map[string]any, identity *age.X25519Identity) (*Entry, error)
MergeEntry merges partial data into an existing entry
func MergeEntryWithRecipients ¶
func MergeEntryWithRecipients(vaultDir, path string, partialData map[string]any, identity *age.X25519Identity) (*Entry, error)
MergeEntryWithRecipients merges partial data into an existing entry, encrypting for all recipients
func ReadEntry ¶
func ReadEntry(vaultDir, path string, identity *age.X25519Identity) (*Entry, error)
ReadEntry reads and decrypts an entry from the vault
func (Entry) MarshalJSON ¶
MarshalJSON implements custom JSON marshaling for Entry
func (*Entry) UnmarshalJSON ¶
UnmarshalJSON implements custom JSON unmarshaling for Entry
type EntryMetadata ¶
type EntryMetadata struct {
Created time.Time `json:"created"`
Updated time.Time `json:"updated"`
Version int `json:"version"`
}
EntryMetadata contains metadata about an entry
func GetEntryMetadata ¶ added in v1.1.1
func GetEntryMetadata(vaultDir, path string, identity *age.X25519Identity) (*EntryMetadata, error)
GetEntryMetadata reads only the metadata from an entry without decrypting the full entry. This is useful for cache validation where only freshness information is needed. Returns the metadata and a boolean indicating if the entry exists.
type FindOptions ¶ added in v1.3.0
type FindOptions struct {
// MaxWorkers controls the number of concurrent decryption workers.
// Values <= 0 use sequential search (same as Find).
MaxWorkers int
// ScopeFilter, if non-nil, restricts search to paths that pass the filter.
// Applied before decryption to avoid decrypting out-of-scope entries.
ScopeFilter func(path string) bool
}
FindOptions configures search behavior for FindWithOptions.
type Match ¶
func Find ¶
Find searches vault entries matching a query. Performance: Uses path-only fast path to avoid decrypting entries when possible. If query appears in a path, entry is included without decryption. Only entries where path doesn't match are decrypted to search field content.
func FindConcurrent ¶ added in v1.1.0
FindConcurrent searches vault entries using bounded parallel decryption. It uses a worker pool with maxWorkers concurrent decryption operations. This is optimal for large vaults (10k+ entries) with field searches that don't match paths.
func FindWithOptions ¶ added in v1.3.0
func FindWithOptions(vaultDir string, query string, opts FindOptions) ([]Match, error)
FindWithOptions searches vault entries with configurable options. It supports both sequential and concurrent decryption, and optional scope filtering before decrypt.
type RecipientInfo ¶
type RecipientInfo struct {
RawString string
Normalized string
Error string
LineNumber int
Valid bool
}
RecipientInfo contains information about a recipient entry
type RecipientsManager ¶
type RecipientsManager struct {
// contains filtered or unexported fields
}
RecipientsManager handles the recipients.txt file operations
func NewRecipientsManager ¶
func NewRecipientsManager(vaultDir string) *RecipientsManager
NewRecipientsManager creates a new recipients manager for the given vault directory
func (*RecipientsManager) AddRecipient ¶
func (rm *RecipientsManager) AddRecipient(recipientStr string) error
AddRecipient adds a new recipient to the recipients.txt file. Validates the recipient format before adding. Returns ErrRecipientAlreadyExists if the recipient is already in the file.
func (*RecipientsManager) ListRecipients ¶
func (rm *RecipientsManager) ListRecipients() ([]RecipientInfo, error)
ListRecipients returns a list of all recipients with their line numbers. Useful for displaying to users.
func (*RecipientsManager) LoadRecipientStrings ¶
func (rm *RecipientsManager) LoadRecipientStrings() ([]string, error)
LoadRecipientStrings loads all recipient strings from the file without validation. Used for listing and management operations.
func (*RecipientsManager) LoadRecipients ¶
func (rm *RecipientsManager) LoadRecipients() ([]*age.X25519Recipient, error)
LoadRecipients loads all valid recipients from the recipients.txt file. Lines starting with # are treated as comments and ignored. Empty lines are skipped. Returns the list of recipients and any validation errors encountered.
func (*RecipientsManager) RecipientsFileExists ¶
func (rm *RecipientsManager) RecipientsFileExists() bool
RecipientsFileExists checks if the recipients.txt file exists
func (*RecipientsManager) RecipientsFilePath ¶
func (rm *RecipientsManager) RecipientsFilePath() string
RecipientsFilePath returns the full path to the recipients.txt file
func (*RecipientsManager) RemoveRecipient ¶
func (rm *RecipientsManager) RemoveRecipient(recipientStr string) error
RemoveRecipient removes a recipient from the recipients.txt file. Returns ErrRecipientNotFound if the recipient is not in the file.
type Vault ¶
type Vault struct {
Identity *age.X25519Identity
Config *vaultconfig.Config
Dir string
}
Vault represents an encrypted password vault
func Open ¶
func Open(vaultDir string, identity *age.X25519Identity) (*Vault, error)
Open opens an existing vault at the given directory with the provided identity. It verifies the identity matches the stored encrypted identity.
func OpenWithPassphrase ¶
OpenWithPassphrase opens a vault using a passphrase-protected identity file.
func (*Vault) AutoCommit ¶
AutoCommit performs a git auto-commit with vault configuration
func (*Vault) GetAllRecipientsForEncryption ¶
func (v *Vault) GetAllRecipientsForEncryption() ([]*age.X25519Recipient, error)
GetAllRecipientsForEncryption returns all recipients that should be used for encryption. This includes the vault's own recipient plus all recipients from the recipients.txt file.
func (*Vault) GetRecipient ¶
func (v *Vault) GetRecipient() (*age.X25519Recipient, error)
GetRecipient returns the vault's recipient (public key)
func (*Vault) ValidateIdentity ¶
func (v *Vault) ValidateIdentity(identity *age.X25519Identity) error
ValidateIdentity validates that the provided identity matches the vault