Если один и тот же пароль позволяет расшифровать файл в Windows, а в iOS считается неверным, не спешите сбрасывать данные. Причина может быть не в алгоритме шифрования, а в несовпадении кодировки пароля, числа итераций или длины производного ключа. Сначала запустите тесты с известными ответами на облачном Mac, затем проверьте реальную обёртку приложения в целевой среде iOS — так вы сможете локализовать проблему на конкретном уровне.
Зафиксируйте контракт входных данных
Эта статья предназначена для кроссплатформенных проектов с уже существующим протоколом на основе PBKDF2. Она не предлагает использовать его по умолчанию во всех новых системах. Для начала явно задайте шесть полей: алгоритм, PRF, кодировку байтов пароля, байты соли, число итераций и длину результата. Здесь используются PBKDF2-HMAC-SHA256 и результат длиной 32 байта.
Пароль кодируется в UTF-8 без автоматического удаления пробелов, изменения регистра или нормализации Unicode. Визуально одинаковый текст может иметь разное байтовое представление. Если в старом протоколе уже предусмотрена нормализация, её правила необходимо сохранить и включить в описание версии.
Тесты с известными ответами проверяют правильность вычислений, а замеры на устройствах — пригодность выбранного фактора трудоёмкости. Одну или две итерации из тестовых векторов ни в коем случае нельзя переносить в рабочую конфигурацию.
Также убедитесь, что соль передаётся как исходные байты, а не как текст в шестнадцатеричной записи. Строка 73616c74 в качестве соли и байты слова salt — это разные входные данные.
Запустите стандартные тестовые векторы
На Mac, где уже выбран полный комплект инструментов разработки, сохраните следующий код в файл pbkdf2-check.swift. Он напрямую вызывает системную библиотеку CommonCrypto и не требует загрузки зависимостей. При ошибке выполнение прерывается; пароль и результат выработки ключа не выводятся.
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)
Выполните xcrun swift pbkdf2-check.swift. Нулевой код завершения означает, что все эти проверки прошли. Здесь скрипт запускается в режиме интерпретации; не заменяйте этот запуск оптимизированной сборкой с отключёнными проверками.
Код явно передаёт длину пароля в байтах: завершающий нулевой байт не учитывается, а нулевые символы внутри пароля сохраняются. Если использовать интерфейс, который определяет длину по первому нулевому байту, оставшаяся часть пароля будет отсечена.
Проверьте рабочую обёртку приложения
После успешного запуска скрипта добавьте те же примеры в тестовый таргет XCTest для iOS, но вызывайте именно ту точку входа для выработки ключа, которую использует приложение. Не ограничивайтесь копированием реализации специально для тестов: иначе нижний уровень будет работать правильно, а обёртка по-прежнему сможет неверно декодировать соль.
Добавьте как минимум четыре группы проверок:
- Пустой пароль: явно разрешайте или отклоняйте его согласно протоколу, не подменяя незаметно другим значением.
- Китайские иероглифы, эмодзи и встроенные нулевые символы: сверяйте входные байты и итоговый результат на обеих платформах.
- Нулевое число итераций, повреждённая кодировка соли, неизвестный идентификатор алгоритма: отклоняйте такие данные до вызова функции выработки ключа.
- Один пароль с разными солями: на фиксированных примерах проверяйте, что результаты различаются, чтобы выявить случайное использование постоянной соли.
Функция из примера отклоняет пустую соль в соответствии с политикой входных данных этого проекта, а не потому, что алгоритм не способен её обработать. В рабочей системе соль нужно генерировать с помощью системного криптографически стойкого источника случайных данных. Если этот источник недоступен, необходимо вернуть ошибку, а не подставлять временную метку.
Разделяйте уровни тестирования
Скрипт на Mac проверяет корректность подключения алгоритма, тесты в симуляторе — обёртку приложения, а тесты на физическом устройстве — время выполнения и влияние на интерфейс. На всех трёх уровнях фиксируйте один и тот же контракт входных данных, но не принимайте скорость Mac за скорость телефона.
Сохраняйте параметры для обновления
В записи с параметрами выработки ключа должны храниться как минимум версия формата, идентификатор алгоритма, число итераций, соль и длина результата. Если используется текстовое кодирование, укажите и его формат — например, конкретные правила Base64 для соли. Соль не является паролем, и безопасность не должна зависеть от её сокрытия.
При чтении записи сначала проверяйте типы и длины полей, а также допустимый объём вычислений, и только затем запускайте выработку ключа. Повреждённый файл не должен задавать настолько большое число итераций, что приложение надолго займёт CPU. Фактор трудоёмкости следует выбирать на основании требований безопасности и замеров на поддерживаемых устройствах, а не копировать из тестовых векторов.
При обновлении параметров нельзя просто изменить глобальные константы и затем читать старые записи с новыми значениями. Для проверки старой записи нужны её прежние параметры. Только после успешного получения необходимых данных в открытом виде можно создавать запись новой версии с новой солью и по новым правилам.
Если производный ключ используется для расшифрования, перейти на новые параметры, имея только старый шифротекст, напрямую нельзя. Сначала нужно успешно расшифровать данные, затем зашифровать их заново. Старую запись необходимо сохранять до тех пор, пока новая не будет полностью записана и проверена. Если же хранится проверочное значение пароля, сравнивайте его с помощью проверенной реализации сравнения за постоянное время, а не обычного сравнения массивов из примера.
Настройте проверки перед слиянием
Предусмотрите в системе непрерывной интеграции две независимые проверки: результаты для стандартных векторов должны совпадать побайтно, а код работы с записями приложения должен читать старые версии и отклонять недопустимые параметры. Добавьте также старую запись, предназначенную только для тестов: убедитесь, что после обновления она по-прежнему открывается, а новые записи действительно создаются в новой версии формата.
В отчётах об ошибках оставляйте только имя тестового случая, алгоритм и неконфиденциальные параметры. Не загружайте пароли, производные ключи или реальные пользовательские записи. Замеры производительности вынесите в отдельные задания на контролируемых физических устройствах; не блокируйте слияние из-за единичного замера времени в общей среде выполнения.
Наконец, проверьте сценарии сбоя миграции: после отмены, ошибки записи на диск или завершения работы приложения старая запись должна оставаться доступной для чтения. Тогда проверку проходит не просто функция обработки пароля, а весь процесс, который не допускает потери данных при обновлении параметров.
Часто задаваемые вопросы
Можно ли использовать одну или две итерации в рабочем приложении?
Нет. Это параметры только для тестов с известным ответом. Рабочий коэффициент трудоёмкости выбирают по требованиям безопасности и замерам на целевых устройствах. Для недоверенных параметров также нужен предел вычислений.
Достаточно ли успешного запуска на Mac для проверки iOS?
Нет. Скрипт на Mac быстро проверяет алгоритм и обработку байтов. Тестовая цель iOS должна вызвать реальную обёртку приложения, а длительность вычисления и влияние на интерфейс нужно измерить на целевых устройствах.
Арендуйте облачный Mac
Аренда выделенного физического Mac на день, неделю, месяц или квартал. Планируйте сборки под свои проекты — по окончании срока аренды компьютер отключится.
Выбрать модель