Safeguard Data in IOS Apps Using Swift

👍
· Dec 23, 2024 · 4 mins read
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.

Comments

Sharing is caring!