Vérifier le décodage sécurisé des archives iOS sur un Mac cloud

Vérifier le décodage sécurisé des archives iOS sur un Mac cloud

Vérifier le décodage sécurisé des archives iOS sur un Mac cloud

Lorsqu’une nouvelle version d’une app lit une archive créée par une ancienne version, le scénario le plus dangereux n’est pas une erreur explicite : c’est une lecture qui renvoie des données vides, suivie d’un enregistrement qui écrase le fichier d’origine. Validez ce parcours sur un Mac cloud : commencez par des tests de décodage reposant uniquement sur Foundation, puis faites lire les mêmes échantillons par une cible de tests iOS. L’objectif n’est pas de réussir une seule opération d’encodage-décodage, mais de démontrer que les types incorrects, les versions inconnues et les données corrompues sont tous explicitement rejetés.

Définir d’abord les limites de la lecture

Cet article porte sur les archives d’objets produites par NSKeyedArchiver, pas sur le chiffrement. L’exemple n’autorise qu’un seul objet racine, contenant un numéro de version et un champ texte. Avant la validation, fixez trois règles :

supportsSecureCoding est une déclaration de prise en charge au niveau du type ; elle ne valide pas automatiquement les champs métier. Le lecteur doit toujours activer requiresSecureCoding, et chaque champ objet doit être décodé avec une méthode qui restreint les types autorisés.

Le décodage sécurisé limite les types d’objets pouvant être instanciés. Il n’assure ni l’authentification de la provenance, ni la confidentialité, ni le respect d’un quota de taille de fichier. Limitez la taille des données avant de les transmettre au décodeur.

Écrire un décodeur qui rejette les entrées invalides

Enregistrez le contenu suivant dans archive-check.swift. Il ne dépend que de Foundation ; vérifiez d’abord que xcrun swift --version fonctionne. Le numéro de version utilisé ici est celui du protocole d’archivage, pas celui du système.

import Foundation

final class Memo: NSObject, NSSecureCoding {
    static var supportsSecureCoding: Bool { true }
    let schema: Int
    let text: String

    init(schema: Int = 1, text: String) {
        self.schema = schema
        self.text = text
        super.init()
    }

    required init?(coder: NSCoder) {
        guard coder.containsValue(forKey: "schema"),
              coder.decodeInteger(forKey: "schema") == 1,
              let value = coder.decodeObject(
                  of: NSString.self, forKey: "text"
              ),
              value.length <= 4096 else {
            return nil
        }
        schema = 1
        text = value as String
        super.init()
    }

    func encode(with coder: NSCoder) {
        coder.encode(schema, forKey: "schema")
        coder.encode(text as NSString, forKey: "text")
    }
}

enum ArchiveFailure: Error {
    case invalidSize
    case invalidObject
}

func readMemo(_ data: Data) throws -> Memo {
    guard !data.isEmpty, data.count <= 1_048_576 else {
        throw ArchiveFailure.invalidSize
    }
    let reader = try NSKeyedUnarchiver(forReadingFrom: data)
    reader.requiresSecureCoding = true
    reader.decodingFailurePolicy = .setErrorAndReturn
    let memo = reader.decodeObject(
        of: Memo.self, forKey: NSKeyedArchiveRootObjectKey
    )
    reader.finishDecoding()
    if let error = reader.error {
        throw error
    }
    guard let memo else {
        throw ArchiveFailure.invalidObject
    }
    return memo
}

func pack(_ value: Any) throws -> Data {
    try NSKeyedArchiver.archivedData(
        withRootObject: value, requiringSecureCoding: true
    )
}

Le code vérifie d’abord la présence du champ de version, afin que la valeur par défaut du décodage d’un entier ne masque pas une clé absente. La limite du texte est exprimée en unités UTF-16, conformément à NSString.length. Dans une implémentation de production, choisissez cette limite selon le protocole du produit : le seuil de l’exemple n’est pas une norme de sécurité universelle.

setErrorAndReturn doit être défini avant toute lecture d’objet. Ne comptez pas sur un do/catch englobant pour intercepter tous les échecs de décodage : le traitement de certains objets invalides relève de la stratégie d’échec du décodeur, et non du mécanisme habituel des erreurs levées en Swift.

Constituer un jeu minimal de régression avec quatre entrées

Ajoutez le code suivant à la suite. L’échantillon valide doit restituer le texte d’origine ; les trois autres doivent être rejetés. Au moindre résultat contraire aux attentes, le programme de validation se termine avec un code de sortie non nul.

func expectRejection(_ data: Data) {
    do {
        _ = try readMemo(data)
    } catch {
        return
    }
    fatalError("Unexpected archive acceptance")
}

let valid = try pack(Memo(text: "draft"))
let restored = try readMemo(valid)
guard restored.text == "draft" else {
    fatalError("Round-trip mismatch")
}

expectRejection(try pack("wrong root" as NSString))
expectRejection(try pack(Memo(schema: 2, text: "future")))
expectRejection(Data(valid.prefix(12)))

Compilez le programme avant de l’exécuter, afin que l’environnement d’exécution interprétée n’influe pas sur les noms de types utilisés dans les échantillons suivants :

set -eu
xcrun swiftc archive-check.swift -o archive-check
./archive-check

Les échantillons invalides peuvent amener Foundation à écrire des diagnostics sur la sortie d’erreur standard ; cela ne signifie pas que le programme de validation a échoué. Fiez-vous au code de sortie du processus et aux assertions, pas à l’absence totale de messages dans les journaux. Si un objet racine de type incorrect est accepté, corrigez d’abord la liste des types autorisés. Ne supprimez pas le cas négatif pour faire passer les tests.

Étendre les tests aux objets imbriqués et aux archives historiques

Restreindre les types autorisés pour chaque champ

Si une archive réelle contient des tableaux, autoriser uniquement NSArray ne suffit pas à garantir la sûreté de leurs éléments. Utilisez la surcharge de decodeObject(of:forKey:) qui accepte un ensemble de classes : incluez à la fois le conteneur et les classes d’éléments attendues, puis vérifiez le nombre d’éléments, la longueur des champs et la cohérence de leurs relations. N’ajoutez pas une classe aussi générale que NSObject pour contourner un échec.

Les contraintes de type d’une archive ne remplacent pas le contrôle de la complexité du graphe d’objets. Limiter la taille des données d’entrée ne garantit pas non plus une consommation mémoire bornée après décodage. Pour les entrées non fiables, prévoyez en plus un protocole plus restrictif ou une séparation du traitement dans un périmètre isolé.

Conserver de vrais fichiers issus des anciennes versions

Ajoutez aux ressources de test des archives d’anciennes versions dont les données sensibles ont été supprimées ou anonymisées. Consignez la version qui les a produites, les champs attendus et les résultats de migration autorisés. Les échantillons générés à la volée conviennent pour tester la logique de rejet, mais ils ne prouvent pas que les octets réellement écrits par une ancienne version restent lisibles.

Le nom d’une classe Swift à l’exécution peut varier selon le module. Avant de déplacer un type ou de renommer un module, vérifiez les archives historiques, puis configurez les correspondances de classes à partir des anciens noms explicitement identifiés. Il est interdit de lire un nom de classe arbitraire dans les données d’entrée pour l’autoriser automatiquement. Même après la réussite des tests en ligne de commande, exécutez le même jeu de fichiers dans la cible de tests iOS afin de couvrir les différences de comportement à l’exécution sur la plateforme cible.

Contenir les échecs dans la couche de lecture, sans les propager à l’enregistrement

Représentez le résultat du décodage par une réussite ou une erreur explicite ; évitez try? suivi d’un objet vide de remplacement. En cas d’échec de lecture, conservez le fichier d’origine et affichez une procédure de récupération dans l’interface. N’autorisez le remplacement de l’ancien fichier qu’une fois la migration terminée et les nouvelles données validées par une relecture.

Avant la fusion, vérifiez cinq points : les champs de l’échantillon valide sont identiques ; le mauvais type racine est rejeté ; la version inconnue est rejetée ; une entrée tronquée ne déclenche aucune réécriture considérée comme réussie ; les archives historiques passent les tests sur la cible iOS. Faites de chaque cas négatif un test distinct pour identifier immédiatement la règle enfreinte en cas d’échec.

Le programme en ligne de commande sur le Mac cloud fournit un retour rapide ; les tests iOS confirment le comportement sur la plateforme cible. Seule la réussite des deux niveaux apporte des preuves de validation reproductibles du parcours de lecture des archives, au-delà d’un simple succès ponctuel sur le Mac de développement.

Questions fréquentes

supportsSecureCoding suffit-il ?

Non. Activez requiresSecureCoding à la lecture, limitez les classes avec les méthodes decodeObject adaptées et contrôlez aussi la version et les valeurs des champs.

Peut-on renvoyer une liste vide après un échec ?

Ne transformez pas l'échec en résultat vide valide. Renvoyez une erreur explicite et conservez l'archive source pour éviter son écrasement lors de la prochaine sauvegarde.

Louez un Mac cloud pour votre prochain build

Mac mini physique dédié, pas une machine virtuelle. Choisissez une durée de location à la journée, à la semaine, au mois ou au trimestre. La configuration et la disponibilité réelle sont indiquées dans la console.