Skip to content

Comments

fix: resolve TOCTOU race in vault encryption key auto-generation#405

Merged
stack72 merged 1 commit intomainfrom
fix/vault-toctou-race-391
Feb 19, 2026
Merged

fix: resolve TOCTOU race in vault encryption key auto-generation#405
stack72 merged 1 commit intomainfrom
fix/vault-toctou-race-391

Conversation

@stack72
Copy link
Contributor

@stack72 stack72 commented Feb 19, 2026

Closes #391

Summary

loadKeyMaterial() in LocalEncryptionVaultProvider had a time-of-check-time-of-use (TOCTOU) race condition. Two concurrent processes could both fail the readTextFile() check, generate different encryption keys, and the second atomicWriteTextFile() (which uses rename internally) would silently overwrite the first. Secrets encrypted by the losing process would become permanently unrecoverable.

Fix

Replace the read-then-write pattern with exclusive file creation:

  1. Deno.writeTextFile(keyFile, generatedKey, { createNew: true, mode: 0o600 })createNew: true maps to O_CREAT | O_EXCL at the OS level, guaranteeing exactly one process wins the creation. No application-level lock needed.

  2. Catch Deno.errors.AlreadyExists on the write — the losing process reads back the winner's key instead of overwriting it.

  3. Catch Deno.errors.NotFound specifically (not bare catch) on the initial read — real errors like permission denied now propagate instead of being silently swallowed.

Plan vs Implementation

The implementation matches the plan exactly:

Aspect Plan Implementation Match
Replace atomicWriteTextFile with Deno.writeTextFile + createNew: true Exact
Catch Deno.errors.NotFound specifically on initial read Exact
Catch Deno.errors.AlreadyExists on write Exact
Read back winner's key on AlreadyExists Exact
Call ensureVaultDirectory() before write Exact
Concurrency test: two vault instances, same dir Exact
Concurrency test: Promise.all() on put() Exact
Concurrency test: cross-read verification Exact
Concurrency test: single .key file assertion Exact

No deviations from the plan. The only additions are the crypto.subtle.importKey calls in each branch, which the plan's pseudocode intentionally omitted for brevity.

Test caveat

The concurrency test runs two vault instances in the same Deno process. Async interleaving at await points can exercise the race, but doesn't guarantee it every run. The real safety comes from the OS-level O_EXCL guarantee, which holds regardless of test coverage.

User impact

  • Existing users: Zero — if a .key file already exists, the code reads it on the first try and never hits the changed path
  • New users: No observable difference in behavior
  • The fix: Prevents silent, permanent data loss when two processes initialize the same vault concurrently

Test plan

  • All existing tests pass (29 steps)
  • New "should handle concurrent key generation safely" test passes
  • deno check — type checking clean
  • deno lint — no lint errors
  • deno fmt — formatting clean

Replace the try/catch read-then-write pattern in loadKeyMaterial() with
exclusive file creation using Deno.writeTextFile with createNew: true.
This uses O_CREAT | O_EXCL at the OS level, guaranteeing only one
process can create the key file. The race loser catches AlreadyExists
and reads the winner's key instead of overwriting it.

Also narrows the initial read catch from bare catch to
Deno.errors.NotFound so real errors (permission denied, etc.) propagate
instead of being silently swallowed.

Co-Authored-By: Magistr <2269529+umag@users.noreply.github.com>
Copy link

@github-actions github-actions bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review: Approved ✅

This PR correctly fixes a TOCTOU race condition in vault encryption key auto-generation. The fix is well-designed and follows best practices.

Code Quality

  • TypeScript strict mode: ✅ No any types, proper typed error handling
  • Code style (CLAUDE.md): ✅ Named exports, test file next to source, license headers present
  • Security: ✅ Correctly uses createNew: true (maps to O_CREAT | O_EXCL) for atomic exclusive creation
  • Test coverage: ✅ New concurrency test verifies both vaults share the same key and only one .key file exists
  • DDD compliance: ✅ Changes are appropriately scoped to infrastructure layer

Implementation Details

The fix correctly:

  1. Catches Deno.errors.NotFound specifically (not bare catch) so permission errors propagate
  2. Uses createNew: true for atomic exclusive file creation
  3. Catches Deno.errors.AlreadyExists and reads back the winner's key on race loss
  4. Maintains file permissions at 0o600

No Blocking Issues

The implementation matches the plan and addresses a real security concern where concurrent processes could generate different keys, causing secrets to become permanently unrecoverable.

The PR author correctly notes in the description that the concurrency test exercises the race through async interleaving, but the real safety comes from the OS-level O_EXCL guarantee - which is the correct observation.

@stack72 stack72 merged commit 772ba80 into main Feb 19, 2026
4 checks passed
@stack72 stack72 deleted the fix/vault-toctou-race-391 branch February 19, 2026 21:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

TOCTOU race in vault encryption key auto-generation causes silent secret loss

1 participant