Article d’ingénierie

Créer un contrôle CI des classes de protection de fichiers iOS

Créer un contrôle CI des classes de protection de fichiers iOS

Le fait que tous les tests réussissent sur un nœud de build distant ne garantit pas que les données écrites sur le disque par l’application bénéficient réellement de la protection attendue. Le problème le plus courant n’est pas l’oubli d’un appel à l’API de chiffrement, mais la perte de la classe de protection initialement définie après un remplacement atomique, une migration de base de données ou la reconstruction d’un cache. En intégrant ce contrôle au processus d’intégration continue sur un Mac dans le cloud, chaque fusion peut vérifier les attributs des fichiers réellement produits, au lieu de se limiter à l’examen d’une ligne de code d’écriture.

Définir d’abord les données à protéger

Avec la protection des fichiers iOS, le niveau le plus strict n’est pas toujours le plus approprié. L’application doit d’abord classer les données selon qu’elles doivent rester lisibles ou non après le verrouillage de l’appareil, puis choisir la classe de protection correspondante. Les jetons, les données métier hors ligne et les contenus confidentiels exportés par l’utilisateur doivent généralement devenir inaccessibles une fois l’appareil verrouillé. Les files d’attente de transfert en arrière-plan, certains états indispensables aux notifications push ou les tâches qui doivent continuer à fonctionner pendant le verrouillage nécessitent en revanche une évaluation distincte.

Type de données Référence recommandée Point de contrôle
Fichier d’exportation de jetons de session Protection complète Ne doit pas être lisible après le verrouillage
Documents confidentiels de l’utilisateur Protection complète Attribut cohérent après création, remplacement et restauration
État des tâches en arrière-plan Disponible après le premier déverrouillage Vérifier que l’accès pendant le verrouillage est réellement nécessaire
Cache pouvant être régénéré Selon le niveau de risque Ne doit contenir ni jetons ni données personnelles
Base de données SQLite Identique aux données métier Contrôler également les fichiers WAL et SHM

Il ne faut pas considérer l’intégralité des répertoires Documents ou Library comme un seul et même type de données. Le contrôle devrait s’appuyer sur une liste explicite indiquant, pour chaque chemin relatif, la classe attendue, l’opération qui crée le fichier et la justification de toute exception autorisée.

Le contrôle doit porter sur les fichiers réellement présents après l’exécution des tests. Une simple recherche de la chaîne .completeFileProtection ne détecte ni les fichiers auxiliaires de base de données, ni les nouveaux inode créés par un remplacement, ni les données persistées par des composants tiers.

Lire les attributs du fichier final avec XCTest

Le test doit d’abord écrire les données par l’intermédiaire du flux métier normal, puis lire la propriété fileProtection depuis l’URL finale. La méthode auxiliaire ci-dessous signale aussi bien l’absence de l’attribut que sa valeur réelle dans le message d’échec, ce qui facilite le diagnostic directement depuis les journaux de CI.

import XCTest

final class FileProtectionTests: XCTestCase {
    private func assertProtection(
        _ url: URL,
        equals expected: URLFileProtection,
        file: StaticString = #filePath,
        line: UInt = #line
    ) throws {
        XCTAssertTrue(
            FileManager.default.fileExists(atPath: url.path),
            "Expected file does not exist: \(url.path)",
            file: file,
            line: line
        )

        let values = try url.resourceValues(forKeys: [.fileProtectionKey])
        XCTAssertEqual(
            values.fileProtection,
            expected,
            "Unexpected protection at \(url.path): \(String(describing: values.fileProtection))",
            file: file,
            line: line
        )
    }

    func testSensitiveExportUsesCompleteProtection() throws {
        let root = FileManager.default.temporaryDirectory
        let url = root.appendingPathComponent("private-export.json")
        let payload = Data(#"{"status":"ready"}"#.utf8)

        try payload.write(to: url, options: [.atomic, .completeFileProtection])
        try assertProtection(url, equals: .complete)
    }
}

Les données de test ne doivent contenir aucun identifiant réel. Les chemins doivent également être générés dynamiquement dans le conteneur de test, sans dépendre du nom d’utilisateur d’un nœud particulier ni d’un répertoire de travail fixe. Si le produit permet d’écraser un fichier existant, le test doit effectuer deux écritures consécutives, car la seconde passe généralement par un fichier temporaire suivi d’un renommage.

Établir une liste explicite des exceptions

Chaque fichier qui doit réellement utiliser .completeUntilFirstUserAuthentication doit être nommé individuellement dans les tests. Il ne faut pas accepter qu’un fichier réussisse simplement parce qu’il n’utilise pas la protection complète. La liste des exceptions doit préciser à la fois le cas d’usage métier et la personne responsable. Dès qu’une fonctionnalité n’a plus besoin d’accéder au fichier pendant le verrouillage, l’exception doit être supprimée et la classe de protection renforcée.

Ne pas oublier les fichiers auxiliaires de SQLite

En mode WAL, SQLite peut créer les fichiers database.sqlite-wal et database.sqlite-shm. Contrôler uniquement le fichier principal donnerait une conclusion de sécurité trompeuse. Le test doit d’abord exécuter une véritable transaction d’écriture, vérifier que les fichiers auxiliaires ont bien été créés, puis appliquer la même assertion aux trois fichiers.

Avec Core Data, le test doit effectuer l’insertion et l’enregistrement par l’intermédiaire du conteneur persistant, et non créer manuellement une base de données vide. Certains fichiers auxiliaires disparaissent après la fermeture de la connexion. Le contrôle doit donc intervenir après la fin de la transaction d’écriture, mais avant la fermeture du stockage. Il est recommandé de consigner les éléments suivants :

  1. les chemins absolus de la base principale, du fichier WAL et du fichier SHM ;
  2. la classe de protection réelle de chaque fichier ;
  3. l’étape de test qui a déclenché la création du fichier ;
  4. lorsqu’un fichier est absent, s’il s’agit d’un nettoyage attendu ou si le test n’a pas réellement effectué d’écriture.

Pour les flux qui téléchargent puis importent une base de données, il faut également couvrir toute la chaîne « fichier temporaire téléchargé — validation — déplacement vers le répertoire définitif ». Le fait que le répertoire cible possède les bons attributs ne garantit pas automatiquement que le fichier déplacé héritera de la même stratégie.

Intégrer le contrôle au pipeline sur un Mac dans le cloud

Le contrôle peut s’exécuter comme un plan de test indépendant afin d’éviter de lancer systématiquement tous les tests d’interface. Il faut d’abord fixer le projet, le Scheme, le simulateur et le chemin du paquet de résultats, puis laisser le code de sortie de xcodebuild déterminer si le pipeline peut continuer.

set -euo pipefail

RESULT_DIR="${PWD}/artifacts"
RESULT_BUNDLE="${RESULT_DIR}/FileProtection.xcresult"

rm -rf "${RESULT_BUNDLE}"
mkdir -p "${RESULT_DIR}"

xcodebuild test \
  -project App.xcodeproj \
  -scheme SecurityRegression \
  -destination 'platform=iOS Simulator,name=iPhone 16' \
  -resultBundlePath "${RESULT_BUNDLE}" \
  -only-testing:AppSecurityTests/FileProtectionTests

Le nom de l’appareil doit correspondre aux environnements d’exécution installés sur le nœud ; il ne faut pas supposer que tous les environnements sont identiques. Avant l’exécution, la commande xcrun simctl list devices available permet de vérifier la cible. Dans le pipeline de production, les appareils et versions système autorisés doivent être déclarés dans la configuration. Si la cible n’existe pas, le pipeline doit échouer immédiatement au lieu de basculer silencieusement vers un autre environnement.

En cas d’échec, il faut au minimum conserver le fichier .xcresult, la sortie standard des tests et la liste des classes de protection. Les échantillons sensibles du conteneur de test ne doivent pas être archivés, et le contenu des fichiers ne doit pas être affiché simplement pour faciliter le diagnostic. Les preuves doivent répondre aux questions suivantes : quel chemin, quelle valeur attendue, quelle valeur réelle et quelle étape de test a créé le fichier. Elles ne doivent pas reproduire les données métier.

Tenir compte des limites du simulateur et des erreurs d’interprétation courantes

Le simulateur permet de vérifier que le code applique bien l’attribut de protection au fichier final, mais il ne reproduit pas complètement la disponibilité des clés après le verrouillage d’un appareil physique. Ce contrôle CI sert donc à détecter les régressions d’attributs. Le comportement réel des accès après verrouillage, redémarrage ou réveil en arrière-plan doit toujours être validé dans un processus utilisant des appareils contrôlés.

L’ordre du nettoyage constitue une autre source de faux diagnostics. Si le test ferme d’abord la base de données ou supprime le répertoire temporaire avant de contrôler les fichiers auxiliaires, le résultat indiquera que le fichier n’existe pas, sans permettre de distinguer un nettoyage sûr d’un test qui n’a pas fonctionné. Les assertions doivent être placées à un moment clairement défini du cycle de vie, et toute absence doit produire un échec indiquant le chemin concerné.

Enfin, quatre catégories de changements doivent être surveillées : le remplacement de l’API d’écriture, le recours à un remplacement atomique, la modification du mode de base de données et l’ajout d’un nouveau chemin persistant par un composant tiers. Dès que l’un de ces éléments change, la liste des protections doit être réexaminée. Un tel contrôle ne dépend pas de la mémoire humaine et évite de confondre « correctement configuré autrefois » avec « toujours correctement protégé sur le disque aujourd’hui ».

Questions fréquentes

Contrôler les options d’écriture dans le code suffit-il ?

Non. Un remplacement atomique, une migration ou une bibliothèque peut recréer le fichier ; il faut donc lire la classe appliquée au fichier final.

Le simulateur remplace-t-il une vérification sur appareil ?

Non. Il valide correctement la présence de l’attribut, mais le comportement après verrouillage doit être testé séparément sur un appareil contrôlé.

VMMini M4

Exécutez votre prochaine tâche sur un nœud physique dédié

Choisissez le nœud et la période de facturation, puis commencez le déploiement avec la configuration fixe M4, 16 Go de RAM et 256 Go de SSD. La disponibilité réelle est indiquée en temps réel dans la console.

Louer un Mac cloud maintenant