security-best-practices
2
总安装量
7
周安装量
#72594
全站排名
安装命令
npx skills add https://github.com/kaakati/rails-enterprise-dev --skill security-best-practices
Agent 安装分布
opencode
6
gemini-cli
6
codex
6
claude-code
5
github-copilot
5
Skill 文档
Security Best Practices â Expert Decisions
Expert decision frameworks for iOS security choices. Claude knows Keychain APIs â this skill provides judgment calls for when security measures add value and implementation trade-offs.
Decision Trees
Storage Selection
What type of data?
ââ Credentials (passwords, tokens, secrets)
â ââ Keychain (always)
â kSecAttrAccessibleAfterFirstUnlock typical
â
ââ User preferences
â ââ Is it sensitive? (e.g., PIN enabled)
â ââ YES â Keychain
â ââ NO â UserDefaults is fine
â
ââ Large sensitive files
â ââ File system + Data Protection
â .completeFileProtection option
â
ââ Non-sensitive app state
ââ UserDefaults or files
No special protection needed
Certificate Pinning Decision
What's your threat model?
ââ Consumer app, standard security
â ââ Trust system CA validation
â ATS (App Transport Security) is sufficient
â
ââ Financial/healthcare/enterprise
â ââ Pin certificates
â But plan for rotation!
â
ââ High-value target (banking, crypto)
â ââ Pin public key (not certificate)
â Survives cert renewal
â
ââ Internal enterprise app
ââ May need custom CA trust
ServerTrustManager with custom evaluator
The trap: Pinning without rotation plan. When cert expires, app stops working.
API Key Protection Strategy
Who controls the server?
ââ You control backend
â ââ Don't embed API keys in app
â Authenticate users, server makes API calls
â
ââ Third-party API, user-specific
â ââ OAuth flow
â User authenticates, gets their own token
â
ââ Third-party API, app-level key
ââ Is key truly needed client-side?
ââ NO â Proxy through your backend
ââ YES â Obfuscate + attestation
Accept risk of extraction
Keychain Access Level
When does data need to be accessible?
ââ Only when device unlocked
â ââ kSecAttrAccessibleWhenUnlocked
â Most secure for user-facing data
â
ââ Background refresh needed
â ââ kSecAttrAccessibleAfterFirstUnlock
â Accessible after first unlock until reboot
â
ââ Shared across apps (same team)
â ââ kSecAttrAccessGroup + appropriate access level
â
ââ Must survive device restore
ââ kSecAttrSynchronizable = true
Syncs via iCloud Keychain
NEVER Do
Storage Mistakes
NEVER store credentials in UserDefaults:
// â UserDefaults is NOT encrypted
UserDefaults.standard.set(token, forKey: "authToken")
// Readable with device backup, jailbreak, or debugging
// â
Always use Keychain for credentials
try KeychainManager.save(key: "authToken", data: tokenData)
NEVER hardcode secrets in code:
// â Compiled into binary â trivially extractable
let apiKey = "sk_live_abc123xyz789"
// â Still in binary as string
let apiKey = String(format: "%@%@", "sk_live_", "abc123xyz789")
// â
Fetch from secure backend after authentication
let apiKey = try await secureConfigService.getAPIKey()
// Or at minimum, obfuscate + accept risk
let apiKey = Obfuscator.decode(encodedKey)
NEVER log sensitive data:
// â Logs are accessible and persisted
print("User token: \(token)")
logger.debug("Password: \(password)")
// â
Never log credentials
logger.info("User authenticated successfully")
// If debugging, redact
#if DEBUG
logger.debug("Token: \(String(repeating: "*", count: token.count))")
#endif
Keychain Mistakes
NEVER use kSecAttrAccessibleAlways:
// â Accessible even before device unlocked â rarely needed
let query: [String: Any] = [
kSecAttrAccessible as String: kSecAttrAccessibleAlways
]
// â
Use appropriate access level
let query: [String: Any] = [
kSecAttrAccessible as String: kSecAttrAccessibleAfterFirstUnlock
]
NEVER ignore Keychain errors:
// â Silently fails â credential may not be saved
_ = SecItemAdd(query as CFDictionary, nil)
// â
Check status and handle errors
let status = SecItemAdd(query as CFDictionary, nil)
guard status == errSecSuccess else {
throw KeychainError.saveFailed(status)
}
Certificate Pinning Mistakes
NEVER pin without expiration handling:
// â App breaks when certificate expires
let pinnedCert = loadBundledCertificate()
if serverCert != pinnedCert {
completionHandler(.cancelAuthenticationChallenge, nil)
}
// â
Pin public key (survives renewal) or have rotation plan
let pinnedPublicKey = loadBundledPublicKey()
let serverPublicKey = extractPublicKey(from: serverCert)
if pinnedPublicKey != serverPublicKey {
completionHandler(.cancelAuthenticationChallenge, nil)
}
NEVER disable ATS for convenience:
// â Disables all transport security
// Info.plist
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>
<true/> <!-- NEVER in production -->
</dict>
// â
Only exception if absolutely needed, with justification
<key>NSExceptionDomains</key>
<dict>
<key>legacy-api.example.com</key>
<dict>
<key>NSExceptionAllowsInsecureHTTPLoads</key>
<true/>
<key>NSExceptionMinimumTLSVersion</key>
<string>TLSv1.2</string>
</dict>
</dict>
Memory Safety
NEVER keep credentials in memory longer than needed:
// â Password stays in memory
class LoginManager {
var currentPassword: String? // May persist in memory
}
// â
Clear sensitive data immediately after use
func authenticate(password: String) async throws {
defer {
// Can't truly clear String, but can clear Data
// For true secure handling, use Data and zero it
}
let result = try await authService.login(password: password)
}
Essential Patterns
Keychain Manager
final class KeychainManager {
enum KeychainError: Error {
case itemNotFound
case duplicateItem
case unexpectedStatus(OSStatus)
}
static func save(key: String, data: Data, accessibility: CFString = kSecAttrAccessibleAfterFirstUnlock) throws {
// Delete existing item first (upsert pattern)
try? delete(key: key)
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrAccount as String: key,
kSecValueData as String: data,
kSecAttrAccessible as String: accessibility
]
let status = SecItemAdd(query as CFDictionary, nil)
guard status == errSecSuccess else {
throw KeychainError.unexpectedStatus(status)
}
}
static func load(key: String) throws -> Data {
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrAccount as String: key,
kSecReturnData as String: true,
kSecMatchLimit as String: kSecMatchLimitOne
]
var result: AnyObject?
let status = SecItemCopyMatching(query as CFDictionary, &result)
guard status != errSecItemNotFound else {
throw KeychainError.itemNotFound
}
guard status == errSecSuccess, let data = result as? Data else {
throw KeychainError.unexpectedStatus(status)
}
return data
}
static func delete(key: String) throws {
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrAccount as String: key
]
let status = SecItemDelete(query as CFDictionary)
guard status == errSecSuccess || status == errSecItemNotFound else {
throw KeychainError.unexpectedStatus(status)
}
}
}
Public Key Pinning
final class PublicKeyPinningDelegate: NSObject, URLSessionDelegate {
private let pinnedPublicKeys: [SecKey]
init(publicKeyHashes: [String]) {
// Load pinned public keys from bundle
self.pinnedPublicKeys = publicKeyHashes.compactMap { hash in
// Convert hash to SecKey
loadPublicKey(hash: hash)
}
}
func urlSession(_ session: URLSession, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
guard challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust,
let serverTrust = challenge.protectionSpace.serverTrust else {
completionHandler(.cancelAuthenticationChallenge, nil)
return
}
// Evaluate trust
guard SecTrustEvaluateWithError(serverTrust, nil) else {
completionHandler(.cancelAuthenticationChallenge, nil)
return
}
// Extract server's public key
guard let serverCertificate = SecTrustGetCertificateAtIndex(serverTrust, 0),
let serverPublicKey = SecCertificateCopyKey(serverCertificate) else {
completionHandler(.cancelAuthenticationChallenge, nil)
return
}
// Check if server's public key matches any pinned key
let matched = pinnedPublicKeys.contains { pinnedKey in
serverPublicKey == pinnedKey
}
if matched {
completionHandler(.useCredential, URLCredential(trust: serverTrust))
} else {
completionHandler(.cancelAuthenticationChallenge, nil)
}
}
}
Secure Configuration
enum SecureConfig {
// Environment-specific (via xcconfig, not hardcoded)
static var apiBaseURL: String {
guard let url = Bundle.main.object(forInfoDictionaryKey: "API_BASE_URL") as? String else {
fatalError("API_BASE_URL not configured")
}
return url
}
// Fetched from backend after authentication
static func fetchSecrets() async throws -> AppSecrets {
// User must be authenticated first
guard let authToken = try? KeychainManager.load(key: "authToken") else {
throw ConfigError.notAuthenticated
}
// Fetch from secure endpoint
var request = URLRequest(url: URL(string: "\(apiBaseURL)/config/secrets")!)
request.setValue("Bearer \(String(data: authToken, encoding: .utf8)!)", forHTTPHeaderField: "Authorization")
let (data, _) = try await URLSession.shared.data(for: request)
return try JSONDecoder().decode(AppSecrets.self, from: data)
}
}
Quick Reference
Storage Selection Matrix
| Data Type | Storage | Protection Level |
|---|---|---|
| Auth tokens | Keychain | AfterFirstUnlock |
| Passwords | Keychain | WhenUnlocked |
| Biometric secret | Keychain | WhenPasscodeSetThisDeviceOnly |
| User preferences | UserDefaults | None needed |
| Sensitive files | Files | .completeFileProtection |
| Cache | Files/Cache | None needed |
Keychain Access Levels
| Level | When Accessible | Use Case |
|---|---|---|
| WhenUnlocked | Device unlocked | User-facing credentials |
| AfterFirstUnlock | After first unlock | Background operations |
| WhenPasscodeSetThisDeviceOnly | With passcode, this device | Biometric-protected |
| Always | Always | Almost never use |
Security Audit Checklist
- Credentials in Keychain, not UserDefaults
- No hardcoded secrets in code
- No sensitive data in logs
- HTTPS only (ATS enabled)
- Certificate/public key pinning (if high-value)
- Appropriate Keychain access levels
- Files use Data Protection
- Clear sensitive data from memory
Red Flags
| Smell | Problem | Fix |
|---|---|---|
| Token in UserDefaults | Not encrypted | Keychain |
| API key in source | Easily extracted | Backend proxy or obfuscate |
| NSAllowsArbitraryLoads = true | No transport security | Proper ATS config |
| kSecAttrAccessibleAlways | Over-permissive | Appropriate access level |
| Ignoring SecItem status | Silent failures | Check and handle errors |
| Pinning certificate, not public key | Breaks on renewal | Pin public key |
| Sensitive data in logs | Exposure risk | Never log credentials |