security-best-practices

Security Best Practices — Expert Decisions

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "security-best-practices" with this command: npx skills add kaakati/rails-enterprise-dev/kaakati-rails-enterprise-dev-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, &#x26;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

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

Coding

flutter conventions & best practices

No summary provided by upstream source.

Repository SourceNeeds Review
Coding

getx state management patterns

No summary provided by upstream source.

Repository SourceNeeds Review
Coding

ruby oop patterns

No summary provided by upstream source.

Repository SourceNeeds Review
Coding

rails localization (i18n) - english & arabic

No summary provided by upstream source.

Repository SourceNeeds Review