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:
| Industry | Regulations | Key Requirements |
|---|---|---|
| Banking/Finance | PCI-DSS, SOX, GLBA | Encryption, access controls, audit trails |
| Healthcare | HIPAA, HITECH | PHI protection, breach notification |
| Government | FedRAMP, FISMA | Device attestation, data classification |
| Enterprise | SOC 2, ISO 27001 | Access 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 Type | iOS | Android |
|---|---|---|
| API tokens | Keychain | Keystore |
| Refresh tokens | Keychain | Keystore |
| User credentials | Keychain (with biometric) | Keystore (with biometric) |
| Encryption keys | Keychain | Keystore |
| Session data | Keychain | EncryptedSharedPreferences |
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:
- Start with platform defaults — ATS and Network Security Config are excellent baselines
- Add layers based on risk — Device attestation, secure storage, biometrics
- Pin certificates only when required — And always with a kill switch
- Monitor rather than block — Certificate Transparency catches the same threats with less risk
- 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.
