Tüm Yazılar
KategoriSecurity
Okuma Süresi
20 dk okuma
Yayın Tarihi
...
Kelime Sayısı
1.769kelime

Kahveni hazırla - bu içerikli bir makale!

Keychain Services API, biometric authentication, certificate pinning ve data protection. iOS güvenliğinin A'dan Z'ye rehberi.

iOS Keychain ve Güvenlik: Biometric Auth'dan Certificate Pinning'e

# 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


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 Security
2 
3// Kaydet
4func 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: kSecAttrAccessibleWhenUnlockedThisDeviceOnly
10 ]
11 
12 // Önce sil (update için)
13 SecItemDelete(query as CFDictionary)
14 
15 let status = SecItemAdd(query as CFDictionary, nil)
16 return status == errSecSuccess
17}
18 
19// Oku
20func 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: kSecMatchLimitOne
26 ]
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? Data
33}
34 
35// Sil
36func deleteFromKeychain(key: String) -> Bool {
37 let query: [String: Any] = [
38 kSecClass as String: kSecClassGenericPassword,
39 kSecAttrAccount as String: key
40 ]
41 return SecItemDelete(query as CFDictionary) == errSecSuccess
42}

Easter Egg

Gizli bir bilgi buldun!

Bu bölümde gizli bir bilgi var. Keşfetmek ister misin?


2. Modern Keychain Wrapper

swift
1import Security
2 
3actor KeychainManager {
4 static let shared = KeychainManager()
5 
6 enum KeychainError: Error, LocalizedError {
7 case duplicateItem
8 case itemNotFound
9 case unexpectedStatus(OSStatus)
10 case invalidData
11 
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: data
30 ]
31 
32 if requireBiometric {
33 let access = SecAccessControlCreateWithFlags(
34 nil,
35 kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly,
36 .biometryCurrentSet,
37 nil
38 )!
39 query[kSecAttrAccessControl as String] = access
40 } else {
41 query[kSecAttrAccessible as String] = kSecAttrAccessibleWhenUnlockedThisDeviceOnly
42 }
43 
44 // Önce sil
45 let deleteQuery: [String: Any] = [
46 kSecClass as String: kSecClassGenericPassword,
47 kSecAttrAccount as String: key
48 ]
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: kSecMatchLimitOne
63 ]
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.itemNotFound
70 }
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: key
79 ]
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 LocalAuthentication
2 
3final class BiometricAuthService {
4 enum BiometricType {
5 case faceID, touchID, none
6 }
7 
8 var availableBiometric: BiometricType {
9 let context = LAContext()
10 guard context.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: nil) else {
11 return .none
12 }
13 switch context.biometryType {
14 case .faceID: return .faceID
15 case .touchID: return .touchID
16 case .opticID: return .faceID // Vision Pro
17 @unknown default: return .none
18 }
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: reason
29 )
30 }
31}
32 
33// Kullanım
34let 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 oku
39 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 belirleme
2func 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 pinning
2class SSLPinningDelegate: NSObject, URLSessionDelegate {
3 private let pinnedHashes: Set<String> = [
4 "BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB=", // Primary
5 "CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC=" // Backup
6 ]
7 
8 func urlSession(
9 _ session: URLSession,
10 didReceive challenge: URLAuthenticationChallenge
11 ) 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 Data
32 var hash = [UInt8](repeating: 0, count: 32)
33 data.withUnsafeBytes { buffer in
34 CC_SHA256(buffer.baseAddress, CC_LONG(buffer.count), &hash)
35 }
36 return Data(hash).base64EncodedString()
37 }
38}

6. CryptoKit ile Şifreleme

swift
1import CryptoKit
2 
3struct EncryptionService {
4 // AES-GCM şifreleme
5 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.encryptionFailed
9 }
10 return combined
11 }
12 
13 // AES-GCM çözme
14 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: 32
27 )
28 return derived
29 }
30 
31 enum CryptoError: Error {
32 case encryptionFailed
33 }
34}

7. App Transport Security

xml
1
2NSAppTransportSecurity
3
4
5 NSExceptionDomains
6
7 legacy-api.example.com
8
9 NSExceptionAllowsInsecureHTTPLoads
10
11 NSExceptionMinimumTLSVersion
12 TLSv1.2
13
14
15

8. Jailbreak Detection

swift
1struct SecurityChecker {
2 static var isJailbroken: Bool {
3 #if targetEnvironment(simulator)
4 return false
5 #else
6 // 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 jailbreak
23 } 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 false
33 #endif
34 }
35}

9. Secure Coding Practices

swift
1// 1. Hassas veriyi bellekten temizle
2func processPassword(_ password: String) {
3 var mutablePassword = Array(password.utf8)
4 defer {
5 // Kullanımdan sonra sıfırla
6 for i in mutablePassword.indices {
7 mutablePassword[i] = 0
8 }
9 }
10 // Password ile işlem yap
11}
12 
13// 2. Clipboard güvenliği
14UIPasteboard.general.setItems(
15 [[UIPasteboard.typeAutomatic: sensitiveText]],
16 options: [.expirationDate: Date().addingTimeInterval(60)] // 60sn sonra sil
17)
18 
19// 3. Screenshot koruması
20extension UIWindow {
21 func makeSecure() {
22 let field = UITextField()
23 field.isSecureTextEntry = true
24 self.addSubview(field)
25 field.centerYAnchor.constraint(equalTo: self.centerYAnchor).isActive = true
26 field.centerXAnchor.constraint(equalTo: self.centerXAnchor).isActive = true
27 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:

Etiketler

#Security#Keychain#Biometric#SSL Pinning#CryptoKit#iOS
Muhittin Çamdalı

Muhittin Çamdalı

Senior iOS Developer

12+ yıllık deneyime sahip iOS Developer. Swift, SwiftUI ve modern iOS mimarileri konusunda uzman. Apple platformlarında performanslı ve kullanıcı dostu uygulamalar geliştiriyorum.

iOS Geliştirme Haberleri

Haftalık Swift tips, SwiftUI tricks ve iOS best practices. Spam yok, sadece değerli içerik.

Gizliliğinize saygı duyuyoruz. İstediğiniz zaman abonelikten çıkabilirsiniz.

Paylaş

Bunu da begenebilirsiniz