| name | ios-security |
| description | Definitive iOS/Swift security skill for client-side Apple-platform apps (iOS 16–26, Swift 6). Covers Keychain Services (SecItem), biometric auth (LAContext, Face ID, Secure Enclave), CryptoKit (AES-GCM, post-quantum), token storage, Sign in with Apple + passkeys + OAuth, certificate pinning, App Transport Security, privacy manifests + ATT, jailbreak/anti-tamper + App Attest, and OWASP MASVS/MASTG audits with App Store rejection checks. Use whenever the user asks "is this secure?", "audit my app", "how do I store a token/password/key", "add Face ID login", "implement certificate pinning", or writes code touching secrets, keys, biometrics, encryption, Keychain, or ATS. |
| license | MIT |
iOS Security
Philosophy — correctness over opinion. This skill provides Apple-documented, verified
patterns, not architecture mandates. Every code pattern is grounded in Apple's Keychain
Services / Platform Security Guide, DTS engineer posts (Quinn "The Eskimo!"), WWDC sessions,
and OWASP MASVS v2.1.0 / MASTG v2 — never from memory alone. Baseline deployment target
iOS 16+; modern recommendations target iOS 17+; forward-looking guidance covers iOS 26
(post-quantum CryptoKit, X25519MLKEM768 TLS).
Scope: client-side Apple-platform security — Keychain, biometrics, CryptoKit, Secure
Enclave, credential/token lifecycle, Sign in with Apple / passkeys / OAuth, certificate trust
& pinning, App Transport Security, privacy manifests & ATT, app hardening / anti-tamper, and
static security auditing (OWASP MASVS, App Store rejection gate).
Out of scope: server-side auth architecture (JWT issuance, OAuth provider config →
OWASP ASVS), CloudKit's server-managed key hierarchy, code signing / provisioning (build-time),
and cross-platform crypto libraries (OpenSSL, LibSodium). Flag these as out of scope and redirect.
What is the task? (route here first)
Identify intent, then jump to the matching mode (table below). If ambiguous, ask one clarifying
question. IMPLEMENT = build a feature from scratch (pick the domain, load 1–2 refs, follow ✅
patterns, satisfy the 13 Core Rules). REVIEW = is THIS snippet secure (run the Anti-Pattern Quick
Scan, cite ✅ fix + severity). AUDIT = full MASVS scan of a repo (run the Audit Workflow, produce
the report + coverage matrix). FIX = "how do I fix X" (identify the pattern, give the ✅ secure
replacement + MASVS rationale + migration if needed).
| Mode | Trigger | Go to |
|---|
| IMPLEMENT | "store a token", "add Face ID", "encrypt this", "implement pinning", "Sign in with Apple" | § IMPLEMENT mode → Domain Guide |
| REVIEW | "is this secure?", "review my keychain/crypto code", paste of a snippet | § REVIEW mode → Anti-Pattern Quick Scan |
| AUDIT | "security audit", "OWASP/MASVS scan", "find vulnerabilities", "is this ready for release" | § AUDIT mode → references/audit-workflow.md |
| FIX | "how do I fix this", "secure alternative for X" | Identify the anti-pattern below → give ✅ replacement |
The 13 Core Rules (non-negotiable invariants)
These are security invariants, not style preferences — use directive tone ("always"/"never")
only for these and the 13 anti-patterns. Everything else is advisory.
- Secrets live in Keychain — never
UserDefaults, @AppStorage, Info.plist, .xcconfig, or NSCoding. Those produce plaintext artifacts readable from unencrypted backups and jailbroken devices. → references/keychain.md, references/anti-patterns.md #1
- Never ignore
OSStatus. Every SecItem* call returns one. Use an exhaustive switch covering at minimum errSecSuccess, errSecDuplicateItem (-25299), errSecItemNotFound (-25300), errSecInteractionNotAllowed (-25308). → references/keychain.md
- Always use add-or-update.
SecItemAdd, then SecItemUpdate on errSecDuplicateItem. Never delete-then-add (race window + destroys persistent references). → references/keychain.md
- Always set
kSecAttrAccessible explicitly (or kSecAttrAccessControl — never both, they conflict → errSecParam). The implicit default (WhenUnlocked) breaks background ops and hides policy from review. → references/keychain.md
- Never call
SecItem* on @MainActor/main thread. Each call is an IPC round-trip to securityd. Use a dedicated actor (iOS 17+) or serial DispatchQueue. → references/keychain.md
- Biometrics must be keychain-bound — never
LAContext.evaluatePolicy() as a standalone gate. That returns a Bool in hookable user-space memory; Frida/objection forces it true in one command. Store the secret behind SecAccessControl with .biometryCurrentSet; let the Secure Enclave gate SecItemCopyMatching. → references/biometrics.md, references/anti-patterns.md #3
- Use the correct
kSecClass. Web credentials → kSecClassInternetPassword (for AutoFill); crypto keys → kSecClassKey; app secrets → kSecClassGenericPassword with kSecAttrService + kSecAttrAccount. → references/keychain.md
- Never reuse a nonce with the same AES-GCM key. A single reuse leaks
C1 ⊕ C2 = P1 ⊕ P2 and the auth key (forbidden attack). Omit the nonce: parameter — CryptoKit generates a random one. → references/cryptokit.md, references/anti-patterns.md #6
- Never use a raw ECDH shared secret as a symmetric key — always derive via
sharedSecret.hkdfDerivedSymmetricKey(...). (CryptoKit's SharedSecret has no withUnsafeBytes on purpose.) → references/cryptokit.md
- Never use
Insecure.MD5/Insecure.SHA1/CC_MD5/CC_SHA1 for security. Both broken (2005/2017, CISA Jan 2025). Use SHA256+; passwords use a KDF (Argon2id/bcrypt server-side; PBKDF2 ≥600,000 iters on-device). → references/cryptokit.md
- Use only cryptographic RNG for tokens/nonces/salts/keys —
SecRandomCopyBytes or CryptoKit. Never arc4random*, rand(), drand48, or GameplayKit RNG. → references/anti-patterns.md #10
- Keep ATS on. Never
NSAllowsArbitraryLoads = true. For dev use NSAllowsLocalNetworking; for legacy production endpoints use a justified per-domain NSExceptionDomains entry. (Global ATS-off also triggers App Store review.) → references/network.md
- macOS targets the data protection keychain — set
kSecUseDataProtectionKeychain: true on every SecItem* call (Mac Catalyst / iOS-on-Mac do it automatically). Without it, queries silently route to the legacy file-based keychain. → references/keychain.md
The 13 Anti-Patterns — Quick Scan (REVIEW/AUDIT backbone)
When reviewing code, search for these. Any match is a finding. ❌ = insecure signature to detect;
✅ = corrective pattern (full ❌/✅ code pairs, MASTG test IDs, and detection heuristics in
references/anti-patterns.md).
| # | Search for | Anti-Pattern | Severity | OWASP 2024 | MASTG |
|---|
| 1 | UserDefaults/@AppStorage + token/key/secret/password | Plaintext credential storage | 🔴 CRITICAL | M9 | MASTG-TEST-0302 |
| 2 | High-entropy string literal (≥20 chars), sk_live_/pk_live_/AIza/AKIA | Hardcoded API key/secret | 🔴 CRITICAL | M1 | MASTG-TEST-0213 |
| 3 | evaluatePolicy without nearby SecItemCopyMatching + SecAccessControl | LAContext-only biometric gate | 🔴 CRITICAL | M3 | MASTG-TEST-0266 |
| 4 | SecItemAdd( with discarded return / no errSecDuplicateItem | Ignored OSStatus | 🟡 HIGH | M8 | MASTG-TEST-0300 |
| 5 | SecItemAdd with no kSecAttrAccessible; kSecAttrAccessibleAlways | Wrong/missing data protection class | 🟡 HIGH | M9 | MASTG-TEST-0299 |
| 6 | AES.GCM.Nonce(data:, .seal(... nonce:, Data(repeating: 0, count: 12) | Nonce reuse / hardcoded IV | 🔴 CRITICAL | M10 | MASTG-TEST-0317 |
| 7 | sharedSecret.withUnsafeBytes without HKDF | Raw ECDH secret as key | 🟡 HIGH | M10 | MASTG-TEST-0318 |
| 8 | print(/NSLog(/os_log(... %{public} + token/password/secret | Logging sensitive data | 🟡 HIGH | M9 | MASTG-TEST-0297 |
| 9 | Insecure.MD5/Insecure.SHA1/CC_MD5/CC_SHA1 | Broken hash for security | 🟡 HIGH | M10 | MASTG-TEST-0211 |
| 10 | arc4random/rand()/drand48/GameplayKit RNG in security context | Non-crypto RNG | 🟡 HIGH | M10 | MASTG-TEST-0311 |
| 11 | NSAllowsArbitraryLoads</key><true/> in Info.plist | Globally disabled ATS | 🔴 CRITICAL | M5 | MASTG-TEST-0233 |
| 12 | NSKeyedUnarchiver.unarchiveObject(; SecTrustEvaluate( (sync, deprecated) | Insecure deserialization / legacy trust eval | 🟡 HIGH | M4 / M5 | MASTG-TEST-0305 |
| 13 | Missing first-launch SecItemDelete across all kSecClass | Stale keychain after reinstall | 🟢 MEDIUM | M9 | MASTG-TEST-0301 |
Severity model: 🔴 CRITICAL = directly exploitable (exposes secrets/PII). 🟡 HIGH = silent
data loss, wrong security boundary, or context-dependent exploit. 🟢 MEDIUM = defense-in-depth
gap. 🔵 LOW = best-practice nit.
IMPLEMENT mode
- Pick the domain(s) from the Domain Guide below. Load the 1–2 named reference file(s).
- Follow the ✅ patterns verbatim for the core security logic — do not improvise crypto/keychain.
- Satisfy every applicable Core Rule.
- Run the reference file's Summary Checklist before declaring done.
- Add tests per references/testing.md (protocol mocks for unit; real keychain on device).
Domain Guide
REVIEW mode
- Run the Anti-Pattern Quick Scan (table above) against the code. Load
references/anti-patterns.md for the ❌/✅ detail of any match.
- Add the domain-specific reference file(s) for whatever the code does (Domain Guide).
- For each finding state: what's wrong → which reference covers it → the ✅ pattern →
severity → MASVS/MASTG ID.
- Always show both ❌ and ✅ code (exception: pure informational queries).
- Cite the iOS version requirement inline for every API ("
HPKE is iOS 17+").
AUDIT mode
A full repo audit. Read references/audit-workflow.md for the complete process, report
template, model-tiering, and the MASVS coverage matrix. Summary:
Phase 0 — Scope gate (MANDATORY, do not skip). Discover targets (.xcodeproj/.xcworkspace),
dependencies (Podfile/Package.swift/Cartfile), languages (Swift / ObjC / mixed), file
counts. Present the scope menu (target scope A–D × MASVS depth 1–4 × profile L1/L2/R) and
wait for the user to choose before scanning.
Phase 1 — Audit (after scope confirmed). In order:
- Run
scripts/quick-scan.sh (zero-token whole-repo grep) and your own search tools in
parallel — neither alone is sufficient (script catches literals at any depth; agent search
catches multi-file/data-flow patterns).
- CRITICAL patterns → references/anti-patterns.md.
- Info.plist + entitlements → references/privacy.md (plist audit §).
- Storage → Keychain accessibility, file protection, leakage.
- Crypto → deprecated algorithms, weak RNG, hardcoded keys/IVs.
- Network → ATS, pinning → references/network.md.
- If ObjC present → references/resilience.md (ObjC surface §): swizzling, KVC, format strings, NSPredicate injection, NSSecureCoding trap, associated-objects, category collisions.
- If depth ≥ 3 and L2/R → resilience: jailbreak/debugger/Frida detection → references/resilience.md.
- If depth = 4 → compliance gaps (HIPAA/PCI/GDPR/SOC2) → references/audit-workflow.md.
- App Store rejection gate (AS1–AS9) → references/appstore-gate.md.
- If a release
.ipa is available → binary self-audit (scripts/binary-scan.sh: secret scan +
cryptid + __RESTRICT) → references/audit-workflow.md. Off-Mac/CI: tier findings
(verified vs heuristic) and always emit a "🍎 requires macOS" list.
Phase 2 — Report. Use the template in references/audit-workflow.md. Every finding has
file:line, MASVS control, MASWE where applicable, risk, and a compiling ✅ fix.
Always end with the MASVS Coverage Matrix (all 8 categories, real counts, status icons) —
this is mandatory even if the user didn't ask for it.
Audit critical rules (apply during any audit)
- L1 vs L2: never flag an L2-only control (mandatory pinning, jailbreak detection) as a failure
for a general-purpose L1 app — downgrade to informational.
- Every CRITICAL finding is double-checked for false positives before it ships in the report.
- No duplicate findings for the same root cause.
- Note static-analysis limits: you cannot observe runtime; mark conditional findings
("only exploitable if this flag is set in production").
- Give credit: list the security controls that ARE in place (secure storage, pinning, obfuscation).
Quick reference — accessibility constants
| Constant | Decryptable when | In backup | Migrates device | Background-safe | Use for |
|---|
WhenPasscodeSetThisDeviceOnly | Unlocked + passcode set | ❌ | ❌ | ❌ | Highest-value secrets (deleted if passcode removed) |
WhenUnlockedThisDeviceOnly | Unlocked | ❌ | ❌ | ❌ | Foreground-only device-bound secrets |
WhenUnlocked (system default) | Unlocked | ✅ | ✅ | ❌ | Syncable secrets — avoid implicit use |
AfterFirstUnlockThisDeviceOnly | After 1st unlock → restart | ❌ | ❌ | ✅ | Background tasks / push handlers, device-bound |
AfterFirstUnlock | After 1st unlock → restart | ✅ | ✅ | ✅ | Background tasks that must survive restore |
Deprecated — never use: kSecAttrAccessibleAlways, kSecAttrAccessibleAlwaysThisDeviceOnly (iOS 12).
Rule of thumb: background access → start AfterFirstUnlockThisDeviceOnly; foreground-only →
WhenUnlockedThisDeviceOnly; high-value → WhenPasscodeSetThisDeviceOnly. Use non-ThisDeviceOnly
only when iCloud sync or backup migration is genuinely required.
Quick reference — CryptoKit algorithm selection
| Need | Algorithm | Min iOS | Notes |
|---|
| Hash | SHA256/SHA384/SHA512 | 13 | SHA3_256/512 are iOS 18+ (not 26) |
| MAC | HMAC<SHA256> | 13 | constant-time verify built in |
| Encrypt (authenticated) | AES.GCM | 13 | 256-bit key, omit nonce (auto-random) |
| Encrypt (no AES-NI hw) | ChaChaPoly | 13 | better on older Apple Watch |
| Sign | P256.Signing / Curve25519.Signing | 13 | P256 for interop, Curve25519 for speed |
| Key agreement | P256/Curve25519.KeyAgreement | 13 | always hkdfDerivedSymmetricKey |
| Hybrid PKE | HPKE | 17 | replaces manual ECDH+HKDF+AES-GCM |
| Hardware signing | SecureEnclave.P256.Signing | 13 | P256 only; key never leaves hardware |
| Post-quantum KEM | MLKEM768/MLKEM1024 | 26 | FIPS 203 |
| Post-quantum sign | MLDSA65/MLDSA87 | 26 | FIPS 204 |
| Password → key | PBKDF2 (CommonCrypto) | 13 | ≥600,000 iters SHA-256 (OWASP 2024) |
| Key → key | HKDF<SHA256> | 13 | always pass info for domain separation |
References
| File | Read when |
|---|
| references/anti-patterns.md | Every REVIEW/AUDIT — the 13 anti-patterns with full ❌/✅ pairs, MASTG IDs, grep heuristics, CI/CD detection |
| references/keychain.md | SecItem CRUD, OSStatus, kSecClass selection, accessibility, actor wrappers, credential/OAuth lifecycle, macOS data-protection keychain, first-launch cleanup |
| references/biometrics.md | Keychain-bound biometrics, the boolean-gate bypass, flag selection, enrollment-change detection, graceful degradation, actor isolation |
| references/cryptokit.md | SHA-2/3, HMAC, AES-GCM/ChaChaPoly, ECDSA/ECDH, HPKE, Secure Enclave, ML-KEM/ML-DSA, HKDF/PBKDF2, key lifecycle |
| references/authentication.md | Sign in with Apple, passkeys/WebAuthn, ASWebAuthenticationSession OAuth, Password AutoFill, credential-state checks, nonce/token validation |
| references/network.md | ATS configuration, the 4 pinning strategies, SPKI hashing (with ASN.1 header), NSPinnedDomains, mTLS, backup pins/rotation |
| references/privacy.md | Privacy manifest (PrivacyInfo.xcprivacy), required-reason APIs, ATT, usage-description keys, permission UX, data minimization |
| references/resilience.md | Jailbreak/debugger/Frida/injection detection, integrity checks, App Attest + DeviceCheck server attestation, ObjC runtime attack surface |
| references/testing.md | Protocol-based mocking, simulator vs device gotchas, CI/CD keychain, Frida bypass verification |
| references/audit-workflow.md | Full audit process, scope menu, report template, MASVS coverage matrix, MASVS↔pattern mapping, model tiering, binary self-audit (own IPA), off-Mac/CI determinism tiers |
| references/appstore-gate.md | App Store rejection checks AS1–AS9 (UIWebView, private-API + false positives, per-app SPM symbol isolation, privacy manifest, usage-key→symbol map, ATT, ATS, export compliance) |
| references/private-api-research.md | "Is API X private?" / "what entitlement gates Y?" / "which framework exports Z?" via the ipsw toolkit (dyld shared cache, class-dump, entitlement DB, Mach-O) — research only |
Authoritative sources
Apple Keychain Services docs · Apple Platform Security Guide · TN3137 (macOS keychain) ·
Quinn "The Eskimo!" DTS posts ("SecItem: Fundamentals" / "Pitfalls and Best Practices") ·
WWDC 2014-711 (Touch ID + Keychain) · WWDC 2019-709 (CryptoKit) · WWDC 2025-314 (post-quantum
crypto) · OWASP Mobile Top 10 (2024) + MASVS v2.1.0 + MASTG v2 · CISA/FBI Product Security
Bad Practices v2.0 (Jan 2025, hardcoded credentials = CWE-798). Never fabricate WWDC session
numbers or citations — if not in a reference file, say it's unverified.