From 309e5e66cb765c5eaf6afde52df30dd718bf7d62 Mon Sep 17 00:00:00 2001 From: Jasper Middendorp Date: Fri, 10 Apr 2026 23:07:57 +0700 Subject: [PATCH] Share 10-min Touch ID context across org operations - 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 --- NoxKey/SocketServer.swift | 40 +++++++++++++++++++++++++++++++-------- NoxKey/noxkey-mcp.mjs | 12 ++++++------ mcp-server/src/index.ts | 12 ++++++------ 3 files changed, 44 insertions(+), 20 deletions(-) diff --git a/NoxKey/SocketServer.swift b/NoxKey/SocketServer.swift index 0b5fed5..b0e2777 100644 --- a/NoxKey/SocketServer.swift +++ b/NoxKey/SocketServer.swift @@ -23,6 +23,35 @@ class SocketServer { private let acceptQueue = DispatchQueue(label: "noxkey.accept", qos: .userInitiated) private let clientQueue = DispatchQueue(label: "noxkey.clients", qos: .userInitiated, attributes: .concurrent) + // MARK: - Shared org management auth context (10-min reuse window) + 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 + } + 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 + } + // MARK: - Known callers tracking (first-seen warning) private static var knownCallersPath: String { guard let home = getpwuid(getuid())?.pointee.pw_dir.flatMap({ String(cString: $0) }) else { @@ -1539,16 +1568,11 @@ class SocketServer { return } - // Use an authenticated context for reading secret values during rename - let context = LAContext() - let policy = KeychainManager.authPolicy - var authError: NSError? - guard context.canEvaluatePolicy(policy, error: &authError) else { - sendResponse(clientSocket, ok: false, error: "Authentication unavailable for secret migration") + // 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 } - // Reuse the already-approved biometric — no second prompt - context.touchIDAuthenticationAllowableReuseDuration = 30 let secrets = keychain.list(context: context) let prefix = fromOrg + "/" diff --git a/NoxKey/noxkey-mcp.mjs b/NoxKey/noxkey-mcp.mjs index 19d6e5d..5418bb1 100644 --- a/NoxKey/noxkey-mcp.mjs +++ b/NoxKey/noxkey-mcp.mjs @@ -27435,9 +27435,9 @@ var server = new McpServer( "- noxkey_delete \u2014 delete a secret (Touch ID required)", "", "- noxkey_org_list \u2014 list all organizations with secret counts", - "- noxkey_org_add \u2014 add an organization (Touch ID required)", - "- noxkey_org_remove \u2014 remove an organization (Touch ID required, secrets are NOT deleted)", - "- noxkey_org_rename \u2014 rename an organization and move all its secrets (Touch ID required)", + "- noxkey_org_add \u2014 add an organization (no Touch ID)", + "- noxkey_org_remove \u2014 remove an organization (no Touch ID, secrets are NOT deleted)", + "- noxkey_org_rename \u2014 rename an organization and move all its secrets (Touch ID once, reused 10 min)", "", "Advanced (use only when needed): noxkey_batch_get, noxkey_authorize, noxkey_meta, noxkey_convert, noxkey_session, noxkey_tokens, noxkey_revoke", "", @@ -27741,7 +27741,7 @@ ${lines.join("\n")}`); ); server.tool( "noxkey_org_add", - "Add an organization. Requires Touch ID. Organizations group secrets under a shared prefix (e.g. 'mycompany/project/KEY').", + "Add an organization. No Touch ID needed. Organizations group secrets under a shared prefix (e.g. 'mycompany/project/KEY').", { org: z2.string().describe("Organization name (e.g. 'mycompany')") }, async ({ org }) => { const trimmed = org.trim().toLowerCase(); @@ -27755,7 +27755,7 @@ server.tool( ); server.tool( "noxkey_org_remove", - "Remove an organization. Requires Touch ID. Secrets using this org prefix are NOT deleted.", + "Remove an organization. No Touch ID needed. Secrets using this org prefix are NOT deleted.", { org: z2.string().describe("Organization name to remove (e.g. 'mycompany')") }, async ({ org }) => { const trimmed = org.trim().toLowerCase(); @@ -27767,7 +27767,7 @@ server.tool( ); server.tool( "noxkey_org_rename", - "Rename an organization. Requires Touch ID. All secrets under the old org prefix are moved to the new one.", + "Rename an organization. Requires Touch ID (once \u2014 reused for 10 minutes). All secrets under the old org prefix are moved to the new one.", { from: z2.string().describe("Current organization name"), to: z2.string().describe("New organization name") diff --git a/mcp-server/src/index.ts b/mcp-server/src/index.ts index 6823cd1..12e8cfe 100644 --- a/mcp-server/src/index.ts +++ b/mcp-server/src/index.ts @@ -223,9 +223,9 @@ const server = new McpServer( "- noxkey_delete — delete a secret (Touch ID required)", "", "- noxkey_org_list — list all organizations with secret counts", - "- noxkey_org_add — add an organization (Touch ID required)", - "- noxkey_org_remove — remove an organization (Touch ID required, secrets are NOT deleted)", - "- noxkey_org_rename — rename an organization and move all its secrets (Touch ID required)", + "- noxkey_org_add — add an organization (no Touch ID)", + "- noxkey_org_remove — remove an organization (no Touch ID, secrets are NOT deleted)", + "- noxkey_org_rename — rename an organization and move all its secrets (Touch ID once, reused 10 min)", "", "Advanced (use only when needed): noxkey_batch_get, noxkey_authorize, noxkey_meta, noxkey_convert, noxkey_session, noxkey_tokens, noxkey_revoke", "", @@ -557,7 +557,7 @@ server.tool( server.tool( "noxkey_org_add", - "Add an organization. Requires Touch ID. Organizations group secrets under a shared prefix (e.g. 'mycompany/project/KEY').", + "Add an organization. No Touch ID needed. Organizations group secrets under a shared prefix (e.g. 'mycompany/project/KEY').", { org: z.string().describe("Organization name (e.g. 'mycompany')") }, async ({ org }) => { const trimmed = org.trim().toLowerCase(); @@ -572,7 +572,7 @@ server.tool( server.tool( "noxkey_org_remove", - "Remove an organization. Requires Touch ID. Secrets using this org prefix are NOT deleted.", + "Remove an organization. No Touch ID needed. Secrets using this org prefix are NOT deleted.", { org: z.string().describe("Organization name to remove (e.g. 'mycompany')") }, async ({ org }) => { const trimmed = org.trim().toLowerCase(); @@ -585,7 +585,7 @@ server.tool( server.tool( "noxkey_org_rename", - "Rename an organization. Requires Touch ID. All secrets under the old org prefix are moved to the new one.", + "Rename an organization. Requires Touch ID (once — reused for 10 minutes). All secrets under the old org prefix are moved to the new one.", { from: z.string().describe("Current organization name"), to: z.string().describe("New organization name"),