Tester le décodage Base64URL strict pour iOS sur un Mac cloud

Tester le décodage Base64URL strict pour iOS sur un Mac cloud

Tester le décodage Base64URL strict pour iOS sur un Mac cloud

Une API encode un curseur binaire en Base64URL, mais le client iOS accepte parfois un curseur corrompu comme s’il était valide. Le réseau n’est pas forcément en cause : un décodage permissif, l’ajout automatique de caractères de remplissage ou l’ignorance des caractères interdits peuvent transformer en octets une entrée qui aurait dû être rejetée. Sur un Mac cloud, un script Swift autonome permet d’abord de fixer le contrat de décodage, puis de reprendre les mêmes cas dans la cible de tests de l’application. Il n’est pas nécessaire de lancer le simulateur pour vérifier les limites de l’encodage.

Définir précisément l’encodage accepté

Dans cet article, seul le Base64URL non vide et sans remplissage est accepté : lettres, chiffres, - et _. Les espaces, les sauts de ligne, +, / et = sont interdits. Il s’agit d’un choix explicite du contrat d’API de l’application, et non d’une règle à imposer à tous les protocoles utilisant Base64.

Un même point d’entrée ne doit pas tenter de reconnaître à la volée le Base64 standard et le Base64URL. Si une ancienne API exige un remplissage, prévoyez un point d’entrée de décodage distinct et des tests séparés. Cela évite qu’une mise à jour du client n’élargisse discrètement l’ensemble des entrées acceptées.

La longueur obéit elle aussi à une contrainte structurelle : pour un texte sans remplissage, le reste de la division du nombre de caractères par quatre ne peut être que zéro, deux ou trois. Un reste de un ne peut pas correspondre à un encodage valide. L’exemple limite l’entrée à 65536 octets UTF-8 ; c’est une règle applicative retenue pour le test, pas une limite du protocole.

Le décodage ne fait que restituer des octets ; il ne garantit pas que leur contenu soit digne de confiance. Pour un jeton, un curseur ou un paramètre de signature, il faut encore vérifier la structure, la provenance et la validité métier.

Imposer une représentation canonique par réencodage

Enregistrez le code suivant dans base64url-check.swift. Il limite d’abord la taille de l’entrée, vérifie ensuite l’alphabet, puis ajoute le remplissage uniquement en interne. N’utilisez pas d’option qui ignore les caractères inconnus et ne supprimez pas les espaces ni les autres caractères blancs en amont.

import Foundation

enum DecodeFailure: Error {
    case size
    case alphabet
    case length
    case payload
    case noncanonical
}

func decodeBase64URL(
    _ text: String,
    maxEncodedBytes: Int = 65536
) throws -> Data {
    let count = text.utf8.count
    guard count > 0, count <= maxEncodedBytes else {
        throw DecodeFailure.size
    }

    let valid = text.utf8.allSatisfy { byte in
        (65...90).contains(byte)
        || (97...122).contains(byte)
        || (48...57).contains(byte)
        || byte == 45
        || byte == 95
    }
    guard valid else {
        throw DecodeFailure.alphabet
    }

    let remainder = count % 4
    guard remainder != 1 else {
        throw DecodeFailure.length
    }

    var padded = text
        .replacingOccurrences(of: "-", with: "+")
        .replacingOccurrences(of: "_", with: "/")
    padded += String(repeating: "=", count: (4 - remainder) % 4)

    guard let data = Data(base64Encoded: padded) else {
        throw DecodeFailure.payload
    }

    let canonical = data.base64EncodedString()
        .replacingOccurrences(of: "+", with: "-")
        .replacingOccurrences(of: "/", with: "_")
        .replacingOccurrences(of: "=", with: "")

    guard canonical == text else {
        throw DecodeFailure.noncanonical
    }
    return data
}

La dernière comparaison est indispensable. La représentation canonique de l’octet FF est _w ; dans _x, les bits de fin inutilisés sont non nuls, mais certains décodeurs peuvent tout de même restituer le même octet. La comparaison après réencodage rend la décision d’acceptation indépendante du degré de permissivité du décodeur sous-jacent.

La fonction renvoie ici un objet Data, sans conversion directe en chaîne. Des données binaires valides ne sont pas nécessairement du texte UTF-8. Si la couche supérieure attend du texte, elle doit vérifier séparément l’encodage, sans masquer une corruption par des caractères de remplacement.

Transformer les cas de rejet en vérifications exécutables

Ajoutez les tests suivants à la fin du même fichier. En cas d’échec, une erreur est levée et le script se termine avec un code de sortie non nul. Le test ne dépend donc pas d’assertions susceptibles d’être supprimées dans une compilation optimisée.

enum CheckFailure: Error {
    case mismatch
    case acceptedInvalid
}

let validCases: [(String, [UInt8])] = [
    ("AA", [0]),
    ("_w", [255]),
    ("-_8", [251, 255]),
    ("Zm9v", [102, 111, 111])
]

for (text, expected) in validCases {
    guard try decodeBase64URL(text) == Data(expected) else {
        throw CheckFailure.mismatch
    }
}

let invalidCases = [
    "", "A", "Zg==", "Zg\n", "+w", "/w", "_x", "中文"
]

for text in invalidCases {
    do {
        _ = try decodeBase64URL(text)
    } catch is DecodeFailure {
        continue
    }
    throw CheckFailure.acceptedInvalid
}

do {
    _ = try decodeBase64URL("AAAA", maxEncodedBytes: 3)
} catch is DecodeFailure {
    exit(0)
}
throw CheckFailure.acceptedInvalid

Sur un Mac cloud où les outils en ligne de commande sont configurés, exécutez :

xcrun swift base64url-check.swift

Si les tests réussissent, le script ne produit aucune sortie et son code de sortie vaut zéro. _x couvre la vérification des bits de fin ; le cas contenant un saut de ligne empêche qu’un nettoyage automatique soit ajouté ultérieurement sans être détecté ; le cas en chinois confirme que l’alphabet est contrôlé octet par octet selon ASCII, et non selon une notion de caractère qui « ressemble à une lettre ».

Vérifier les limites de taille et l’emplacement des contrôles

La limite de taille doit être vérifiée avant les remplacements dans la chaîne et le décodage. Sinon, une entrée trop longue aura déjà entraîné la création de copies supplémentaires. La plus grande entrée canonique autorisée ici peut produire 49152 octets décodés, mais ce n’est qu’un budget au niveau de l’encodage ; les champs métier devraient généralement avoir des limites plus basses.

La chaîne passée à la fonction est déjà en mémoire. Cette fonction ne remplace donc pas une limite de taille sur les réponses réseau. La couche de réception doit limiter le corps de la réponse, la couche de décodage doit limiter les champs, et la couche d’analyse structurelle doit limiter le nombre d’éléments des collections ainsi que la profondeur d’imbrication. Chaque couche a sa propre responsabilité.

Ajoutez également trois cas limites :

N’utilisez pas le résultat du décodage comme identifiant tout en acceptant ailleurs des variantes décodées de manière permissive. Le contrat d’API doit définir une représentation commune pour les clés de cache, les clés de déduplication et les données soumises à signature. Sinon, un même contenu peut être traité différemment selon sa représentation.

Intégrer les tests de régression et limiter les diagnostics

Une fois le script autonome validé, placez la fonction de décodage dans un module partagé et transférez les cas de test dans la cible de tests unitaires de l’application. Le script en ligne de commande facilite une reproduction rapide ; les tests de la cible applicative permettent de détecter les différences liées aux dépendances réelles et à la configuration de compilation. Il ne faut pas maintenir durablement deux implémentations séparées.

En complément des cas fixes, vous pouvez prendre des tableaux d’octets de différentes longueurs, les encoder, les décoder, puis comparer les résultats octet par octet. Couvrez en priorité les différents restes de la division de la longueur par trois : ils déterminent le nombre de bits de fin utilisés dans le dernier groupe encodé. Modifiez ensuite le dernier caractère pour vérifier qu’aucune représentation non canonique ne passe le contrôle.

Les rapports d’échec ne doivent conserver que la catégorie d’erreur, la longueur de l’entrée et le numéro du cas de test, jamais de vrais jetons ni de curseurs complets. Avant la mise en production, vérifiez qu’il n’y a ni suppression automatique des caractères blancs, ni ignorance des caractères inconnus, ni mélange d’alphabets, et que la validation structurelle intervient après le décodage. Le but n’est pas de réussir à décoder davantage d’entrées, mais de n’accepter que la représentation convenue entre les deux parties.

Questions fréquentes

Pourquoi réencoder les octets après le décodage ?

Certains décodeurs acceptent des bits inutilisés non nuls à la fin. Réencoder sous forme Base64URL canonique sans remplissage puis comparer au texte reçu permet de rejeter ces variantes.

Ce décodeur vérifie-t-il la signature d’un jeton ?

Non. Il contrôle uniquement la représentation encodée. La signature, l’expiration et l’émetteur exigent une vérification cryptographique et des contrôles applicatifs distincts.

Validez votre pipeline sur un Mac dans le cloud

ArmRDP met à votre disposition des Mac mini physiques dédiés, et non des machines virtuelles. Louez à la journée : vérifiez d’abord votre chaîne d’outils et les résultats de compilation, puis choisissez la durée adaptée à votre équipe.

Louer un Mac dans le cloud

Des configurations vérifiables, des builds reproductibles

Vérifiez le modèle, la durée de location et les étapes de connexion, puis confiez vos tâches à un nœud physique dédié.