Tester les enregistrements chiffrés iOS sur un Mac cloud

Tester les enregistrements chiffrés iOS sur un Mac cloud

Tester les enregistrements chiffrés iOS sur un Mac cloud

Chiffrer une note hors ligne à l’enregistrement, puis la déchiffrer à la lecture : le parcours nominal se valide facilement. Le vrai danger apparaît lorsqu’un fichier est tronqué ou que la mauvaise clé est utilisée, mais que l’application masque l’erreur et enregistre ensuite une note vide. Sur un Mac cloud, isoler cette logique dans un package Swift indépendant permet de vérifier les comportements en cas d’échec avant de la relier à l’interface. Cet article porte uniquement sur l’intégrité des enregistrements chiffrés, sans aborder la persistance des clés ni la gestion des comptes.

Définir d’abord le format des enregistrements et le contrat d’échec

Avec AES-GCM de CryptoKit, chaque enregistrement est stocké sous la forme des octets renvoyés par SealedBox.combined. Le nonce généré par défaut, le texte chiffré et le tag d’authentification sont conservés ensemble : la lecture doit restituer cet ensemble dans son intégralité. Ne tronquez pas vous-même le tag et ne considérez pas le nonce comme un secret.

Un nonce ne doit jamais être réutilisé avec la même clé. Dans les appels courants, laissez AES.GCM.seal le générer automatiquement. Ne le dérivez ni du nom de fichier, ni de l’heure courante à la seconde près, ni du numéro d’ordre de l’enregistrement. Les clés sont générées temporairement dans les tests et ne doivent figurer ni dans le dépôt ni dans les rapports de test.

Le contenu ne doit être transmis à la couche métier qu’après un déchiffrement réussi. Un échec d’authentification n’est pas un contenu vide et ne doit jamais déclencher l’écrasement automatique du fichier d’origine.

Si la logique métier exige aussi des champs externes, comme un identifiant d’enregistrement, vous pouvez les encoder sous une forme canonique et les utiliser comme données authentifiées additionnelles. Le chiffrement et le déchiffrement doivent recevoir exactement les mêmes octets. Nous commençons ici par valider un enregistrement minimal, sans champs externes.

Créer un package de test exécutable

Sur un nœud physique dédié équipé de la chaîne d’outils Xcode, exécutez les commandes suivantes avec un compte de développement ordinaire. Utilisez un nouvel espace de travail, sans écraser de projet existant. Vérifiez d’abord que swift --version renvoie bien un résultat, puis initialisez le package :

mkdir SecureRecord
cd SecureRecord
swift --version
swift package init --type library

Remplacez le fichier généré Tests/SecureRecordTests/SecureRecordTests.swift par le contenu de la section suivante. Le fichier source peut conserver son implémentation par défaut : ces tests appellent directement CryptoKit fourni par le système et ne nécessitent aucune dépendance tierce à télécharger.

Nous utilisons XCTest pour que l’exemple ne dépende pas de la syntaxe particulière d’un nouveau framework de test. Dans un projet réel, déplacez les fonctions utilitaires dans le module de production et importez celui-ci dans les tests. Cela évite de valider une implémentation de test correcte alors que celle de l’application est différente.

Couvrir le cas nominal et quatre cas particuliers

Voici le fichier de test complet. Le parcours nominal vérifie d’abord que les octets sont identiques après un aller-retour de chiffrement et de déchiffrement. Les tests couvrent ensuite un tag modifié, une mauvaise clé, une entrée tronquée et un contenu vide valide. Chaque cas utilise sa propre clé, sans partager d’état mutable.

import Foundation
import CryptoKit
import XCTest

final class SecureRecordTests: XCTestCase {
    private func encode(
        _ data: Data,
        key: SymmetricKey
    ) throws -> Data {
        let box = try AES.GCM.seal(data, using: key)
        return try XCTUnwrap(box.combined)
    }

    private func decode(
        _ record: Data,
        key: SymmetricKey
    ) throws -> Data {
        let box = try AES.GCM.SealedBox(combined: record)
        return try AES.GCM.open(box, using: key)
    }

    func testRoundTripAndFreshNonce() throws {
        let key = SymmetricKey(size: .bits256)
        let input = Data("draft-v1".utf8)
        let first = try encode(input, key: key)
        let second = try encode(input, key: key)

        XCTAssertEqual(try decode(first, key: key), input)
        XCTAssertEqual(try decode(second, key: key), input)
        XCTAssertNotEqual(first, second)
    }

    func testChangedTagIsRejected() throws {
        let key = SymmetricKey(size: .bits256)
        var record = try encode(Data("draft-v1".utf8), key: key)
        let last = record.index(before: record.endIndex)
        record[last] ^= 0x01

        XCTAssertThrowsError(try decode(record, key: key))
    }

    func testWrongKeyIsRejected() throws {
        let key = SymmetricKey(size: .bits256)
        let otherKey = SymmetricKey(size: .bits256)
        let record = try encode(Data("draft-v1".utf8), key: key)

        XCTAssertThrowsError(try decode(record, key: otherKey))
    }

    func testTruncatedRecordsAreRejected() throws {
        let key = SymmetricKey(size: .bits256)
        let record = try encode(Data("draft-v1".utf8), key: key)

        for length in [0, 1, record.count - 1] {
            let truncated = Data(record.prefix(length))
            XCTAssertThrowsError(try decode(truncated, key: key))
        }
    }

    func testEmptyPlaintextIsValid() throws {
        let key = SymmetricKey(size: .bits256)
        let record = try encode(Data(), key: key)

        XCTAssertFalse(record.isEmpty)
        XCTAssertEqual(try decode(record, key: key), Data())
    }
}

Exécutez swift test : les cinq tests doivent réussir. Même un texte en clair vide produit un enregistrement non vide, puisque l’enveloppe contient aussi le nonce et le tag. Un simple contrôle indiquant que « la taille du fichier est supérieure à zéro » ne remplace pas le déchiffrement authentifié.

L’assertion qui vérifie que les deux textes chiffrés diffèrent sert à détecter une régression introduisant accidentellement un nonce fixe. Elle ne prouve pas la qualité de l’aléa. Les tests ne comparent pas non plus le texte chiffré complet à un instantané de référence : le caractère aléatoire attendu provoquerait alors un faux échec.

Relier l’échec cryptographique au comportement de l’application

La réussite des tests d’échec d’authentification signifie seulement que la couche inférieure rejette les octets invalides. Ajoutez aussi des critères de validation côté appelant : après un échec de lecture, l’application doit afficher une erreur permettant la récupération, conserver le fichier d’origine et empêcher la sauvegarde automatique d’un modèle vide en arrière-plan. Si une copie de la version précédente existe, sa sélection doit passer par une procédure de récupération explicite, et non par un basculement silencieux.

Valider séparément le format et les données métier

Même après le déchiffrement réussi d’un enregistrement complet, le décodage des données métier peut échouer. Séparez le contrôle de la version du format, le déchiffrement authentifié et l’analyse du contenu en trois étapes, chacune renvoyant une erreur interne distincte. Un message bref suffit pour l’utilisateur. Les journaux doivent uniquement indiquer l’étape et la catégorie d’erreur, sans afficher le texte en clair ni les clés.

Limitez également la taille des entrées : vérifiez la taille du fichier avant de le charger et de le déchiffrer, afin qu’un fichier anormalement volumineux ne sature pas la mémoire. Fixez la limite selon la capacité des enregistrements autorisée par les besoins métier, sans reprendre une valeur arbitraire. Lors de l’ajout d’une nouvelle version du format, conservez à la fois des exemples valides de l’ancienne version et des exemples de versions explicitement non prises en charge.

Conserver des éléments de validation reproductibles

Livrez le fichier de test, la description du format et les commandes d’exécution. Pour chaque exécution, consignez l’identifiant du commit, la version de Swift, le nombre de tests et le statut de sortie, sans téléverser de véritables enregistrements utilisateur. Pendant le développement, vous pouvez ajouter des tests de modification octet par octet. Si un test de mutation aléatoire échoue, conservez impérativement la position de la mutation et la longueur de l’entrée.

Terminez par une vérification d’intégration iOS : transmettez un enregistrement réellement écrit par l’application à la même fonction de décodage, puis vérifiez que la lecture d’un fichier corrompu n’écrase pas le contenu d’origine. Un Mac cloud convient aux tests rapides du cœur de chiffrement partagé ; l’accès aux fichiers et la récupération dans l’interface doivent toutefois être validés séparément dans la cible de l’application. Le critère final doit être « l’entrée invalide est rejetée, les données d’origine sont conservées et l’utilisateur peut les récupérer », et non la seule réussite d’un aller-retour de chiffrement et de déchiffrement.

Questions fréquentes

Faut-il un simulateur iOS pour ces tests ?

La logique CryptoKit seule se teste avec swift test sur le Mac cloud. La lecture des fichiers, les messages d’erreur et l’intégration nécessitent aussi des tests sur la cible iOS.

Peut-on renvoyer des données vides après un échec d’authentification ?

Pas comme résultat réussi. Conservez les octets d’origine et signalez l’échec pour ne pas confondre un contenu vide valide avec un enregistrement endommagé.

Louez un Mac cloud pour votre prochain build

Choisissez un modèle, une ville et une durée de location, puis utilisez un nœud physique dédié avec vos outils de développement. La disponibilité est indiquée en temps réel dans la console.

Préparez votre prochain build

Vérifiez le modèle et la durée de location avant de planifier votre projet.