Safeguard Data in IOS Apps Using Swift
In today’s mobile‑first world, safeguarding our apps’ most sensitive data — whether it lives in memory or on disk — is non‑negotiable. In this guide, we’ll explore how we can protect data in our apps by leveraging Swift‑native APIs and industry best practices.
Why Layered Data Security Matters
Even when network security is handled elsewhere, our apps still faces risks at every other layer:
- In-memory: exposed via debuggers, memory dumps, or heap analysis.
- At-rest: vulnerable if the device storage or backups are compromised.
- Runtime/UI: UI snapshots, clipboard leaks, and crash logs can reveal sensitive information.
A layered approach ensures that if one defense is bypassed, others still protect the users’ data.
In-Memory Protection
Limit Secret Lifespans
func performSecureOperation() {
func operationUsingPassword(_ password: String?) {
...
}
var password: String? = getPasswordFromSomewhere()
operationUsingPassword(password)
// Deallocate password
password = nil
}
Why: Keeps secret data confined to the narrowest possible scope.
Use when: Handling ephemeral tokens or user credentials in functions.
Prefer Data Over String
var secretData = Data("mySecret".utf8)
// Use secretData ...
// Wiping out data
_ = secretData.withUnsafeMutableBytes { bytes in
bytes.initializeMemory(as: UInt8.self, repeating: 0)
}
Why: Allows explicit overwriting of memory contents.
Use when: Managing cryptographic keys or session tokens.
Zero-Out Structs & Buffers
var credentials = MyCredentials()
// Use credentials ...
// Wiping out
memset(&credentials, 0, MemoryLayout.size(ofValue: credentials))
Why: Ensures custom C-style structures don’t retain data after use.
Use when: Working with low-level buffers or interop structs.
At-Rest Protection
File Protection Classes
let data = try Data(contentsOf: fileURL)
try data.write(to: fileURL, options: .completeFileProtection)
Why: Encrypts files on disk so they’re only accessible when the device is unlocked.
Use when: Storing data into files on disk.
UserDefaults Encryption
import CryptoKit
let symmetricKey = SymmetricKey(size: .bits256)
func setEncrypted(_ value: String, forKey key: String) {
let sealedBox = try! ChaChaPoly.seal(Data(value.utf8), using: symmetricKey)
UserDefaults.standard.set(sealedBox.combined, forKey: key)
}
func getDecrypted(forKey key: String) -> String? {
guard let data = UserDefaults.standard.data(forKey: key) else { return nil }
let box = try! ChaChaPoly.SealedBox(combined: data)
let decrypted = try! ChaChaPoly.open(box, using: symmetricKey)
return String(data: decrypted, encoding: .utf8)
}
Why: Prevents easy inspection of the stored values in UserDefaults.
Use when: Storing feature flags, small tokens, or non-critical secrets.
Secure Key & Credential Storage
let accessControl = SecAccessControlCreateWithFlags(
kCFAllocatorDefault,
kSecAttrAccessibleWhenUnlockedThisDeviceOnly,
.privateKeyUsage,
nil
)!
let attributes: [String: Any] = [
kSecClass as String: kSecClassKey,
kSecAttrTokenID as String: kSecAttrTokenIDSecureEnclave,
kSecAttrKeySizeInBits as String: 256,
kSecAttrAccessControl as String: accessControl
]
SecItemAdd(attributes as CFDictionary, nil)
Why: Hardware-backed Secure Enclave keys never leave the enclave.
Use when: Generating or storing cryptographic keys for signing or encryption.
UI & Runtime Defenses
Snapshot Obfuscation
func applicationWillResignActive(_ application: UIApplication) {
let coverView = UIView(frame: window!.bounds)
coverView.backgroundColor = .black
window?.addSubview(coverView)
}
Why: Keeps sensitive screens out of the task switcher.
Use when: Displaying financial, health, or personal data.
Clipboard & Screenshots
// Clear clipboard after use
UIPasteboard.general.items.removeAll() // iOS 14+
// OR
UIPasteboard.general.string = ""
// Detect screenshot
NotificationCenter.default.addObserver(
self,
selector: #selector(userDidTakeScreenshot),
name: UIApplication.userDidTakeScreenshotNotification,
object: nil
)
@objc func userDidTakeScreenshot() {
// Respond to screenshot event
}
Why: Prevents leaks of OTPs, card numbers, or personal info.
Use when: Showing one-time codes or payment details.
Jailbreak & Debugger Detection
import Darwin
func isDebuggerAttached() -> Bool {
var info = kinfo_proc()
var mib = [CTL_KERN, KERN_PROC, KERN_PROC_PID, getpid()]
var size = MemoryLayout<kinfo_proc>.stride
sysctl(&mib, u_int(mib.count), &info, &size, nil, 0)
return (info.kp_proc.p_flag & P_TRACED) != 0
}
// Indicative solution, can also consider other open source or commercial frameworks for this
func detectJailbreak() -> Bool {
let paths = ["/Applications/Cydia.app",
"/Library/MobileSubstrate/MobileSubstrate.dylib"]
return paths.contains { FileManager.default.fileExists(atPath: $0) }
}
Why: Mitigates runtime tampering and reverse engineering.
Use when: Apps that must enforce strict runtime integrity.
Logging & Analytics Hygiene
import os
let logger = Logger(subsystem: "my system", category: "my category")
// Configure privacy settings if needed (default behavior):
// Sensitive data can be marked with .private; other values with .public
func trackEvent(_ name: String, properties: [String: String]) {
// Log event name as public
logger.debug("Event: \(name, privacy: .public)")
// Iterate through properties, marking privacy appropriately
for (key, value) in properties {
if key == "userId" {
// Mark userId as private
logger.debug("Property: \(key) = \(value), privacy: .private)")
} else {
// Non-sensitive properties can be public
logger.debug("Property: \(key) = \(value), privacy: .public)")
}
}
}
Why: Prevents accidental leaks via logs or telemetry.
Use when: Any diagnostic logging or usage tracking.
Conclusion
Safeguarding data within an iOS app requires vigilance far beyond encrypting network traffic. The methods in this article can significantly help reduce the attack surface available to malicious actors. Implement them consistently, review them regularly, and stay informed on emerging platform features to keep the app resilient against evolving threats.
Sharing is caring!