Vérifier PBKDF2 pour iOS sur un Mac cloud

Vérifier PBKDF2 pour iOS sur un Mac cloud

Un même mot de passe permet de déchiffrer un fichier sous Windows, mais provoque une erreur sous iOS ? Ne réinitialisez pas les données tout de suite. Le problème ne vient pas forcément de l’algorithme de chiffrement : l’encodage du mot de passe, le nombre d’itérations ou la longueur dérivée peuvent différer. Exécutez d’abord des tests à réponse connue sur un Mac cloud, puis faites appeler par la cible iOS la couche d’encapsulation réellement utilisée par l’application. Vous pourrez ainsi isoler précisément la couche en cause.

Fixer le contrat d’entrée

Cet article s’adresse aux projets multiplateformes dont le protocole repose déjà sur PBKDF2. Il ne présente pas PBKDF2 comme le choix par défaut de tout nouveau système. Commencez par définir six champs : l’algorithme, la PRF, l’encodage du mot de passe en octets, les octets du sel, le nombre d’itérations et la longueur de sortie. Ici, nous retenons PBKDF2-HMAC-SHA256 avec une sortie de 32 octets.

Le mot de passe est encodé en UTF-8, sans suppression automatique des espaces, changement de casse ni normalisation Unicode. Deux textes visuellement identiques peuvent correspondre à des octets différents. Si le protocole existant impose déjà une normalisation, conservez-la et intégrez cette règle à la gestion des versions.

Les tests à réponse connue vérifient que le calcul est correct ; les mesures sur appareil déterminent si le facteur de travail est adapté. Les une et deux itérations des vecteurs de test ne doivent en aucun cas devenir des paramètres de production.

Vérifiez aussi que le sel est transmis sous forme d’octets bruts, et non de texte hexadécimal. Utiliser la chaîne 73616c74 comme sel et transmettre les octets du mot salt constituent deux entrées différentes.

Exécuter les vecteurs de référence

Sur un Mac où la chaîne d’outils de développement complète est déjà sélectionnée, enregistrez le contenu suivant dans pbkdf2-check.swift. Le script appelle directement CommonCrypto, fourni par le système, sans dépendance à télécharger. En cas d’échec, il interrompt l’exécution sans afficher le mot de passe ni le résultat dérivé.

import Foundation
import CommonCrypto

enum DerivationError: Error {
    case invalidInput
    case failed(Int32)
}

func derive(
    password: String,
    salt: [UInt8],
    rounds: UInt32
) throws -> [UInt8] {
    guard rounds > 0, !salt.isEmpty else {
        throw DerivationError.invalidInput
    }

    let passwordBytes = password.utf8CString
    var output = [UInt8](repeating: 0, count: 32)

    let status = passwordBytes.withUnsafeBufferPointer { p in
        salt.withUnsafeBufferPointer { s in
            output.withUnsafeMutableBufferPointer { o in
                CCKeyDerivationPBKDF(
                    CCPBKDFAlgorithm(kCCPBKDF2),
                    p.baseAddress,
                    p.count - 1,
                    s.baseAddress,
                    s.count,
                    CCPseudoRandomAlgorithm(kCCPRFHmacAlgSHA256),
                    rounds,
                    o.baseAddress,
                    o.count
                )
            }
        }
    }

    guard status == kCCSuccess else {
        throw DerivationError.failed(status)
    }
    return output
}

func hex(_ bytes: [UInt8]) -> String {
    bytes.map { String(format: "%02x", $0) }.joined()
}

let salt = Array("salt".utf8)
let vectors: [(UInt32, String)] = [
    (1, "120fb6cffcf8b32c43e7225256c4f837a86548c92ccc35480805987cb70be17b"),
    (2, "ae4d0c95af6b46d32d0adff928f06dd02a303f8ef3c251dfd6e2d85a95474c43")
]

for (rounds, expected) in vectors {
    let actual = try derive(
        password: "password",
        salt: salt,
        rounds: rounds
    )
    precondition(hex(actual) == expected)
}

let full = try derive(password: "ab\u{0000}cd", salt: salt, rounds: 2)
let prefix = try derive(password: "ab", salt: salt, rounds: 2)
precondition(full != prefix)

let composed = try derive(password: "\u{00e9}", salt: salt, rounds: 2)
let decomposed = try derive(password: "e\u{0301}", salt: salt, rounds: 2)
precondition(composed != decomposed)

Exécutez xcrun swift pbkdf2-check.swift. Un code de sortie nul indique que ces assertions ont réussi. Le script est ici exécuté en mode interprété : ne le remplacez pas par une compilation optimisée qui désactive les assertions.

Le code transmet explicitement la longueur du mot de passe en octets. Il exclut le terminateur final, mais conserve les caractères nuls intégrés. Une interface qui détermine la longueur à partir du premier caractère nul tronquerait la suite du mot de passe.

Tester la couche d’encapsulation applicative

Une fois le script validé, ajoutez les mêmes échantillons à la cible XCTest d’iOS, mais appelez le point d’entrée de dérivation réellement utilisé par l’application. Ne vous contentez pas de recopier une implémentation réservée aux tests : même si la couche basse est correcte, la couche supérieure peut encore mal décoder le sel.

Prévoyez au minimum quatre groupes de contrôles :

  • Mot de passe vide : l’autoriser ou le refuser explicitement selon le protocole, sans le remplacer silencieusement par une autre valeur.
  • Caractères chinois, émojis et caractères nuls intégrés : vérifier les octets d’entrée et le résultat final sur les deux plateformes.
  • Nombre d’itérations nul, encodage du sel endommagé, identifiant d’algorithme inconnu : les refuser avant d’appeler la fonction de dérivation.
  • Même mot de passe avec des sels différents : vérifier, à l’aide d’échantillons fixes, que les résultats diffèrent, afin de détecter l’utilisation accidentelle d’un sel constant.

La fonction d’exemple refuse un sel vide par choix de validation propre à ce projet, et non parce que l’algorithme serait incapable de le traiter. En production, générez le sel à partir d’une source aléatoire sécurisée du système. Si cette source échoue, renvoyez une erreur au lieu de vous rabattre sur un horodatage.

Distinguer les niveaux de test

Le script sur Mac vérifie le raccordement à l’algorithme, les tests sur simulateur vérifient la couche d’encapsulation de l’application, et les tests sur appareil physique mesurent la durée et l’incidence sur l’interface. Consignez le même contrat d’entrée aux trois niveaux, sans assimiler la vitesse du Mac à celle du téléphone.

Conserver les paramètres pour les évolutions

Un enregistrement de dérivation doit conserver au minimum la version du format, l’identifiant de l’algorithme, le nombre d’itérations, le sel et la longueur de sortie. Lorsqu’un encodage textuel intervient, précisez aussi son format, par exemple la variante de Base64 utilisée pour le sel. Le sel n’est pas un mot de passe : la sécurité ne doit pas dépendre de sa dissimulation.

À la lecture d’un enregistrement, validez d’abord les types des champs, les longueurs et le budget de calcul, puis lancez la dérivation. Un fichier endommagé ne doit pas pouvoir imposer un nombre d’itérations démesuré et monopoliser le CPU de l’application. Fixez le facteur de travail selon les exigences de sécurité et les mesures effectuées sur les appareils pris en charge, sans recopier les valeurs des vecteurs de test.

Pour faire évoluer les paramètres, ne modifiez pas simplement une constante globale avant de lire les anciens enregistrements. Leur validation exige les anciens paramètres. Ce n’est qu’après avoir obtenu les données en clair nécessaires que vous pouvez produire un nouvel enregistrement avec un nouveau sel et la nouvelle politique.

Si le résultat dérivé sert au déchiffrement, l’ancien texte chiffré ne suffit pas à lui seul pour passer directement aux nouveaux paramètres. Il faut d’abord réussir le déchiffrement, puis chiffrer à nouveau, tout en conservant l’ancien enregistrement jusqu’à l’écriture complète et à la validation du nouveau. Si vous stockez une valeur de vérification du mot de passe, utilisez une implémentation de comparaison en temps constant ayant fait l’objet d’une revue, et non la simple comparaison de tableaux de l’exemple.

Mettre en place les contrôles avant fusion

Prévoyez deux contrôles indépendants dans l’intégration continue : les vecteurs de référence doivent correspondre octet par octet, et la couche applicative doit savoir lire les anciennes versions des enregistrements tout en refusant les paramètres invalides. Ajoutez aussi un ancien enregistrement réservé aux tests pour vérifier qu’il reste ouvrable après la mise à niveau et que les nouveaux enregistrements utilisent bien la nouvelle version.

Les rapports d’échec doivent se limiter au nom du cas de test, à l’algorithme et aux paramètres non sensibles. N’y téléversez ni mots de passe, ni clés dérivées, ni véritables enregistrements d’utilisateurs. Réservez les mesures de performance à une tâche contrôlée sur appareil physique ; une mesure isolée dans un environnement d’exécution partagé ne doit pas bloquer une fusion.

Enfin, vérifiez les scénarios d’échec de la migration : après une annulation, un échec d’écriture sur disque ou la fermeture de l’application, l’ancien enregistrement doit rester lisible. Vous validerez ainsi non seulement une fonction de dérivation de mot de passe, mais aussi un parcours complet qui ne perd pas de données lors d’une évolution des paramètres.

Questions fréquentes

Peut-on utiliser une ou deux itérations en production ?

Non. Ces valeurs servent uniquement aux tests dont le résultat est connu. Le facteur de travail en production dépend de la politique de sécurité et des mesures sur les appareils cibles. Les paramètres non fiables doivent aussi être plafonnés.

Un test réussi sur Mac suffit-il à valider iOS ?

Non. Le script Mac vérifie rapidement le calcul et le contrat des octets. Une cible de test iOS doit appeler le code réellement utilisé par l’application, puis les appareils cibles doivent servir à mesurer la durée et l’impact sur les interactions.

Louez un Mac dans le cloud

Louez un ordinateur physique dédié à la journée, à la semaine, au mois ou au trimestre. Planifiez vos builds selon votre projet ; la location s’arrête à l’échéance sans renouvellement.

Choisir un modèle

Préparez votre prochain build

Choisissez un modèle et une durée de location, puis vérifiez la date d’échéance. La location s’arrête à l’échéance sans renouvellement.