vault

package
v1.1.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Apr 24, 2026 License: MIT Imports: 18 Imported by: 0

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:

  1. 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.

  2. 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.

  3. Cryptographic Operations: Age decryption involves CPU-intensive operations (X25519 key exchange, ChaCha20-Poly1305). Too many concurrent operations can cause CPU cache thrashing.

  4. 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

Constants

This section is empty.

Variables

View Source
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

View Source
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 CollectFieldMatches(matches map[string]struct{}, prefix string, value any, needle string)

func DeleteEntry

func DeleteEntry(vaultDir, path string) error

DeleteEntry removes an entry from the vault

func EnsureDir

func EnsureDir(v *Vault, path string) error

EnsureDir ensures the directory for an entry exists

func EntryPath

func EntryPath(v *Vault, path string) string

EntryPath returns the full file path for a vault entry

func ExtractTOTP added in v1.1.1

func ExtractTOTP(data map[string]any) (secret, algorithm string, digits, period int, hasTOTP bool)

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

func IsInitialized(vaultDir string) bool

IsInitialized checks if a vault is initialized at the given directory

func List

func List(vaultDir string, prefix string) ([]string, error)

func ListFast added in v1.1.1

func ListFast(vaultDir string, prefix string) ([]string, error)

ListFast returns all entry paths in the vault, optionally filtered by prefix. It uses os.ReadDir for efficient directory traversal without stat calls.

func SafeMkdirAll added in v1.1.0

func SafeMkdirAll(path string, perm os.FileMode) error

func SafeRemove added in v1.1.0

func SafeRemove(path string) error

func SafeWriteFile added in v1.1.0

func SafeWriteFile(path string, data []byte, perm os.FileMode) error

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) GetField

func (e *Entry) GetField(name string) (any, bool)

GetField retrieves a field value from the entry's data map.

func (*Entry) HasField

func (e *Entry) HasField(name string) bool

HasField checks if a field exists in the entry

func (Entry) MarshalJSON

func (e Entry) MarshalJSON() ([]byte, error)

MarshalJSON implements custom JSON marshaling for Entry

func (*Entry) SetField

func (e *Entry) SetField(name string, value any)

SetField sets a field value in the entry's data map

func (*Entry) UnmarshalJSON

func (e *Entry) UnmarshalJSON(data []byte) error

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 Match

type Match struct {
	Path   string
	Fields []string
}

func Find

func Find(vaultDir string, query string) ([]Match, error)

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

func FindConcurrent(vaultDir string, query string, maxWorkers int) ([]Match, error)

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.

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

func OpenWithPassphrase(vaultDir string, passphrase string) (*Vault, error)

OpenWithPassphrase opens a vault using a passphrase-protected identity file.

func (*Vault) AutoCommit

func (v *Vault) AutoCommit(message string) error

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

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL