Loading...
Loading...
iOS security best practices including Keychain Services, CryptoKit encryption, biometric authentication with Face ID and Touch ID, Secure Enclave key storage, LAContext configuration, App Transport Security (ATS), certificate pinning, data protection classes, secure coding patterns, and encryption implementation. Use when implementing app security features, auditing privacy manifests, configuring App Transport Security, securing keychain access, adding biometric authentication, or encrypting sensitive data with CryptoKit.
npx skill4agent add dpearson2699/swift-ios-skills ios-securityfunc saveToKeychain(account: String, data: Data, service: String) throws {
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrAccount as String: account,
kSecAttrService as String: service,
kSecValueData as String: data,
kSecAttrAccessible as String: kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly
]
let status = SecItemAdd(query as CFDictionary, nil)
if status == errSecDuplicateItem {
let updateQuery: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrAccount as String: account,
kSecAttrService as String: service
]
let updates: [String: Any] = [kSecValueData as String: data]
let updateStatus = SecItemUpdate(updateQuery as CFDictionary, updates as CFDictionary)
guard updateStatus == errSecSuccess else {
throw KeychainError.updateFailed(updateStatus)
}
} else if status != errSecSuccess {
throw KeychainError.saveFailed(status)
}
}func readFromKeychain(account: String, service: String) throws -> Data {
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, let data = result as? Data else {
throw KeychainError.readFailed(status)
}
return data
}func deleteFromKeychain(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.deleteFailed(status)
}
}| Value | When Available | Device-Only | Use For |
|---|---|---|---|
| Device unlocked | No | General credentials |
| Device unlocked | Yes | Sensitive credentials |
| After first unlock | No | Background-accessible tokens |
| After first unlock | Yes | Background tokens, no backup |
| Passcode set + unlocked | Yes | Highest security |
ThisDeviceOnlyAfterFirstUnlockWhenPasscodeSetThisDeviceOnlykSecAttrAccessibleAlwayslet query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrAccount as String: "shared-token",
kSecAttrAccessGroup as String: "TEAMID.com.company.shared"
]| Class | When Available | Use For |
|---|---|---|
| Only when unlocked | Sensitive user data |
| Open handles survive lock | Active downloads, recordings |
| After first unlock (default) | Most app data |
| Always | Non-sensitive, system-needed data |
// Set file protection
try data.write(to: url, options: .completeFileProtection)
// Check protection level
let attributes = try FileManager.default.attributesOfItem(atPath: path)
let protection = attributes[.protectionKey] as? FileProtectionType.complete.completeUntilFirstUserAuthenticationimport CryptoKit
let key = SymmetricKey(size: .bits256)
func encrypt(_ data: Data, using key: SymmetricKey) throws -> Data {
let sealed = try AES.GCM.seal(data, using: key)
guard let combined = sealed.combined else {
throw CryptoError.sealFailed
}
return combined
}
func decrypt(_ data: Data, using key: SymmetricKey) throws -> Data {
let box = try AES.GCM.SealedBox(combined: data)
return try AES.GCM.open(box, using: key)
}let hash = SHA256.hash(data: data)
let hashString = hash.compactMap { String(format: "%02x", $0) }.joined()
// Also available: SHA384, SHA512let key = SymmetricKey(size: .bits256)
// Sign
let signature = HMAC<SHA256>.authenticationCode(for: data, using: key)
// Verify
let isValid = HMAC<SHA256>.isValidAuthenticationCode(signature, authenticating: data, using: key)references/cryptokit-advanced.md// Check availability first
guard SecureEnclave.isAvailable else {
// Fall back to software-based keys
return
}
// Create access control
let accessControl = SecAccessControlCreateWithFlags(
nil,
kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly,
[.privateKeyUsage, .biometryCurrentSet],
nil
)!
// Create a Secure Enclave key with access control
let privateKey = try SecureEnclave.P256.Signing.PrivateKey(
accessControl: accessControl
)
// Sign data (may trigger biometric prompt)
let signature = try privateKey.signature(for: data)
// Verify with the public key (no hardware access needed)
let isValid = privateKey.publicKey.isValidSignature(signature, for: data)
// Persist the key for later use
let keyData = privateKey.dataRepresentation
// Store keyData in Keychain, then restore with:
let restored = try SecureEnclave.P256.Signing.PrivateKey(
dataRepresentation: keyData
)import LocalAuthentication
func authenticateWithBiometrics() async throws -> Bool {
let context = LAContext()
var error: NSError?
guard context.canEvaluatePolicy(
.deviceOwnerAuthenticationWithBiometrics, error: &error
) else {
// Biometrics not available -- fall back to passcode
if context.canEvaluatePolicy(.deviceOwnerAuthentication, error: &error) {
return try await context.evaluatePolicy(
.deviceOwnerAuthentication,
localizedReason: "Authenticate to access your account"
)
}
throw AuthError.biometricsUnavailable
}
return try await context.evaluatePolicy(
.deviceOwnerAuthenticationWithBiometrics,
localizedReason: "Authenticate to access your account"
)
}NSFaceIDUsageDescription<key>NSFaceIDUsageDescription</key>
<string>Authenticate to access your secure data</string>let context = LAContext()
// Allow fallback to device passcode
context.localizedFallbackTitle = "Use Passcode"
// Reuse authentication for 30 seconds
context.touchIDAuthenticationAllowableReuseDuration = 30
// Detect biometry enrollment changes by comparing domain state
let currentState = context.evaluatedPolicyDomainState
// Compare currentState to a previously stored valuelet access = SecAccessControlCreateWithFlags(
nil,
kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly,
.biometryCurrentSet,
nil
)!
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrAccount as String: "auth-token",
kSecValueData as String: tokenData,
kSecAttrAccessControl as String: access,
kSecUseAuthenticationContext as String: LAContext()
].biometryCurrentSet.biometryAny.userPresence<!-- Only for legacy servers you cannot upgrade -->
<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>NSAllowsArbitraryLoadsimport CryptoKit
class PinnedSessionDelegate: NSObject, URLSessionDelegate {
// SHA-256 hash of the certificate's Subject Public Key Info
private let pinnedHashes: Set<String> = [
"base64EncodedSHA256HashOfSPKI=="
]
func urlSession(
_ session: URLSession,
didReceive challenge: URLAuthenticationChallenge
) async -> (URLSession.AuthChallengeDisposition, URLCredential?) {
guard let trust = challenge.protectionSpace.serverTrust,
let chain = SecTrustCopyCertificateChain(trust) as? [SecCertificate],
let certificate = chain.first else {
return (.cancelAuthenticationChallenge, nil)
}
guard let publicKey = SecCertificateCopyKey(certificate),
let publicKeyData = SecKeyCopyExternalRepresentation(
publicKey, nil
) as Data? else {
return (.cancelAuthenticationChallenge, nil)
}
let hash = SHA256.hash(data: publicKeyData)
let hashString = Data(hash).base64EncodedString()
if pinnedHashes.contains(hashString) {
return (.useCredential, URLCredential(trust: trust))
}
return (.cancelAuthenticationChallenge, nil)
}
}// WRONG
logger.debug("User logged in with token: \(token)")
// CORRECT
logger.debug("User logged in successfully")var sensitiveData = Data(/* ... */)
defer {
sensitiveData.resetBytes(in: 0..<sensitiveData.count)
}// Validate URL schemes
guard let url = URL(string: input),
["https"].contains(url.scheme?.lowercased()) else {
throw SecurityError.invalidURL
}
// Prevent path traversal
let resolved = url.standardized.path
guard resolved.hasPrefix(allowedDirectory.path) else {
throw SecurityError.pathTraversal
}func isDeviceCompromised() -> Bool {
let paths = [
"/Applications/Cydia.app",
"/Library/MobileSubstrate/MobileSubstrate.dylib",
"/usr/sbin/sshd",
"/etc/apt",
"/private/var/lib/apt/"
]
for path in paths {
if FileManager.default.fileExists(atPath: path) { return true }
}
// Check if app can write outside sandbox
let testPath = "/private/test_jailbreak"
do {
try "test".write(toFile: testPath, atomically: true, encoding: .utf8)
try FileManager.default.removeItem(atPath: testPath)
return true
} catch {
return false
}
}PrivacyInfo.xcprivacyreferences/privacy-manifest.mdreferences/app-review-guidelines.mdNSAllowsArbitraryLoads = true.biometryAny.biometryCurrentSet@MainActorkSecAttrAccessibleThisDeviceOnly.completeNSFaceIDUsageDescriptionSecAccessControlLAContextNSAllowsArbitraryLoads