Skip to content

Commit 0a121f2

Browse files
committed
Initial commit: kpcache - tmpfs cache for keepassxc-cli
Shell wrapper that caches the KeePassXC master password in tmpfs so that keepassxc-cli lookups don't prompt on every call. Designed for headless / WSL / server setups where you want to use KeePassXC entries from CLI tools (himalaya, mbsync, mutt, restic, ...) without running a long-lived daemon. Three scripts: - kpunlock: prompt for master password, cache in /dev/shm - kpget: retrieve entry fields via cached password + keepassxc-cli - kplock: clear the cache
0 parents  commit 0a121f2

10 files changed

Lines changed: 456 additions & 0 deletions

File tree

.github/workflows/shellcheck.yml

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
name: ShellCheck
2+
3+
on:
4+
push:
5+
branches: [main]
6+
pull_request:
7+
8+
jobs:
9+
shellcheck:
10+
runs-on: ubuntu-latest
11+
steps:
12+
- uses: actions/checkout@v4
13+
- uses: ludeeus/action-shellcheck@master
14+
with:
15+
scandir: ./bin
16+
severity: warning

.gitignore

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
# Editor / OS
2+
*.swp
3+
*~
4+
.DS_Store
5+
.idea/
6+
.vscode/
7+
8+
# Logs
9+
*.log

LICENSE

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2026 magnattic
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in
13+
all copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21+
THE SOFTWARE.

README.md

Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
1+
# kpcache
2+
3+
Tiny shell wrapper that caches your **KeePassXC master password in tmpfs**
4+
so that `keepassxc-cli` lookups don't prompt for it on every call.
5+
6+
For headless / WSL / server setups where you want to use KeePassXC entries
7+
from CLI tools (himalaya, mbsync, mutt, isync, restic, …) without typing
8+
the master password each time and without running a long-lived daemon.
9+
10+
## Quickstart
11+
12+
```sh
13+
git clone https://github.com/magnattic/kpcache ~/.local/share/kpcache
14+
ln -s ~/.local/share/kpcache/bin/kp{unlock,get,lock} ~/.local/bin/
15+
16+
export KP_DB=~/Passwords.kdbx
17+
kpunlock # type master password once
18+
kpget "Email/personal" # → password, no prompt
19+
kpget "Email/personal" -a User # any keepassxc-cli show option
20+
kplock # clear the cache
21+
```
22+
23+
## Why
24+
25+
`keepassxc-cli` is fast - it's the official C++ binary that uses native
26+
Argon2/AES (via Botan) - but it prompts for the master password on every
27+
invocation. That's unworkable when an MTA polls IMAP every 60 seconds or
28+
when scripts make many lookups.
29+
30+
Existing solutions don't fit headless setups well:
31+
32+
- **kpsh** - Python daemon, but uses pure-Python Argon2/AES. With modern
33+
KeePassXC default KDF settings, the initial unlock can take *minutes*
34+
in pure Python (vs. seconds with the official binary).
35+
- **git-credential-keepassxc** - needs the KeePassXC GUI running and
36+
unlocked, with the browser-extension protocol. Doesn't work headless.
37+
- **keepasxcli-wrapper** - unmaintained since 2021.
38+
39+
`kpcache` is ~80 lines of bash that:
40+
41+
1. Prompts for the master password once.
42+
2. Stores it in `/dev/shm/<uid>-kpcache` with mode `0600`.
43+
3. Each subsequent `kpget` reads from the cache and shells out to
44+
`keepassxc-cli show` - fast because it's the official binary.
45+
46+
The master password lives in RAM only (tmpfs is in-memory). It's gone
47+
after reboot or `kplock`.
48+
49+
## Install
50+
51+
### Dependencies
52+
53+
You need `keepassxc-cli` in `$PATH`.
54+
55+
```sh
56+
# Debian/Ubuntu (newer releases also have keepassxc-minimal - no GUI deps)
57+
sudo apt install keepassxc
58+
59+
# Arch
60+
sudo pacman -S keepassxc
61+
62+
# Fedora
63+
sudo dnf install keepassxc
64+
65+
# macOS - brew installs CLI inside the .app bundle, symlink it into PATH:
66+
brew install --cask keepassxc
67+
sudo ln -s /Applications/KeePassXC.app/Contents/MacOS/keepassxc-cli \
68+
/usr/local/bin/keepassxc-cli
69+
```
70+
71+
### kpcache itself
72+
73+
```sh
74+
git clone https://github.com/magnattic/kpcache ~/.local/share/kpcache
75+
ln -s ~/.local/share/kpcache/bin/kp{unlock,get,lock} ~/.local/bin/
76+
```
77+
78+
Make sure `~/.local/bin` is in your `$PATH`.
79+
80+
## Configure
81+
82+
Set the database path either as an env var or in
83+
`~/.config/kpcache/config`:
84+
85+
```sh
86+
mkdir -p ~/.config/kpcache
87+
cat > ~/.config/kpcache/config <<'EOF'
88+
KPCACHE_DB="/path/to/your.kdbx"
89+
# KPCACHE_KEYFILE="/path/to/keyfile" # if your DB uses one
90+
# KPCACHE_TTL=28800 # optional: expire cache after 8h
91+
EOF
92+
```
93+
94+
## Use
95+
96+
```sh
97+
kpunlock # prompts for master password, caches it
98+
kpget "Email/personal" # prints the password
99+
kpget "Email/personal" -a Username # prints the username
100+
kpget "Servers/prod" -a URL # any keepassxc-cli show -a value
101+
kpget "Email/personal" -s # show all attributes
102+
kplock # clears the cache (manual lock)
103+
```
104+
105+
## Integrate with other tools
106+
107+
### himalaya (CLI mail client)
108+
109+
```toml
110+
# ~/.config/himalaya/config.toml - tested with himalaya v1.2+
111+
backend.auth.cmd = "kpget Email/personal"
112+
```
113+
114+
### mbsync / isync
115+
116+
```
117+
# ~/.mbsyncrc
118+
PassCmd "+kpget 'Email/personal'"
119+
```
120+
121+
The `+` prefix tells mbsync to suppress logging the command.
122+
123+
### mutt
124+
125+
```
126+
set imap_pass = "`kpget Email/personal`"
127+
```
128+
129+
### restic
130+
131+
```sh
132+
export RESTIC_PASSWORD_COMMAND="kpget Backup/restic-repo"
133+
restic snapshots
134+
```
135+
136+
See [`examples/`](examples/) for full configs.
137+
138+
## Security model
139+
140+
**What kpcache protects against:**
141+
142+
- The master password is never passed as a command-line argument, so it
143+
doesn't appear in `ps aux` or shell history.
144+
- The cache file is created atomically with `install -m 600 /dev/null`,
145+
so there's no readable window even before `chmod`.
146+
- `set -euo pipefail` plus an `ERR/INT/TERM` trap ensure the cache is
147+
removed if `kpunlock` is interrupted mid-write.
148+
- Entry paths are passed to `keepassxc-cli` after `--`, so an entry name
149+
starting with `-` cannot be misinterpreted as an option.
150+
151+
**What kpcache does *not* protect against:**
152+
153+
- **Root.** A root user can read any process memory or any file on the
154+
system. Not a goal here.
155+
- **Memory forensics.** The decrypted master password sits in tmpfs RAM.
156+
An attacker with kernel access (or a coredump) can read it.
157+
- **A compromised user account.** Any other process running as your user
158+
can read `/dev/shm/<uid>-kpcache`. The model assumes your local user
159+
is trusted.
160+
- **Hardware tokens (YubiKey challenge-response).** Not currently
161+
supported. Patches welcome.
162+
163+
If your threat model requires per-process secret isolation, hardware
164+
tokens, or memory hardening, use a kernel-keyring solution or the
165+
KeePassXC Secret Service integration with a running GUI session.
166+
167+
## Why not just lower the KDF rounds?
168+
169+
You could lower the Argon2 (or AES-KDF) parameters in the database
170+
settings to make pykeepass-based daemons (kpsh, passhole) usable. But
171+
that weakens the KDF's brute-force resistance against an attacker who
172+
has stolen the `.kdbx` file. Sticking with the official KeePassXC binary
173+
(which has native C Argon2 via Botan) means you keep the strong KDF and
174+
still get fast lookups.
175+
176+
## Alternatives
177+
178+
- [kpsh](https://git.goral.net.pl/keepass-shell.git) - daemon, pure-Python
179+
pykeepass. Good if your DB uses moderate KDF rounds.
180+
- [git-credential-keepassxc](https://github.com/Frederick888/git-credential-keepassxc)
181+
- talks to a running KeePassXC GUI via the browser-extension protocol.
182+
Best if you have a GUI session anyway.
183+
- [pass](https://www.passwordstore.org/) - drop KeePass, use GPG-encrypted
184+
files with `gpg-agent` for caching. Larger migration.
185+
186+
## License
187+
188+
MIT - see [LICENSE](LICENSE).

bin/kpget

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
#!/usr/bin/env bash
2+
# kpget - retrieve a field from a KeePass entry using the cached master password.
3+
#
4+
# Usage:
5+
# kpget <entry-path> # default: print Password attribute
6+
# kpget <entry-path> -a Username # any keepassxc-cli show option
7+
#
8+
# Configuration: see kpunlock(1).
9+
10+
set -euo pipefail
11+
12+
CACHE="${KPCACHE_FILE:-/dev/shm/$(id -u)-kpcache}"
13+
CONFIG="${KPCACHE_CONFIG:-$HOME/.config/kpcache/config}"
14+
15+
if [[ -f "$CONFIG" ]]; then
16+
# shellcheck source=/dev/null
17+
source "$CONFIG"
18+
fi
19+
20+
DB="${KP_DB:-${KPCACHE_DB:-}}"
21+
KEYFILE="${KP_KEYFILE:-${KPCACHE_KEYFILE:-}}"
22+
23+
if [[ -z "$DB" ]]; then
24+
echo "❌ No database configured (set \$KP_DB or KPCACHE_DB in config)." >&2
25+
exit 1
26+
fi
27+
28+
if [[ $# -lt 1 ]]; then
29+
echo "Usage: kpget <entry-path> [keepassxc-cli show options...]" >&2
30+
exit 2
31+
fi
32+
33+
if [[ ! -f "$CACHE" ]]; then
34+
echo "❌ No cached master password - run 'kpunlock' first." >&2
35+
exit 1
36+
fi
37+
38+
# Optional TTL check (portable mtime - GNU stat -c, BSD/macOS stat -f)
39+
if [[ -n "${KPCACHE_TTL:-}" ]]; then
40+
if mtime=$(stat -c %Y "$CACHE" 2>/dev/null); then :
41+
elif mtime=$(stat -f %m "$CACHE" 2>/dev/null); then :
42+
else
43+
echo "❌ Could not read cache mtime - run 'kpunlock' again." >&2
44+
exit 1
45+
fi
46+
age=$(( $(date +%s) - mtime ))
47+
if (( age > KPCACHE_TTL )); then
48+
rm -f "$CACHE"
49+
echo "❌ Cache expired (>${KPCACHE_TTL}s) - run 'kpunlock' again." >&2
50+
exit 1
51+
fi
52+
fi
53+
54+
ENTRY="$1"; shift
55+
56+
# Default to Password attribute when no show options given
57+
if [[ "$#" -eq 0 ]]; then
58+
set -- -s -a Password
59+
fi
60+
61+
extra_args=()
62+
[[ -n "$KEYFILE" ]] && extra_args+=(--key-file "$KEYFILE")
63+
64+
if ! keepassxc-cli show -q "${extra_args[@]}" "$@" "$DB" -- "$ENTRY" < "$CACHE"; then
65+
echo "❌ keepassxc-cli failed - cache may be stale, try 'kpunlock'." >&2
66+
exit 1
67+
fi

bin/kplock

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
#!/usr/bin/env bash
2+
# kplock - clear the cached KeePass master password.
3+
#
4+
# Note: shred has no effect on tmpfs (RAM-backed, no physical blocks),
5+
# so we just unlink - the kernel reclaims the pages.
6+
7+
CONFIG="${KPCACHE_CONFIG:-$HOME/.config/kpcache/config}"
8+
if [[ -f "$CONFIG" ]]; then
9+
# shellcheck source=/dev/null
10+
source "$CONFIG"
11+
fi
12+
13+
CACHE="${KPCACHE_FILE:-/dev/shm/$(id -u)-kpcache}"
14+
15+
if [[ -f "$CACHE" ]]; then
16+
rm -f "$CACHE"
17+
echo "✓ Cache cleared"
18+
else
19+
echo "(no cache)"
20+
fi

0 commit comments

Comments
 (0)