# Swift Security

> When to activate: Keychain, certificate pinning, encryption, biometrics, secure storage, App Transport Security in Swift

- Skill: `mattakushi432/swift-security` (Agent Skill)
- Install (CLI): `npx skillmds@latest add mattakushi432/swift-security`
- Raw SKILL.md: https://api.skillmd.com/api/skills/mattakushi432/swift-security/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Security
- Author: Mattakushi432 (https://skillmd.com/u/mattakushi432)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/mattakushi432/swift-security

---


# Swift Security Patterns

## Keychain Storage

Never store secrets in UserDefaults. Use the Keychain.

```swift
import Security

struct KeychainManager {
    static func save(_ value: String, forKey key: String, service: String) throws {
        let data = Data(value.utf8)
        let query: [String: Any] = [
            kSecClass as String:       kSecClassGenericPassword,
            kSecAttrService as String: service,
            kSecAttrAccount as String: key,
            kSecValueData as String:   data,
            // Store in Secure Enclave-backed class when available
            kSecAttrAccessible as String: kSecAttrAccessibleWhenUnlockedThisDeviceOnly,
        ]
        SecItemDelete(query as CFDictionary)  // delete existing before adding
        let status = SecItemAdd(query as CFDictionary, nil)
        guard status == errSecSuccess else { throw KeychainError.saveFailed(status) }
    }

    static func load(forKey key: String, service: String) throws -> String {
        let query: [String: Any] = [
            kSecClass as String:            kSecClassGenericPassword,
            kSecAttrService as String:      service,
            kSecAttrAccount as String:      key,
            kSecReturnData as String:       true,
            kSecMatchLimit as String:       kSecMatchLimitOne,
        ]
        var result: AnyObject?
        let status = SecItemCopyMatching(query as CFDictionary, &result)
        guard status == errSecSuccess, let data = result as? Data,
              let string = String(data: data, encoding: .utf8) else {
            throw KeychainError.loadFailed(status)
        }
        return string
    }
}

enum KeychainError: Error {
    case saveFailed(OSStatus)
    case loadFailed(OSStatus)
}
```

## Biometric Authentication

```swift
import LocalAuthentication

func authenticateWithBiometrics() async throws -> Bool {
    let context = LAContext()
    var error: NSError?
    guard context.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: &error) else {
        throw error ?? LAError(.biometryNotAvailable)
    }

    return try await context.evaluatePolicy(
        .deviceOwnerAuthenticationWithBiometrics,
        localizedReason: "Confirm your identity to access sensitive data"
    )
}
```

## Certificate Pinning

```swift
final class PinnedURLSessionDelegate: NSObject, URLSessionDelegate {
    private let pinnedPublicKeyHashes: Set<String>

    init(pinnedHashes: Set<String>) {
        self.pinnedPublicKeyHashes = pinnedHashes
    }

    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 the server certificate chain
        var secResult = SecTrustResultType.invalid
        SecTrustGetTrustResult(serverTrust, &secResult)

        if let publicKey = SecTrustCopyKey(serverTrust),
           let keyData = SecKeyCopyExternalRepresentation(publicKey, nil) as Data? {
            let hash = SHA256.hash(data: keyData).compactMap { String(format: "%02x", $0) }.joined()
            if pinnedPublicKeyHashes.contains(hash) {
                completionHandler(.useCredential, URLCredential(trust: serverTrust))
                return
            }
        }
        completionHandler(.cancelAuthenticationChallenge, nil)
    }
}
```

## AES-GCM Encryption

```swift
import CryptoKit

struct Encryptor {
    static func encrypt(_ data: Data, key: SymmetricKey) throws -> Data {
        let sealed = try AES.GCM.seal(data, using: key)
        return sealed.combined!
    }

    static func decrypt(_ encryptedData: Data, key: SymmetricKey) throws -> Data {
        let box = try AES.GCM.SealedBox(combined: encryptedData)
        return try AES.GCM.open(box, using: key)
    }

    static func generateKey() -> SymmetricKey {
        SymmetricKey(size: .bits256)
    }
}
```

## App Transport Security

```xml
<!-- Info.plist — do NOT disable ATS globally -->
<!-- If a third-party domain requires HTTP, be specific -->
<key>NSAppTransportSecurity</key>
<dict>
    <key>NSExceptionDomains</key>
    <dict>
        <key>legacy-api.example.com</key>
        <dict>
            <key>NSExceptionAllowsInsecureHTTPLoads</key>
            <true/>
            <key>NSIncludesSubdomains</key>
            <false/>
        </dict>
    </dict>
</dict>
```

## Preventing Screen Capture of Sensitive Fields

```swift
// SwiftUI — redact content from screenshots and screen recording
TextField("Card Number", text: $cardNumber)
    .privacySensitive()

// UIKit
let field = UITextField()
field.isSecureTextEntry = true  // also hides keyboard in screenshots
```

## Common Anti-Patterns

- **UserDefaults for tokens** — always use Keychain
- **Hardcoded API keys in source** — load from config or secure backend
- **Disabled ATS globally** — Apple may reject; use targeted exceptions
- **MD5/SHA1 for hashing** — use SHA-256 or CryptoKit primitives
- **No certificate pinning for sensitive APIs** — at minimum pin the leaf certificate
- **Logging sensitive data** — never log tokens, passwords, or PII; redact in crash reporters

