Прерывистая и временная ошибка Keychain iOS

У нас есть приложение, которое в значительной степени полагается на возможность доступа к токену пользовательского сеанса с помощью iOS Keychain. Когда наше приложение открывается, первое, что было проверено, - это то, доступен ли токен, а если нет, мы показываем пользователю экран входа в систему. Мы не используем для этого какую-либо стороннюю библиотеку и используем Keychain SecItemAdd()/SecItemCopyMatching() напрямую со следующими параметрами:

  • kSecClassGenericPassword
  • kSecAttrAccessibleAlwaysThisDeviceOnly

Мы не видим никаких проблем с этим при нормальном использовании.

Эта проблема

У нас были пользователи, которые сообщали, что после открытия своего приложения они отображают экран входа в систему (предполагая, что Keychain не смог найти значение), когда они действительно вошли в систему. Мы обнаружили, что в этом случае мы обнаружили, что после убийства и перезагрузка приложения, пользователи вернулись к нормальной работе (сеанс был найден в Keychain). Как только мы это обнаружили, мы попытались добавить экспоненциальную отсрочку, чтобы продолжать запрашивать Keychain, думая, что она может быть недоступна только в течение первых нескольких секунд. Это не сработало и доказало нам, что Keychain, кажется, недоступен для всего сеанса запуска приложения. Мы не можем воспроизвести эту проблему. Кажется, это происходит очень прерывисто.

При дальнейшем расследовании мы обнаружили, что обычно пользователи получали VoIP-сигналы до возникновения этой проблемы. Мы по-прежнему не в состоянии достоверно воспроизвести эту проблему, но после отладки выяснили, что наш Keychain.session был признан nil при получении этих нажатий (мы также полагаемся на него), который до того, как пользователь откроет свое приложение чтобы увидеть, что они pseudo- "вышли из системы".

Мы используем PushKit и PKPushRegistry для того, чтобы сделать VoIP Push. Это требует, чтобы наше приложение включало "Фоновые режимы: голос поверх IP". Мы используем все это, как указано в документации Apple. Здесь образец:

func pushRegistry(_ registry: PKPushRegistry, didReceiveIncomingPushWith payload: PKPushPayload, for type: PKPushType) {
    guard let _ = Keychain.session else {
        print("Session token not available")
        return
    }
    handle(notification: payload.dictionaryPayload)
}

Этот код основывается на том, что Keychain.session сразу доступен после получения VoIP Push. Как упоминалось ранее, мы видели случаи, когда они недоступны, и поэтому эта функция выходит из системы и просто возвращается.

Я также нашел эту важную публикацию на форуме Apple, предполагая, что это может быть ошибка iOS.

Может ли кто-нибудь помочь нам в этом вопросе? Любая помощь будет принята с благодарностью. Даже если это на самом деле ошибка iOS, мы открыты для обходных решений.

Ответ 1

У меня были похожие проблемы, и после разговора с инженерами на WWDC мы выявили несколько проблем, которые приводят к такому поведению.

  1. Мы слишком часто звонили в брелок, и это может привести к ситуации, когда это будет дорого, а некоторые звонки не будут завершены или истечет время ожидания.

  2. Если приложение не предоставляет Группу доступа для цепочки для ключей или не добавляет ее в транзакцию сохранения с цепочкой для ключей, тогда Xcode генерирует одну, но это динамично и может меняться между средами разработки

  3. Если вы предоставили группу доступа позже в жизни вашего приложения, то ваше старое значение цепочки для ключей останется связанным с приложением, и новое значение будет создано с группой доступа, которую вы теперь предоставите.
  4. Это также означает, что когда вы запрашиваете связку ключей, вы должны предоставить группу доступа, в противном случае вы получите ссылку на то, что когда-либо было найдено первым.

Редактировать: форматирование

Ответ 2

Я думаю, что эта тема уже давно сводит с ума сообщество разработчиков iOS. Я прочитал так много статей и веток об этом, что потерял счет.

Итак, я тоже занимаюсь этой проблемой, и, основываясь на ответе @naqi, я начал двигаться в этом направлении и, возможно, нашел рабочее решение.

Предположения:

Кажется, проблема возникает, когда приложение приостанавливается по какой-либо причине (например, приложение находится в фоновом режиме из-за долгого времени и из-за нехватки памяти ОС отправляет его в приостановленном состоянии, или приложение находится в состоянии bg, а пользователь обновляется до новая версия из магазина или автоматическое обновление приложения делает это). Я больше не совсем уверен, что означает приостановка, но я сильно подозреваю, что приложение может быть возобновлено/пробуждено без фактического отображения на экране (из-за задачи bg, но не обязательно). Один из сотрудников Apple на форуме разработчиков Apple, был смел, чтобы ответить, сказав что-то вроде "попробуйте поставить задержку между addDidFinishLaunchingWithOptions и appDidBecomeActive". Помимо этого безумия, чтение этого комментария заставило меня задуматься о том, что, возможно, когда приложение возобновляется, оно вызывает addDidFinishLaunchingWithOptions, но, конечно, не appDidBecomeActive (так как оно может быть возобновлено не пользователем, а из какого-либо события, обработанного). ОС в фоновом режиме, для которого у нас может не отображаться элемент управления, или любая другая функция, такая как bg task или тихие push-уведомления и т.д.).

Дополнительная информация:

Согласно довольно старой статье по тестированию на проникновение, которую я обнаружил в сети (https://resources.infosecinstitute.com/iphone-penetration-testing-3/), запрос цепочки для ключей фактически направил бы запрос другому процессу под названием securityd, выполняющемуся на устройстве, который волшебным образом планирует запросы и выполняет их для keychain db и, наконец, возвращает содержимое запрашивающему приложению, предполагая, что приложение все еще живо, когда запрос выполняется по цепочке ключей db. Это заставило меня подумать, что, если это все еще верно, такой процесс может упасть в своего рода "DDoS" -таки, когда ваше приложение (или, может быть, все приложения в процессе выполнения? Непонятно, как это на самом деле работает) выполняет слишком много запросов. Это может привести к ложноположительным ошибкам, таким как secItemNotFound или аналогичным. Опять же, поскольку никто не подтвердил это, оставьте это как личное соображение.

Подход:

Из ответа @Naqi я мог бы исключить пункты 2,3 и 4, оставив пункт 1 в качестве единственно действующей причины. Сказал, что я решил переместить любую операцию CRUD, связанную с цепочкой для ключей, с addDidFinishLaunchingWithOptions на appDidBecomeActive, обрабатывая там инструкции по настройке приложения (все, что нужно вашему приложению), чтобы убедиться, что возможные фоновые события приостановка/возобновление действий вообще не повлияет на цепочку для ключей, поскольку appDidBecomeActive не будет вызван, если пользователь фактически не заставит приложение открыться и отобразиться на экране. Конечно, это должно происходить только при запуске приложения, поэтому вы можете использовать флаг bool или аналогичный семафорный подход в appDidBecomeActive, чтобы убедиться, что ваша логика настройки выполняется только один раз.

В сочетании с описанным выше подходом в приложении "Делегат приложений" я также явно реализовал application(shouldSaveApplicationState:), вернув false и application(shouldRestoreApplicationState:, по-прежнему возвращая false (хотя я предполагаю, что по умолчанию это уже должно быть так). Это не обязательно может быть полезно, но оно того стоило.

Подводя итог, можно сказать, что на некоторых затронутых устройствах, которые случайным образом отказывали в доступе к цепочке для ключей, уже несколько недель не происходит больше, и, учитывая, что это происходило в течение некоторых, скажем, почти каждые 2/3 дня, я предполагаю, что это могло бы по крайней мере ослабить проблему.

Я могу сказать, что все отслеживаемые устройства были в разных конфигурациях, с множеством установленных приложений или без них, с почти полностью заполненной памятью или без нее, с множеством приложений в bg или вообще без них, с любым форм-фактором и разными версиями iOS. Резервное копирование цепочки для ключей в облако не включено, и элементы связываются в цепочке для ключей как минимум как kSecAttrAccessibleWhenUnlockedThisDeviceOnly

Открытая точка не полностью проверена. Предположительно, доступ к цепочке для ключей должен быть безопасным для потоков, но я обнаружил, что в Интернете существуют разные потоки, противоречащие друг другу. Может быть, это может как-то повлиять на вашу логику и вызвать неожиданное состояние

(Поздравляю, если вам удалось прочитать здесь :))

Надеюсь, что это поможет лучше понять, что происходит с этой трагедией брелка в iOS