Золотая лихорадка, связанная с искусственным интеллектом, в самом разгаре.
А во время золотой лихорадки злоумышленники крадут лопаты (поскольку в золотую лихорадку богатеют не добытчики золота, а продавцы лопат).
Этому способствует одна убийственная и повсеместно распространенная ошибка: привязка API-ключей ChatGPT непосредственно к приложению ИИ. Она упрощает кражу ваших ключей, растрату ваших кредитов и накопление неоплаченных долгов на ваших счетах.
Вы должны усвоить один из основных законов безопасности:
Не храните API-ключи на клиенте. Никогда.
Я продемонстрирую вам два метода, которые позволяют злоумышленникам получить доступ к вашим API-ключам, и расскажу, как можно избежать подобного.
Декомпиляция приложения
Храня API-ключи на своем устройстве, вы можете использовать различные уровни сложности для их защиты. Рассмотрим подробно наиболее популярные подходы и простейшие способы их обхода.
Неформатированные строки в коде
Как правило, это первый подход, который используют инженеры до того, как станут заниматься безопасностью. Запрашивая данные из API, вы пишете что-то вроде этого:
func callChatGPTAPI(prompt: String) async throws -> Data {
let url = URL(string: "https://api.chatgpt.com")!
var request = URLRequest(url: url)
let apiKey = "12345678-90ab-cdef-1234-567890abcdef"
request.addValue(apiKey, forHTTPHeaderField: "Authorization")
return try await URLSession.shared.data(for: request).0
}
Этот код компилируется и предоставляется пользователям в виде бандла .app, содержащего весь исполняемый код.
Получить доступ к .ipa-файлу любого приложения в App Store, который содержит бандл .app, довольно просто (ссылка). И это первое, что попытается сделать злоумышленник, желающий украсть ваши ключи.

Следуя данному подходу, можно легко заглянуть внутрь бандла .app. Это всего лишь папка, которая выглядит примерно так.

Самое ценное находится в Bev — UNIX-исполняемом файле моего стороннего проекта. Он содержит весь скомпилированный и связанный код приложения.
С помощью встроенной UNIX-команды strings можно инспектировать каждую неформатированную строку, которая находится внутри этого скомпилированного исполняемого файла.
strings Bev
Естественно, вывод будет довольно многословным:

Помимо неформатированных строк, вы увидите множество имен символов для скомпилированных функций.
Но в самом низу этого списка будет приз:
12345678-90ab-cdef-1234-567890abcdef
Неформатированная строка для API-ключа, доступная всем — ее легко найдет любой человек, умеющий обращаться с регулярными выражениями (regex).
Info.plist
Возможно, вы не собираетесь помещать неформатированные строки в свое приложение, где они могут быть легко декомпилированы.
Вы помещаете их в Info.plist, где им и следует находиться.

Точка API-вызова становится сложнее, и теперь получить это значение не так-то легко.
func callChatGPTAPI(prompt: String) async throws -> Data {
let url = URL(string: "https://api.chatgpt.com")!
var request = URLRequest(url: url)
let apiKey = getAPIKeyPlist()
request.addValue(apiKey, forHTTPHeaderField: "Authorization")
return try await URLSession.shared.data(for: request).0
}
func getAPIKeyPlist() -> String? {
Bundle.main.object(forInfoDictionaryKey: "API_KEY") as? String
}
Здесь несколько подуровней сложности. Вместо непосредственного жесткого написания кода ключей, вы можете использовать несколько наборов ключей для разработки и производства. Можно создавать файлы .xcconfig для каждой среды, которые разрешаются в значения Info.plist во время сборки.
Если вы фанатичный приверженец защиты информации, то знаете, что никогда не следует коммитить секреты в систему управления версиями исходного кода. Возможно, вы еще более продвинуты в вопросах безопасности и надежно храните API-ключи в своем CI/CD-конвейере, внедряя их в .xcconfig или Info.plist как часть сценариев сборки.
Но все это напрасно.
Взгляните еще раз на это декомпилированное приложение, доступное любому, у кого есть MacBook, iPhone и достаточно свободного времени:


Значения, помещенные в Info.plist, являются общедоступными независимо от того, как они туда попали.
Обфусцированные строки
Если вы некритично относитесь ко всему, что читаете в интернете, можете подумать, что ловко обфуцируете (маскируете) свои строки. Именно так я думал в 2019 году, когда был начинающим экспертом по безопасности.
В действительности, что бы вы ни фантазировали на счет своего алгоритма обфускации и используемой соли, стоит поручить ChatGPT написать пару функций для обфускации и декодирования API-ключей.
private func obfuscate(key: String) -> String? {
let salt = "00000000-0000-0000-0000-000000000000"
guard let keyData = key.data(using: .utf8),
let saltData = salt.data(using: .utf8) else {
return nil
}
let obfuscatedData = zip(keyData, saltData).map { $0 ^ $1 }
let obfuscatedBase64 = Data(obfuscatedData).base64EncodedString()
return obfuscatedBase64
}
private func decode(obfuscated: String) -> String? {
let salt = "00000000-0000-0000-0000-000000000000"
guard let obfuscatedData = Data(base64Encoded: obfuscated),
let saltData = salt.data(using: .utf8) else {
return nil
}
let apiKeyData = zip(obfuscatedData, saltData).map { $0 ^ $1 }
return String(bytes: apiKeyData, encoding: .utf8)
}
Получение ключа во время выполнения программы сводится к декодированию неформатированной обфусцированной строки.
func callChatGPTAPI(prompt: String) async throws -> Data {
let url = URL(string: "https://api.chatgpt.com")!
var request = URLRequest(url: url)
let apiKey = getAPIKeyObfuscated()
request.addValue(apiKey, forHTTPHeaderField: "Authorization")
return try await URLSession.shared.data(for: request).0
}
func getAPIKeyObfuscated() -> String? {
decode(obfuscated: "AQIDBAUGBwgACQBRUgBTVFVWAAECAwQABQYHCAkAUVJTVFVW")
}
Хотя читать это немного сложнее, чем неформатированные строки, достаточно мотивированный хакер все равно сможет легко украсть ваши ключи.
Если вы используете такой небезопасный подход, как кодирование base-64 или XORing, учтите: его можно легко перепрограммировать. Даже с приличным алгоритмом и солью злоумышленники могут воспользоваться такими инструментами, как Frida, чтобы извлечь ваши API-ключи из памяти устройства во время выполнения.
Обфускация обеспечивает безопасность через неясность (security by obscurity), но это неэффективный подход. Какой бы надежной ни была обфускация, в определенный момент API-ключ должен быть декодирован и отправлен в API.
Это приводит нас к последнему и, возможно, самому простому способу кражи API-ключей.
Мониторинг сетевого трафика
Это одна из самых мощных техник в вашем распоряжении.
Бесплатные инструменты вроде Proxyman (или Charles Proxy для опытных пользователей) позволят вам просмотреть весь трафик, входящий и исходящий из приложения или сайта.

Через несколько секунд после загрузки программы сразу же обнаружите в сетевом запросе auth-заголовок, содержащий API-ключ.
12345678-90ab-cdef-1234-567890abcdef
Я привык делать еще кое-что, собираясь приступить к новой работе: проверяю сетевой трафик, чтобы выяснить, работает ли он через REST-дом или GraphQL-магазин.
Защита от проксирования
Для борьбы с этим вектором атак можно использовать такие методы, как SSL-пиннинг.
Он проверяет TLS-сертификаты, представленные внутренними серверами, на соответствие сертификатам, хранящимся на устройстве. Это предотвращает атаки типа «незаконный посредник» и блокирует корневой сертификат прокси-сервера сетевых прокси-инструментов.
Чтобы реализовать SSL-пиннинг на iOS, можно создать декоратор сетевых запросов с помощью URLSessionDelegate, реализующего urlSession(didReceive: URLAuthenticationChallenge).
Но, как вы уже догадались, SSL-пиннинг — еще не все, что вам требуется. Специалисты реверс-инжиниринга (обратной разработки) могут использовать инструменты для обхода пиннинга, в том числе и на устройствах с джейлбрейком.
К тому же, если вы не знаете, что именно делаете, использование SSL-пиннинга может быть рискованным.
Когда я был относительно молодым инженером без достаточного опыта, по наивности внедрил SSL-пиннинг с помощью корневого EC2-сертификата, принадлежащего AWS. Естественно, на той же неделе со стороны AWS была проведена ротация сертификата. В общем, центры тестов на COVID-19 в Великобритании не работали в течение самых долгих 59 минут в моей жизни.
Что же делать?
Надеюсь, к этому моменту вы усвоили главный урок: защита API-ключей на стороне клиента — основная стратегия для обеспечения безопасности. Ваши API-ключи никогда не будут в безопасности, если вы включите их в свое публично распространяемое приложение.
Теперь, когда вы знаете, чего не надо делать, поговорим о том, что можно сделать.
Хотя клиентская сторона по своей природе подвержена опасностям, гораздо проще защитить бэкенд от злоумышленников. «Хрестоматийный» подход к защите сторонних API-ключей заключается в размещении их в менеджере секретов (периодически ротируемом) и проксировании сетевых запросов через ваш бэкенд.
При таком подходе вы можете аутентифицировать сетевые запросы с помощью собственных систем и минимизировать потенциальный вектор атаки.
Если вы еще не развернули собственную инфраструктуру, Firebase Cloud Functions — простой способ реализовать этот промежуточный слой.
В то же время можете добавить еще несколько уровней защиты своего устройства с помощью таких инструментов, как AppAttest на iOS, Play Integrity на Android, а также путем внедрения библиотек RASP (runtime application self-protection — защита безопасности приложения во время выполнения). Они не позволят вашему приложению работать на устройствах с джейлбрейком и защитят от инструментов реверс-инжиниринга.
Небольшое дополнение к моим рекомендациям: при использовании Firebase я обнаружил серьезные проблемы с AppAttest. Возможно, они уже устранены, но я никогда не упускаю случая упомянуть о недоработках.
Заключение
Не храните ключи API на клиенте. Никогда.
Любой, кто скажет вам, что ваши API-ключи в безопасности на клиенте, либо глупец, либо злоумышленник.
Все, что входит в состав вашего приложения, можно проинспектировать. API-ключи в неформатированных строках и файлах Info.plist легко найти, а более сложные подходы вроде обфускации все еще уязвимы для мотивированных злоумышленников.
Сетевое проксирование — мощный инструмент, используемый как во благо, так и во зло. Такие механизмы защиты, как SSL-пиннинг, могут принести больше проблем, чем пользы, если вы не знаете, что делаете. Механизмы защиты на стороне клиента, такие как проверка целостности приложения или библиотеки для обнаружения реверс-инжиниринга, помогают повысить уровень защиты приложения или сайта.
Если хотите, чтобы ваши API-ключи (в разумной степени) были в безопасности, храните все на бэкенде или используйте такие продукты, как Firebase Cloud Functions. Можете спать спокойно, зная, что обеспечили безопасность своим API-ключам. Насколько это возможно.
Блог Jacob’s Tech Tavern не несет никакой ответственности, если, последовав рекомендациям Jacob Bartlett (автора блога и этой статьи), вы выставите счет на $5 000 ChatGPT или Firebase.
Читайте также:
- Создаем ИИ с помощью OpenAI
- Как автоанализ кода с помощью ИИ повышает безопасность приложений
- Секреты в Android. Часть 1
Читайте нас в Telegram, VK и Дзен
Перевод статьи Jacob Bartlett: How I Stole Your ChatGPT API Keys





