Chapitre 15
15 - Sécurité Mobile
15 - Sécurité Mobile
15 - Sécurité Mobile : Cours complet
Niveau : Intermédiaire → Avancé Durée estimée : 6 h Objectif : sécuriser une application mobile de bout en bout : stockage, transport, biométrie, code, signes, vie privée.
Table des matières
- OWASP Mobile Top 10
- Stockage sécurisé
- Biométrie
- TLS et certificate pinning
- Sécurité des deep links
- Anti-tampering
- ProGuard / R8
- Code signing
- Config sécurisée et dépendances
- Vie privée
1. OWASP Mobile Top 10
1.1 Les 10 risques (2023-2026)
| # | Risque | Description |
|---|---|---|
| M1 | Mauvaises pratiques de credentials | Mots de passe faibles, stockage de tokens |
| M2 | Stockage non sécurisé des données | Données sensibles en clair dans SQLite/SharedPrefs/logs |
| M3 | Communication non sécurisée | HTTP, TLS faible, certs non vérifiés |
| M4 | Mauvaise gestion de session | Tokens statiques, pas d'expiration |
| M5 | Code d'authentification faible | 2FA absent, biométrie mal intégrée |
| M6 | Reverse engineering | APK décompilable, logique exposée |
| M7 | Injection (client) | SQLi, XSS dans les WebViews, code injection |
| M8 | Décisions non fiables côté client | Règles de sécurité dans l'app au lieu du serveur |
| M9 | Gestion incorrecte de la plateforme | Mauvais usage des permissions, WebViews, intents |
| M10 | Attaques du réseau / MITM | DNS poisoning, Wi-Fi rogue, interception TLS |
1.2 La carte de la sécurité mobile
Diagramme en cours de génération...
1.3 Le principe fondamental
Le client mobile n'est jamais fiable. Toutes les règles de sécurité importantes doivent être ré-enforcées côté serveur (M8).
2. Stockage sécurisé
2.1 La hiérarchie de sécurité
| Donnée | Solution |
|---|---|
| Secrets app (clés API, secrets de signature) | Ne pas stocker dans l'app (déplacer vers le serveur) ou Keystore/Keychain |
| Tokens de session | Keystore / Keychain |
| Mots de passe | Ne JAMAIS stocker (hash côté serveur) ; stocker que le token |
| Données personnelles | Chiffrées at-rest (SQLCipher, EncryptedSharedPreferences) |
| Données publiques | SQLite/DataStore normaux |
2.2 Android Keystore
Le Keystore Android conserve des clés de chiffrement dans un conteneur matériel ou logiciel sécurisé. L'app ne manipule jamais la clé en clair : elle demande des opérations de chiffrement.
class KeystoreProvider {
private val keyStore = KeyStore.getInstance("AndroidKeyStore").apply { load(null) }
fun getOrCreateKey(alias: String = "app_key"): SecretKey {
keyStore.getKey(alias, null)?.let { return it as SecretKey }
val generator = KeyGenerator.getInstance(
KeyProperties.KEY_ALGORITHM_AES, "AndroidKeyStore"
)
generator.init(
KeyGenParameterSpec.Builder(
alias,
KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT
)
.setBlockModes(KeyProperties.BLOCK_MODE_GCM)
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
.setUserAuthenticationRequired(false) // option biométrie : true
.build()
)
return generator.generateKey()
}
fun encrypt(data: ByteArray): ByteArray {
val cipher = Cipher.getInstance("AES/GCM/NoPadding")
cipher.init(Cipher.ENCRYPT_MODE, getOrCreateKey())
return cipher.iv + cipher.doFinal(data)
}
}
2.3 EncryptedSharedPreferences
AndroidX fournit une version chiffrée de SharedPreferences (clés AES + HMAC) :
val prefs = EncryptedSharedPreferences.create(
context,
"secure_prefs",
MasterKeys.getOrCreate(MasterKeys.AES256_GCM_SPEC),
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
)
prefs.edit().putString("refresh_token", refreshToken).apply()
2.4 iOS Keychain
Le Keychain est l'équivalent iOS : stockage chiffré géré par le système (Secure Enclave pour les clés).
import Security
struct KeychainStore {
static func save(key: String, value: String) throws {
let data = Data(value.utf8)
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrAccount as String: key,
kSecValueData as String: data,
kSecAttrAccessible as String: kSecAttrAccessibleWhenUnlockedThisDeviceOnly,
]
SecItemDelete(query as CFDictionary) // nettoyer l'existant
let status = SecItemAdd(query as CFDictionary, nil)
guard status == errSecSuccess else { throw KeychainError(status: status) }
}
static func read(key: String) throws -> String? {
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 != errSecItemNotFound else { return nil }
guard status == errSecSuccess, let data = result as? Data else {
throw KeychainError(status: status)
}
return String(data: data, encoding: .utf8)
}
}
Points clés :
kSecAttrAccessibleWhenUnlockedThisDeviceOnly: pas de sync iCloud, accès seulement déverrouillé.- Ne pas mettre de secrets en
UserDefaults.
2.5 Cross-platform
| Plateforme | Lib |
|---|---|
| Flutter | flutter_secure_storage (Keystore/Keychain sous le capot) |
| React Native | react-native-keychain / expo-secure-store |
| KMP | multiplatform-settings + secure storage |
3. Biométrie
3.1 Pourquoi la biométrie ?
- UX (pas de mot de passe à retaper)
-
- sécurité (l'authentification est liée au device)
- La clé n'est utilisable que si la biométrie est vérifiée (Keystore
setUserAuthenticationRequired(true))
3.2 Android — BiometricPrompt
class BiometricAuth(
private val fragmentActivity: FragmentActivity,
private val onSuccess: (Cipher) -> Unit,
private val onError: (String) -> Unit
) {
fun authenticate() {
val promptInfo = BiometricPrompt.PromptInfo.Builder()
.setTitle("Déverrouiller TaskFlow")
.setSubtitle("Utilisez votre empreinte ou Face")
.setNegativeButtonText("Annuler")
.build()
val cipher = CryptoHelper.getCipherWithBiometrics() // clé Keystore biométrique
val crypto = BiometricPrompt.CryptoObject(cipher)
BiometricPrompt(fragmentActivity, object : BiometricPrompt.AuthenticationCallback() {
override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
onSuccess(result.cryptoObject?.cipher ?: cipher)
}
override fun onAuthenticationFailed() { /* essai échoué */ }
override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
onError(errString.toString())
}
}).authenticate(promptInfo, crypto)
}
}
La clé créée avec setUserAuthenticationRequired(true) (avec ou sans setInvalidatedByBiometricEnrollment) garantit que le déchiffrement n'est possible qu'après une vérification biométrique.
3.3 iOS — LocalAuthentication
import LocalAuthentication
final class BiometricAuth {
private let context = LAContext()
var canEvaluate: Bool {
var error: NSError?
return context.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: &error)
}
func authenticate() async throws -> Bool {
let reason = "Déverrouillez TaskFlow pour continuer"
return try await context.evaluatePolicy(
.deviceOwnerAuthenticationWithBiometrics,
localizedReason: reason
)
}
}
Face ID : il faut déclarer NSFaceIDUsageDescription dans Info.plist, sinon l'app crash au premier appel.
3.4 La biométrie et le chiffrement : le pattern crypto+biométrie
Le flux complet :
- Créer une clé Keystore/Keychain avec exigence biométrique.
- Chiffrer un secret (refresh token) avec cette clé.
- À l'usage : prompter la biométrie → le système déchiffre.
- Si l'utilisateur change d'empreinte/Face (enrollment), la clé est invalidée → demander une re-auth forte.
3.5 Les pièges
- Fallback : toujours proposer le code PIN/schéma (policy
.deviceOwnerAuthentication). - Ne jamais traiter la biométrie comme une authentification à 100 % serveur — c'est une authentification locale ; le serveur garde la main (M5).
- Tester les cas d'erreur (5 échecs → fallback).
4. TLS et certificate pinning
4.1 TLS correct
- HTTPS partout.
- TLS 1.2 minimum, TLS 1.3 préféré.
- Android :
network_security_config.xmlpour interdire le cleartext. - iOS : ATS (App Transport Security) actif par défaut.
<!-- Android network_security_config.xml -->
<network-security-config>
<base-config cleartextTrafficPermitted="false">
<trust-anchors>
<certificates src="system"/>
</trust-anchors>
</base-config>
</network-security-config>
<!-- iOS Info.plist : ATS -->
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>
<false/>
</dict>
4.2 Certificate pinning (rappel chapitre 12)
Épingler le hash de la clé publique :
val pinner = CertificatePinner.Builder()
.add("api.taskflow.app", "sha256/AAAA", "sha256/BBBB") // + fallback
.build()
Importants :
- Pinner en production uniquement (dev/staging avec des configs séparées).
- Prévoir la rotation : double pin + délai de grâce.
- En cas de pin mal configuré, toute la base utilisateurs perd l'accès — attention !
5. Sécurité des deep links
5.1 Le risque
Un deep link mal configuré peut :
- rediriger l'utilisateur vers des pages de phishing,
- déclencher des actions sans consentement,
- être intercepté par une autre app malveillante.
5.2 Android — Verified App Links
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:scheme="https"
android:host="taskflow.app"
android:pathPrefix="/tickets/" />
</intent-filter>
Ajouter le fichier assetlinks.json sur le domaine et déclarer android:autoVerify="true" : Android ne laisse que la vraie app gérer le domaine.
5.3 iOS — Universal Links
- Configurer les Associated Domains (
applinks:taskflow.app). - Héberger le fichier
apple-app-site-associationsur le domaine. - Dans
onContinueUserActivity: vérifier laURLet leURLComponents(jamais utiliser la URL brute sans validation).
func application(_ app: UIApplication,
continue userActivity: NSUserActivity,
restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void) -> Bool {
guard userActivity.activityType == NSUserActivityTypeBrowsingWeb,
let url = userActivity.webpageURL,
url.host == "taskflow.app" else { return false }
// analyser le path ET valider les paramètres
return Router.handle(url)
}
5.4 Les règles deep links
- Toujours valider la source (host, path) et les paramètres.
- Ne jamais repasser par JavaScript/WebView sans contrôle.
- Pour les actions sensibles (paiement, suppression) : exiger une ré-authentification.
- Utiliser les schemes custom (
taskflow://) avec parcimonie (moins sûrs que https/universal links).
6. Anti-tampering
6.1 Qu'est-ce qu'un appareil compromis ?
- Root / jailbreak : l'attaquant a les droits admin sur le device.
- App modifiée : re-signée, code injecté.
- Debugger attaché : inspection en temps réel.
6.2 Détections courantes (légitimes en contexte de sécurité)
// Android — détection root (version simplifiée)
fun isRooted(): Boolean {
val paths = arrayOf(
"/system/app/Superuser.apk",
"/sbin/su",
"/system/bin/su",
"/system/xbin/su"
)
return paths.any { File(it).exists() } || runCatching {
Runtime.getRuntime().exec("su").apply { destroy() } // tentative d'exécution
}.isSuccess
}
// iOS — détection jailbreak (version simplifiée)
func isJailbroken() -> Bool {
let paths = [
"/Applications/Cydia.app",
"/usr/sbin/sshd",
"/bin/bash",
]
return paths.contains { FileManager.default.fileExists(atPath: $0) }
}
6.3 Les limites (à assumer)
La détection de root/jailbreak est bypassable (cat-and-mouse). Vaut mieux :
- Refuser/limiter certaines actions sensibles sur device compromis (transaction haute valeur).
- Défendre en profondeur : chiffrement, serveur, monitoring (fraude).
- Ne pas pénaliser les utilisateurs simplement « rootés » de bonne foi (paiement, etc.).
7. ProGuard / R8
7.1 Le rôle
- Minification : supprimer les classes/membres inutilisés → APK plus petit.
- Obfuscation : renommer les classes/champs en
a,b… → reverse engineering plus dur. - Optimisation : inlining, élimination de code mort.
7.2 Configuration Android
// build.gradle.kts (module)
android {
buildTypes {
release {
isMinifyEnabled = true
isShrinkResources = true
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
}
}
# proguard-rules.pro
# Conserver les modèles sérialisés (kotlinx / Moshi)
-keep class com.taskflow.app.domain.model.** { *; }
# Conserver les annotations Retrofit (interfaces API)
-keep interface com.taskflow.app.data.remote.** { *; }
# Règles des libs (Room, Retrofit...) via les consumer rules
7.3 Les pièges
- Oublier les règles pour les libs utilisant la réflexion (Retrofit, Room, kotlinx.serialization).
- Tester la release build (comportements différents de debug).
- La sauvegarde des stack traces : mapper le stacktrace dé-obfusqué (
retrace) dans Crashlytics.
7.4 iOS — le cas particulier
iOS n'a pas d'équivalent R8 pour l'App Store (le code est compilé en natif). La strip des symboles (STRIP_INSTALLED_PRODUCT) et le Bitcode (abandonné) limitaient la rétro-ingénierie. La protection iOS repose surtout sur : compilation native, code signing, et l'effort OWASP M6 reste plus faible que sur Android.
8. Code signing
8.1 Android — APK/AAB signing
- Générer un keystore (fichier
.jks+ alias + passwords) → à protéger absolument (gitignored, secret manager en CI). app signing by Google Play(Play Console) : la clé d'app est conservée par Google, vous gardez la clé de upload.- Ne jamais perdre la clé d'upload : sans elle, pas de mise à jour.
// build.gradle.kts
android {
signingConfigs {
create("release") {
storeFile = file("keystore/taskflow.jks")
storePassword = System.getenv("KEYSTORE_PASSWORD")
keyAlias = "taskflow"
keyPassword = System.getenv("KEY_PASSWORD")
}
}
buildTypes {
release { signingConfig = signingConfigs.getByName("release") }
}
}
8.2 iOS — Certificats et provisioning
- Certificat de distribution (signe le binaire) — géré par Developer Account.
- Provisioning profile (lie app + device/capabilities).
- Xcode 14+ : le certificat peut être géré automatiquement (Signing & Capabilities).
8.3 Fastlane match (voir chapitre 16)
match stocke les certificats/profiles chiffrés dans un repo et les synchronise entre machines — le standard des équipes.
9. Config sécurisée et dépendances
9.1 Les secrets ne vont PAS dans l'app
| Mauvaise pratique | Alternative |
|---|---|
apiKey = "abc123" en clair dans le code | Clé serveur (l'app appelle votre API qui cache ses secrets) |
| Clé API Firebase dans le build | Firebase config protégée + règles de sécurité côté serveur |
Secrets dans local.properties | Déjà gitignored — mais pas livrés non plus ! |
La règle : aucun secret qui vaille la peine ne doit être embarqué dans un APK/IPA (facilement décompilable).
9.2 Les builds de configuration
// Android BuildConfig (minifié en release)
buildConfigField("String", "API_URL", "\"https://api.taskflow.app\"")
Utiliser des environnements séparés (dev/staging/prod) avec des URLs/configs différentes, jamais les secrets de prod en debug.
9.3 Scanner les dépendances
| Outil | Rôle |
|---|---|
| Dependabot (GitHub) | Alertes de vulnérabilités |
| Renovate | Mises à jour automatiques des deps |
| osv-scanner | Scan des vulnérabilités (OWASP + Google) |
| Snyk / Trivy | Scan applicatif |
# Exemple : osv-scanner
osv-scanner scan -r .
9.4 Gradle — plugin de sécurité (bonus)
dependency-check-gradle(OWASP) pour le scan en CI.- Toujours garder Gradle/AGP/NDK à jour.
10. Vie privée
10.1 GDPR et le mobile
- Minimisation : ne collecter que le nécessaire.
- Consentement : obtenir avant de collecter (notamment pour les analytiques).
- Droit à l'effacement : prévoir la suppression des données utilisateur.
- Data Protection Impact Assessment pour les données sensibles.
10.2 Permissions Android
- Principle of least privilege : ne demander que ce qui est nécessaire.
- Runtime permissions : demander au moment de l'usage, avec explication.
- Fichier
android:maxSdkVersionpour limiter les permissions obsolètes.
10.3 iOS Privacy
- App Privacy labels (App Store) : déclarer les données collectées.
- App Tracking Transparency (ATT) : demander l'autorisation de tracking via
ATTrackingManager.requestTrackingAuthorization(). - Photo/Contact/Microphone : requêtes runtime avec
NS*UsageDescription.
10.4 La checklist vie privée
- Consentement explicite avant la collecte
- Données chiffrées at-rest et en transit
- Minimisation des données
- Droit d'effacement implémenté
- Labels de confidentialité à jour (iOS)
- Permissions demandées au bon moment
- Analytics anonymisés ou pseudonymisés
- Pas de tracking sans consentement (ATT / EU)
11. Synthèse
Diagramme en cours de génération...
Points à retenir :
- Le client n'est jamais fiable → sécurité serveur.
- Les secrets sensibles n'existent pas dans l'app ; les tokens vont dans Keystore/Keychain.
- TLS + pinning pour le transport.
- Deep links validés et ré-authentification pour les actions sensibles.
- R8 activé en release, stack traces dé-obfusquées.
- Code signing maîtrisé et protégé.
- GDPR : minimisation, consentement, effacement.
Aller plus loin
- Chapitre 16 (DevOps) : signing dans la CI, match, scan de deps.
- Chapitre 18 (Projet-Fil-Rouge) : la sécurité complète de TaskFlow.