security-best-practices
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 |
More from kaakati/rails-enterprise-dev
flutter conventions & best practices
Dart 3.x and Flutter 3.x conventions, naming patterns, code organization, null safety, and async/await best practices
55getx state management patterns
GetX controllers, reactive state, dependency injection, bindings, navigation, and best practices
52tailadmin ui patterns
TailAdmin dashboard UI framework patterns and Tailwind CSS classes. ALWAYS use this skill when: (1) Building any dashboard or admin panel interface, (2) Creating data tables, cards, charts, or metrics displays, (3) Implementing forms, buttons, alerts, or modals, (4) Building navigation (sidebar, header, breadcrumbs), (5) Any UI work that should follow TailAdmin design. This skill REQUIRES fetching from the official GitHub repository to ensure accurate class usage - NEVER invent classes.
39mvvm-architecture
Expert MVVM decisions for iOS/tvOS: choosing between ViewModel patterns (state enum vs published properties vs Combine), service layer boundaries, dependency injection strategies, and testing approaches. Use when designing ViewModel architecture, debugging data flow issues, or deciding where business logic belongs. Trigger keywords: MVVM, ViewModel, ObservableObject, @StateObject, service layer, dependency injection, unit test, mock, architecture
36rails localization (i18n) - english & arabic
Comprehensive internationalization skill for Ruby on Rails applications with proper English and Arabic translations, RTL support, pluralization rules, date/time formatting, and culturally appropriate content adaptation.
34rspec testing patterns
Complete guide to testing Ruby on Rails applications with RSpec. Use this skill when writing unit tests, integration tests, system tests, or when setting up test infrastructure including factories, shared examples, and mocking strategies.
31