本文へ移動
cccskills
無料GitHub で公開

iOS Security

iOS security best practices including Keychain Services, biometrics, data protection, network security, and App Attest

インストール方法を見る

含まれるファイル(2)

  • SKILL.md12.7 KB
  • references/owasp-mobile-top10.md2.7 KB

SKILL.md(原文)

インストールする前に、エージェントに与えられる指示の中身を確認できます。

iOS Security Skill

Comprehensive guide to iOS security covering secure storage, authentication, network security, and compliance with Apple's security requirements.

Keychain Services

Secure Credential Storage

import Security

enum KeychainError: Error {
    case duplicateItem
    case itemNotFound
    case unexpectedStatus(OSStatus)
}

final class KeychainManager {
    static let shared = KeychainManager()
    private init() {}

    func save(password: String, for account: String, service: String) throws {
        let passwordData = password.data(using: .utf8)!

        let query: [String: Any] = [
            kSecClass as String: kSecClassGenericPassword,
            kSecAttrAccount as String: account,
            kSecAttrService as String: service,
            kSecValueData as String: passwordData,
            kSecAttrAccessible as String: kSecAttrAccessibleWhenUnlockedThisDeviceOnly
        ]

        let status = SecItemAdd(query as CFDictionary, nil)

        if status == errSecDuplicateItem {
            // Update existing item
            let updateQuery: [String: Any] = [
                kSecClass as String: kSecClassGenericPassword,
                kSecAttrAccount as String: account,
                kSecAttrService as String: service
            ]
            let updates: [String: Any] = [
                kSecValueData as String: passwordData
            ]
            let updateStatus = SecItemUpdate(updateQuery as CFDictionary, updates as CFDictionary)
            guard updateStatus == errSecSuccess else {
                throw KeychainError.unexpectedStatus(updateStatus)
            }
        } else if status != errSecSuccess {
            throw KeychainError.unexpectedStatus(status)
        }
    }

    func retrievePassword(for account: String, service: String) throws -> String {
        let query: [String: Any] = [
            kSecClass as String: kSecClassGenericPassword,
            kSecAttrAccount as String: account,
            kSecAttrService as String: service,
            kSecReturnData as String: true,
            kSecMatchLimit as String: kSecMatchLimitOne
        ]

        var result: AnyObject?
        let status = SecItemCopyMatching(query as CFDictionary, &result)

        guard status == errSecSuccess else {
            if status == errSecItemNotFound {
                throw KeychainError.itemNotFound
            }
            throw KeychainError.unexpectedStatus(status)
        }

        guard let data = result as? Data,
              let password = String(data: data, encoding: .utf8) else {
            throw KeychainError.unexpectedStatus(errSecDecode)
        }

        return password
    }

    func delete(account: String, service: String) throws {
        let query: [String: Any] = [
            kSecClass as String: kSecClassGenericPassword,
            kSecAttrAccount as String: account,
            kSecAttrService as String: service
        ]

        let status = SecItemDelete(query as CFDictionary)
        guard status == errSecSuccess || status == errSecItemNotFound else {
            throw KeychainError.unexpectedStatus(status)
        }
    }
}

Keychain Access Levels

AttributeDescription
kSecAttrAccessibleWhenUnlockedAccessible when device unlocked
kSecAttrAccessibleWhenUnlockedThisDeviceOnlySame, but not backed up
kSecAttrAccessibleAfterFirstUnlockAccessible after first unlock
kSecAttrAccessibleAfterFirstUnlockThisDeviceOnlySame, but not backed up
kSecAttrAccessibleWhenPasscodeSetThisDeviceOnlyRequires passcode, not backed up

Biometric Authentication

Face ID / Touch ID

import LocalAuthentication

final class BiometricAuthManager {
    enum BiometricType {
        case none
        case touchID
        case faceID
        case opticID  // Vision Pro
    }

    var biometricType: BiometricType {
        let context = LAContext()
        guard context.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: nil) else {
            return .none
        }

        switch context.biometryType {
        case .touchID: return .touchID
        case .faceID: return .faceID
        case .opticID: return .opticID
        case .none: return .none
        @unknown default: return .none
        }
    }

    func authenticate(reason: String) async throws -> Bool {
        let context = LAContext()
        context.localizedFallbackTitle = "Use Passcode"

        var error: NSError?
        guard context.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: &error) else {
            if let error {
                throw error
            }
            return false
        }

        return try await context.evaluatePolicy(
            .deviceOwnerAuthenticationWithBiometrics,
            localizedReason: reason
        )
    }

    func authenticateWithFallback(reason: String) async throws -> Bool {
        let context = LAContext()

        // Use deviceOwnerAuthentication to allow passcode fallback
        return try await context.evaluatePolicy(
            .deviceOwnerAuthentication,
            localizedReason: reason
        )
    }
}

Keychain with Biometric Protection

func saveBiometricProtectedItem(data: Data, account: String) throws {
    let access = SecAccessControlCreateWithFlags(
        nil,
        kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly,
        .biometryCurrentSet,  // Invalidates if biometrics change
        nil
    )!

    let query: [String: Any] = [
        kSecClass as String: kSecClassGenericPassword,
        kSecAttrAccount as String: account,
        kSecValueData as String: data,
        kSecAttrAccessControl as String: access,
        kSecUseAuthenticationContext as String: LAContext()
    ]

    let status = SecItemAdd(query as CFDictionary, nil)
    guard status == errSecSuccess else {
        throw KeychainError.unexpectedStatus(status)
    }
}

Data Protection

File Protection

// Set protection level when writing files
func writeSecureFile(data: Data, to url: URL) throws {
    try data.write(to: url, options: .completeFileProtection)
}

// Set protection on existing file
func setProtection(for url: URL) throws {
    try FileManager.default.setAttributes(
        [.protectionKey: FileProtectionType.complete],
        ofItemAtPath: url.path
    )
}

Protection Levels

LevelDescription
.completeOnly accessible when device unlocked
.completeUnlessOpenAccessible if file was open when locked
.completeUntilFirstUserAuthenticationAccessible after first unlock
.noneNo protection (avoid for sensitive data)

Cryptography

CryptoKit (Preferred)

import CryptoKit

// Symmetric Encryption (AES-GCM)
func encrypt(data: Data, using key: SymmetricKey) throws -> Data {
    let sealedBox = try AES.GCM.seal(data, using: key)
    return sealedBox.combined!
}

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

// Key Derivation
func deriveKey(from password: String, salt: Data) -> SymmetricKey {
    let passwordData = Data(password.utf8)
    let key = HKDF<SHA256>.deriveKey(
        inputKeyMaterial: SymmetricKey(data: passwordData),
        salt: salt,
        info: Data("encryption".utf8),
        outputByteCount: 32
    )
    return key
}

// Hashing
func hash(data: Data) -> String {
    let digest = SHA256.hash(data: data)
    return digest.map { String(format: "%02x", $0) }.joined()
}

// HMAC
func authenticate(data: Data, key: SymmetricKey) -> Data {
    let authCode = HMAC<SHA256>.authenticationCode(for: data, using: key)
    return Data(authCode)
}

Network Security

App Transport Security (ATS)

<!-- Info.plist - Secure Configuration -->
<key>NSAppTransportSecurity</key>
<dict>
    <!-- ATS enabled by default, don't add anything for HTTPS-only -->
</dict>

<!-- If you MUST allow exceptions (document why!) -->
<key>NSAppTransportSecurity</key>
<dict>
    <key>NSExceptionDomains</key>
    <dict>
        <key>legacy-api.example.com</key>
        <dict>
            <key>NSExceptionAllowsInsecureHTTPLoads</key>
            <true/>
            <key>NSExceptionMinimumTLSVersion</key>
            <string>TLSv1.2</string>
        </dict>
    </dict>
</dict>

Certificate Pinning

import CryptoKit

final class CertificatePinningDelegate: NSObject, URLSessionDelegate {
    private let pinnedCertificateHashes: Set<String>

    init(pinnedHashes: [String]) {
        self.pinnedCertificateHashes = Set(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
        }

        // Get server certificate
        guard let certificate = SecTrustCopyCertificateChain(serverTrust) as? [SecCertificate],
              let serverCert = certificate.first else {
            completionHandler(.cancelAuthenticationChallenge, nil)
            return
        }

        // Hash the public key
        let serverCertData = SecCertificateCopyData(serverCert) as Data
        let hash = SHA256.hash(data: serverCertData)
        let hashString = hash.map { String(format: "%02x", $0) }.joined()

        if pinnedCertificateHashes.contains(hashString) {
            completionHandler(.useCredential, URLCredential(trust: serverTrust))
        } else {
            completionHandler(.cancelAuthenticationChallenge, nil)
        }
    }
}

App Attest (iOS 14+)

Device Integrity Verification

import DeviceCheck

final class AppAttestManager {
    private let service = DCAppAttestService.shared

    var isSupported: Bool {
        service.isSupported
    }

    func generateKey() async throws -> String {
        try await service.generateKey()
    }

    func attestKey(_ keyId: String, clientDataHash: Data) async throws -> Data {
        try await service.attestKey(keyId, clientDataHash: clientDataHash)
    }

    func generateAssertion(_ keyId: String, clientDataHash: Data) async throws -> Data {
        try await service.generateAssertion(keyId, clientDataHash: clientDataHash)
    }
}

// Usage in authentication flow
final class SecureAuthService {
    private let attestManager = AppAttestManager()
    private var keyId: String?

    func setupAttestation() async throws {
        guard attestManager.isSupported else {
            throw SecurityError.attestationNotSupported
        }

        keyId = try await attestManager.generateKey()

        // Get challenge from server
        let challenge = try await fetchChallenge()
        let clientDataHash = SHA256.hash(data: challenge)

        // Attest the key
        let attestation = try await attestManager.attestKey(
            keyId!,
            clientDataHash: Data(clientDataHash)
        )

        // Send attestation to server for verification
        try await verifyAttestation(attestation)
    }
}

Secure Coding Practices

Input Validation

// Validate and sanitize all external input
struct InputValidator {
    static func validateEmail(_ email: String) -> Bool {
        let pattern = "^[A-Z0-9a-z._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}$"
        return email.range(of: pattern, options: .regularExpression) != nil
    }

    static func sanitizeForDisplay(_ input: String) -> String {
        // Prevent potential XSS in WebViews
        input
            .replacingOccurrences(of: "<", with: "&lt;")
            .replacingOccurrences(of: ">", with: "&gt;")
    }
}

Secure Memory

// Clear sensitive data when done
extension Data {
    mutating func secureZero() {
        guard count > 0 else { return }
        withUnsafeMutableBytes { ptr in
            memset_s(ptr.baseAddress, ptr.count, 0, ptr.count)
        }
    }
}

// Use for temporary sensitive data
class SecureString {
    private var data: Data

    init(_ string: String) {
        data = Data(string.utf8)
    }

    var value: String {
        String(data: data, encoding: .utf8) ?? ""
    }

    deinit {
        data.secureZero()
    }
}

References

  • See references/owasp-mobile-top10.md for OWASP Mobile security guidelines
  • See references/privacy-manifest.md for privacy manifest requirements

レビュー

まだレビューはありません。使ってみた感想をお寄せください。

同じリポジトリのスキル

概要と使いどころ

When the user wants to A/B test App Store product page elements to improve conversion rate. Also use when the user mentions "A/B test", "product page optimization", "test my screenshots", "test my icon", "conversion rate optimization", "CPP", or "custom product pages". For screenshot design, see screenshot-optimization. For metadata optimization, see metadata-optimization.

日本語の概要は準備中です。原文の説明を表示しています。

JordanCoin/ios-skills-collection62026年9月10日 更新

Discover and configure Bluetooth and Wi-Fi accessories using AccessorySetupKit. Use when presenting a privacy-preserving accessory picker, defining discovery descriptors for BLE or Wi-Fi devices, handling accessory session events, migrating from CoreBluetooth permission-based scanning, or setting up accessories without requiring broad Bluetooth permissions.

日本語の概要は準備中です。原文の説明を表示しています。

JordanCoin/ios-skills-collection62026年9月10日 更新

Implement, review, or improve Live Activities and Dynamic Island experiences in iOS apps using ActivityKit. Use when building real-time updating widgets for the Lock Screen and Dynamic Island — delivery tracking, sports scores, ride-sharing status, workout timers, media playback, or any time-sensitive information that updates in real time. Also use when working with ActivityKit, ActivityAttributes, Activity lifecycle (request/update/end), Dynamic Island layouts (compact/minimal/expanded), push-to-update Live Activities, or Lock Screen live widgets.

日本語の概要は準備中です。原文の説明を表示しています。

JordanCoin/ios-skills-collection62026年9月10日 更新

Measure ad effectiveness with privacy-preserving attribution using AdAttributionKit. Use when registering ad impressions, handling attribution postbacks, updating conversion values, implementing re-engagement attribution, configuring publisher or advertiser apps, or replacing SKAdNetwork with AdAttributionKit for ad measurement.

日本語の概要は準備中です。原文の説明を表示しています。

JordanCoin/ios-skills-collection62026年9月10日 更新

alarmkit

無料

Implement AlarmKit alarms and countdown timers for iOS and iPadOS with Lock Screen, Dynamic Island, and Apple Watch system UI. Covers AlarmManager scheduling, AlarmAttributes and AlarmPresentation, AlarmButton stop and snooze actions, authorization, state observation, and Live Activity integration. Use when building wake-up alarms, countdown timers, or alarm-style alerts that need Apple's system alarm experience.

日本語の概要は準備中です。原文の説明を表示しています。

JordanCoin/ios-skills-collection62026年9月10日 更新

When the user wants to optimize their Google Play Store listing — title, short description, full description, keywords, ratings, or Play Store-specific features. Use when the user mentions "Google Play", "Android", "Play Store", "Play Console", "short description", "full description indexed", "Google Play ASO", or wants Google Play-specific keyword, creative, or ratings strategy. For iOS App Store optimization, see aso-audit and metadata-optimization.

日本語の概要は準備中です。原文の説明を表示しています。

JordanCoin/ios-skills-collection62026年9月10日 更新

JordanCoin のスキルをすべて見る

このスキルの問題を報告する