# iOS Keychain ve Güvenlik: Biometric Auth'dan Certificate Pinning'e
Mobil güvenlik, bir uygulamanın en kritik katmanıdır. Kullanıcı verileri, API token'ları ve hassas bilgiler doğru korunmazsa hem kullanıcılarınızı hem de şirketinizi riske atarsınız. Bu rehberde iOS'un sunduğu tüm güvenlik mekanizmalarını production-grade örneklerle inceleyeceğiz.
İçindekiler
- Keychain Services
- Modern Keychain Wrapper
- Biometric Authentication
- Data Protection
- Certificate Pinning
- CryptoKit
- App Transport Security
- Jailbreak Detection
- Secure Coding Practices
- Security Audit Checklist
1. Keychain Services Temelleri
Keychain, iOS'un şifreli anahtar-değer deposudur. UserDefaults'tan farklı olarak veriler AES-256 ile şifrelenir.
Keychain vs UserDefaults vs FileManager
Özellik | Keychain | UserDefaults | FileManager |
|---|---|---|---|
**Şifreleme** | AES-256-GCM | Yok | Data Protection |
**Erişim** | Kilitli cihaz kontrolü | Her zaman | Dosya korumasına bağlı |
**Kapasite** | Sınırsız* | ~4MB ideal | Disk alanı |
**Backup** | iCloud Keychain opsiyonel | iTunes/iCloud | iTunes/iCloud |
**Uygulama silme** | Kalabilir | Silinir | Silinir |
**Kullanım** | Token, şifre, sertifika | Tercihler, ayarlar | Dosya, medya |
Raw Keychain API
swift
1import Security2 3// Kaydet4func saveToKeychain(key: String, data: Data) -> Bool {5 let query: [String: Any] = [6 kSecClass as String: kSecClassGenericPassword,7 kSecAttrAccount as String: key,8 kSecValueData as String: data,9 kSecAttrAccessible as String: kSecAttrAccessibleWhenUnlockedThisDeviceOnly10 ]11 12 // Önce sil (update için)13 SecItemDelete(query as CFDictionary)14 15 let status = SecItemAdd(query as CFDictionary, nil)16 return status == errSecSuccess17}18 19// Oku20func readFromKeychain(key: String) -> Data? {21 let query: [String: Any] = [22 kSecClass as String: kSecClassGenericPassword,23 kSecAttrAccount as String: key,24 kSecReturnData as String: true,25 kSecMatchLimit as String: kSecMatchLimitOne26 ]27 28 var result: AnyObject?29 let status = SecItemCopyMatching(query as CFDictionary, &result)30 31 guard status == errSecSuccess else { return nil }32 return result as? Data33}34 35// Sil36func deleteFromKeychain(key: String) -> Bool {37 let query: [String: Any] = [38 kSecClass as String: kSecClassGenericPassword,39 kSecAttrAccount as String: key40 ]41 return SecItemDelete(query as CFDictionary) == errSecSuccess42}Easter Egg
Gizli bir bilgi buldun!
Bu bölümde gizli bir bilgi var. Keşfetmek ister misin?
2. Modern Keychain Wrapper
swift
1import Security2 3actor KeychainManager {4 static let shared = KeychainManager()5 6 enum KeychainError: Error, LocalizedError {7 case duplicateItem8 case itemNotFound9 case unexpectedStatus(OSStatus)10 case invalidData11 12 var errorDescription: String? {13 switch self {14 case .duplicateItem: return "Keychain'de zaten var"15 case .itemNotFound: return "Keychain'de bulunamadı"16 case .unexpectedStatus(let s): return "Keychain hatası: \(s)"17 case .invalidData: return "Geçersiz veri"18 }19 }20 }21 22 func save<T: Codable>(_ item: T, forKey key: String,23 requireBiometric: Bool = false) throws {24 let data = try JSONEncoder().encode(item)25 26 var query: [String: Any] = [27 kSecClass as String: kSecClassGenericPassword,28 kSecAttrAccount as String: key,29 kSecValueData as String: data30 ]31 32 if requireBiometric {33 let access = SecAccessControlCreateWithFlags(34 nil,35 kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly,36 .biometryCurrentSet,37 nil38 )!39 query[kSecAttrAccessControl as String] = access40 } else {41 query[kSecAttrAccessible as String] = kSecAttrAccessibleWhenUnlockedThisDeviceOnly42 }43 44 // Önce sil45 let deleteQuery: [String: Any] = [46 kSecClass as String: kSecClassGenericPassword,47 kSecAttrAccount as String: key48 ]49 SecItemDelete(deleteQuery as CFDictionary)50 51 let status = SecItemAdd(query as CFDictionary, nil)52 guard status == errSecSuccess else {53 throw KeychainError.unexpectedStatus(status)54 }55 }56 57 func read<T: Codable>(_ type: T.Type, forKey key: String) throws -> T {58 let query: [String: Any] = [59 kSecClass as String: kSecClassGenericPassword,60 kSecAttrAccount as String: key,61 kSecReturnData as String: true,62 kSecMatchLimit as String: kSecMatchLimitOne63 ]64 65 var result: AnyObject?66 let status = SecItemCopyMatching(query as CFDictionary, &result)67 68 guard status == errSecSuccess, let data = result as? Data else {69 throw KeychainError.itemNotFound70 }71 72 return try JSONDecoder().decode(T.self, from: data)73 }74 75 func delete(forKey key: String) throws {76 let query: [String: Any] = [77 kSecClass as String: kSecClassGenericPassword,78 kSecAttrAccount as String: key79 ]80 let status = SecItemDelete(query as CFDictionary)81 guard status == errSecSuccess || status == errSecItemNotFound else {82 throw KeychainError.unexpectedStatus(status)83 }84 }85}3. Biometric Authentication
swift
1import LocalAuthentication2 3final class BiometricAuthService {4 enum BiometricType {5 case faceID, touchID, none6 }7 8 var availableBiometric: BiometricType {9 let context = LAContext()10 guard context.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: nil) else {11 return .none12 }13 switch context.biometryType {14 case .faceID: return .faceID15 case .touchID: return .touchID16 case .opticID: return .faceID // Vision Pro17 @unknown default: return .none18 }19 }20 21 func authenticate(reason: String) async throws -> Bool {22 let context = LAContext()23 context.localizedCancelTitle = "İptal"24 context.localizedFallbackTitle = "Şifre Kullan"25 26 return try await context.evaluatePolicy(27 .deviceOwnerAuthenticationWithBiometrics,28 localizedReason: reason29 )30 }31}32 33// Kullanım34let bioAuth = BiometricAuthService()35if bioAuth.availableBiometric != .none {36 let success = try await bioAuth.authenticate(reason: "Hesabınıza erişmek için doğrulayın")37 if success {38 // Token'ı Keychain'den oku39 let token = try await KeychainManager.shared.read(String.self, forKey: "authToken")40 }41}4. Data Protection API
Protection Level | Ne Zaman Erişilebilir | Kullanım Alanı |
|---|---|---|
**Complete** | Sadece cihaz açıkken | Hassas kullanıcı verileri |
**CompleteUnlessOpen** | Oluşturulduktan sonra | Devam eden indirmeler |
**UntilFirstAuth** | İlk kilit açmadan sonra | Background fetch verileri |
**None** | Her zaman | Hassas olmayan veriler |
swift
1// Dosya koruma seviyesi belirleme2func saveSecureFile(_ data: Data, filename: String) throws {3 let url = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]4 .appendingPathComponent(filename)5 6 try data.write(to: url, options: [.completeFileProtection, .atomic])7}5. Certificate Pinning
swift
1// URLSession delegate ile SSL pinning2class SSLPinningDelegate: NSObject, URLSessionDelegate {3 private let pinnedHashes: Set<String> = [4 "BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB=", // Primary5 "CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC=" // Backup6 ]7 8 func urlSession(9 _ session: URLSession,10 didReceive challenge: URLAuthenticationChallenge11 ) async -> (URLSession.AuthChallengeDisposition, URLCredential?) {12 13 guard let serverTrust = challenge.protectionSpace.serverTrust,14 let certificate = SecTrustCopyCertificateChain(serverTrust)15 as? [SecCertificate],16 let serverCert = certificate.first else {17 return (.cancelAuthenticationChallenge, nil)18 }19 20 // Public key hash kontrolü21 let serverHash = sha256Hash(of: serverCert)22 23 if pinnedHashes.contains(serverHash) {24 return (.useCredential, URLCredential(trust: serverTrust))25 } else {26 return (.cancelAuthenticationChallenge, nil)27 }28 }29 30 private func sha256Hash(of certificate: SecCertificate) -> String {31 let data = SecCertificateCopyData(certificate) as Data32 var hash = [UInt8](repeating: 0, count: 32)33 data.withUnsafeBytes { buffer in34 CC_SHA256(buffer.baseAddress, CC_LONG(buffer.count), &hash)35 }36 return Data(hash).base64EncodedString()37 }38}6. CryptoKit ile Şifreleme
swift
1import CryptoKit2 3struct EncryptionService {4 // AES-GCM şifreleme5 static func encrypt(_ data: Data, using key: SymmetricKey) throws -> Data {6 let sealedBox = try AES.GCM.seal(data, using: key)7 guard let combined = sealedBox.combined else {8 throw CryptoError.encryptionFailed9 }10 return combined11 }12 13 // AES-GCM çözme14 static func decrypt(_ data: Data, using key: SymmetricKey) throws -> Data {15 let sealedBox = try AES.GCM.SealedBox(combined: data)16 return try AES.GCM.open(sealedBox, using: key)17 }18 19 // Anahtar türetme (HKDF)20 static func deriveKey(from password: String, salt: Data) -> SymmetricKey {21 let inputKey = SymmetricKey(data: Data(password.utf8))22 let derived = HKDF<SHA256>.deriveKey(23 inputKeyMaterial: inputKey,24 salt: salt,25 info: Data("com.app.encryption".utf8),26 outputByteCount: 3227 )28 return derived29 }30 31 enum CryptoError: Error {32 case encryptionFailed33 }34}7. App Transport Security
xml
12NSAppTransportSecurity 3 4 5 NSExceptionDomains 6 7 legacy-api.example.com 8 9 NSExceptionAllowsInsecureHTTPLoads 10 11 NSExceptionMinimumTLSVersion 12 TLSv1.2 13 14 158. Jailbreak Detection
swift
1struct SecurityChecker {2 static var isJailbroken: Bool {3 #if targetEnvironment(simulator)4 return false5 #else6 // 1. Bilinen jailbreak dosyaları7 let paths = [8 "/Applications/Cydia.app",9 "/Library/MobileSubstrate/MobileSubstrate.dylib",10 "/bin/bash", "/usr/sbin/sshd", "/etc/apt",11 "/private/var/lib/apt/"12 ]13 for path in paths {14 if FileManager.default.fileExists(atPath: path) { return true }15 }16 17 // 2. Sandbox bütünlüğü18 let testPath = "/private/jailbreak_test_\(UUID().uuidString)"19 do {20 try "test".write(toFile: testPath, atomically: true, encoding: .utf8)21 try FileManager.default.removeItem(atPath: testPath)22 return true // Sandbox dışına yazabiliyorsa jailbreak23 } catch {24 // Normal davranış — sandbox korumalı25 }26 27 // 3. URL scheme kontrolü28 if let url = URL(string: "cydia://package/com.example") {29 if UIApplication.shared.canOpenURL(url) { return true }30 }31 32 return false33 #endif34 }35}9. Secure Coding Practices
swift
1// 1. Hassas veriyi bellekten temizle2func processPassword(_ password: String) {3 var mutablePassword = Array(password.utf8)4 defer {5 // Kullanımdan sonra sıfırla6 for i in mutablePassword.indices {7 mutablePassword[i] = 08 }9 }10 // Password ile işlem yap11}12 13// 2. Clipboard güvenliği14UIPasteboard.general.setItems(15 [[UIPasteboard.typeAutomatic: sensitiveText]],16 options: [.expirationDate: Date().addingTimeInterval(60)] // 60sn sonra sil17)18 19// 3. Screenshot koruması20extension UIWindow {21 func makeSecure() {22 let field = UITextField()23 field.isSecureTextEntry = true24 self.addSubview(field)25 field.centerYAnchor.constraint(equalTo: self.centerYAnchor).isActive = true26 field.centerXAnchor.constraint(equalTo: self.centerXAnchor).isActive = true27 self.layer.superlayer?.addSublayer(field.layer)28 field.layer.sublayers?.first?.addSublayer(self.layer)29 }30}10. Security Audit Checklist
- Hassas veriler Keychain'de (UserDefaults değil)
- Biometric auth implementasyonu
- SSL/TLS certificate pinning
- ATS açık, HTTP istisnaları minimize
- Jailbreak detection aktif
- Screenshot/screen recording koruması
- Clipboard timeout ayarlı
- Debug log'larda hassas veri yok
- Obfuscation (release build)
- OWASP MASVS L2 uyumluluğu
ALTIN İPUCU
Bu yazının en değerli bilgisi
Bu ipucu, yazının en önemli çıkarımını içeriyor.
Okuyucu Ödülü
Tebrikler! Bu yazıyı sonuna kadar okuduğun için sana özel bir hediyem var:

