The touchstore CLI safely stores your secrets, and then unlocks them with Touch ID. Every fetch requires your biometric OK.
Values are encrypted and decrypted within your Mac's Secure Enclave, and the resulting ciphertext stored in your Keychain.
Useful when you want a clear permission gate, and the convenience of biometric touch-to-confirm. A prime example being scripts you want agents to use, but in a controlled way, and without secrets lying about in the filesystem.
$ touchstore put github-token # no-echo prompt, or value piped on stdin
$ touchstore get github-token... # Touch ID prompt, value(s) on stdout
$ touchstore list
$ touchstore delete github-token... # Touch ID prompt
$ touchstore export | age -p > backup.age # whole store, one Touch ID prompt
$ age -d backup.age | touchstore import # restore (e.g. after an OS reinstall)
$ brew install jeremywohl/tap/touchstoreOr build from source — see Development.
- macOS 13+ on a Mac with a Secure Enclave (Apple Silicon, or Intel with T2)
- Touch ID enrolled
| Command | Touch ID | Notes |
|---|---|---|
put <label> [-v value] [--force] |
on overwrite | stdin, a no-echo prompt, or value from -v; upserts |
get <label>... [--no-newline] [-0] |
once for all | values to stdout, newline-separated |
delete <label>... |
once for all | |
list |
no | labels only, one per line |
exists <label> |
no | see exit codes |
export |
once for all | JSON to stdout, pipe to an encryptor |
import [--overwrite] [--force] |
on overwrite | reads from stdin |
Exit codes: 0 success, 1 not found, 2 authentication canceled, 3 any other error.
Labels are letters, digits, ., _, and -, starting with a letter or digit (enforced on put).
Values are arbitrary bytes, and we read them exactly. get adds newlines, by default.
Use --print0/-0 or --no-newline to adjust this.
By design, the Secure Enclave has no exports or backups. The master key cannot survive an erase-and-reinstall, machine death, or the move to a new Mac.
But you can export and import touchstore values, piping through the encryptor of your choice.
$ touchstore export | age -p > backup.age # passphrase-protect with age
$ age -d backup.age | touchstore import # later: fresh master key
Note: export refuses to spam a bare terminal, for modest safety,
but the protection of those values is up to you. Be careful!
Each value is encrypted with ECIES, using P-256 ECDH, HKDF-SHA256 and AES-256-GCM.
The label and format version are bound into the seal as associated data.
The ciphertext is stored as a keychain generic password. The master key lives in the Secure
Enclave; an opaque Enclave reference handle is persisted as a keychain item under its own
service (com.jeremywohl.touchstore.master).
What Touch ID protects:
- Decryption.
get,export. - Destructive writes.
delete,putover an existing label, orimport --overwrite.
Note: Encrypting a new label needs only the public key, so first-time put doesn't prompt.
What it does not protect:
- Label names.
listandexistsare unauthenticated by design; any process in your session can enumerate labels (though not values). - The gate itself. A compromise of keychain items or the enclave handle will break touchstore, but the values remain confidential, if entombed.
- Command-line visibility.
put -v secretexposes the value to shell history andps. Use the interactive prompt or a pipe by default. - Caller identity. Prompts name the calling script or program (e.g.
Unlock "github-token" for deploy.sh), taken from the parent process's argv. This is a courtesy label for spotting unexpected prompts, not provenance, as any caller can name itself anything.
The master key uses .biometryAny: fingerprints enrolled after setup can
also unlock. The stricter alternative, .biometryCurrentSet, invalidates the
key whenever fingerprint enrollment changes, which one might consider if
compiling locally.
[0x03 version][65-byte X9.63 ephemeral public key][AES-GCM nonce‖ciphertext‖tag],
with version‖label as AES-GCM associated data. Older formats are no longer readable.
swift test # crypto layer tests (software keys, no enclave needed)
make # release build + sign
make install # builds, signs, installs to ~/.bin
make install PREFIX=~/bin # or wherever you like
By default, builds are ad-hoc, which produces a new signature on every build, along with challenges every build. To make something stable, use an Apple identity.
Set IDENTITY to a certificate from security find-identity -v -p codesigning
(a free Apple Development certificate is fine):
IDENTITY="Apple Development: Your Name (TEAMID1234)" make install
To make that stick, put it in local/local.mk — the local/ directory is
gitignored, so machine-local settings (IDENTITY, PREFIX, …) can live there.
# local/local.mk
IDENTITY = Apple Development: Your Name (TEAMID1234)(You probably won't be doing this.)
Release builds are universal (arm64 + x86_64), signed with a Developer ID certificate under the hardened runtime, and notarized:
make release # universal build, hardened-runtime signature
make notarize # submit to Apple's notary service and wait
make dist # notarize + produce releases/touchstore-vX.Y.Z.tar.gz
Notarization needs stored credentials (app-specific password or App Store Connect API key):
xcrun notarytool store-credentials touchstore-notary
The profile name can be overridden with NOTARY_PROFILE in local/local.mk.
Bare executables can't be stapled, so Gatekeeper fetches the notarization
ticket online — nothing extra to ship.
MIT — see LICENSE.