Skip to content

varlock() native local encryption#567

Open
theoephraim wants to merge 27 commits intomainfrom
feature/secure-enclave-plugin
Open

varlock() native local encryption#567
theoephraim wants to merge 27 commits intomainfrom
feature/secure-enclave-plugin

Conversation

@theoephraim
Copy link
Copy Markdown
Member

@theoephraim theoephraim commented Apr 8, 2026

Summary

Adds built-in device-local encryption to varlock via a new varlock() resolver function and native platform binaries. Encrypted secrets can be stored directly in .env files and are automatically decrypted at load time — no external secret manager required.

Encryption backends

The best available backend is selected automatically at runtime:

Platform Backend Key Storage Biometric
macOS Secure Enclave (Swift binary) Hardware Secure Enclave Touch ID / Face ID
Windows DPAPI + Windows Hello (Rust binary) Windows credential store Windows Hello (face/fingerprint/PIN)
Linux Secret Service + optional TPM2 layer (Rust binary) GNOME Keyring / KWallet, sealed with TPM2 when available Opt-in via polkit → PAM (fingerprint/face/YubiKey/password)
All File-based fallback (pure JS) `~/.varlock/` directory No

All backends use the same ECIES wire format (P-256 ECDH + HKDF-SHA256 + AES-256-GCM), so encrypted payloads are portable across implementations.

On macOS, the Secure Enclave provides hardware-backed keys that cannot be extracted, with optional biometric gating via Touch ID. On Windows, DPAPI protects keys scoped to the current user with optional Windows Hello biometric verification. On Linux, private keys live in the user's Secret Service keyring (GNOME Keyring / KWallet) and are additionally sealed to the TPM2 chip when one is available — so the "just works" path covers mainstream desktop distros, with defense-in-depth on hardware that supports it. TPM2-alone and plaintext tiers handle headless and minimal environments. A daemon process manages biometric sessions and IPC on all native platforms.

Linux biometric unlock (opt-in)

A new `setup --linux-biometrics` subcommand installs a polkit policy that gates decrypts behind a PAM auth prompt. PAM picks up whatever factors the user has configured — fingerprint via fprintd, face via Howdy, YubiKey via pam_u2f, or just the login password. The daemon's existing TTY-scoped session cache wraps the polkit check, mirroring the Windows Hello flow (prompt on first decrypt per terminal, cached for 5 minutes).

New CLI commands

  • `varlock encrypt` — Encrypt values interactively or in bulk (`--file .env.local` encrypts all `@sensitive` plaintext values in-place)
  • `varlock reveal` — Securely view/copy decrypted sensitive values (uses alternate screen buffer to avoid scrollback capture, `--copy` for clipboard with auto-clear)
  • `varlock lock` — Invalidate the biometric session, requiring re-authentication for next decrypt

The `varlock()` resolver

Two modes:

  • Decrypt: `varlock("local:")` — decrypts an encrypted payload at load time
  • Prompt: `varlock(prompt)` — prompts for a secret on first load, encrypts it, and writes the encrypted value back to the source file

New packages

  • `@varlock/encryption-binary-swift` — macOS Secure Enclave binary (Swift, universal arm64+x86_64)
  • `@varlock/encryption-binary-rust` — Windows/Linux encryption binary (Rust, cross-compiled for x64/arm64)

Local encryption library (`src/lib/local-encrypt/`)

  • `index.ts` — Public API: `encryptValue`, `decryptValue`, `ensureKey`, `getBackendInfo`, `lockSession`
  • `crypto.ts` — Pure JS ECIES implementation (wire-compatible with native binaries)
  • `file-backend.ts` — File-based key storage fallback
  • `daemon-client.ts` — IPC client for native binary daemons (Unix sockets / named pipes)
  • `binary-resolver.ts` — Platform-specific binary discovery (SEA sibling → npm bundled → dev fallback)
  • `builtin-resolver.ts` — The `varlock()` resolver registration

CI/CD

  • New workflows: `build-native-macos.yaml`, `build-native-rust.yaml`, `notarize-native-macos.yaml`, `binary-release.yaml`
  • Updated `test.yaml` and `release.yaml` to include native binary build steps

Documentation

  • CLI reference: added `encrypt`, `reveal`, `lock` commands
  • Functions reference: added `varlock()` resolver docs
  • Secrets guide: replaced "coming soon" placeholder with full local encryption guide
  • Introduction: updated features list to mention built-in encryption
  • Usage guide: added `encrypt` and `reveal` command overviews

Test plan

  • `bun run test` — unit tests pass (includes `crypto.test.ts` and `file-backend.test.ts`)
  • `varlock encrypt` — interactive single-value encryption works
  • `varlock encrypt --file .env.local` — batch file encryption works
  • `varlock reveal` — interactive picker shows sensitive values in alt screen
  • `varlock reveal KEY --copy` — clipboard copy + auto-clear works
  • `varlock lock` — invalidates biometric session on macOS
  • `varlock(prompt)` — prompts for value and writes back encrypted payload
  • `varlock("local:...")` — decrypts values during `varlock load` / `varlock run`
  • File-based fallback works when no native binary is available
  • macOS Secure Enclave binary builds and signs correctly
  • Rust binary cross-compiles for linux-x64, linux-arm64, windows-x64, windows-arm64
  • Linux: private key stored in Secret Service keyring on GNOME/KDE; TPM2 layer engages when `/dev/tpmrm0` and `tpm2-tools` are available
  • Linux: `sudo varlock-local-encrypt setup --linux-biometrics` installs polkit policy; `decrypt --via-daemon` then triggers a PAM prompt
  • Linux headless (no Secret Service): falls back to TPM2-alone or plaintext with a setup hint
  • Docs site builds without errors

@changeset-bot
Copy link
Copy Markdown

changeset-bot bot commented Apr 8, 2026

🦋 Changeset detected

Latest commit: 4e3649c

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

This PR includes changesets to release 18 packages
Name Type
varlock Minor
@varlock/astro-integration Major
@varlock/cloudflare-integration Major
@varlock/expo-integration Major
@varlock/nextjs-integration Major
@varlock/vite-integration Major
@varlock/1password-plugin Major
@varlock/aws-secrets-plugin Major
@varlock/azure-key-vault-plugin Major
@varlock/bitwarden-plugin Major
@varlock/dashlane-plugin Major
@varlock/google-secret-manager-plugin Major
@varlock/hashicorp-vault-plugin Major
@varlock/infisical-plugin Major
@varlock/keepass-plugin Major
@varlock/pass-plugin Major
@varlock/passbolt-plugin Major
@varlock/proton-pass-plugin Major

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

Comment thread .github/workflows/binary-release.yaml Fixed
Comment thread .github/workflows/build-native-macos.yaml Fixed
Comment thread .github/workflows/notarize-native-macos.yaml Fixed
Comment thread .github/workflows/release-preview.yaml Fixed
Comment thread .github/workflows/release.yaml Fixed
Comment thread .github/workflows/test.yaml Fixed
@theoephraim theoephraim force-pushed the feature/secure-enclave-plugin branch 4 times, most recently from 9fe515d to d89302e Compare April 8, 2026 07:29
@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new bot commented Apr 8, 2026

Open in StackBlitz

varlock

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

@varlock/astro-integration

npm i https://pkg.pr.new/@varlock/astro-integration@567

@varlock/cloudflare-integration

npm i https://pkg.pr.new/@varlock/cloudflare-integration@567

@varlock/expo-integration

npm i https://pkg.pr.new/@varlock/expo-integration@567

@varlock/nextjs-integration

npm i https://pkg.pr.new/@varlock/nextjs-integration@567

@varlock/vite-integration

npm i https://pkg.pr.new/@varlock/vite-integration@567

@varlock/1password-plugin

npm i https://pkg.pr.new/@varlock/1password-plugin@567

@varlock/aws-secrets-plugin

npm i https://pkg.pr.new/@varlock/aws-secrets-plugin@567

@varlock/azure-key-vault-plugin

npm i https://pkg.pr.new/@varlock/azure-key-vault-plugin@567

@varlock/bitwarden-plugin

npm i https://pkg.pr.new/@varlock/bitwarden-plugin@567

@varlock/dashlane-plugin

npm i https://pkg.pr.new/@varlock/dashlane-plugin@567

@varlock/google-secret-manager-plugin

npm i https://pkg.pr.new/@varlock/google-secret-manager-plugin@567

@varlock/hashicorp-vault-plugin

npm i https://pkg.pr.new/@varlock/hashicorp-vault-plugin@567

@varlock/infisical-plugin

npm i https://pkg.pr.new/@varlock/infisical-plugin@567

@varlock/keepass-plugin

npm i https://pkg.pr.new/@varlock/keepass-plugin@567

@varlock/pass-plugin

npm i https://pkg.pr.new/@varlock/pass-plugin@567

@varlock/passbolt-plugin

npm i https://pkg.pr.new/@varlock/passbolt-plugin@567

@varlock/proton-pass-plugin

npm i https://pkg.pr.new/@varlock/proton-pass-plugin@567

commit: 0edf2ae

@theoephraim theoephraim force-pushed the feature/secure-enclave-plugin branch 10 times, most recently from 5fb2d57 to 48f4feb Compare April 8, 2026 23:29
@socket-security
Copy link
Copy Markdown

socket-security bot commented Apr 9, 2026

@theoephraim theoephraim force-pushed the feature/secure-enclave-plugin branch 3 times, most recently from 7de0e68 to 10d20b2 Compare April 9, 2026 06:14
@theoephraim theoephraim force-pushed the feature/secure-enclave-plugin branch from 10d20b2 to 17081fd Compare April 9, 2026 06:33
@cloudflare-workers-and-pages
Copy link
Copy Markdown

cloudflare-workers-and-pages bot commented Apr 9, 2026

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 ae17eaa Commit Preview URL

Branch Preview URL
Apr 14 2026, 04:21 AM

@philmillman
Copy link
Copy Markdown
Member

Testing in Windows:

WSL2:
image
(this is a pretty old image, I will update and try again)

Native windows works (including fingerprint prompt)
image

theoephraim and others added 3 commits April 9, 2026 12:04
gnu-linked binaries require GLIBC_2.39 which isn't available in most
WSL2 distros. musl produces fully static binaries that work everywhere.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
WSL2 support: detect WSL environment and use the Windows .exe binary
for DPAPI key protection and Windows Hello biometric. The .exe handles
its own daemon lifecycle via --via-daemon flag, avoiding TCP complexity.

Size logging: add binary size reporting to Rust and macOS build workflows,
and a release archive size summary to build-binaries.ts.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Each package's vitest config now has a name so the GitHub Actions
test summary shows which package each "Vitest Test Report" belongs to.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
theoephraim and others added 8 commits April 9, 2026 14:15
…ux SEA

- Bundle Windows .exe in Linux SEA archives for WSL2 support
- Add VARLOCK_DEBUG=1 stderr logging through binary resolution + backend detection
- Warn on stderr when falling back to file-based encryption
- Add isFileFallback flag to BackendInfo for CLI display

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Cache resolveNativeBinary() result (was called 6x per invocation)
- Reuse keys from status response in keyExists() to skip key-exists .exe spawn
- Fix daemon spawn polling: simpler pipe_exists check at 50ms intervals
- Increase timeout to 60s for WSL2 decrypt (includes Windows Hello prompt)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…ping

- Restrict named pipe to current user via SECURITY_ATTRIBUTES with DACL
  (prevents other users from connecting to the daemon)
- Pass ciphertext and TTY ID via stdin JSON instead of CLI args
  (prevents exposure in process listings via tasklist/procfs)
- Forward TTY ID to daemon for per-terminal biometric session scoping
  (extracted from /proc/self/fd/0 symlink in WSL2)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Security:
- Verify connecting client process via GetNamedPipeClientProcessId +
  QueryFullProcessImageName — only allow varlock/node/bun binaries
- Add SecureBytes wrapper: VirtualLock/mlock prevents key material from
  being swapped to disk, zeroize-on-drop clears secrets from memory
- Apply SecureBytes to private keys in daemon decrypt and one-shot decrypt

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Fix regex in writeBackEncryptedValue to match any varlock() call, not just
  varlock(prompt), so prompt=1 works when a value already exists
- Use singleton DaemonClient in prompt path to avoid redundant spawn attempts
- Handle cross-process daemon spawn race by retrying connect on spawn failure

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…provements

- Add peer identity verification and process validation for IPC connections (Swift + Rust)
- Strengthen memory protection for key material with zeroize on drop
- Add entitlements file for macOS sandbox/hardened runtime
- Update build scripts for universal binary support
- Fix CI workflow to use build:universal command

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@theoephraim theoephraim force-pushed the feature/secure-enclave-plugin branch from 5677f89 to 7a31401 Compare April 10, 2026 06:55
theoephraim and others added 2 commits April 10, 2026 00:07
…etup

- Use PSECURITY_DESCRIPTOR wrapper instead of raw pointers
- Use MULTIPLE_TRUSTEE_OPERATION instead of TRUSTEE_FORM for MultipleTrusteeOperation
- Replace removed SECURITY_DESCRIPTOR_REVISION and NO_INHERITANCE with literal values

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@philmillman
Copy link
Copy Markdown
Member

latest WSL binary is giving this error on decrypt:

image

theoephraim and others added 9 commits April 10, 2026 08:16
On WSL2, arguments passed to Windows .exe binaries can get mangled
across the interop boundary. Add --data-stdin support for encrypt
(matching decrypt), and use it on WSL2 to pass base64 data via stdin.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Adds a new `keychain()` resolver function that reads secrets from the macOS
Keychain via the Swift daemon binary. Supports IT-managed credentials pushed
via MDM, as well as user-created secure notes.

Syntax:
  keychain("com.company.service")
  keychain(service="com.company.db", account="admin")
  keychain(service="com.company.db", keychain="System")
  keychain(service="com.company.db", field="account")
  keychain(prompt)  — interactive picker with create-new flow

Swift binary changes:
- KeychainManager: search, get, add (secure notes), ACL management
- KeychainPickerDialog: native picker with search, ACL auto-fix, create-new
- KeychainLegacy module: isolates deprecated SecKeychain ACL APIs
- IPC actions: keychain-get (biometric-gated), keychain-search, keychain-pick
- Build script auto-detects APPLE_SIGNING_IDENTITY env var

TypeScript changes:
- keychain-resolver.ts: keychain() resolver with get/prompt modes
- daemon-client.ts: keychainGet, keychainSearch, keychainPick methods
- types.ts: keychain action types and interfaces
- env-graph.ts: register KeychainResolver as built-in

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Moves Linux's default private-key protection from TPM2-or-plaintext to the
Secret Service (GNOME Keyring / KWallet), with TPM2 automatically layered
on top when available for defense-in-depth. Falls back to TPM2-alone on
headless hosts with a TPM, and plaintext as a last resort. This makes
"just works" the common path on desktop Linux, since Secret Service is
available on virtually all mainstream distros while working TPM2 setups
are rare.

Also adds opt-in biometric unlock via polkit. A new `setup --linux-biometrics`
subcommand installs a polkit policy so decrypt prompts go through PAM —
picking up whatever factors the user has configured (fingerprint via
fprintd, face via Howdy, YubiKey via pam_u2f, or password). The daemon's
existing TTY-scoped session cache wraps the polkit check, mirroring the
Windows Hello flow.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…mons

A .exe launched via WSL2 interop runs in WSL's interop session, which
lacks the interactive window-station access UserConsentVerifier needs.
Spawning the daemon directly via CreateProcess inherits that broken
session and the Windows Hello prompt hangs forever — manifesting as a
60s ETIMEDOUT on the TS side.

Route the spawn through `cmd.exe /c start "" /B` so the daemon child
lands in the user's interactive desktop session.

Also clean up any stale daemon before respawning: when spawn_daemon()
runs we already know the pipe was unresponsive, so any process in the
pid file (e.g. a hung WSL2-context daemon) needs to go. Verify the PID
actually points to varlock-local-encrypt.exe via QueryFullProcessImageName
before taskkill, to avoid killing an unrelated process if Windows
recycled the PID.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
With DETACHED_PROCESS on cmd.exe, cmd has no console for `start /B` to
inherit, so start allocates a visible console for the daemon child.
CREATE_NO_WINDOW alone gives cmd a hidden console that /B can use.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…elper

A .exe launched via WSL2 interop runs in WSL's interop session, which
lacks the interactive Windows desktop access UserConsentVerifier needs.
Spawning the daemon from that context produces a hung daemon whose
Hello prompt never renders, manifesting as a 60s ETIMEDOUT.

Detect WSL2 invocation via inherited env vars (WSL_DISTRO_NAME /
WSL_INTEROP) and bail with a clear, copy-pasteable PowerShell command
instead of attempting the doomed spawn. The new `start-daemon`
subcommand spawns the daemon detached and exits, so users can seed a
properly-sessioned daemon from a native Windows terminal in one step.

Also:
- Bump Windows daemon idle timeout from 30 min to 24 h so WSL2 users
  rarely need to re-seed the daemon.
- Call AllowSetForegroundWindow(ASFW_ANY) before the Hello prompt so
  it foregrounds itself instead of just flashing in the taskbar
  (the daemon is windowless, so focus-stealing prevention would
  otherwise hide the prompt).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…sion

Replace the WSL2 "run this PowerShell command yourself" workaround with
an automatic schtasks-based spawn. `schtasks /Run` launches the task in
the user's interactive desktop session, escaping the WSL interop scope
that was preventing the Hello prompt from rendering. Zero user
intervention in the happy path.

Flow:
- Detect WSL2 invocation (env vars).
- On first run with no daemon, idempotently register a never-firing
  scheduled task (/SC ONCE /ST 23:59 /F) that invokes the daemon.
- Trigger it via /Run, poll the pipe.
- On schtasks failure (locked-down corporate machines, etc.) fall back
  to a clear error with the manual `start-daemon` PowerShell command.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Keeps secrets out of shell history by allowing piped input
(e.g. `printf '%s' "$SECRET" | varlock encrypt`). Falls back
to the interactive hidden-input prompt when stdin is a TTY.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
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.

3 participants