Documentation
¶
Overview ¶
Package tomb hides the entire password store when it is not in use: no file names, no directory structure, no metadata visible at rest. Three backends implement different trade-offs between security and portability.
Backends ¶
Coffin (the default) packs the store into a single age-encrypted tar archive (store.coffin.age). Open decrypts to a private directory (tmpfs on Linux, 0700 + shred elsewhere). Close re-encrypts and shreds the plaintext. Works everywhere, no root required.
LUKS (Linux only) uses a dm-crypt container managed via the external cryptsetup binary. Plaintext exists only in the kernel dm-crypt mapping, never on disk. Requires root or polkit. Not yet implemented.
Sparsebundle (macOS only) uses an encrypted APFS sparse bundle via hdiutil. Not yet implemented.
Lifecycle ¶
A tomb follows a strict state machine:
Uninitialised ──Init──▶ Closed ──Open──▶ Open ──Close──▶ Closed
▲ │
└─────────┘
(auto-close: timer, screen lock, suspend)
Init creates the encrypted container from the existing store directory. The plaintext is left in place; the user should close or remove it manually.
Open decrypts the container and writes state to a sidecar directory ($XDG_DATA_HOME/binpass/<name>-tomb.state). The state file records the PID, backend, timer, and timestamp so that crash recovery can detect stale state.
Close re-encrypts the current plaintext into the container (atomic tmp+rename), shreds the plaintext directory, and removes the state file.
Auto-close ¶
The Watcher monitors OS events and fires a callback when the tomb should auto-close:
- Timer: close after a configurable duration.
- Screen lock (Linux): D-Bus signals from GNOME/KDE screensaver.
- Suspend (Linux): D-Bus PrepareForSleep from logind.
On non-Linux, only the timer is available. The watcher is started by the CLI's "tomb open" command and runs in background goroutines.
State file ¶
The state file is stored outside the store directory so that sync (git, cloud) never picks it up. It lives in $XDG_DATA_HOME/binpass/ (or $BINPASS_DATA_DIR) as <basename>-tomb.state, keyed by the store directory's base name. The state includes:
- Backend type (coffin, luks, sparsebundle)
- Store directory absolute path
- Process PID (for stale-state detection)
- OpenedAt timestamp
- Timer duration
If the opening process has crashed, IsStale() returns true (by checking whether the PID still exists via syscall.Kill). The next Open call detects this and cleans up the stale state.
Security considerations ¶
Plaintext exposure: the coffin backend writes decrypted files to a directory on disk (tmpfs on Linux, 0700 directory elsewhere). The plaintext is exposed for the duration of the session. Auto-close mitigates but does not eliminate this. The LUKS backend (when implemented) will provide stronger guarantees.
Shred limitations: overwriteRandom writes crypto/rand bytes over each file before removal. On SSDs with wear-leveling, this does not guarantee that the original data is physically overwritten. On tmpfs (Linux), the data lives only in RAM and is destroyed on unmount or reboot.
mlock: on Linux, mlockDir attempts to mmap and mlock files under 64KB in the store directory. This prevents them from being swapped to disk. Failures are silently ignored (mlock is advisory hardening).
Archive integrity: the coffin archive is age-encrypted. Age provides authenticated encryption; tampering with the ciphertext causes decryption to fail. However, a replacement coffin encrypted under the same recipient will decrypt successfully. Trust in the coffin's contents depends on the integrity of the storage medium (local disk, git, cloud sync).
Dotfile preservation: Close preserves dotfiles (.age-recipients, .gpg-id, .git) so that the next Open can find recipients and git can work. This means an attacker with write access to the store directory could plant a dotfile that survives close. In the single-user local threat model, anyone with write access to the directory already has access to the plaintext during the open session.
Package tomb hides the entire password store when it is not in use: no file names, no directory structure, no metadata visible at rest. Three backends implement different trade-offs between security and portability.
Coffin (the default) packs the store into a single age-encrypted archive that works everywhere. LUKS (Linux) uses a dm-crypt container for stronger guarantees. Sparsebundle (macOS) uses an encrypted APFS image.
The auto-close watcher (timer, screen lock, suspend) is critical: an open tomb exposes plaintext, and the value of the feature depends on that window being as short as practical.
Index ¶
- Variables
- func DoctorCheck() string
- func FormatWatcherStatus(timer time.Duration) string
- func HasContainer(dir string) bool
- func RemoveState(storeDir string) error
- type Backend
- type Coffin
- func (c *Coffin) Close(dir string, force bool) error
- func (c *Coffin) Init(dir string, rcp []string, _ int64) error
- func (c *Coffin) Name() Backend
- func (c *Coffin) Open(dir string, timer time.Duration) error
- func (c *Coffin) SetIdentities(fn crypto.IdentityFunc)
- func (c *Coffin) Status(dir string) (State, bool, error)
- type IdentityAware
- type LUKS
- func (l *LUKS) Close(dir string, _ bool) error
- func (l *LUKS) Init(dir string, rcp []string, size int64) error
- func (l *LUKS) Name() Backend
- func (l *LUKS) Open(dir string, timer time.Duration) error
- func (l *LUKS) SetIdentities(fn crypto.IdentityFunc)
- func (l *LUKS) Status(dir string) (State, bool, error)
- type SparseBundle
- type State
- type Tomb
- type Watcher
Constants ¶
This section is empty.
Variables ¶
var ( // ErrNotInitialised reports that no tomb exists for this store. ErrNotInitialised = errors.New("tomb: not initialised") // ErrAlreadyOpen reports that the tomb is already open. ErrAlreadyOpen = errors.New("tomb: already open") // ErrNotOpen reports that the tomb is not open. ErrNotOpen = errors.New("tomb: not open") // ErrUnsupported reports that the backend is not available on this OS. ErrUnsupported = errors.New("tomb: backend not supported on this OS") // ErrNotImplemented reports that the backend is defined but not yet built. ErrNotImplemented = errors.New("tomb: backend not yet implemented") // ErrNeedRoot reports that the backend requires elevated privileges. ErrNeedRoot = errors.New("tomb: this backend requires root or polkit") // ErrShredWarning is appended to close output to warn about SSD limits. ErrShredWarning = errors.New("shred on SSD with wear-leveling does not guarantee data destruction") )
Sentinel errors.
Functions ¶
func DoctorCheck ¶
func DoctorCheck() string
DoctorCheck inspects the D-Bus session for screen lock capability. Returns a human-readable status for binpass doctor.
func FormatWatcherStatus ¶
FormatWatcherStatus returns a human-readable description of the watcher state for binpass tomb status.
func HasContainer ¶
HasContainer reports whether a store has a tomb container at all.
This is what distinguishes "the tomb is open" from "there is no tomb": the state file is absent in both cases, and a close that cannot tell them apart reports success without having protected anything.
func RemoveState ¶
RemoveState deletes the state file for a store. Called on successful close.
Types ¶
type Backend ¶
type Backend string
Backend names the container type.
const ( // BackendCoffin is the cross-platform default: a single age-encrypted tar // archive. Works without root, works on every OS. BackendCoffin Backend = "coffin" // BackendLUKS is a dm-crypt container managed via cryptsetup. Linux only, // requires root or polkit. Maximum protection: plaintext exists only in // the kernel dm-crypt mapping. BackendLUKS Backend = "luks" // BackendSparseBundle is an encrypted APFS sparse bundle via hdiutil. // macOS only. BackendSparseBundle Backend = "sparsebundle" )
Supported backends.
func DefaultBackend ¶
func DefaultBackend() Backend
DefaultBackend returns the best backend for the current OS.
func DetectBackend ¶
DetectBackend reports which backend a store's container belongs to.
The container on disk is the authority, not the state file: `tomb init` leaves no state behind, and a clean close removes it, so after either one the file is the only evidence of which backend was chosen.
type Coffin ¶
type Coffin struct {
// contains filtered or unexported fields
}
Coffin is the cross-platform tomb backend: the entire store is packed into a single age-encrypted tar archive. Open decrypts to a private directory (tmpfs on Linux, 0700 + shred elsewhere). Close re-encrypts and shreds the plaintext.
Security properties:
- At rest: only the encrypted archive is visible. No file names, no directory structure, no metadata leaks.
- Open: plaintext exists on disk (tmpfs on Linux, RAM-backed), protected by directory permissions and auto-close.
- Close: plaintext is shredded (best-effort on SSD), then removed.
Limitations:
- Plaintext is exposed for the duration of the session. Auto-close mitigates but does not eliminate this.
- shred on SSD with wear-leveling does not guarantee data destruction.
func (*Coffin) Close ¶
Close re-encrypts the plaintext store into the coffin, then shreds the plaintext directory. If force is true, skip the overwrite-random pass and just remove files (faster, but no shred guarantee — appropriate during shutdown or when the operator accepts the SSD caveat explicitly).
func (*Coffin) Init ¶
Init creates a coffin archive from the store directory. If the store has content, it is packed and encrypted. The recipients must be valid age recipient strings (age1..., ssh-..., or plugin recipients).
func (*Coffin) Open ¶
Open decrypts the coffin archive into a private directory. On Linux, the target is a tmpfs mount (/dev/shm); elsewhere, it is a 0700 directory that will be shredded on close.
func (*Coffin) SetIdentities ¶
func (c *Coffin) SetIdentities(fn crypto.IdentityFunc)
SetIdentities configures the identity resolver for decryption. Call this before Open if the default resolver is not wired in.
type IdentityAware ¶
type IdentityAware interface {
// SetIdentities configures the resolver used for decryption.
SetIdentities(fn crypto.IdentityFunc)
}
IdentityAware is implemented by backends that decrypt something with the store's age identities: the coffin archive, or the LUKS container key.
Callers wire identities through this rather than by type-asserting each backend, so that a backend added later cannot quietly end up without them.
type LUKS ¶
type LUKS struct {
// contains filtered or unexported fields
}
LUKS manages a dm-crypt container via the external cryptsetup binary. The plaintext exists only in the kernel's dm-crypt mapping and never touches disk, which is the one thing the coffin backend cannot offer.
The container's key file is itself encrypted to the store's age recipients. That is what makes hardware tokens work here without any LUKS-specific support: unlocking the tomb is an ordinary age decryption, and whether the identity lives in a file or on a YubiKey is not this code's concern.
Requires root, or a polkit policy granting the user access to cryptsetup, losetup and mount.
func (*LUKS) Init ¶
Init creates a LUKS container and an age-encrypted key file.
The store's existing entries are moved into the container, so that after Init the plaintext lives inside it rather than beside it.
func (*LUKS) SetIdentities ¶
func (l *LUKS) SetIdentities(fn crypto.IdentityFunc)
SetIdentities configures the identity resolver used to decrypt the key file. Call this before Open.
type SparseBundle ¶
type SparseBundle struct{}
SparseBundle is not available on non-macOS platforms.
func (SparseBundle) Init ¶
func (SparseBundle) Init(string, []string, int64) error
Init is not available.
type State ¶
type State struct {
// Backend is the container type in use.
Backend Backend `json:"backend"`
// StoreDir is the absolute path where the plaintext store is mounted or
// extracted.
StoreDir string `json:"store_dir"`
// CoffinPath is the path to the encrypted archive (coffin backend only).
CoffinPath string `json:"coffin_path,omitempty"`
// MountPoint is the dm-crypt or sparse bundle mount (LUKS/sparsebundle).
MountPoint string `json:"mount_point,omitempty"`
// MapperName is the dm-crypt mapper name (LUKS only).
MapperName string `json:"mapper_name,omitempty"`
// OpenedAt is when the tomb was opened.
OpenedAt time.Time `json:"opened_at"`
// Timer is the auto-close duration; zero means no timer.
Timer time.Duration `json:"timer,omitempty"`
// PID is the process that opened the tomb, for stale-state detection.
PID int `json:"pid"`
}
State records the runtime state of an open tomb. It is persisted to the sidecar directory so that binpass doctor can detect an open container after a crash.
type Tomb ¶
type Tomb interface {
// Init creates an encrypted container for the store at dir. If the store
// already has content, it is packed into the container.
Init(dir string, rcp []string, size int64) error
// Open decrypts the container and makes the plaintext available at dir.
// If timer > 0, the tomb auto-closes after that duration.
Open(dir string, timer time.Duration) error
// Close re-encrypts the plaintext back into the container and removes the
// plaintext. If force is true, skip the dirty check.
Close(dir string, force bool) error
// Status reports the current state of the tomb for this store.
Status(dir string) (State, bool, error)
// Name returns the backend name for diagnostics.
Name() Backend
}
Tomb is the interface each backend implements. Callers should not construct a backend directly; use Open to select the right one.
func SelectBackend ¶
SelectBackend picks the best available backend for the current OS. The caller can override with an explicit choice.
type Watcher ¶
type Watcher struct {
// contains filtered or unexported fields
}
Watcher monitors OS events (screen lock, suspend) and fires a callback when the tomb should be auto-closed. It also manages a timer-based auto-close.
On Linux, it listens to:
- org.freedesktop.login1: PrepareForSleep (suspend/resume)
- org.freedesktop.ScreenSaver: ActiveChanged (screen lock/unlock, KDE)
- org.gnome.ScreenSaver: ActiveChanged (GNOME)
The D-Bus connection is session-bus for screensaver signals and system-bus for login1. If D-Bus is unavailable (headless, container), only the timer is active.
func NewWatcher ¶
NewWatcher starts watching for auto-close triggers. It returns immediately; the actual monitoring runs in background goroutines.