PD-98 : Stockage K_master dans iOS Keychain¶
Vue d'ensemble¶
Cette fonctionnalité implémente le stockage sécurisé de la clé maître (K_master) dans le iOS Keychain via expo-secure-store, avec l'attribut kSecAttrAccessibleWhenUnlockedThisDeviceOnly pour une sécurité maximale.
Objectifs de sécurité¶
- Stockage sécurisé de K_master (256 bits / 32 bytes)
- Accessible uniquement quand l'appareil est déverrouillé
- Non exportable (pas de sync iCloud, pas de migration entre appareils)
- Aucune fuite de données sensibles dans les logs
- Pas de fallback vers AsyncStorage/MMKV
Architecture¶
┌─────────────────────────────────────────────────────────────┐
│ Application React Native │
│ │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ keychainStorage.ts │ │
│ │ ┌─────────────────────────────────────────────────┐ │ │
│ │ │ API Publique: │ │ │
│ │ │ • storeMasterKey(kMaster: Uint8Array) │ │ │
│ │ │ • getMasterKey(): KeychainResult<Uint8Array> │ │ │
│ │ │ • deleteMasterKey(): KeychainResult<void> │ │ │
│ │ │ • existsMasterKey(): boolean │ │ │
│ │ │ • clearMasterKeyBuffer(buffer) │ │ │
│ │ └─────────────────────────────────────────────────┘ │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ ┌─────────────────────────────────────────────────┐ │ │
│ │ │ expo-secure-store │ │ │
│ │ │ keychainAccessible: │ │ │
│ │ │ WHEN_UNLOCKED_THIS_DEVICE_ONLY │ │ │
│ │ └─────────────────────────────────────────────────┘ │ │
│ └────────────────────────────────────────────────────────┘ │
│ │ │
└──────────────────────────────┼───────────────────────────────┘
▼
┌─────────────────────────────────────────────────────────────┐
│ iOS Keychain │
│ ┌─────────────────────────────────────────────────────────┐│
│ │ Attributs de sécurité: ││
│ │ • kSecAttrAccessibleWhenUnlockedThisDeviceOnly ││
│ │ • Pas de synchronisation iCloud ││
│ │ • Non exportable via backup ││
│ │ • Effacé si appareil réinitialisé ││
│ └─────────────────────────────────────────────────────────┘│
└─────────────────────────────────────────────────────────────┘
Attribut Keychain : kSecAttrAccessibleWhenUnlockedThisDeviceOnly¶
Cet attribut offre le niveau de protection le plus élevé pour les données sensibles :
| Propriété | Valeur |
|---|---|
| Accessible | Uniquement quand appareil déverrouillé |
| Synchronisation iCloud | Non |
| Migration backup | Non |
| Effacement reset | Oui |
| Chiffrement | AES-256 avec clé dérivée du Secure Enclave |
API Publique¶
storeMasterKey¶
Stocke K_master dans le Keychain. La clé doit être exactement 32 bytes.
Exemple :
const kMaster = new Uint8Array(32);
crypto.getRandomValues(kMaster); // ou dérivé via Argon2id
const result = await storeMasterKey(kMaster);
if (!result.success) {
console.error("Erreur:", result.error);
}
getMasterKey¶
Récupère K_master depuis le Keychain.
Exemple :
const result = await getMasterKey();
if (result.success && result.data) {
// Utiliser result.data (Uint8Array de 32 bytes)
// ...
// IMPORTANT: Effacer après utilisation
clearMasterKeyBuffer(result.data);
}
deleteMasterKey¶
Supprime K_master du Keychain. Utilisé lors du logout ou reset.
existsMasterKey¶
Vérifie si K_master existe sans la charger en mémoire.
clearMasterKeyBuffer¶
Efface un buffer K_master en mémoire (best effort).
Types¶
KeychainResult¶
KeychainError¶
type KeychainError =
| "NOT_AVAILABLE" // SecureStore non disponible
| "DEVICE_LOCKED" // Appareil verrouillé (iOS)
| "NOT_FOUND" // Clé non trouvée
| "INVALID_KEY_SIZE" // Taille incorrecte (doit être 32 bytes)
| "ENCODING_ERROR" // Erreur d'encodage/décodage
| "UNKNOWN"; // Erreur inconnue
Gestion des erreurs¶
| Erreur | Cause | Action recommandée |
|---|---|---|
| NOT_AVAILABLE | Web platform ou SecureStore indisponible | Informer l'utilisateur que l'app nécessite un appareil iOS/Android |
| DEVICE_LOCKED | Appareil verrouillé, Face ID/Touch ID échoué | Redemander l'authentification |
| NOT_FOUND | K_master non stockée | Créer un nouveau compte ou reconnecter |
| INVALID_KEY_SIZE | Clé de mauvaise taille | Bug dans le code de dérivation |
| ENCODING_ERROR | Données corrompues | Supprimer et recréer la clé |
| UNKNOWN | Autre erreur | Logger et informer support |
Sécurité¶
Ce qui est fait¶
- Pas de logs de K_master - La valeur n'est jamais loguée
- Options Keychain strictes -
WHEN_UNLOCKED_THIS_DEVICE_ONLY - Pas de fallback - Si SecureStore échoue, on échoue aussi
- Validation de taille - Exactement 32 bytes requis
- Effacement mémoire -
clearMasterKeyBuffer()pour nettoyer
TODO : Améliorations futures¶
// TODO (PD-XX): Activer Face ID/Touch ID comme protection supplémentaire
const KEYCHAIN_OPTIONS = {
keychainAccessible: SecureStore.WHEN_UNLOCKED_THIS_DEVICE_ONLY,
requireAuthentication: true, // <-- À activer
authenticationPrompt: "Déverrouiller pour accéder à votre coffre",
};
Recommandations de sécurité¶
Pourquoi expo-secure-store plutôt que react-native-keychain ?¶
| Critère | expo-secure-store | react-native-keychain |
|---|---|---|
| Compatibilité Expo Go | Oui | Non |
| Options Keychain iOS | Oui | Oui |
| Biométrie optionnelle | Oui | Oui |
| Maintenance | Expo team | Communauté |
| EAS Build | Natif | Nécessite config |
Verdict : expo-secure-store est recommandé pour les projets Expo, offrant toutes les options de sécurité nécessaires avec une meilleure intégration.
Bonnes pratiques¶
- Toujours effacer après utilisation :
const result = await getMasterKey();
if (result.success && result.data) {
try {
// Utiliser result.data
} finally {
clearMasterKeyBuffer(result.data);
}
}
- Vérifier l'existence avant de stocker :
- Gérer les erreurs explicitement :
const result = await getMasterKey();
if (!result.success) {
switch (result.error) {
case 'DEVICE_LOCKED':
// Redemander l'authentification
break;
case 'NOT_FOUND':
// Rediriger vers création compte
break;
default:
// Afficher erreur générique
}
}
Tests¶
Tests unitaires¶
Fichier : src/__tests__/services/keychainStorage.test.ts
- Tests des constantes (KMASTER_KEY, KMASTER_SIZE, KEYCHAIN_OPTIONS)
- Tests des helpers (uint8ToBase64, base64ToUint8, clearBuffer, mapError)
- Tests de storeMasterKey (success, invalid size, not available, errors)
- Tests de getMasterKey (success, not found, corrupted, errors)
- Tests de deleteMasterKey (success, not exists, errors)
- Tests de existsMasterKey (true, false, errors)
- Tests de sécurité (pas de logs sensibles)
Tests d'acceptation¶
Fichier : src/__tests__/services/keychainStorage.acceptance.test.ts
- AC1: kSecAttrAccessibleWhenUnlockedThisDeviceOnly
- AC2: API publique complète
- AC3: Aucune fuite dans les logs
- AC4: Pas de fallback AsyncStorage/MMKV
- AC5: Gestion exhaustive des erreurs
- AC6: Documentation complète
- Scénarios E2E (création compte, reconnexion, déconnexion)
Fichiers¶
src/
├── services/
│ └── keychainStorage.ts # Service principal
├── __mocks__/
│ └── expo-secure-store.ts # Mock Jest (mis à jour)
└── __tests__/
└── services/
├── keychainStorage.test.ts # Tests unitaires
└── keychainStorage.acceptance.test.ts # Tests d'acceptation
docs/
└── security/
└── keychain-kmaster.md # Cette documentation
Compatibilité¶
| Plateforme | Support |
|---|---|
| iOS | Oui (Keychain natif) |
| Android | Oui (Keystore/EncryptedSharedPreferences) |
| Web | Non (retourne NOT_AVAILABLE) |