Aller au contenu

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

async function storeMasterKey(kMaster: Uint8Array): Promise<KeychainResult<void>>

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

async function getMasterKey(): Promise<KeychainResult<Uint8Array>>

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

async function deleteMasterKey(): Promise<KeychainResult<void>>

Supprime K_master du Keychain. Utilisé lors du logout ou reset.

existsMasterKey

async function existsMasterKey(): Promise<boolean>

Vérifie si K_master existe sans la charger en mémoire.

clearMasterKeyBuffer

function clearMasterKeyBuffer(kMaster: Uint8Array): void

Efface un buffer K_master en mémoire (best effort).

Types

KeychainResult

interface KeychainResult<T> {
  success: boolean;
  data?: T;
  error?: KeychainError;
}

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

  1. Pas de logs de K_master - La valeur n'est jamais loguée
  2. Options Keychain strictes - WHEN_UNLOCKED_THIS_DEVICE_ONLY
  3. Pas de fallback - Si SecureStore échoue, on échoue aussi
  4. Validation de taille - Exactement 32 bytes requis
  5. 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

  1. Toujours effacer après utilisation :
const result = await getMasterKey();
if (result.success && result.data) {
  try {
    // Utiliser result.data
  } finally {
    clearMasterKeyBuffer(result.data);
  }
}
  1. Vérifier l'existence avant de stocker :
if (await existsMasterKey()) {
  // Demander confirmation avant d'écraser
}
  1. 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)

Références