Skip to content
All essays
iOSMarch 27, 202514 min

Swift Security Best Practices: Protect User Data

Implement security best practices in Swift apps. Encryption, keychain, and secure data storage.

Ü
Ümit Uz
Mobile & Full Stack Developer

Security is paramount in iOS app development. Learn to implement encryption, secure storage, and protect user data effectively.

Keychain Services

Store Sensitive Data

swift
import Security
import Foundation

class KeychainManager {
    enum KeychainError: Error {
        case duplicateEntry
        case unknown(OSStatus)
    }

    func store(key: String, data: Data) throws {
        let query: [String: Any] = [
            kSecClass as String: kSecClassGenericPassword,
            kSecAttrAccount as String: key,
            kSecValueData as String: data,
            kSecAttrAccessible as String: kSecAttrAccessibleWhenUnlocked
        ]

        let status = SecItemAdd(query as CFDictionary, nil)

        if status == errSecDuplicateItem {
            throw KeychainError.duplicateEntry
        } else if status != errSecSuccess {
            throw KeychainError.unknown(status)
        }
    }

    func retrieve(key: String) throws -> Data? {
        let query: [String: Any] = [
            kSecClass as String: kSecClassGenericPassword,
            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 else {
            return nil
        }

        return data
    }

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

        let status = SecItemDelete(query as CFDictionary)

        if status != errSecSuccess && status != errSecItemNotFound {
            throw KeychainError.unknown(status)
        }
    }
}

Data Encryption

Encrypt with CryptoKit

swift
import CryptoKit

class EncryptionManager {
    private let key: SymmetricKey

    init() {
        // Generate or load key
        self.key = SymmetricKey(size: .bits256)
    }

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

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

    func encryptString(_ string: String) throws -> String {
        guard let data = string.data(using: .utf8) else {
            return ""
        }
        let encryptedData = try encrypt(data: data)
        return encryptedData.base64EncodedString()
    }

    func decryptString(_ encryptedString: String) throws -> String {
        guard let combined = Data(base64Encoded: encryptedString) else {
            return ""
        }
        let decryptedData = try decrypt(combined: combined)
        return String(data: decryptedData, encoding: .utf8) ?? ""
    }
}

Secure Data Storage

UserDefaults Encryption

swift
class SecureUserDefaults {
    private let keyPrefix = "secure_"
    private let encryptionManager = EncryptionManager()

    func set(_ value: String, forKey key: String) throws {
        let encrypted = try encryptionManager.encryptString(value)
        UserDefaults.standard.set(encrypted, forKey: keyPrefix + key)
    }

    func get(forKey key: String) throws -> String? {
        guard let encrypted = UserDefaults.standard.string(forKey: keyPrefix + key) else {
            return nil
        }
        return try encryptionManager.decryptString(encrypted)
    }
}

Network Security

Certificate Pinning

swift
import Foundation

class SecureURLSessionDelegate: NSObject, URLSessionDelegate {
    func urlSession(
        _ session: URLSession,
        didReceive challenge: URLAuthenticationChallenge,
        completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void
    ) {
        guard let serverTrust = challenge.protectionSpace.serverTrust else {
            completionHandler(.performDefaultHandling, nil)
            return
        }

        // Validate certificate
        let credential = URLCredential(trust: serverTrust)

        if challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust {
            completionHandler(.useCredential, credential)
        } else {
            completionHandler(.performDefaultHandling, nil)
        }
    }
}

SSL Pinning

swift
class CertificatePinner {
    func validateCertificate(for url: URL) -> Bool {
        guard let certificateData = getCertificateData() else {
            return false
        }

        // Compare with server certificate
        return true
    }

    private func getCertificateData() -> Data? {
        guard let certPath = Bundle.main.path(forResource: "certificate", ofType: "cer") else {
            return nil
        }
        return try? Data(contentsOf: URL(fileURLWithPath: certPath))
    }
}

Biometric Authentication

Face ID / Touch ID

swift
import LocalAuthentication

class BiometricAuthManager: ObservableObject {
    @Published var isAuthenticated = false
    @Published var errorMessage: String?

    private let context = LAContext()

    func authenticate() {
        var error: NSError?

        if context.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: &error) {
            let reason = "Authenticate to access secure data"

            context.evaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, localizedReason: reason) { success, error in
                DispatchQueue.main.async {
                    if success {
                        self.isAuthenticated = true
                    } else {
                        self.errorMessage = error?.localizedDescription
                    }
                }
            }
        } else {
            errorMessage = "Biometric authentication not available"
        }
    }

    func getBiometricType() -> String {
        switch context.biometryType {
        case .faceID:
            return "Face ID"
        case .touchID:
            return "Touch ID"
        default:
            return "None"
        }
    }
}

App Transport Security

Configure ATS

xml
<!-- Info.plist -->
<key>NSAppTransportSecurity</key>
<dict>
    <key>NSAllowsArbitraryLoads</key>
    <false/>
    <key>NSExceptionDomains</key>
    <dict>
        <key>api.example.com</key>
        <dict>
            <key>NSExceptionAllowsInsecureHTTPLoads</key>
            <true/>
            <key>NSExceptionMinimumTLSVersion</key>
            <string>TLSv1.2</string>
        </dict>
    </dict>
</dict>

Data Protection

Enable Data Protection

  1. 1Go to Project Settings > Capabilities
  2. 2Enable "Data Protection"
  3. 3Choose protection level:

- Complete Protection (default) - Protected Unless Open - Protected Until First User Authentication

File Protection

swift
class SecureFileManager {
    func writeProtectedData(_ data: Data, to url: URL) throws {
        try data.write(to: url)
        try addFileProtection(to: url)
    }

    private func addFileProtection(to url: URL) throws {
        var attributes = [FileAttributeKey: Any]()
        attributes[.protectionKey] = FileProtectionType.complete

        try FileManager.default.setAttributes(attributes, ofItemAtPath: url.path)
    }
}

Input Validation

Validate User Input

swift
class InputValidator {
    func validateEmail(_ email: String) -> Bool {
        let emailRegex = "[A-Z0-9a-z._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,64}"
        let predicate = NSPredicate(format: "SELF MATCHES %@", emailRegex)
        return predicate.evaluate(with: email)
    }

    func validatePassword(_ password: String) -> Bool {
        // At least 8 characters, 1 uppercase, 1 lowercase, 1 number
        let passwordRegex = "^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d).{8,}$"
        let predicate = NSPredicate(format: "SELF MATCHES %@", passwordRegex)
        return predicate.evaluate(with: password)
    }

    func sanitizeInput(_ input: String) -> String {
        return input
            .trimmingCharacters(in: .whitespacesAndNewlines)
            .filter { $0.isASCII }
    }
}

Best Practices

  1. 1Keychain: Use Keychain for sensitive data
  2. 2Encryption: Encrypt data at rest and in transit
  3. 3HTTPS: Always use HTTPS for network calls
  4. 4Biometrics: Implement biometric authentication
  5. 5Validation: Validate all user inputs
  6. 6Updates: Keep security libraries updated
  7. 7Testing: Test for security vulnerabilities
  8. 8Privacy: Minimize data collection

Security is an ongoing process. Stay vigilant and protect user data!

Related essays

Next essay
Swift Background Tasks: Schedule and Execute