Skip to content

feat(local-encrypt): multiple keys with per-key biometric opt-in/out#867

Open
theoephraim wants to merge 1 commit into
mainfrom
local-encrypt-multikey
Open

feat(local-encrypt): multiple keys with per-key biometric opt-in/out#867
theoephraim wants to merge 1 commit into
mainfrom
local-encrypt-multikey

Conversation

@theoephraim

Copy link
Copy Markdown
Member

What & why

Follows up the local-encryption threat-model work (#852) and Windows TPM support (#854): makes device-local encryption support multiple keys, each with its own presence-gate behavior, and lets a project or an individual value choose which key to use.

Previously keyId existed at the crypto/storage layer but wasn't usable end-to-end — the varlock("local:...") reference didn't encode a key, and the resolver always decrypted with varlock-default (so encrypt --key-id was hidden). This wires it up and adds the per-key biometric opt-in/out we'd discussed.

What you can do now

varlock keys                       # list keys + their presence settings
varlock keys create deploy         # prompts for presence on decrypt (where a gate exists)
varlock keys create ci --no-auth   # always decrypts unattended (servers / CI)
# @defaultLocalKey=ci                       # project-wide default key
# ---
DATABASE_URL=varlock("local:...")           # uses the project default (bare ref)
DEPLOY_TOKEN=varlock("local:...", key="deploy")  # explicit per-value override

Design

  • Presence intent lives on the key, not the machine or the reference. A key records requireAuth at creation; decrypt prompts when intent(key) AND a gate exists on the machine. This is the durable half of the earlier opt-in/out discussion — availability stays a per-machine property, so no staleness trap.
  • Enforcement strength differs by platform (documented, not hidden): macOS is hardware-enforced via the Secure Enclave ACL (immutable at creation); Windows/Linux are consent-grade — varlock's own decrypt flow honors the flag, but the TPM seal isn't presence-bound. Same trade-off as the local-encrypt: document unattended/headless threat model #852 threat model.
  • Selection cascade: built-in varlock-default@defaultLocalKey (project) → key= (per value). Single-key projects keep bare references; only a non-default key writes a key= arg.
  • The default key also encrypts the disk cache (CacheStore was already keyed by keyId) — per-project cache file, and the cache inherits the key's presence behavior.

Implementation

  • Rust binary: StoredKey.require_auth (back-compat: missing → prompts), --no-auth on generate-key, keyDetails in status/list-keys, daemon skips the gate for no-auth keys without warming the session for auth-required keys.
  • Swift binary: SE access-control flags can't be read back, so a small {keyId}.meta.json sidecar records requireAuth; daemon skips the LAContext prompt for no-auth keys; keyDetails in status/list-keys.
  • TS: per-key decrypt routing, generateKey({ requireAuth }), listKeyDetails(), key-id validation; key= arg + @defaultLocalKey in the resolver; CacheStore threading; new varlock keys command; encrypt --key-id unhidden.

Tests

  • Rust: serde back-compat (legacy key file → prompts) + round-trip.
  • Vitest: key= round-trip, wrong-key failure, invalid key id, missing-key device-bound hint, @defaultLocalKey validation, key metadata listing (950 unit tests pass).
  • Manual end-to-end via the built CLI (file backend + real Rust binary): project-default and explicit-key values both encrypt and decrypt correctly.

Notes / follow-ups

  • The per-machine "gate was configured but is now gone" fail-open warning (the other local-encrypt: document unattended/headless threat model #852 follow-up) is still separate and not included here.
  • Key-id charset is restricted (they become filenames). Keys remain device-bound; a reference naming a key the device lacks fails with an actionable hint.

Adds support for multiple device-local encryption keys, each with its own
presence-gate behavior, and lets projects/values choose which key to use.

- `varlock keys` (list/create) — create named keys; `--no-auth` opts a key
  out of presence verification for unattended use (servers/CI)
- Per-key `requireAuth` recorded in key metadata across all backends
  (Rust StoredKey field, Swift SE sidecar since the ACL can't be read back);
  legacy keys normalize to "requires auth"
- decrypt routing honors the per-key flag: a no-auth key takes the one-shot
  path and never warms the biometric session for auth-required keys
- `@defaultLocalKey` root decorator sets the project default key; per-value
  override via `varlock("local:...", key="...")`; `varlock encrypt --key-id`
  emits the key= arg only when it differs from the default
- the default key also encrypts the disk cache (per-project cache file +
  inherited presence behavior)
- device-bound decrypt errors now hint when a referenced key is missing

Docs: local-encryption guide "Choosing a key", @defaultLocalKey reference,
`varlock keys` + `encrypt --key-id` in the CLI reference.
@github-actions

github-actions Bot commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

bumpy-frog

The changes in this PR will be included in the next version bump.

minor Minor releases

  • varlock 1.10.0 → 1.11.0

Bump files in this PR

Click here if you want to add another bump file to this PR


This comment is maintained by bumpy.

@cloudflare-workers-and-pages

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Preview URL Updated (UTC)
✅ Deployment successful!
View logs
varlock-website bcf1393 Commit Preview URL

Branch Preview URL
Jul 07 2026, 06:43 PM

@pkg-pr-new

pkg-pr-new Bot commented Jul 7, 2026

Copy link
Copy Markdown

Open in StackBlitz

npm i https://pkg.pr.new/varlock@867

commit: bcf1393

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant