Skip to content

Share 10-min Touch ID context across org operations#390

Merged
JasperNoBoxDev merged 1 commit intomainfrom
fix/org-touch-id-reuse
Apr 10, 2026
Merged

Share 10-min Touch ID context across org operations#390
JasperNoBoxDev merged 1 commit intomainfrom
fix/org-touch-id-reuse

Conversation

@JasperNoBoxDev
Copy link
Copy Markdown
Contributor

@JasperNoBoxDev JasperNoBoxDev commented Apr 10, 2026

Summary

  • Add shared LAContext with 600s reuse window for org rename operations — one Touch ID prompt covers all renames within 10 minutes
  • Fix MCP tool descriptions: org_add and org_remove don't touch Keychain, so no Touch ID needed
  • Org config lives in ConfigStore (JSON file), not Keychain — only rename needs auth (to read/write secret values)

Test plan

  • Run multiple org renames in sequence — only one Touch ID prompt
  • Verify org_add works without Touch ID
  • Verify org_remove works without Touch ID
  • After 10 minutes, a new rename should prompt Touch ID again

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Organization rename now requires Touch ID only once, with the authentication reused for 10 minutes within a session.
    • Organization add and remove operations no longer require Touch ID.
  • Documentation

    • Updated help text and descriptions for organization management commands to accurately reflect new authentication requirements.

- Add shared LAContext with 600s reuse for org rename operations
- Single Touch ID prompt covers all org renames within 10 minutes
- Fix MCP tool descriptions: org_add and org_remove don't need Touch ID
- Rebuild MCP bundle

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 10, 2026

📝 Walkthrough

Walkthrough

This PR implements LAContext caching with a 10-minute expiry window in SocketServer.swift to reduce redundant Touch ID evaluations. Corresponding user-facing documentation in the MCP server files is updated to reflect that Touch ID is no longer required for organization add/remove operations, but required once for rename with context reuse.

Changes

Cohort / File(s) Summary
LAContext Caching Implementation
NoxKey/SocketServer.swift
Added shared, mutex-protected orgContext cache with 10-minute expiry and orgAuthContext(reason:) helper function. Updated orgRename to reuse cached context instead of creating fresh LAContext instances per request, avoiding repeated canEvaluatePolicy validation and touchIDAuthenticationAllowableReuseDuration adjustments.
Documentation & Help Text Updates
NoxKey/noxkey-mcp.mjs, mcp-server/src/index.ts
Updated user-facing tool descriptions for noxkey_org_add, noxkey_org_remove, and noxkey_org_rename to reflect new authentication behavior: add/remove now state "No Touch ID needed"; rename now states "Touch ID once — reused for 10 minutes".

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • Add quick access panel, fix Touch ID prompts #2 — Both PRs modify Touch ID and LAContext handling in SocketServer.swift; the related PR updated keychain auth prompts and SocketServer to pass reason strings, while this PR introduces context caching and reuse.

Poem

🐰 A shared context hops through time,
Ten minutes sweet, no prompt each climb,
Touch ID once, then rest with ease,
The cache remembers, if you please!
~Noxkey's speedy friend

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately and concisely describes the main change: introducing a shared 10-minute Touch ID context cache for organization operations, which is the core technical change across all three modified files.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/org-touch-id-reuse

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@NoxKey/SocketServer.swift`:
- Around line 27-35: orgContext is currently a global cached LAContext shared
across all socket clients which lets any same-UID caller reuse another caller's
auth for 10 minutes; change the cache to be per-caller identity (keyed by the
same identity used elsewhere: resolvedPid and resolvedStartTime or a single
combined CallerID) instead of a single orgContext. Replace the
single-orgContext/orgContextExpiry with a dictionary mapping CallerID ->
(LAContext, expiry) protected by orgContextLock, update orgAuthContext(reason:)
to look up and return the caller-specific LAContext (and to store a new context
under that caller key when created), and update any invalidation logic to remove
only that caller’s entry rather than clearing a global context.
- Around line 42-49: The current evaluatePolicy callback blocks indefinitely on
sem.wait(), risking a permanent hang while holding orgContextLock; change the
synchronous wait to a bounded wait using DispatchSemaphore.wait(timeout:) with
the same timeout used by authenticateForMutation and treat a timeout as failure
(return nil). Specifically, replace the unconditional sem.wait() with a timed
wait (DispatchTime.now() + the authenticateForMutation timeout constant/value)
and check the wait result before the existing guard ok else { return nil } so
that if the semaphore times out you fail closed.
- Around line 1571-1574: The auth flow is still prompting too early: update
handleOrg to stop calling authenticateForMutation for the "add" and "remove"
actions and to avoid the outer authenticateForMutation before invoking orgRename
so that orgRename can solely rely on orgAuthContext; specifically, remove or
short-circuit the authenticateForMutation invocation in handleOrg for cases
"add" and "remove" and ensure orgRename continues to call
orgAuthContext(reason:) (and only falls back to sendResponse on nil), so that
add/remove use the inner orgAuthContext behavior and rename uses the cached
orgAuthContext without an extra outer prompt.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: c553aebd-6184-41c5-bcfe-8d9a29c61d44

📥 Commits

Reviewing files that changed from the base of the PR and between 7f91ece and 309e5e6.

📒 Files selected for processing (3)
  • NoxKey/SocketServer.swift
  • NoxKey/noxkey-mcp.mjs
  • mcp-server/src/index.ts

Comment on lines +27 to +35
private var orgContext: LAContext?
private var orgContextExpiry: Date = .distantPast
private let orgContextLock = NSLock()

private func orgAuthContext(reason: String) -> LAContext? {
orgContextLock.lock()
defer { orgContextLock.unlock() }
if let ctx = orgContext, Date() < orgContextExpiry {
return ctx
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Scope the cached LAContext to the caller, not the whole server.

orgContext is shared across every socket client. Once one process authenticates, any other same-UID caller can reuse that context for the next 10 minutes and rename org secrets without its own prompt. That’s a broader trust boundary than the rest of this file, which keys access by resolvedPid and resolvedStartTime. Cache this per caller identity instead of process-wide.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@NoxKey/SocketServer.swift` around lines 27 - 35, orgContext is currently a
global cached LAContext shared across all socket clients which lets any same-UID
caller reuse another caller's auth for 10 minutes; change the cache to be
per-caller identity (keyed by the same identity used elsewhere: resolvedPid and
resolvedStartTime or a single combined CallerID) instead of a single orgContext.
Replace the single-orgContext/orgContextExpiry with a dictionary mapping
CallerID -> (LAContext, expiry) protected by orgContextLock, update
orgAuthContext(reason:) to look up and return the caller-specific LAContext (and
to store a new context under that caller key when created), and update any
invalidation logic to remove only that caller’s entry rather than clearing a
global context.

Comment on lines +42 to +49
var ok = false
let sem = DispatchSemaphore(value: 0)
ctx.evaluatePolicy(policy, localizedReason: reason) { success, _ in
ok = success
sem.signal()
}
sem.wait()
guard ok else { return nil }
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Add a timeout to the LocalAuthentication wait path.

This helper blocks on sem.wait() indefinitely while still holding orgContextLock. If evaluatePolicy never calls back, one hung auth attempt stalls all later org renames. Match authenticateForMutation here and fail closed after a bounded timeout.

Suggested fix
         var ok = false
         let sem = DispatchSemaphore(value: 0)
         ctx.evaluatePolicy(policy, localizedReason: reason) { success, _ in
             ok = success
             sem.signal()
         }
-        sem.wait()
-        guard ok else { return nil }
+        guard sem.wait(timeout: .now() + 60) == .success, ok else { return nil }
         orgContext = ctx
         orgContextExpiry = Date().addingTimeInterval(600)
         return ctx
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
var ok = false
let sem = DispatchSemaphore(value: 0)
ctx.evaluatePolicy(policy, localizedReason: reason) { success, _ in
ok = success
sem.signal()
}
sem.wait()
guard ok else { return nil }
var ok = false
let sem = DispatchSemaphore(value: 0)
ctx.evaluatePolicy(policy, localizedReason: reason) { success, _ in
ok = success
sem.signal()
}
guard sem.wait(timeout: .now() + 60) == .success, ok else { return nil }
orgContext = ctx
orgContextExpiry = Date().addingTimeInterval(600)
return ctx
🧰 Tools
🪛 ast-grep (0.42.1)

[info] 43-43: The application was observed to leverage biometrics via Local
Authentication, which returns a simple boolean result for authentication.
This design is subject to bypass with runtime tampering tools such as
Frida, Substrate, and others. Although this is limited to rooted
(jailbroken) devices, consider implementing biometric authentication the
reliable way - via Keychain Services.
Context: ctx.evaluatePolicy
Note: [CWE-305] Authentication Bypass by Primary Weakness [REFERENCES]
- https://mobile-security.gitbook.io/mobile-security-testing-guide/ios-testing-guide/0x06f-testing-local-authentication
- https://shirazkhan030.medium.com/biometric-authentication-in-ios-6c53c54f17df

(insecure-biometrics-swift)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@NoxKey/SocketServer.swift` around lines 42 - 49, The current evaluatePolicy
callback blocks indefinitely on sem.wait(), risking a permanent hang while
holding orgContextLock; change the synchronous wait to a bounded wait using
DispatchSemaphore.wait(timeout:) with the same timeout used by
authenticateForMutation and treat a timeout as failure (return nil).
Specifically, replace the unconditional sem.wait() with a timed wait
(DispatchTime.now() + the authenticateForMutation timeout constant/value) and
check the wait result before the existing guard ok else { return nil } so that
if the semaphore times out you fail closed.

Comment on lines +1571 to 1574
// Use shared org auth context (10-min reuse window — single Touch ID for batch org operations)
guard let context = orgAuthContext(reason: "Rename organization \"\(fromOrg)\" → \"\(toOrg)\" — \(caller.label)") else {
sendResponse(clientSocket, ok: false, error: "Authentication failed for secret migration")
return
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

This still won’t deliver the advertised org auth behavior.

orgRename now has a cached context, but handleOrg still runs authenticateForMutation for every action except list before this code is reached. So rename still incurs the outer prompt, and add / remove still require Touch ID. To match the new behavior, remove the outer auth gate for add and remove, and let rename rely on orgAuthContext alone.

Suggested fix outside this hunk
     private func handleOrg(_ clientSocket: Int32, _ request: [String: Any], _ caller: CallerInfo) {
         let action = request["action"] as? String ?? "list"

-        // "list" needs no auth — everything else gets one Touch ID prompt
-        if action != "list" {
+        // Only actions that mutate Keychain-backed secrets need auth here.
+        // `rename` performs its own cached auth via `orgAuthContext`.
+        if action == "some_future_mutation_requiring_global_auth" {
             guard authenticateForMutation(reason: "Manage organizations — \(caller.label)") else {
                 sendResponse(clientSocket, ok: false, error: "Authentication canceled")
                 return
             }
         }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@NoxKey/SocketServer.swift` around lines 1571 - 1574, The auth flow is still
prompting too early: update handleOrg to stop calling authenticateForMutation
for the "add" and "remove" actions and to avoid the outer
authenticateForMutation before invoking orgRename so that orgRename can solely
rely on orgAuthContext; specifically, remove or short-circuit the
authenticateForMutation invocation in handleOrg for cases "add" and "remove" and
ensure orgRename continues to call orgAuthContext(reason:) (and only falls back
to sendResponse on nil), so that add/remove use the inner orgAuthContext
behavior and rename uses the cached orgAuthContext without an extra outer
prompt.

Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request introduces a shared authentication context in SocketServer.swift to allow Touch ID reuse for organization management within a 10-minute window. It also updates the MCP server documentation and tool descriptions to reflect that certain organization operations no longer require immediate biometric authentication. Feedback suggests logging specific authentication errors for better diagnostics and addresses a potential thread starvation issue caused by synchronous blocking during the authentication process.

Comment on lines +40 to +41
var authError: NSError?
guard ctx.canEvaluatePolicy(policy, error: &authError) else { return nil }
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

It would be beneficial to log the authError if canEvaluatePolicy fails. This information is crucial for diagnosing why biometric authentication might be unavailable (e.g., biometry locked out, not enrolled, or hardware issues), which is more helpful than a generic failure message.

Comment on lines +31 to +53
private func orgAuthContext(reason: String) -> LAContext? {
orgContextLock.lock()
defer { orgContextLock.unlock() }
if let ctx = orgContext, Date() < orgContextExpiry {
return ctx
}
let ctx = LAContext()
ctx.touchIDAuthenticationAllowableReuseDuration = 600 // 10 minutes
let policy = KeychainManager.authPolicy
var authError: NSError?
guard ctx.canEvaluatePolicy(policy, error: &authError) else { return nil }
var ok = false
let sem = DispatchSemaphore(value: 0)
ctx.evaluatePolicy(policy, localizedReason: reason) { success, _ in
ok = success
sem.signal()
}
sem.wait()
guard ok else { return nil }
orgContext = ctx
orgContextExpiry = Date().addingTimeInterval(600)
return ctx
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

The orgAuthContext method uses a synchronous wait (sem.wait()) while holding an NSLock. While this correctly prevents multiple simultaneous Touch ID prompts, it blocks the calling thread (likely from clientQueue) for the entire duration of the user interaction. In a server environment, this can lead to thread starvation if multiple requests are pending. Consider using an asynchronous approach or ensuring that the thread pool is sufficiently sized to handle these long-running blocking operations.

@JasperNoBoxDev JasperNoBoxDev merged commit c1ec72b into main Apr 10, 2026
1 check passed
@JasperNoBoxDev JasperNoBoxDev deleted the fix/org-touch-id-reuse branch April 10, 2026 16:37
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.

1 participant