Строгая проверка декодирования Base64URL для iOS на облачном Mac
API кодирует бинарный курсор в Base64URL, но iOS-клиент иногда принимает повреждённый курсор за корректные данные. Причина не обязательно в сети: нестрогое декодирование, автоматическое добавление заполнения и игнорирование недопустимых символов могут превратить в байты входные данные, которые следовало отклонить. На облачном Mac можно сначала зафиксировать контракт декодирования в отдельном Swift-скрипте, а затем перенести тот же набор примеров в тестовый таргет приложения. Для проверки граничных случаев кодирования запускать симулятор не потребуется.
Сначала определите допустимый формат кодирования
В этой статье допускается только непустой Base64URL без заполнения: латинские буквы, цифры и символы -, _. Пробелы, переводы строк, +, / и = не допускаются. Это явно заданное правило API приложения, а не универсальное требование ко всем протоколам, использующим Base64.
Не следует принимать стандартный Base64 и Base64URL через одну точку входа, пытаясь угадать формат. Если старому API нужно заполнение, предусмотрите отдельную функцию разбора и тестируйте её отдельно. Иначе после обновления клиент может незаметно начать принимать более широкий набор входных данных.
У длины тоже есть структурные ограничения: при делении числа символов текста без заполнения на четыре остаток может быть только нулём, двумя или тремя. Остаток один означает, что корректная кодировка невозможна. В примере максимальный размер входа установлен в 65536 байт UTF-8. Это политика приложения для данного теста, а не ограничение протокола.
Декодирование лишь восстанавливает байты, но не подтверждает достоверность содержимого. Для токенов, курсоров и параметров подписи по-прежнему нужно проверять структуру, источник и допустимость в контексте бизнес-логики.
Проверяйте каноническое представление повторным кодированием
Сохраните следующий код в файл base64url-check.swift. Сначала ограничьте размер входа, затем проверьте алфавит и только после этого добавляйте заполнение внутри функции. Не используйте параметры, позволяющие игнорировать неизвестные символы, и не удаляйте пробельные символы заранее.
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
}
Последнее сравнение обязательно. Каноническое представление одного байта FF — _w. В строке _x неиспользуемые биты в конце ненулевые, но некоторые декодеры всё равно могут восстановить из неё тот же байт. Сравнение после повторного кодирования позволяет не зависеть от того, насколько нестрог нижележащий декодер.
Функция возвращает Data, не преобразуя результат сразу в строку. Корректные бинарные данные не обязательно являются текстом UTF-8. Если вышележащему коду нужен текст, он должен отдельно проверить кодировку, а не скрывать повреждения символами замены.
Оформите отрицательные примеры как исполняемые проверки
Добавьте следующие тесты в конец того же файла. При неудаче тест выбрасывает ошибку, и скрипт завершается с ненулевым кодом. Так проверка не зависит от утверждений assert, которые могут быть удалены в оптимизированной сборке.
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
На облачном Mac с настроенными инструментами командной строки выполните:
xcrun swift base64url-check.swift
При успехе скрипт ничего не выводит и завершается с кодом ноль. Пример _x проверяет неиспользуемые биты в конце; пример с переводом строки защищает от последующего добавления автоматической очистки; пример с китайскими иероглифами подтверждает, что алфавит проверяется по байтам ASCII, а не по принципу «похоже на букву».
Проверьте ограничения размера и место вызова
Проверять максимальный размер нужно до замен в строке и декодирования. Иначе слишком длинные данные уже успеют породить дополнительные копии. Самый длинный допустимый канонический вход в этом примере декодируется в 49152 байт, но это лишь лимит на уровне кодирования. Для полей бизнес-данных обычно следует задавать меньшие пределы.
Строка, которую получает функция, уже находится в памяти, поэтому эта проверка не заменяет ограничение размера сетевого ответа. На уровне приёма нужно ограничивать тело ответа, на уровне декодирования — отдельные поля, а при разборе структуры — количество элементов коллекций и глубину вложенности. У каждого из трёх уровней своя ответственность.
Добавьте ещё три граничных примера:
- Корректный текст длиной ровно до установленного предела должен успешно декодироваться.
- Превышение предела на один символ должно приводить к отказу. Нельзя сначала обрезать строку, а затем декодировать её.
- Текст допустимой длины, но с недопустимым содержимым, всё равно должен отклоняться.
Если результат декодирования используется как идентификатор, нельзя в других путях обработки принимать менее строгое представление. В контракте API нужно единообразно определить представление для ключей кеша, ключей дедупликации и входных данных подписи. Иначе одно и то же содержимое может обрабатываться по-разному.
Добавьте регрессионные тесты и ограничьте диагностические данные
Когда отдельный скрипт пройдёт проверки, перенесите функцию декодирования в общий модуль, а примеры — в таргет модульных тестов приложения. Скрипт командной строки удобен для быстрого воспроизведения проблемы, а тесты в таргете приложения выявляют различия в реальных зависимостях и настройках сборки. Не стоит постоянно поддерживать две отдельные реализации.
Помимо фиксированных примеров, проверяйте массивы байтов разной длины по схеме «кодирование, декодирование, побайтовое сравнение». Особенно важны разные остатки от деления длины на три: от них зависит число неиспользуемых битов в последней группе кодировки. Затем изменяйте последний символ и проверяйте, что неканонические представления не проходят проверку.
В отчётах об ошибках сохраняйте только категорию ошибки, длину входа и номер примера. Не записывайте реальные токены или полные курсоры. Перед выпуском убедитесь, что нет автоматического удаления пробелов по краям, игнорирования неизвестных символов и смешивания алфавитов, а проверка структуры выполняется после декодирования. Итоговая цель — не декодировать как можно больше входных данных, а принимать только то представление, о котором договорились обе стороны.
Часто задаваемые вопросы
Зачем повторно кодировать полученные байты?
Некоторые декодеры принимают ненулевые неиспользуемые биты в конце. Повторное кодирование в канонический Base64URL без заполнения и сравнение с исходной строкой позволяют отвергнуть такие варианты.
Проверяет ли этот декодер подпись токена?
Нет. Он проверяет только форму кодирования. Подпись, срок действия и издателя нужно проверять отдельно средствами криптографии и прикладной логики.
Проверьте облачный Mac на собственной сборке
ArmRDP предоставляет выделенный физический Mac mini, а не виртуальную машину. Арендуйте посуточно: сначала проверьте инструменты и результат сборки, затем выберите подходящий срок аренды для команды.
Арендовать облачный Mac