Tester les codes TOTP iOS sur un Mac cloud
Après l’intégration des codes à usage unique dans un projet iOS, les anomalies les plus difficiles à reproduire sont souvent celles du type « cela fonctionnait à l’instant, mais les codes ne correspondent plus ». Si vous développez une interface multiplateforme sous Windows avant de valider les modules natifs sur un Mac cloud, inutile de saisir des codes à répétition en espérant tomber juste. Traitez le temps comme une donnée d’entrée et les vecteurs de référence comme arbitres : vous pourrez alors distinguer une erreur d’algorithme, un défaut d’affichage et un problème de tolérance temporelle côté serveur.
Définir le contrat
TOTP convertit le temps Unix en un compteur croissant, puis utilise HMAC pour produire un code numérique de longueur fixe. Commencez par fixer quatre paramètres : les octets bruts de la clé secrète, l’algorithme de hachage, le pas de temps et le nombre de chiffres en sortie. Cet article utilise la configuration de test SHA-1 de la RFC 6238 : une origine à zéro, un pas de trente secondes et une sortie de huit chiffres.
SHA-1 sert ici à vérifier la conformité aux vecteurs de référence du standard ; ce n’est pas une invitation à choisir un algorithme sans précaution pour un nouveau protocole. Si les deux parties conviennent d’un autre algorithme de hachage, elles doivent aussi remplacer les vecteurs de test correspondants, et pas seulement modifier le client.
La clé secrète en Base32 que l’on trouve couramment dans les pages de configuration n’est pas l’entrée directe de HMAC. Il faut d’abord la décoder selon la convention retenue, puis transmettre les octets bruts obtenus. Ne confondez pas les octets UTF-8 du texte avec le résultat du décodage.
La clé ci-dessous est une donnée de test publique, réservée aux tests de non-régression. Les clés réelles, le contenu des codes QR et les codes à usage unique ne doivent jamais figurer dans les journaux de compilation.
Implémenter le calcul
Dans le répertoire du projet sur le Mac cloud XcodeHost, enregistrez un fichier totp-check.swift. Vérifiez d’abord que xcrun swift --version s’exécute correctement, puis ajoutez le code suivant. Il ne dépend que du SDK système et ne nécessite aucun paquet tiers.
Le compteur doit être encodé sur huit octets en ordre gros-boutiste. La troncature dynamique utilise les quatre bits de poids faible du dernier octet de l’empreinte comme décalage, puis met à zéro le bit de poids fort du résultat avant d’appliquer le modulo correspondant au nombre de chiffres. La valeur renvoyée doit être une chaîne de caractères, faute de quoi les zéros initiaux seront perdus.
import Foundation
import CryptoKit
enum TOTPError: Error {
case invalidInput
}
func totp(
secret: Data,
unixSeconds: Int64,
digits: Int = 8,
period: Int64 = 30
) throws -> String {
guard !secret.isEmpty,
unixSeconds >= 0,
period > 0,
digits == 6 || digits == 8 else {
throw TOTPError.invalidInput
}
let counter = UInt64(unixSeconds / period)
let message = Data((0..<8).map { index in
UInt8(truncatingIfNeeded: counter >> ((7 - index) * 8))
})
let digest = Array(HMAC<Insecure.SHA1>.authenticationCode(
for: message,
using: SymmetricKey(data: secret)
))
let offset = Int(digest[digest.count - 1] & 0x0f)
let binary =
(UInt32(digest[offset] & 0x7f) << 24) |
(UInt32(digest[offset + 1]) << 16) |
(UInt32(digest[offset + 2]) << 8) |
UInt32(digest[offset + 3])
let modulus: UInt32 = digits == 6 ? 1_000_000 : 100_000_000
let text = String(binary % modulus)
return String(repeating: "0", count: digits - text.count) + text
}
Ne déduisez pas le compteur d’une chaîne de date locale, d’un décalage de fuseau horaire ou du compte à rebours de l’interface. Le protocole attend un temps Unix en secondes ; le compte à rebours n’est qu’un élément d’affichage. Changer le fuseau horaire d’affichage ne doit pas modifier le code produit pour un même instant.
Vérifier les vecteurs de référence
Ajoutez ensuite les six vecteurs de référence à la fin du même fichier. La clé ASCII de vingt octets utilisée ici correspond aux octets bruts explicitement définis par le standard : elle ne doit pas être décodée en Base32. Les horodatages élevés permettent aussi de repérer une implémentation qui utiliserait un type entier trop étroit.
let secret = Data("12345678901234567890".utf8)
let vectors: [(Int64, String)] = [
(59, "94287082"),
(1111111109, "07081804"),
(1111111111, "14050471"),
(1234567890, "89005924"),
(2000000000, "69279037"),
(20000000000, "65353130")
]
for (time, expected) in vectors {
let actual = try totp(secret: secret, unixSeconds: time)
precondition(actual == expected, "TOTP vector mismatch")
}
Exécutez le script avec xcrun swift totp-check.swift. En cas de succès, il ne produit aucune sortie ; si une assertion échoue, il se termine avec un code de retour non nul. N’ajoutez pas d’options d’optimisation qui désactiveraient les vérifications. Une fois le code intégré à la cible de tests de l’application, remplacez ces assertions par XCTAssertEqual afin que chaque cas en échec soit signalé séparément.
Si les six vecteurs échouent, vérifiez d’abord la clé et l’ordre des octets. Si seuls les cas comportant des zéros initiaux échouent, examinez la conversion du nombre en chaîne. Si seuls les horodatages élevés posent problème, vérifiez la largeur du type entier au lieu d’élargir la tolérance d’authentification.
Tester les limites temporelles
Une fois les vecteurs de référence validés, testez aussi la division entière et le rejet des paramètres invalides. Le code suivant vérifie qu’à cinquante-neuf secondes, on se trouve encore dans le deuxième pas de temps, alors qu’à soixante secondes, le pas a changé. Il n’est nécessaire ni de mettre le programme en veille ni de modifier l’horloge de la machine.
let at30 = try totp(secret: secret, unixSeconds: 30)
let at59 = try totp(secret: secret, unixSeconds: 59)
let at60 = try totp(secret: secret, unixSeconds: 60)
precondition(at30 == "94287082")
precondition(at59 == at30)
precondition(at60 == "37359152")
do {
_ = try totp(secret: secret, unixSeconds: -1)
preconditionFailure("Negative time accepted")
} catch TOTPError.invalidInput {
}
Ajoutez également des tests vérifiant le rejet d’une clé vide, d’un pas de temps nul et d’un nombre de chiffres non pris en charge. Deux pas de temps différents peuvent produire le même code par hasard. N’utilisez donc pas comme assertion générale « le code change forcément d’une fenêtre à l’autre » : comparez plutôt des résultats connus, comme ci-dessus.
Si l’application reçoit des horodatages en millisecondes, effectuez explicitement la conversion une seule fois dans la couche d’adaptation. Diviser plusieurs fois par mille, ou ne pas convertir du tout, produit dans les deux cas des codes au bon format, mais inutilisables pour l’authentification.
Intégrer les tests de non-régression
Placez la fonction de génération dans un module accessible à la cible de tests. La couche d’interface doit récupérer l’heure courante et la transmettre à la fonction ; celle-ci ne doit pas lire directement l’horloge. Lorsque l’application revient du mode arrière-plan, elle doit relire l’heure, et non poursuivre la décrémentation du compte à rebours comme si aucun rappel du minuteur n’avait été manqué.
Le simulateur permet de valider le nombre de chiffres, les zéros initiaux et l’affichage au changement de fenêtre. La validation côté serveur doit couvrir séparément les codes incorrects, ceux qui dépassent la tolérance temporelle et les soumissions répétées. Accepter les pas de temps adjacents sert uniquement à compenser un décalage d’horloge ; cela ne signifie pas qu’un code peut être réutilisé indéfiniment.
Avant la mise en production, conservez trois types de preuves : les résultats des vecteurs de référence, ceux du rejet des paramètres invalides et ceux du rejet des rejeux côté serveur. Consignez uniquement le nom des cas de test et leur statut de réussite, sans conserver de données d’authentification réelles. Vous pourrez ainsi relancer les mêmes vérifications hors ligne après un changement de poste Mac cloud ou une mise à niveau de la chaîne d’outils, et ramener les incidents d’authentification intermittents à des écarts précis par rapport au contrat.
Questions fréquentes
Faut-il une connexion réseau ou attendre pour tester TOTP ?
Non. Passez explicitement les secondes Unix à la fonction. Une clé fixe et les vecteurs de référence suffisent pour tester hors ligne les limites temporelles.
Un code correctement calculé suffit-il à sécuriser l’authentification ?
Non. Le serveur doit limiter les tentatives, définir la dérive horaire admise et mémoriser les pas de temps acceptés par identifiant pour empêcher leur réutilisation.
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