Skip to content

fix(core): validate secret.key, stop first-run overwrite race - #1

Open
Dextheking1 wants to merge 2 commits into
localhost8083:mars-desktopfrom
Dextheking1:fix/secret-key-file-validation
Open

Dextheking1 wants to merge 2 commits into
localhost8083:mars-desktopfrom
Dextheking1:fix/secret-key-file-validation

Conversation

@Dextheking1

@Dextheking1 Dextheking1 commented Oct 23, 2026 •

Copy link
Copy Markdown

Issue for this PR

No linked issue.

Type of change

  • Bug fix
  • New feature
  • Refactor / code improvement
  • Documentation

What does this PR do?

secret.ts derives the credential encryption key by hashing whatever is in secret.key, with no check on the content.

Two problems:

  • An empty or half-written key file hashes a known string. sha256 of "" is a public constant, so credentials stored after a truncated write are encrypted under a key anyone can compute from this source.
  • First run creates the file with a plain write. Two processes starting together can each generate a key and overwrite the other one. Credentials written under the losing key stop decrypting.

Fix:

  • Reject a key file that is not exactly 64 hex chars. Fail closed with a clear error instead of hashing garbage.
  • Create the file with flag "wx" and fall back to the existing file on EEXIST, so the loser of the race uses the winner's key.

Valid existing key files are 64 hex chars and pass unchanged. Legacy plaintext values still pass through decrypt as before.

How did you verify your code works?

Verified locally with the file logic isolated: encrypt/decrypt roundtrip ok, key file stays 0600 and stable across calls, empty key file rejected, partial key file rejected. packages/core/test/util/secret.test.ts adds coverage for concurrent key creation, atomic key visibility, malformed key rejection, and leftover temporary files.

Screenshots / recordings

Not a UI change.

Checklist

  • I have tested my changes locally
  • I have not included unrelated changes in this PR

Summary by Sourcery

Harden credential key initialization and validation to fail closed and remain safe during concurrent first runs.

Bug Fixes:

  • Reject malformed credential key files instead of deriving encryption keys from invalid or incomplete contents.
  • Prevent concurrent first-run initialization from overwriting the credential key and stranding encrypted credentials.

Enhancements:

  • Publish newly generated key files atomically while preserving secure permissions and ignoring leftover temporary files.

Tests:

  • Add coverage for concurrent key creation, atomic key visibility, malformed key rejection, and leftover temporary files.

@Dextheking1
Dextheking1 marked this pull request as ready for review October 23, 2026 03:16
@github-actions

github-actions Bot commented Oct 23, 2026 •

Copy link
Copy Markdown

This PR doesn't fully meet our contributing guidelines and PR template.

What needs to be fixed:

  • No issue referenced. Please add Closes #<number> linking to the relevant issue.

Please edit this PR description to address the above within 72 hours, or it will be automatically closed.

If you believe this was flagged incorrectly, please let a maintainer know.

@sourcery-ai

sourcery-ai Bot commented Oct 23, 2026 •

Copy link
Copy Markdown
Reviewer's guide (collapsed on small PRs)

Reviewer's Guide

Harden credential key initialization by rejecting malformed key files before key derivation and using exclusive creation so concurrent first runs converge on one stable key instead of overwriting credentials.

Sequence diagram for safe credential key initialization

sequenceDiagram
    participant ProcessA
    participant ProcessB
    participant KeyFile

    par First process
        ProcessA->>KeyFile: writeFileSync(flag: wx)
        KeyFile-->>ProcessA: key created
    and Concurrent process
        ProcessB->>KeyFile: writeFileSync(flag: wx)
        KeyFile-->>ProcessB: EEXIST
        ProcessB->>KeyFile: readFileSync
    end

    ProcessA->>KeyFile: readFileSync
    KeyFile-->>ProcessA: 64 hex characters
    KeyFile-->>ProcessB: winner's key
    ProcessA->>ProcessA: createHash(sha256)
    ProcessB->>ProcessB: createHash(sha256)
Loading

Flow diagram for credential key validation

flowchart TD
    A[Read secret.key] --> B{Exactly 64 hex characters?}
    B -->|Yes| C[createHash sha256]
    B -->|No| D[Throw Invalid credential key file]
    C --> E[Use derived credential key]
Loading

File-Level Changes

Change Details Files
Validate credential key material before deriving the encryption key.
  • Read and trim the key file, requiring exactly 64 hexadecimal characters.
  • Throw a clear error for empty, partial, or malformed key files instead of hashing invalid content.
  • Preserve the existing SHA-256 derivation and legacy decryption behavior for valid keys.
packages/core/src/util/secret.ts
Make first-run key creation safe under concurrent initialization.
  • Create the key file exclusively with the wx flag to prevent overwrites.
  • Treat EEXIST as a race loss and read the already-created winner's key.
  • Retain restrictive directory and file permissions.
packages/core/src/util/secret.ts

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@github-actions

Copy link
Copy Markdown

Thanks for your contribution!

This PR doesn't have a linked issue. All PRs must reference an existing issue.

Please:

  1. Open an issue describing the bug/feature (if one doesn't exist)
  2. Add Fixes #<number> or Closes #<number> to this PR description

See CONTRIBUTING.md for details.

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Hey - I've found 1 issue

Prompt for AI Agents
Please address the comments from this code review:

## Individual Comments

### Comment 1
<location path="packages/core/src/util/secret.ts" line_range="27" />
<code_context>
+      // "wx" fails instead of overwriting when a concurrent first run won the race.
+      // The loser has to encrypt with the winner's key; replacing it would strand
+      // every credential the winner already stored.
+      writeFileSync(keyFile, randomBytes(32).toString("hex"), { mode: 0o600, flag: "wx" })
+    } catch (error) {
+      if ((error as NodeJS.ErrnoException).code !== "EEXIST") throw error
</code_context>
<issue_to_address>
**Credential operations fail on first run**

When a second process reads `secret.key` after `wx` creates it but before the winning write completes, or the writer stops mid-write, `key()` catches `EEXIST` and immediately calls `readKey`; validation rejects the empty or partial file, so credential operations fail. A file left incomplete keeps later operations failing.

Write the key completely to a separate file, then publish it atomically without replacing an existing key.

Also at `packages/core/src/util/secret.ts:28-31`, `packages/core/src/util/secret.ts:33`.
</issue_to_address>

Sourcery assessment

Needs a human reviewer. 1 finding to address first, and if key-file validation or race handling is wrong, credential encryption or decryption can fail or use unintended key material, potentially leaving stored credentials inaccessible. Reverting restores the previous behavior, but credentials already encrypted under a different key cannot be recovered by the revert.

Blocking findings: packages/core/src/util/secret.ts:27


Sourcery is free for open source - if you like our reviews please consider sharing them ✨

Comment thread packages/core/src/util/secret.ts Outdated
An empty or partial secret.key was hashed as-is, so a truncated
write derived the credential key from a known string. Reject any
key file that is not 64 hex chars.

Create the file with flag wx so two first runs cannot overwrite
each other's key and strand stored credentials.
the wx create from the last commit still left a window: secret.key
existed before the write finished, so a concurrent first run could
read an empty file and fail validation, and a crash mid-write left a
broken key file behind for every later run.

write the key to a temp file in the same dir, fsync it, then link it
into place. link fails with EEXIST instead of replacing, so the real
path only ever holds a finished key and the loser of the race reads
the winner's key.

tests: concurrent first runs in child processes agree on one key, a
watcher polling the real path during creation never sees a partial
key, a damaged key file still fails closed, and a leftover temp file
does not break creation.
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