App security
Skill markdavidgan/apple-dev-skills/platforms/codex/skills/app-security
Apple platform development skills for Claude Code, Cursor, Kimi Code, Antigravity, Codex CLI, and Agy.
npx -y skills add markdavidgan/apple-dev-skills --skill app-securityAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 2 stars2 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
What its author says it does
Copied from the file, not written here
On-device app security — Keychain storage, Sign in with Apple, biometric auth (Face ID / Touch ID), CryptoKit encryption/hashing, App Attest / DeviceCheck, and certificate pinning. Use when storing tokens/secrets, adding Sign in with Apple, gating with Face ID/Touch ID, encrypting data, verifying device integrity to your server, or pinning TLS certificates. Trigger on "Keychain", "Sign in with Apple", "Face ID", "biometric", "CryptoKit", "App Attest", "encrypt", or "certificate pinning".
SKILL.md
6.4 KB, ~1.3k tokens by cl100k_base, as published. Nobody here has run it
App Security
Store secrets, authenticate users, and protect data correctly on Apple platforms. The golden rule: never put secrets, tokens, or keys in UserDefaults, @AppStorage, plist, or source — those are plaintext and back up off-device.
Keychain — the only place for secrets
Use a thin wrapper around the C API (or a vetted micro-library). Store tokens, refresh tokens, encryption keys.
import Security
enum Keychain {
static func set(_ data: Data, account: String) throws {
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrAccount as String: account,
kSecValueData as String: data,
kSecAttrAccessible as String: kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly
]
SecItemDelete(query as CFDictionary) // replace existing
let status = SecItemAdd(query as CFDictionary, nil)
guard status == errSecSuccess else { throw KeychainError(status) }
}
static func get(account: String) -> Data? {
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrAccount as String: account,
kSecReturnData as String: true,
kSecMatchLimit as String: kSecMatchLimitOne
]
var out: CFTypeRef?
return SecItemCopyMatching(query as CFDictionary, &out) == errSecSuccess ? out as? Data : nil
}
}
- Choose accessibility deliberately:
...WhenUnlockedThisDeviceOnly(most secrets) or...AfterFirstUnlockThisDeviceOnly(needed in the background). TheThisDeviceOnlyvariants don't migrate to new devices/backups — correct for tokens. - Gate the most sensitive items with
SecAccessControl(.biometryCurrentSet,.userPresence) so reading requires Face ID/Touch ID.
Sign in with Apple
Required if you offer other third-party social logins (App Review guideline 4.8). Privacy-friendly: email relay, minimal data.
let request = ASAuthorizationAppleIDProvider().createRequest()
request.requestedScopes = [.fullName, .email]
request.nonce = sha256(currentNonce) // bind to your backend to prevent replay
let controller = ASAuthorizationController(authorizationRequests: [request])
controller.delegate = self
controller.performRequests()
- The
useridentifier is stable; name/email are returned only on first authorization — persist them then or you can't get them again. - Send the
identityToken+nonceto your server and verify the JWT against Apple's public keys. Never trust the client result alone. - Use
SignInWithAppleButtonin SwiftUI for the HIG-compliant button.
Biometric gate (local)
import LocalAuthentication
let ctx = LAContext()
var err: NSError?
if ctx.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: &err) {
let ok = try await ctx.evaluatePolicy(.deviceOwnerAuthentication, // falls back to passcode
localizedReason: "Unlock your vault")
}
Biometric success is a UI gate, not a cryptographic guarantee — back it with Keychain access control for real protection. Always provide a passcode fallback (.deviceOwnerAuthentication).
CryptoKit — encryption & hashing
import CryptoKit
let key = SymmetricKey(size: .bits256) // store in Keychain
let sealed = try AES.GCM.seal(plaintext, using: key) // authenticated encryption
let data = sealed.combined!
let opened = try AES.GCM.open(.init(combined: data), using: key)
let digest = SHA256.hash(data: payload) // hashing
let signature = try P256.Signing.PrivateKey().signature(for: payload)
- Prefer AES-GCM (authenticated) over CBC. Never roll your own crypto or reuse nonces.
- For keys that must never leave hardware, generate in the Secure Enclave (
kSecAttrTokenIDSecureEnclave/SecureEnclave.P256).
App Attest / DeviceCheck — prove requests come from your real app
For high-value backends (anti-fraud, anti-cheat), verify the client is a genuine, unmodified instance of your app on a real device.
DCAppAttestService.shared→generateKey,attestKey(_:clientDataHash:)(once), thengenerateAssertionper request.- Your server verifies the attestation/assertion against Apple's App Attest root. Client-side checks alone are worthless.
- Use
DeviceCheck(DCDevice.generateToken) for lightweight per-device flags (e.g. "already claimed free trial").
Certificate pinning (only when justified)
Pinning defends against compromised CAs/MITM but breaks when servers rotate certs — pin to a public key, not a leaf cert, and ship a backup pin.
func urlSession(_ s: URLSession, didReceive challenge: URLAuthenticationChallenge) async
-> (URLSession.AuthChallengeDisposition, URLCredential?) {
guard let trust = challenge.protectionSpace.serverTrust,
isPinned(publicKeyOf: trust) else { return (.cancelAuthenticationChallenge, nil) }
return (.useCredential, URLCredential(trust: trust))
}
Don't pin if you can't operationally manage rotation — a stale pin bricks every install until they update. Pairs with networking.
Quick audit
- No secrets in
UserDefaults/plist/source. - Tokens in Keychain with a
ThisDeviceOnlyaccessibility class. - Sign in with Apple verified server-side with nonce.
- Biometric gate backed by Keychain access control, with passcode fallback.
- AES-GCM (not CBC); nonces never reused; keys in Keychain/Secure Enclave.
- App Attest/DeviceCheck verified server-side (if used).
- Cert pins are public-key + have a backup (if used).
- Required-reason/privacy declarations current — see
privacy-manifest.
What ships with it
Read from the repository
Just SKILL.md. No reference files, no scripts.
Gives 0 of the 12 instructions most security skills give in ~1.3k tokens
Counted across 648 of the 828 authors here whose files we hold, read 2026-08-07
- Parameterize all database queriesin 68 of 648, across 51 files
- Hash passwords using bcrypt, scrypt, or argon2in 49 of 648, across 36 files
- Apply rate limiting to authentication endpointsin 48 of 648, across 24 files
- Configure security headersin 35 of 648, across 19 files
- Validate all inputsin 32 of 648, across 24 files
- Validate all external input at the system boundaryin 29 of 648, across 19 files
- Run containers as a non-root userin 28 of 648, across 15 files
- Use httponly secure samesite cookies for sessionsin 26 of 648, across 15 files
- Run dependency audits before every releasein 21 of 648, across 10 files
- Encode output to prevent cross-site scriptingin 21 of 648, across 11 files
- Copy dependencies before source codein 20 of 648, across 9 files
- Store secrets in environment variablesin 20 of 648, across 18 files
Said here and by no other author read
- Gate sensitive items with SecAccessControl
- Verify Sign in with Apple tokens server-side
- Generate hardware-bound keys in Secure Enclave
- Verify App Attest assertions server-side
- Pin public keys with a backup pin
- Ship a backup certificate pin
Grouped from the skills themselves: near-identical wordings counted once, and counted by distinct author, so one author publishing three of these counts once. Length counted with cl100k_base; the agent that loads this file may tokenize it differently.