Разработка
Использование on-demand ресурсов для безопасного хранения ключей API в iOS-приложениях
Следует помнить, что извлечь строки из файлов IPA довольно просто, и поэтому, если мы храним API-ключи в коде, кто-то другой может получить к ним доступ. Это, конечно, большая проблема для безопасности.
Многие приложения используют API-ключи при аутентификации сетевых запросов. Хотя есть и более эффективные способы аутентификации запросов, например OAuth с PKCE, но они не всегда возможны. Следует помнить, что извлечь строки из файлов IPA довольно просто, и поэтому, если мы храним API-ключи в коде, кто-то другой может получить к ним доступ. Это, конечно, большая проблема для безопасности. Один из способов избежать этого — использовать ресурсы Apple загружаемые по требованию с включенной предварительной загрузкой. Это означает, что как только мы установим приложение, iOS загрузит дополнительные ресурсы отдельно, и эти ресурсы могут содержать наши API-ключи. Такое разделение позволяет не помещать API-ключи в IPA-файл. Никто больше не сможет заглянуть в IPA-файл и попытаться извлечь из него строковые константы. Давайте посмотрим, как это настроить.
Первым делом мы создадим тег с поддержкой предварительной загрузки. Apple использует теги для идентификации ресурсов. Откройте настройки проекта Xcode, цель приложения, а затем вкладку Resource Tags. Добавим новый тег ресурса под названием «APIKeys».
Следующим шагом будет прикрепление ресурса к тегу. Мы будем использовать JSON-файл для наших API-ключей, поэтому добавьте новый JSON-файл для API-ключей. Мы просто создадим пары ключ-значение в этом файле и назначим ему тег ресурса, который можно найти в области утилит > вкладка File Inspector. В нашем примере тег имеет то же имя, что и файл «APIKeys».
Мы создали тег ресурса и назначали тег файлу JSON. По умолчанию тег рассматривается как ресурс по требованию и загружается только тогда, когда он требуется приложению. В случае с API-ключами имеет смысл загружать его вместе с бинарным файлом приложения, когда пользователь устанавливает это приложение. Тогда при первом запуске мы сможем сразу же сохранить API-ключ в связке ключей для дальнейшего использования. Предварительную загрузку можно включить на вкладке Resource Tags. Нажмите на кнопку Prefetched и перетащите тег APIKeys в раздел Initial Install Tags.
Важно отметить, что даже если мы установили этот тег как часть тегов начальной установки, все равно есть вероятность, что он будет удален. Это возможно когда пользователь устанавливает приложение, а затем долго ждет. В этом случае системе приходится загружать его снова, когда мы хотим получить к нему доступ. Поэтому код, обращающийся к тегу, может занять некоторое время. Рассмотрим простую функцию, которая получает доступ к файлу JSON через API NSBundleResourceRequest и делает ключи API доступными для приложения.
enum Constants { static func loadAPIKeys() async throws { let request = NSBundleResourceRequest(tags: ["APIKeys"]) try await request.beginAccessingResources() let url = Bundle.main.url(forResource: "APIKeys", withExtension: "json")! let data = try Data(contentsOf: url) // TODO: Store in keychain and skip NSBundleResourceRequest on next launches APIKeys.storage = try JSONDecoder().decode([String: String].self, from: data) request.endAccessingResources() } enum APIKeys { static fileprivate(set) var storage = [String: String]() static var mySecretAPIKey: String { storage["MyServiceX"] ?? "" } static var mySecretAPIKey2: String { storage["MyServiceY"] ?? "" } } }
При такой настройке нам нужно убедиться, что функция loadAPIkeys
будет вызвана до того, как мы получим доступ к mySecretAPIKey
и mySecretAPIKey2
. Если у нас есть централизованное место для сетевых запросов, скажем, какой-нибудь сетевой модуль, который оборачивает URLSession, то это может быть отличным местом для запуска этого асинхронного кода. Другим способом может быть задержка показа основного UI до завершения функции. Лично я бы выбрал первый вариант и интегрировал его в сетевой стек.
Пример OnDemandAPIKeyExample на GitHub.