Security14 min read

Mobile Security for Regulated Industries: A Practical Guide

Security strategies for banking, healthcare, and enterprise mobile apps — when to use certificate pinning, modern alternatives, and compliance essentials.

Security Requirements Vary by Context

Not every mobile app needs the same security posture. A casual game has different requirements than a banking app handling financial transactions. This guide focuses on security practices for regulated industries — banking, healthcare, government, and enterprise — where compliance requirements and data sensitivity demand stronger measures.

For general consumer apps, modern platform defaults (App Transport Security, Network Security Config) often provide sufficient protection. But when you're handling PHI, PCI data, or sensitive enterprise information, you need to go further.

Understanding Your Security Requirements

Before implementing security measures, understand your compliance landscape:

IndustryRegulationsKey Requirements
Banking/FinancePCI-DSS, SOX, GLBAEncryption, access controls, audit trails
HealthcareHIPAA, HITECHPHI protection, breach notification
GovernmentFedRAMP, FISMADevice attestation, data classification
EnterpriseSOC 2, ISO 27001Access management, encryption

Transport Security: The Foundation

Platform Defaults Are Your Baseline

Both iOS and Android now enforce secure connections by default:

iOS App Transport Security (ATS):

<!-- ATS is enabled by default in iOS 9+ -->
<!-- Only configure if you need exceptions (not recommended) -->
<key>NSAppTransportSecurity</key>
<dict>
    <key>NSAllowsArbitraryLoads</key>
    <false/>
</dict>

Android Network Security Config:

<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
    <base-config cleartextTrafficPermitted="false">
        <trust-anchors>
            <certificates src="system" />
        </trust-anchors>
    </base-config>
</network-security-config>

For most apps, these defaults combined with TLS 1.3 and Certificate Transparency provide strong protection.

Certificate Pinning: When It Makes Sense

Certificate pinning has become controversial. Here's the nuanced take:

When TO Use Certificate Pinning:

  • Banking apps processing financial transactions
  • Healthcare apps handling PHI
  • Apps where MITM attacks have severe consequences
  • Regulatory requirements mandate it (some PCI-DSS interpretations)
  • High-value targets (government, defense contractors)

When NOT To Use Certificate Pinning:

  • General consumer apps
  • Apps used in enterprise environments with SSL inspection proxies
  • When you can't guarantee timely app updates for cert rotation
  • When Certificate Transparency monitoring is sufficient

The Risks of Pinning:

  • Certificate rotation requires app updates — missed updates = app outage
  • Enterprise proxies/firewalls break pinned connections
  • Debugging and testing become more complex
  • Recovery from pinning mistakes is slow (App Store review times)

If You Must Pin: Do It Right

// Pin to Subject Public Key Info (SPKI) hash
// Survives certificate rotation if the key pair remains the same
class CertificatePinningManager {
    // Always include backup pins!
    private let pinnedHashes: Set<String> = [
        "sha256/CURRENT_CERT_HASH_HERE=",
        "sha256/BACKUP_CERT_HASH_HERE=",  // Different CA
        "sha256/DISASTER_RECOVERY_HASH=", // Emergency backup
    ]

    // Implement graceful degradation
    private let pinningEnabled: Bool = RemoteConfig.shared.isPinningEnabled

    func validateCertificate(_ trust: SecTrust) -> Bool {
        // Feature flag to disable pinning in emergencies
        guard pinningEnabled else { return true }

        guard let serverCert = SecTrustGetCertificateAtIndex(trust, 0) else {
            return false
        }

        let serverHash = computeSPKIHash(serverCert)
        return pinnedHashes.contains(serverHash)
    }
}

Critical: Always include a remote kill switch for pinning. When (not if) something goes wrong with certificate rotation, you need to disable pinning without an app update.

Modern Alternative: Certificate Transparency Monitoring

Instead of pinning, consider monitoring Certificate Transparency logs:

// Monitor CT logs for unauthorized certificates
class CertificateTransparencyMonitor {
    func checkCertificateTransparency(for domain: String) async -> Bool {
        // Query CT logs for certificates issued for your domain
        // Alert if unexpected certificates appear
        // This catches CA compromise without breaking your app
    }
}

Certificate Transparency catches the same attacks as pinning (rogue CA certificates) without the operational risks.

TLS Configuration

Enforce modern TLS versions:

let configuration = URLSessionConfiguration.default
configuration.tlsMinimumSupportedProtocolVersion = .TLSv12

// Prefer TLS 1.3 when available
if #available(iOS 16.0, *) {
    configuration.tlsMinimumSupportedProtocolVersion = .TLSv13
}

Device Attestation

Device attestation verifies that your app is running on a genuine, untampered device.

iOS: App Attest

import DeviceCheck

class AppAttestManager {
    private let service = DCAppAttestService.shared

    func attestDevice() async throws -> Data {
        guard service.isSupported else {
            throw AttestError.notSupported
        }

        // Generate key
        let keyId = try await service.generateKey()

        // Get challenge from server
        let challenge = try await fetchChallenge()

        // Create attestation
        let attestation = try await service.attestKey(keyId,
            clientDataHash: challenge.sha256)

        // Send attestation to server for verification
        return attestation
    }
}

Android: Play Integrity API

class PlayIntegrityManager(private val context: Context) {

    suspend fun requestIntegrityVerdict(nonce: String): IntegrityTokenResponse {
        val integrityManager = IntegrityManagerFactory.create(context)

        val request = IntegrityTokenRequest.builder()
            .setNonce(nonce)
            .build()

        return integrityManager.requestIntegrityToken(request).await()
    }
}

Server-Side Verification

Never trust attestation results verified client-side. Always verify on your server:

// Server-side attestation verification (Node.js)
async function verifyAttestation(attestation, challenge) {
    // Verify attestation signature
    const isValid = await verifySignature(attestation);

    // Verify challenge matches
    const challengeMatches = attestation.challenge === challenge;

    // Check device properties
    const deviceTrusted = attestation.deviceIntegrity.includes('MEETS_DEVICE_INTEGRITY');

    return isValid && challengeMatches && deviceTrusted;
}

Secure Storage

Keychain (iOS)

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

        let status = SecItemAdd(query as CFDictionary, nil)
        guard status == errSecSuccess else {
            throw SecureStorageError.writeFailed(status)
        }
    }
}

Keystore (Android)

class SecureStorage(private val context: Context) {
    private val keyStore = KeyStore.getInstance("AndroidKeyStore").apply { load(null) }

    fun storeSecret(alias: String, data: ByteArray) {
        val keyGenerator = KeyGenerator.getInstance(
            KeyProperties.KEY_ALGORITHM_AES,
            "AndroidKeyStore"
        )

        keyGenerator.init(
            KeyGenParameterSpec.Builder(
                alias,
                KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT
            )
            .setBlockModes(KeyProperties.BLOCK_MODE_GCM)
            .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
            .setUserAuthenticationRequired(true)
            .build()
        )

        val key = keyGenerator.generateKey()
        // Encrypt and store data
    }
}

What to Store Securely

Data TypeiOSAndroid
API tokensKeychainKeystore
Refresh tokensKeychainKeystore
User credentialsKeychain (with biometric)Keystore (with biometric)
Encryption keysKeychainKeystore
Session dataKeychainEncryptedSharedPreferences

Authentication Security

OAuth 2.0 / OpenID Connect

Use industry-standard authentication protocols:

// Use ASWebAuthenticationSession for OAuth flows
class AuthManager {
    func authenticate() async throws -> OAuthToken {
        let authURL = buildAuthorizationURL(
            clientId: Config.oauthClientId,
            redirectUri: Config.redirectUri,
            scope: "openid profile email",
            state: generateState(),
            codeChallenge: generatePKCEChallenge()
        )

        let callbackURL = try await ASWebAuthenticationSession(
            url: authURL,
            callbackURLScheme: Config.callbackScheme
        ).start()

        return try await exchangeCodeForToken(from: callbackURL)
    }
}

PKCE (Proof Key for Code Exchange)

Always use PKCE for mobile OAuth:

func generatePKCEChallenge() -> (verifier: String, challenge: String) {
    let verifier = generateRandomString(length: 64)
    let challengeData = verifier.data(using: .utf8)!.sha256
    let challenge = challengeData.base64URLEncodedString()

    return (verifier, challenge)
}

Biometric Authentication

Protect sensitive operations with biometrics:

class BiometricAuth {
    func authenticate(reason: String) async throws -> Bool {
        let context = LAContext()

        var error: NSError?
        guard context.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics,
                                         error: &error) else {
            throw BiometricError.notAvailable
        }

        return try await context.evaluatePolicy(
            .deviceOwnerAuthenticationWithBiometrics,
            localizedReason: reason
        )
    }
}

Anti-Tampering

Jailbreak / Root Detection

Detect compromised devices:

class DeviceIntegrityChecker {
    func isDeviceCompromised() -> Bool {
        // Check for common jailbreak indicators
        let suspiciousPaths = [
            "/Applications/Cydia.app",
            "/private/var/lib/apt",
            "/private/var/stash",
            "/usr/sbin/sshd",
            "/etc/apt"
        ]

        for path in suspiciousPaths {
            if FileManager.default.fileExists(atPath: path) {
                return true
            }
        }

        // Check if app can write outside sandbox
        let testPath = "/private/jailbreak_test.txt"
        do {
            try "test".write(toFile: testPath, atomically: true, encoding: .utf8)
            try FileManager.default.removeItem(atPath: testPath)
            return true
        } catch {
            // Expected — can't write outside sandbox
        }

        return false
    }
}

Binary Protection

  • Code obfuscation — Make reverse engineering harder
  • Anti-debugging — Detect debugger attachment
  • Integrity checks — Verify binary hasn't been modified

Runtime Protection

Detect and respond to tampering at runtime:

class RuntimeProtection {
    func startMonitoring() {
        // Check at app launch
        performIntegrityChecks()

        // Periodic checks
        Timer.scheduledTimer(withTimeInterval: 60.0, repeats: true) { _ in
            self.performIntegrityChecks()
        }
    }

    private func performIntegrityChecks() {
        if isDebuggerAttached() || isDeviceCompromised() || isBinaryModified() {
            handleTamperingDetected()
        }
    }

    private func handleTamperingDetected() {
        // Log to analytics
        // Clear sensitive data
        // Limit functionality or exit
    }
}

Data Protection

Encryption at Rest

Encrypt sensitive data stored locally:

class DataEncryption {
    func encrypt(_ data: Data, with key: SymmetricKey) throws -> Data {
        let sealedBox = try AES.GCM.seal(data, using: key)
        return sealedBox.combined!
    }

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

Memory Protection

Clear sensitive data from memory:

extension Data {
    mutating func wipe() {
        guard count > 0 else { return }
        withUnsafeMutableBytes { bytes in
            memset(bytes.baseAddress!, 0, count)
        }
    }
}

// Usage
var sensitiveData = password.data(using: .utf8)!
defer { sensitiveData.wipe() }

Screenshot Protection

Prevent sensitive screens from appearing in screenshots:

class SecureWindow: UIWindow {
    override func becomeKey() {
        super.becomeKey()

        if sensitiveContentVisible {
            addSecurityOverlay()
        }
    }

    private func addSecurityOverlay() {
        let field = UITextField()
        field.isSecureTextEntry = true
        self.addSubview(field)
        field.centerYAnchor.constraint(equalTo: centerYAnchor).isActive = true
        field.centerXAnchor.constraint(equalTo: centerXAnchor).isActive = true
        self.layer.superlayer?.addSublayer(field.layer)
        field.layer.sublayers?.first?.addSublayer(self.layer)
    }
}

Security Checklist by Industry

All Regulated Apps (Baseline)

  • HTTPS enforced (ATS/Network Security Config)
  • TLS 1.2+ required (prefer 1.3)
  • Sensitive data in Keychain/Keystore
  • OAuth 2.0 with PKCE
  • Biometric authentication option
  • Session timeout handling
  • Logging sanitization (no PII/PHI in logs)
  • Crash report sanitization

Banking/Finance (Add to Baseline)

  • Certificate pinning with kill switch
  • Device attestation (App Attest / Play Integrity)
  • Jailbreak/root detection
  • Screenshot protection on sensitive screens
  • Transaction signing
  • Fraud detection integration

Healthcare (Add to Baseline)

  • Encryption at rest for all PHI
  • Audit logging for data access
  • Device attestation
  • Remote wipe capability
  • Automatic session timeout (shorter)
  • BAA with all third-party SDKs

Enterprise/Government (Add to Baseline)

  • MDM integration support
  • Certificate pinning (with enterprise proxy exceptions)
  • Device attestation
  • Data classification handling
  • Offline data protection
  • Binary integrity verification

Decision Framework: Do You Need Certificate Pinning?

Conclusion

Security for regulated industries requires going beyond platform defaults, but more security isn't always better security. Certificate pinning can protect against sophisticated attacks, but it can also cause outages that lock users out of their banking apps.

The right approach:

  1. Start with platform defaults — ATS and Network Security Config are excellent baselines
  2. Add layers based on risk — Device attestation, secure storage, biometrics
  3. Pin certificates only when required — And always with a kill switch
  4. Monitor rather than block — Certificate Transparency catches the same threats with less risk
  5. Test your failure modes — What happens when certificates rotate? When attestation fails?

Security is not about implementing every possible measure — it's about implementing the right measures for your threat model and operational reality.


Security practices refined through enterprise mobile development for banking, healthcare, and government clients.

Abraham Jeyaraj

Written by Abraham Jeyaraj

AI-Powered Solutions Architect with 20+ years of experience in enterprise software development.