Skip to content

iOS: security hardening — Keychain migration, concurrency fixes, attack surface reduction#31979

Closed
Rocuts wants to merge 9 commits intoopenclaw:mainfrom
Rocuts:ios/security-hardening-2026
Closed

iOS: security hardening — Keychain migration, concurrency fixes, attack surface reduction#31979
Rocuts wants to merge 9 commits intoopenclaw:mainfrom
Rocuts:ios/security-hardening-2026

Conversation

@Rocuts
Copy link
Copy Markdown

@Rocuts Rocuts commented Mar 2, 2026

Summary

  • Keychain hardening: enforce kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly on every write via delete-then-add pattern (KeychainStore, GatewayTLSPinning)
  • Sensitive data migration: move TLS fingerprints and gateway connection metadata from UserDefaults to Keychain with automatic legacy cleanup
  • Concurrency safety: replace objc_sync_enter/exit and bare Bool flags with OSAllocatedUnfairLock<T> in TLS probe, camera delegates, and diagnostics log counter
  • Attack surface reduction: gate ElevenLabs env var behind #if DEBUG, tighten deep link rate limit (1s → 5s), cap WebSocket message size (16 MB → 4 MB)
  • Diagnostics hardening: move log files from Documents to Caches directory with FileProtectionType.completeUntilFirstUserAuthentication

Findings addressed

ID Severity Fix
SEC-H1 High TLS fingerprints → Keychain
SEC-H2 High Keychain accessibility attribute enforced
SEC-H3 High Gateway metadata → Keychain
SEC-M2 Medium Diagnostics logs → Caches + file protection
SEC-M3 Medium Env var API key gated behind DEBUG
SEC-M5 Medium Deep link rate limit 1s → 5s
SEC-M7 Medium WebSocket max message 16 MB → 4 MB
CON-C1 Critical TLS probe data race → OSAllocatedUnfairLock
CON-C2 Critical Camera delegate double-resume → atomic guard
CON-C3 Critical Diagnostics counter race → OSAllocatedUnfairLock

Test plan

  • GatewaySettingsStoreTests — Keychain round-trip, migration, overwrite, provider API key
  • GatewayConnectionControllerTests — Keychain snapshot/restore for connection load tests
  • Verify Xcode build succeeds (Swift 6 strict concurrency)
  • Manual: confirm gateway reconnects after upgrade (migration path)
  • Manual: verify camera capture completes without crashes under rapid switching

🤖 Generated with Claude Code

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 673d732dc6

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps bot commented Mar 2, 2026

Greptile Summary

This PR comprehensively addresses 10 security and concurrency findings from a 2026 security audit, implementing critical hardening measures across the iOS app.

Key improvements:

  • Keychain security hardening: Enforces kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly on all writes via delete-then-add pattern, preventing weaker accessibility attributes from persisting
  • Sensitive data migration: Moves TLS fingerprints and gateway connection metadata from backup-extractable UserDefaults to Keychain with automatic legacy cleanup
  • Concurrency safety: Replaces unsafe synchronization patterns (objc_sync_enter/exit, bare Bool flags, nonisolated(unsafe) counters) with OSAllocatedUnfairLock<T> to eliminate data races in TLS probe, camera delegates, and diagnostics logging
  • Attack surface reduction: Gates ElevenLabs API key behind #if DEBUG, tightens deep link rate limit (1s → 5s), caps WebSocket message size (16 MB → 4 MB)
  • Diagnostics hardening: Relocates logs from Documents to Caches directory with file protection attribute

Migration safety: The UserDefaults-to-Keychain migration logic is idempotent and runs on first access. Both GatewayTLSStore and GatewaySettingsStore check for existing Keychain entries before migrating and clean up legacy keys afterward.

Test coverage: Updated tests validate Keychain round-trips, migration paths, and connection metadata persistence. The camera and TLS probe concurrency fixes eliminate critical race conditions that could cause crashes.

Confidence Score: 4/5

  • This PR is safe to merge with minor considerations for edge cases
  • The implementation correctly addresses all cited audit findings with appropriate security controls and concurrency primitives. The OSAllocatedUnfairLock usage follows correct atomic read-modify-write patterns, the Keychain delete-then-add pattern properly enforces accessibility attributes, and attack surface reductions are sensible. One minor consideration: migration logic discards Keychain save return values, so data could theoretically be lost if Keychain writes fail during migration (low probability, low impact—user would just need to reconnect). The comprehensive test coverage and alignment with audit recommendations support high confidence.
  • No files require special attention - all changes align with security audit findings and follow established patterns

Last reviewed commit: ea47df9

Copy link
Copy Markdown
Contributor

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

16 files reviewed, 2 comments

Edit Code Review Agent Settings | Greptile

Comment on lines +262 to +263
port: kind == .manual ? port : nil)
guard self.saveLastGatewayConnectionData(payload) else { return }
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

migration could lose data if saveLastGatewayConnectionData() fails (discarded return value) but removeLastGatewayDefaults() proceeds - consider checking save success before cleanup

Suggested change
port: kind == .manual ? port : nil)
guard self.saveLastGatewayConnectionData(payload) else { return }
if self.saveLastGatewayConnectionData(payload) {
self.removeLastGatewayDefaults(defaults)
}

(requires changing saveLastGatewayConnectionData to return Bool)

Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/ios/Sources/Gateway/GatewaySettingsStore.swift
Line: 262-263

Comment:
migration could lose data if `saveLastGatewayConnectionData()` fails (discarded return value) but `removeLastGatewayDefaults()` proceeds - consider checking save success before cleanup

```suggestion
        if self.saveLastGatewayConnectionData(payload) {
            self.removeLastGatewayDefaults(defaults)
        }
```

(requires changing `saveLastGatewayConnectionData` to return `Bool`)

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines +48 to +51
if self.keychainLoad(account: stableID) == nil {
guard self.keychainSave(existing, account: stableID) else { return }
}
defaults.removeObject(forKey: legacyKey)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

same data loss risk - if keychainSave() fails (returns false), the legacy entry is removed anyway, permanently losing the TLS fingerprint - consider checking save success before cleanup

Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/shared/OpenClawKit/Sources/OpenClawKit/GatewayTLSPinning.swift
Line: 48-51

Comment:
same data loss risk - if `keychainSave()` fails (returns false), the legacy entry is removed anyway, permanently losing the TLS fingerprint - consider checking save success before cleanup

How can I resolve this? If you propose a fix, please make it concise.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: ea47df92fa

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

let task = self.webSocketTask(with: url)
// Avoid "Message too long" receive errors for large snapshots / history payloads.
task.maximumMessageSize = 16 * 1024 * 1024 // 16 MB
task.maximumMessageSize = 4 * 1024 * 1024 // 4 MB
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Align client WebSocket limit with server history budget

Reducing task.maximumMessageSize to 4 MB here (and similarly in apps/shared/OpenClawKit/Sources/OpenClawKit/GatewayTLSPinning.swift) introduces a hard mismatch with the gateway’s chat-history response budget, which is still 6 MB (src/gateway/server-constants.ts:6) and is actively enforced when building chat.history payloads (src/gateway/server-methods/chat.ts:553-560). Any history response between 4 MB and 6 MB is now valid server output but will be rejected by these clients with a "Message too long" receive failure, causing reconnect/fetch failures for larger sessions.

Useful? React with 👍 / 👎.

@Rocuts
Copy link
Copy Markdown
Author

Rocuts commented Mar 2, 2026

The failing test src/cron/isolated-agent.uses-last-non-empty-agent-text-as.test.ts ("applies model overrides with correct precedence") is unrelated to this PR.

This PR only touches iOS/Swift files under apps/ios/ and apps/shared/OpenClawKit/ — zero changes to src/cron/ or any TypeScript files. The failure appears to be a pre-existing flaky test on main (last touched in #31594).

Could a maintainer re-run CI to confirm? Thanks!

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: e301d720b9

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +31 to 33
SecItemDelete(query as CFDictionary)

var insert = query
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Preserve existing secrets when keychain rewrite fails

saveString deletes the current keychain item before attempting to add the replacement, so any SecItemAdd failure turns a failed update into data loss: the old value is already gone. This impacts all callers persisting gateway tokens/passwords and stable IDs, and can leave users without recoverable credentials after a transient keychain write error; the write path should keep the prior item until the new value is durably stored.

Useful? React with 👍 / 👎.

Comment on lines +79 to +83
SecItemDelete(query as CFDictionary)
var insert = query
insert[kSecValueData as String] = data
insert[kSecAttrAccessible as String] = kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly
return SecItemAdd(insert as CFDictionary, nil) == errSecSuccess
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Preserve existing TLS pin on keychain write failure

The TLS pin store also uses delete-then-add, which means a failed SecItemAdd erases an existing fingerprint instead of leaving the prior pin in place. In TOFU flows (GatewayTLSPinningSession saves fingerprints during successful handshakes), this can silently drop trust continuity and force future connections to behave as unpinned until a new pin is stored.

Useful? React with 👍 / 👎.

@ngutman
Copy link
Copy Markdown
Contributor

ngutman commented Mar 3, 2026

Thanks for the effort here. I’m going to ask for a split before we review this.

This PR is too broad: it bundles multiple independent changes (keychain migration, TLS pinning storage, concurrency fixes, rate-limit/message-size tweaks, diagnostics path/protection changes, and
test updates) in one go. Please break this into smaller focused PRs so each change can be reviewed and validated independently.

Also, please don’t include the large generated audit docs in this PR. Files like:

  • apps/ios/AUDIT-REPORT-2026.md
  • apps/ios/audit-api-modernization.md
  • apps/ios/audit-architecture.md
  • apps/ios/audit-concurrency.md
  • apps/ios/audit-security.md
  • apps/ios/audit-uiux.md

add a lot of noise and make review harder. Keep this PR to code + tests needed for the actual fix set, and move docs to separate targeted PR(s) only if they’re needed.

Please re-open as a series of focused PRs (one concern per PR), and we can review quickly.

@ngutman ngutman closed this Mar 3, 2026
@mbelinky
Copy link
Copy Markdown
Contributor

mbelinky commented Mar 3, 2026

I'll cherry pick, merge and give attribution as needed. Thanks @Rocuts

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants