-
Notifications
You must be signed in to change notification settings - Fork 0
Share 10-min Touch ID context across org operations #390
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -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 } | ||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+40
to
+41
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||||||||||||||||||||||||||||||||||||||
| 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 } | ||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+42
to
+49
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Add a timeout to the LocalAuthentication wait path. This helper blocks on 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
Suggested change
🧰 Tools🪛 ast-grep (0.42.1)[info] 43-43: The application was observed to leverage biometrics via Local (insecure-biometrics-swift) 🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||
| orgContext = ctx | ||||||||||||||||||||||||||||||||||||||
| orgContextExpiry = Date().addingTimeInterval(600) | ||||||||||||||||||||||||||||||||||||||
| return ctx | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+31
to
+53
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The |
||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| // 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 | ||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+1571
to
1574
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This still won’t deliver the advertised org auth behavior.
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 |
||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
| // Reuse the already-approved biometric — no second prompt | ||||||||||||||||||||||||||||||||||||||
| context.touchIDAuthenticationAllowableReuseDuration = 30 | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| let secrets = keychain.list(context: context) | ||||||||||||||||||||||||||||||||||||||
| let prefix = fromOrg + "/" | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Scope the cached
LAContextto the caller, not the whole server.orgContextis 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 byresolvedPidandresolvedStartTime. Cache this per caller identity instead of process-wide.🤖 Prompt for AI Agents