8.0. Введение
Безопасность — центральный элемент операционных систем iOS и OS X. Можно пользоваться функциями безопасности в iOS, чтобы без опасений хранить файлы на различных накопителях. Например, можно приказать iOS заблокировать и защитить на диске файлы с информацией из вашего приложения, если пользователь активизировал на устройстве защиту с применением пароля, а устройство перешло в режим блокировки. Если вы явно этого не затребуете, iOS не будет использовать с вашим приложением какого-либо защищенного хранилища данных. В таком случае ваши данные будут открыты для считывания любому процессу, имеющему доступ на считывание файловой системы вашего устройства. Существуют разнообразные приложения Mac, способные исследовать файловую систему устройства с iOS, не вызывая при этом джейлбрейка.
Джейлбрейк — это процесс предоставления доступа с правами администратора и снятия многих уровней защиты, действующих над операционной системой — например, над iOS. В частности, если устройство подверглось джейлбрейку, приложение может выполнить на нем неподписанный двоичный код. Но на обычном устройстве iOS приложение сможет быть выполнено на устройстве, лишь если оно имеет подпись Apple, полученную через App Store или верифицированный портал для разработки под iOS.
В Mac OS X Apple уже давно используется программа Keychain Access. Это инструмент, обеспечивающий пользователям iOS безопасное хранение данных на компьютере. Разработчики могут пользоваться программой Keychain Access, а также другими функциями обеспечения безопасности, выстроенными на базе общей архитектуры защиты данных (CDSA). Keychain Access может управлять различными связками ключей. Каждая связка ключей, в свою очередь, может содержать защищенные данные, например пароли. Если на машине с операционной системой OS X вы заходите под своим именем на сайт через браузер Safari, то вам будут предложены две возможности: приказать Safari запомнить ваш пароль либо отклонить этот запрос. Если вы прикажете Safari запомнить пароль, то браузер сохранит указанный пароль в заданной по умолчанию связке ключей.
Связки ключей в OS X и iOS различаются во многих отношениях. В OS X:
• у пользователя может быть много связок ключей, в iOS же действует всего одна глобальная связка ключей;
• пользователь может блокировать связку ключей. В iOS та связка ключей, которая используется по умолчанию, блокируется и разблокируется вместе с самим устройством;
• существует концепция стандартной связки ключей (default keychain), которая автоматически разблокируется операционной системой, когда пользователь входит в эту систему. Дело в том, что пароль стандартной связки ключей совпадает с паролем к пользовательской учетной записи. Как было указано ранее, в iOS есть всего одна связка ключей и она разблокируется iOS по умолчанию.
Чтобы помочь вам лучше понять природу связки ключей в OS X, прежде чем углубиться в изучение концепций iOS, связанных со связкой ключей и безопасностью, я хотел бы привести один пример. Откройте окно терминала на компьютере с Mac, введите следующую команду и нажмите Enter:
security list-keychains
В зависимости от настроек машины и имени пользователя вы получите примерно такой вывод:
«/Users/vandadnp/Library/Keychains/login.keychain»
«/Library/Keychains/System.keychain»
Как видите, здесь у меня две связки ключей: регистрационная и системная. Чтобы выяснить, какая связка ключей используется по умолчанию, введите следующую команду в окно терминала, а затем нажмите Enter:
security default-keychain
В типичной установке OS X эта команда вернет примерно такой результат:
«/Users/vandadnp/Library/Keychains/login.keychain»
Вывод свидетельствует, что у меня на машине по умолчанию используется регистрационная связка ключей. Поэтому по умолчанию все пароли, которые я приказываю запомнить различным программам в системе OS X, будут сохраняться именно в этой связке ключей, если конкретное приложение не решит, что пароль должен быть сохранен в другой связке ключей. Если нужной связки ключей пока не существует, приложению потребуется ее создать.
А теперь давайте попробуем кое-что интересное. Попытаемся узнать, какие пароли уже сохранены в нашей стандартной связке ключей. При этом будем исходить из того, что по умолчанию на машине используется связка ключей login.keychain, как мы определили ранее. Введите в окне терминала следующую команду и нажмите Enter:
security dump-keychain login.keychain | grep «password» — i
Аргумент dump-keychain для команды security в окне терминала дампирует все содержимое связки ключей в стандартный вывод. Для поиска паролей мы применили команду grep. Вывод этой команды может получиться примерно таким, как в следующем примере, в зависимости от того, какие пароли запомнил ваш компьютер:
«desc»
«desc»
«desc»
«desc»
«desc»
«desc»
«desc»
«desc»
«desc»
«desc»
«desc»
«desc»
Хорошо, все это очень интересно, но зачем я об этом рассказываю, как все это связано с iOS? Оказывается, что в архитектурном отношении связка ключей в iOS очень похожа на связку ключей в OS X, так как операционная система iOS создавалась на основе исходного кода OS X. Многие концепции iOS похожи на соответствующие элементы OS X, это касается и связки ключей. Необходимо отметить ряд очень важных аспектов, связанных со связками ключей в iOS, — в частности, поговорить о группах доступа и сервисах. Чтобы упростить вам изучение этой темы, я объясню, как эти концепции реализуются в OS X, а потом мы подробнее рассмотрим вариант связки ключей, применяемый в iOS.
На компьютере Mac нажмите клавиши Command + Пробел или просто нажмите значок Spotlight (Поиск) на верхней панели меню на экране (рис. 8.1).
Рис. 8.1. Нажмите значок Spotlight на панели меню для OS X
Когда откроется строка поиска, введите в нее запрос Keychain Access и нажмите Enter, чтобы открыть программу Keychain Access. В левой части окна этой программы находится область Keychains (Связки ключей). В ней выберите регистрационную связку ключей login, а затем в области Category (Категория) слева — запись Passwords (Пароли). Теперь перед вами должен открыться примерно такой интерфейс, как на рис. 8.2.
Рис. 8.2. Программа Keychain Access в Mac OS X
Keychain Access — это программа с графическим пользовательским интерфейсом, которая выстроена в OS X на базе интерфейсов API для работы со связкой ключей и обеспечения безопасности. Этот красивый и удобный интерфейс скрывает большую часть тех сложностей, которыми отличаются фреймворки безопасности в OS X. Теперь, если в системе есть какие-либо пароли, сохраненные в приложениях, например в Safari, вам потребуется дважды щелкнуть на одной из записей-паролей в правой части экрана. Откроется такое диалоговое окно, как на рис. 8.3.
Рис. 8.3. Диалоговое окно Keychain Access, в котором отображается информация о сохраненном пароле
Познакомимся с некоторыми свойствами пароля, показанного на рис. 8.3.
• Name (Имя) — имя пароля, присвоенное ему приложением, сохранившим этот пароль. Так, в данном случае мы имеем дело с паролем для входа в беспроводную сеть 206-NET. Это имя иногда называют подписью.
• Kind (Вид) — вид элемента. В данном случае пароль относится к виду «пароль для беспроводных сетей». Это обычная строка, ее можно использовать для выполнения запросов к связке ключей (рассмотрено далее).
• Account (Учетная запись) — обычно это ключ для того значения, которое мы хотим сохранить. Связка ключей использует хранилище для пар «ключ/значение», так же как словарь из языка Objective-C. Ключ — это произвольная строка, и большинство приложений, сохраняющих элементы в связке ключей, хранят ключи и значения именно в этой секции.
• Where (местоположение) — часто именуется сервисом. Это идентификатор сервиса, сохранившего данный элемент в связке ключей. Этот идентификатор — как раз тот пароль, который вы запоминаете, и связка ключей фактически не имеет с ним дел, так как вы считаете этот пароль осмысленным. В iOS принято задавать в качестве имени этого сервиса идентификатор пакета нашего приложения. Так мы отличаем данные, сохраненные в конкретном приложении, от сохраненных данных из других приложений. Подробнее поговорим об этом в дальнейшем.
Кроме того, вы видите на рис. 8.3 флажок Show Password (Показать пароль). Если установить этот флажок, система будет запрашивать разрешение на показ пароля к конкретному элементу. Если вы введете свой пароль и дадите такое разрешение, то программа Keychain Access получит для вас защищенный пароль и отобразит его на экране.
Мы можем использовать команду security в окне терминала для выборки ровно той же информации. Если ввести в окне терминала следующую команду:
security find-generic-password — help
то получится примерно такой вывод:
Usage: find-generic-password [-a account] [-s service]
[options…] [-g] [keychain…]
— a Match «account» string
— c Match «creator» (four-character code)
— C Match «type» (four-character code)
— D Match «kind» string
— G Match «value» string (generic attribute)
— j Match «comment» string
— l Match «label» string
— s Match «service» string
— g Display the password for the item found
— w Display only the password on stdout
If no keychains are specified to search, the default search list is used.
Find a generic password item.
Итак, если передавать команде security требуемые параметры один за другим, то можно получить свойства интересующего вас пароля (см. рис. 8.3):
security find-generic-password
— a «AirPort»
— s «com.apple.network.wlan.ssid.206-NET»
— D «AirPort network password»
— l «206-NET»
— g login.keychain
Как было показано ранее, команда — g запросит команду security отобразить пароль, ассоциированный с указанным элементом (при наличии такого пароля). Следовательно, после ввода этой команды в окно терминала вам будет предложено ввести пароль к вашей учетной записи перед продолжением работы. Аналогичным образом мы указывали пароль к учетной записи, чтобы отобразить пароль на рис. 8.3.
В iOS, притом что во всей операционной системе применяется единая глобальная область действия связки ключей, приложение все равно может считывать и записывать информацию лишь на небольшом экранированном участке связки ключей (по принципу работы в песочнице). Два приложения, написанные одним и тем же разработчиком (и подписанные одинаковым профилем инициализации с одного и того же портала для разработки в iOS), могут получать доступ к общей разделяемой области связки ключей, но при этом сохраняют и собственный ограниченный доступ каждый к своей связке ключей. Итак, два приложения, App X и App Y, написанные одним и тем же iOS-разработчиком, могут получить доступ к следующим областям связки ключей.
• App X — к области связки ключей приложения App X.
• App Y — к области связки ключей приложения App Y.
• Приложения App X и App Y обладают доступом к общему разделяемому участку связки ключей (с применением групп доступа, если программист соответствующим образом сконфигурирует разрешения для этого приложения).
• App X не может считывать информацию из связки ключей приложения App Y, а приложение App Y не может считывать аналогичные данные приложения App X.
Операционная система iOS просматривает разрешения приложения, чтобы определить требуемый тип доступа. Разрешения для конкретной программы кодируются в профиле инициализации, который использовался при подписывании приложения. Допустим, мы создали новый профиль инициализации под названием KeychainTest_Dev.mobileprovision. Этот профиль был размещен на Рабочем столе. С помощью следующей команды можно извлечь разрешения, соответствующие данному профилю, вот так:
cd ~/Desktop
Эта команда выведет вас на Рабочий стол. Уже здесь нужно выполнить следующую команду, чтобы считать разрешения из профиля инициализации:
security cms — D -i KeychainTest_Dev.mobileprovision | grep — A12 «Entitlements»
Показанная здесь команда security декодирует весь профиль инициализации, после чего команда grep просмотрит раздел Entitlements (Разрешения) в профиле и считает 12 строк текста от начала этого раздела. Если в вашем разделе с разрешениями содержится больше или меньше текста, то нужно соответствующим образом изменить аргумент — A12.
Вывод этой команды будет выглядеть примерно следующим образом в зависимости от вашего профиля:
Самый важный раздел, который нас здесь интересует, называется keychain-access-groups. Здесь указываются группы доступа к нашим элементам из связки ключей. Это групповой идентификатор для всех приложений, разработанных одним и тем же программистом. Здесь F3FU372W5M — это мой командный идентификатор с портала разработки для iOS. Идущий далее астериск показывает, в какие группы доступа я могу позже поместить те элементы, которые требуется хранить в защищенном режиме. В данном случае астериск означает любую группу. Поэтому по умолчанию это приложение получает доступ к элементам связок ключей любого приложения, которое разработано членами вышеупомянутой команды. Не волнуйтесь, если пока не совсем улавливаете суть. В этой главе вы подробно изучите тему безопасности в iOS, в частности, к концу главы будете знать все необходимое об использовании связок ключей в iOS.
Совершенно необходимо добавить к вашему приложению фреймворк Security (Безопасность), перед тем как переходить к изучению разделов этой главы. Большинство приемов, описанных в данной главе, работает со службами связки ключей в iOS, а для этого необходимо наличие фреймворка Security. В iOS SDK 7 появилась концепция модулей. Поэтому если вы просто импортируете обобщающий заголовок фреймворка безопасности в ваш проект, LLVM сам свяжет ваше приложение с нужным модулем безопасности — делать это вручную не придется. Вам всего лишь понадобится гарантировать, что в настройках сборки активизирована функция Enable Modules (Использовать модули), а также импортировать в проект следующий заголовок:
#import
В Xcode 5 также появилась поддержка «возможностей» (Capabilities). Это новая вкладка, расположенная рядом с вкладкой Build Settings (Настройки сборки). Здесь вы можете с легкостью добавлять разрешения к вашему приложению и даже активизировать связку ключей без особых хлопот. Тем не менее, работая таким образом, вы теряете доступ практически ко всем деталям и не можете создавать собственные профили инициализации. В вашем распоряжении будут только джокерные профили инициализации, которыми мы обычно не пользуемся, реализуя в приложениях пуш-уведомления и другие возможности. Рекомендую хотя бы познакомиться с этой новой вкладкой. Просто щелкните на файле с вашим проектом в Xcode, посмотрите на правую часть экрана и выберите Capabilities (Возможности). Затем можно будет отключить компоненты iCloud и Keychain Access.
8.1. Обеспечение безопасности и защиты в приложениях
Постановка задачи
Требуется сохранять значения в связке ключей и обеспечить в приложении безопасное хранение данных.
Решение
Создайте в приложении профиль инициализации, в котором активизирована защита файлов.
Обсуждение
Профили инициализации, уже упоминавшиеся ранее (см. раздел 8.0), содержат разрешения. Разрешения указывают системе iOS, как ваше приложение использует возможности безопасности, предоставляемые операционной системой. В эмуляторе iOS используется код без подписи (отсутствует этап, называемый code signing), следовательно, такие концепции будут лишены смысла. Но при отладке приложения или отправке его в App Store вы должны будете гарантировать, что приложение подписано корректным профилем инициализации одновременно для схем Debug (Отладка) и Release (Выпуск).
Я продемонстрирую все шаги, необходимые для создания действительного профиля реализации при разработке, а также опишу профили Ad Hoc и App Store. Выполните следующие шаги, чтобы создать валидный профиль инициализации для целей разработки (с поддержкой отладки). Он будет использоваться с приложениями, над которыми мы будем работать в данной главе. Начнем с создания ID приложения.
Предполагается, что вы уже создали для себя валидные сертификаты на разработку и распространение приложения.
1. Перейдите в iOS Dev Center и войдите в систему под своими именем и паролем.
2. Найдите раздел iOS Development Program (Программа разработки для iOS) и выберите Certificates, Identifiers & Profiles (Сертификаты, идентификаторы и профили).
3. Найдите в левой части экрана область App IDs (Идентификаторы приложений) и нажмите кнопку +, чтобы создать новый такой идентификатор.
4. В области Name (Имя) введите имя Security App. На самом деле здесь можно указать любое имя, но, чтобы избежать путаницы в этой главе, лучше воспользоваться данным именем, которое я буду применять в примерах.
5. В области App Services (Сервисы приложения) установите флажок Data Protection (Защита данных) и убедитесь, что выбран параметр Complete Protection (Полная защита). Остальные настройки оставьте без изменений.
6. Убедитесь, что в области App ID Suffix (Суффикс идентификатора приложения) установлен флажок Explicit App ID (Явный идентификатор приложения), и введите в поле Bundle ID (Идентификатор пакета) имя сервиса с точкой в качестве разделителя. Рекомендую имя com.NAME.ios.cookbook.SecurityApp, где NAME — название вашей компании. Если у вас нет компании с названием, просто придумайте его! В примерах я использую название com.pixolity.ios.cookbook.SecurityApp, но вам потребуется уникальное имя, это вы не сможете использовать.
7. Выполнив эти шаги, нажмите кнопку Continue (Продолжить).
8. Теперь система запросит у вас подтверждения заданных настроек, прежде чем будет создан идентификатор приложения. Вы увидите картинку, примерно как на рис. 8.4.
Рис. 8.4. Подтверждение настроек App ID перед созданием идентификатора приложения
9. Когда закончите подготовку настроек, нажмите кнопку Submit (Отправить), чтобы создать ID приложения.
Прекрасно! Теперь у нас есть идентификатор приложения, но еще требуется создать профили инициализации. Я подробно расскажу, как создается профиль инициализации для разработки, а создание профилей Ad Hoc и App Store оставляю вам в качестве самостоятельной работы, так как процесс практически идентичен. Выполните следующие шаги, чтобы создать профиль инициализации для целей разработки.
1. В области Certificates, Identifiers & Profiles (Сертификаты, идентификаторы и профили) портала разработки выберите область Development (Разработка) в категории Provisioning Profiles (Профили инициализации). Затем нажмите кнопку +.
2. В открывшемся окне в области Development (Разработка) установите флажок iOS App Development (Разработка приложения для iOS) и нажмите кнопку Continue (Продолжить).
3. Когда система потребует выбрать идентификатор приложения (App ID), выберите тот App ID, который вы создали ранее. В моем случае это будет App ID, показанный на рис. 8.5. Сделав выбор, нажмите кнопку Continue (Продолжить).
Рис. 8.5. Выбор нового идентификатора приложения для нового профиля инициализации (для целей разработки)
4. Выберите сертификат (-ы) разработки, с которыми хотите связать ваш профиль. Затем нажмите кнопку Continue (Продолжить).
5. Выберите список устройств, на которые можно будет установить ваш профиль (это делается только для профилей Development и Ad Hoc, но не для App Store) и нажмите кнопку Continue (Продолжить).
6. На следующем экране система потребует от вас указать имя вашего профиля. Введите имя, соответствующее правилам из Security App Dev Profile, а потом нажмите кнопку Generate (Сгенерировать), чтобы создать профиль инициализации.
7. Теперь ваш профиль готов к загрузке (рис. 8.6). Нажмите кнопку Download (Загрузить), чтобы загрузить профиль.
Рис. 8.6. Профиль для разработки сгенерирован и готов к загрузке
8. Для установки профиля перетащите загруженный профиль в iTunes. В результате профиль с его оригинальным именем будет установлен в каталоге ~/Library/MobileDevice/Provisioning Profiles/. Мне известно, что многие iOS-разработчики устанавливают профиль инициализации, просто дважды щелкнув на нем кнопкой мыши. Такой способ частично работает, то есть при двойном щелчке профиль действительно устанавливается в упомянутом каталоге. Но оригинальное имя профиля при этом стирается и заменяется SHA1-хешем профиля. Если позже вы откроете каталог, то не сможете понять, какой профиль вам нужен. Придется просматривать все профили и выяснять их имена. Поэтому я настоятельно не рекомендую устанавливать профили двойным щелчком кнопкой мыши. Лучше перетаскивать их в iTunes или вручную вставлять в данный каталог.
Блестяще. Вы установили на компьютере профиль инициализации для вашего приложения. Воспользуйтесь настройками сборки проекта, чтобы убедиться в том, что выбран правильный профиль для схемы Debug (Отладка). Затем повторите описанный процесс при создании профилей Ad Hoc и App Store, чтобы гарантировать сборку приложения с нужными профилями для схемы Release (Выпуск).
Созданный вами профиль инициализации обеспечивает отладку ваших приложений на устройстве с iOS и удобное сохранение данных на диске с применением связки ключей.
См. также
Раздел 8.0.
8.2. Хранение значений в связке ключей
Постановка задачи
Требуется обеспечить безопасное хранение конфиденциальных данных в связке ключей.
Решение
Необходимо гарантировать, что ваше приложение будет скомпоновано с учетом требований фреймворка Security (Безопасность). Затем воспользуйтесь функцией SecItemAdd для добавления нового элемента в связку ключей приложения.
Обсуждение
API связки ключей в операционных системах iOS и OS X написаны на языке C. Таким образом, у нас нет мостика к Objective-C или какого-то промежуточного уровня, который предоставлял бы взаимодействие программы с API на C. Поэтому работать с этими API несколько сложнее, чем с обычными. Основной момент при изучении этих API заключается в том, что запросы, отправляемые к API связки ключей, обычно упакованы в словарях. Например, если требуется запросить у сервисов связки ключей безопасное хранение тех или иных данных, то вы помещаете этот запрос в словарь (а вместе с запросом — все данные, которые собираетесь хранить, ключ к этим данным, идентификатор приложения и т. д.). Этот словарь вы отправляете к API, примером которого может служить функция SecItemAdd. Чтобы хранить информационный фрагмент в связке ключей, создайте словарь со следующими ключами:
• kSecClass — при необходимости хранения конфиденциальных информационных фрагментов, например строк, в качестве значения этого ключа обычно задается kSecClassGenericPassword;
• kSecAttrService — значение ключа чаще всего представляет собой строку. Как правило, эта строка — идентификатор нашего приложения;
• kSecAttrAccount — значением является строка, указывающая ключ к значению, которое мы хотим сохранить. Это произвольная строка, которая должна иметь смысл для вас и в контексте приложения;
• kSecValueData — значением является экземпляр NSData, который вы хотите сохранить по указанному ключу (kSecAttrAccount).
Возвращаемое значение функции SecItemAdd относится к типу OSStatus. Различные значения, которые вы можете получить от этой функции, определяются в файле SecBase.h SDK. Поэтому, находясь в Xcode, просто нажмите комбинацию клавиш Command+Shift+O, введите SecBase.h и попробуйте найти значение errSecSuccess. После того как найдете errSecSuccess в перечне, вы сможете просмотреть остальные значения, которые могут быть возвращены в экземпляре OSStatus:
enum
{
errSecSuccess = 0,
errSecUnimplemented = -4,
errSecParam = -50,
errSecAllocate = -108,
errSecNotAvailable = -25291,
errSecDuplicateItem = -25299,
errSecItemNotFound = -25300,
errSecInteractionNotAllowed = -25308,
errSecDecode = -26275,
errSecAuthFailed = -25293,
};
Если функция SecItemAdd завершится успешно, то в качестве ее возвращаемого значения вы получите errSecSuccess. Иное значение, получаемое от этой функции, означает ошибку. Итак, давайте объединим изученное и сделаем небольшой код, который будет записывать строковое значение в связку ключей:
#import «AppDelegate.h»
#import
@implementation AppDelegate
— (BOOL) application:(UIApplication *)application
didFinishLaunchingWithOptions:(NSDictionary *)launchOptions{
NSString *key = @"Full Name";
NSString *value = @"Steve Jobs";
NSData *valueData = [value dataUsingEncoding: NSUTF8StringEncoding];
NSString *service = [[NSBundle mainBundle] bundleIdentifier];
NSDictionary *secItem = @{
(__bridge id)kSecClass: (__bridge id)kSecClassGenericPassword,
(__bridge id)kSecAttrService: service,
(__bridge id)kSecAttrAccount: key,
(__bridge id)kSecValueData: valueData,
};
CFTypeRef result = NULL;
OSStatus status = SecItemAdd((__bridge CFDictionaryRef)secItem, &result);
if (status == errSecSuccess){
NSLog(@"Successfully stored the value");
} else {
NSLog(@"Failed to store the value with code: %ld", (long)status);
}
self.window = [[UIWindow alloc]
initWithFrame: [[UIScreen mainScreen] bounds]];
self.window.backgroundColor = [UIColor whiteColor];
[self.window makeKeyAndVisible];
return YES;
}
При первом запуске этого приложения (предполагается, что вы выполнили все рекомендации из предыдущих разделов этой главы и правильно настроили свой профиль) вы получите от функции SecItemAdd значение errSecSuccess. Однако если вы повторно запустите это приложение, то получите значение errSecDuplicateItem. Таким образом iOS сообщает вам, что нельзя перезаписать имеющееся значение. В контексте строгих требований безопасности, применяемых в связке ключей, перезаписать имеющееся значение действительно нельзя. Но вот обновить имеющееся значение вы можете. О том, как это делается, рассказано далее в данной главе.
См. также
Раздел 8.1.
8.3. Нахождение значений в связке ключей
Постановка задачи
Требуется найти в связке ключей имеющийся там элемент.
Решение
Воспользуйтесь функцией SecItemCopyMatching. Выполните следующие шаги.
1. Создайте словарь для передачи вышеупомянутой функции. Добавьте к словарю ключ kSecClass. Присвойте ключу такое значение, чтобы он отражал тип искомого элемента. Как правило, здесь требуется значение kSecClassGenericPassword.
2. Добавьте в словарь ключ kSecAttrService. В качестве значения этого ключа установите сервисную строку того элемента, который вы ищете. В этой главе в качестве имен сервисов мы используем идентификатор пакета нашего приложения и устанавливаем в качестве идентификаторов пакета всех приложений одну и ту же строку. Таким образом, одно приложение может считывать связку ключей, другое — записывать в нее данные и т. д.
3. Добавьте в словарь ключ kSecAttrAccount и задайте в качестве его значения текущий ключ того значения, которое вы ранее сохранили в связке ключей. Если вы внимательно проработали пример из раздела 8.2, то в данном случае именем учетной записи будет служить строка Full Name.
4. Добавьте к словарю атрибут kSecReturnAttributes. В качестве его значения задайте kCFBooleanTrue, если хотите получить атрибуты значения, присутствующего в связке ключей (например, дату создания и дату последнего изменения). Если хотите получить текущее значение элемента, сохраненного вами в связке ключей, а не ключ kSecReturnAttributes, добавьте к словарю ключ kSecReturnData и задайте для него значение kCFBooleanTrue.
Как только ваш словарь будет готов, вы сможете передать его функции SecItemCopyMatching в качестве первого параметра. Второй параметр — это указатель на тот объект, который будет возвращаться функцией. Этот указатель должен относиться к типу CFTypeRef *. Это обобщенный тип данных, и в каждом конкретном случае тип зависит от того, что именно вы передадите функции SecItemCopyMatching в качестве первого параметра. Например, если в вашем словаре содержится ключ kSecReturnAttributes, то вторым параметром этой функции должен быть либо NULL, либо указатель на непрозрачный тип CFDictionaryRef. Если же вместо этого вы передадите словарю ключ kSecReturnData, то второй параметр этой функции должен относиться к типу CFDataRef. Это непрозрачный тип, который будет получать точные данные имеющегося элемента. Затем вы сможете преобразовать эти данные в экземпляр NSString и работать с ним.
Обсуждение
Предположим, вы хотите считать свойства той строки, которую записали в связку ключей в разделе 8.2. Можно написать такой код:
— (BOOL) application:(UIApplication *)application
didFinishLaunchingWithOptions:(NSDictionary *)launchOptions{
NSString *keyToSearchFor = @"Full Name";
NSString *service = [[NSBundle mainBundle] bundleIdentifier];
NSDictionary *query = @{
(__bridge id)kSecClass: (__bridge id)kSecClassGenericPassword,
(__bridge id)kSecAttrService: service,
(__bridge id)kSecAttrAccount: keyToSearchFor,
(__bridge id)kSecReturnAttributes: (__bridge id)kCFBooleanTrue,
};
CFDictionaryRef valueAttributes = NULL;
OSStatus results = SecItemCopyMatching((__bridge CFDictionaryRef)query,
(CFTypeRef *)&valueAttributes);
NSDictionary *attributes =
(__bridge_transfer NSDictionary *)valueAttributes;
if (results == errSecSuccess){
NSString *key, *accessGroup, *creationDate, *modifiedDate, *service;
key = attributes[(__bridge id)kSecAttrAccount];
accessGroup = attributes[(__bridge id)kSecAttrAccessGroup];
creationDate = attributes[(__bridge id)kSecAttrCreationDate];
modifiedDate = attributes[(__bridge id)kSecAttrModificationDate];
service = attributes[(__bridge id)kSecAttrService];
NSLog(@"Key = %@\n \
Access Group = %@\n \
Creation Date = %@\n \
Modification Date = %@\n \
Service = %@", key, accessGroup, creationDate,
modifiedDate, service);
} else {
NSLog(@"Error happened with code: %ld", (long)results);
}
self.window = [[UIWindow alloc]
initWithFrame: [[UIScreen mainScreen] bounds]];
self.window.backgroundColor = [UIColor whiteColor];
[self.window makeKeyAndVisible];
return YES;
}
После запуска этого приложения на консоль будут выведены примерно такие результаты:
Key = Full Name
Access Group = F3FU372W5M.com.pixolity.ios.cookbook.SecurityApp
Creation Date = 2013-06-09 10:44:55 +0000
Modification Date = 2013-06-09 10:44:55 +0000
Service = com.pixolity.ios.cookbook.SecurityApp
Это, конечно, хорошо, но как считывать актуальную информацию о значении? В подразделе «Решение» данного раздела я уже ответил на этот вопрос: необходимо включить в запрос ключ kSecReturnData. После того как это будет сделано, в качестве второго параметра функция SecItemCopyMatching потребует либо NULL, либо непрозрачную переменную CFDataRef, вот так:
#import «AppDelegate.h»
#import
@implementation AppDelegate
— (BOOL) application:(UIApplication *)application
didFinishLaunchingWithOptions:(NSDictionary *)launchOptions{
NSString *keyToSearchFor = @"Full Name";
NSString *service = [[NSBundle mainBundle] bundleIdentifier];
NSDictionary *query = @{
(__bridge id)kSecClass: (__bridge id)kSecClassGenericPassword,
(__bridge id)kSecAttrService: service,
(__bridge id)kSecAttrAccount: keyToSearchFor,
(__bridge id)kSecReturnData: (__bridge id)kCFBooleanTrue,
};
CFDataRef cfValue = NULL;
OSStatus results = SecItemCopyMatching((__bridge CFDictionaryRef)query,
(CFTypeRef *)&cfValue);
if (results == errSecSuccess){
NSString *value = [[NSString alloc]
initWithData:(__bridge_transfer NSData *)cfValue
encoding: NSUTF8StringEncoding];
NSLog(@"Value = %@", value);
} else {
NSLog(@"Error happened with code: %ld", (long)results);
}
self.window = [[UIWindow alloc]
initWithFrame: [[UIScreen mainScreen] bounds]];
self.window.backgroundColor = [UIColor whiteColor];
[self.window makeKeyAndVisible];
return YES;
}
По умолчанию функция SecItemCopyMatching ищет первое совпадение в связке ключей. Допустим, вы сохранили в связке ключей 10 безопасных элементов класса kSecClassGenericPassword и хотите запросить их все. Как это сделать? Ничего сложного. Просто добавьте ключ kSecMatchLimit в словарь вашего запроса и укажите максимальное количество совпадающих элементов, которые сервисы должны отыскивать в связке ключей. Можно также присвоить этому ключу значение kSecMatchLimitAll — при нем осуществляется поиск всех совпадений. Когда вы внедрите ключ kSecMatchLimit в ваш словарь запроса для функции SecItemCopyMatching, второй параметр этого типа обязательно потребует указатель на непрозрачный тип CFArrayRef, а состоять этот массив будет только из тех элементов, которые вы запросили.
Рассмотрим пример, в котором мы пытаемся найти все элементы связки ключей, удовлетворяющие определенным критериям:
#import «AppDelegate.h»
#import
@implementation AppDelegate
— (BOOL) application:(UIApplication *)application
didFinishLaunchingWithOptions:(NSDictionary *)launchOptions{
NSString *keyToSearchFor = @"Full Name";
NSString *service = [[NSBundle mainBundle] bundleIdentifier];
NSDictionary *query = @{
(__bridge id)kSecClass: (__bridge id)kSecClassGenericPassword,
(__bridge id)kSecAttrService: service,
(__bridge id)kSecAttrAccount: keyToSearchFor,
(__bridge id)kSecReturnData: (__bridge id)kCFBooleanTrue,
(__bridge id)kSecMatchLimit: (__bridge id)kSecMatchLimitAll
};
CFArrayRef allCfMatches = NULL;
OSStatus results = SecItemCopyMatching((__bridge CFDictionaryRef)query,
(CFTypeRef *)&allCfMatches);
if (results == errSecSuccess){
NSArray *allMatches = (__bridge_transfer NSArray *)allCfMatches;
for (NSData *itemData in allMatches){
NSString *value = [[NSString alloc]
initWithData: itemData
encoding: NSUTF8StringEncoding];
NSLog(@"Value = %@", value);
}
} else {
NSLog(@"Error happened with code: %ld", (long)results);
}
self.window = [[UIWindow alloc]
initWithFrame: [[UIScreen mainScreen] bounds]];
self.window.backgroundColor = [UIColor whiteColor];
[self.window makeKeyAndVisible];
return YES;
}
См. также
Раздел 8.2.
8.4. Обновление значений в связке ключей
Постановка задачи
Вы уже сохранили значение в связке ключей, а теперь хотите обновить его.
Решение
Учитывая, что вы смогли найти значение в связке ключей (см. раздел 8.3), можно схожим образом выполнить функцию SecItemUpdate с двумя параметрами. Ее первым параметром будет словарь вашего запроса, а вторым — словарь, описывающий изменения, которые вы хотите внести в имеющееся значение. Обычно этот обновляющий словарь (второй параметр метода) содержит всего один ключ (kSecValueData). Значением этого словарного ключа являются данные, которые нужно заново установить для имеющегося в связке ключа.
Обсуждение
Допустим, вы выполнили все указания, изложенные в разделе 8.2, и сохранили в связке ключей приложения строку Steve Jobs с ключом Full Name. Теперь вы хотите обновить это значение. Первым делом понадобится определить, присутствует ли уже нужное значение в связке ключей. Для этого создадим простой запрос, который вы уже видели ранее в этой главе:
NSString *keyToSearchFor = @"Full Name";
NSString *service = [[NSBundle mainBundle] bundleIdentifier];
NSDictionary *query = @{
(__bridge id)kSecClass: (__bridge id)kSecClassGenericPassword,
(__bridge id)kSecAttrService: service,
(__bridge id)kSecAttrAccount: keyToSearchFor,
};
Затем сделаем запрос к этому словарю и проверим, находится ли интересующий нас элемент в связке ключей:
OSStatus found = SecItemCopyMatching((__bridge CFDictionaryRef)query,
NULL);
Можно и не проверять наличие значения перед тем, как его обновлять. Вполне допустимо просто попытаться обновить значение. Если же его не существует, функция SecItemUpdate вернет значение errSecItemNotFound. Выбор заключается в том, проводить ли поиск в связке ключей самостоятельно или перепоручить эту задачу SecItemUpdate.
Если эта функция вернет значение errSecSuccess, вы будете знать, что интересовавшее вас значение уже обновлено. Обратите внимание: в качестве второго параметра мы передали NULL. Дело в том, что мы не собираемся получать из связки ключей старое значение. Мы просто хотим определить, существует ли значение, а сделать это можем, только проверив возвращаемое значение функции. Если возвращаемое значение равно errSecSuccess, делаем вывод, что значение уже было сохранено и может быть обновлено. Обновлять значение мы будем вот так:
NSData *newData = [@"Mark Tremonti"
dataUsingEncoding: NSUTF8StringEncoding];
NSDictionary *update = @{
(__bridge id)kSecValueData: newData,
};
OSStatus updated = SecItemUpdate((__bridge CFDictionaryRef)query,
(__bridge CFDictionaryRef)update);
if (updated == errSecSuccess){
NSLog(@"Successfully updated the existing value");
} else {
NSLog(@"Failed to update the value. Error = %ld", (long)updated);
}
Обновляющий словарь, который мы передаем функции SecItemUpdate в качестве второго параметра, может содержать больше ключей чем один ключ kSecValueData, использованный в нашем примере. На самом деле этот словарь может содержать обновления для любого имеющегося элемента. Например, если вы хотите добавить комментарий к имеющемуся значению (комментарий — это строка), то можете выполнить обновление следующим образом:
#import «AppDelegate.h»
#import
@implementation AppDelegate
— (void) readExistingValue{
NSString *keyToSearchFor = @"Full Name";
NSString *service = [[NSBundle mainBundle] bundleIdentifier];
NSDictionary *query = @{
(__bridge id)kSecClass: (__bridge id)kSecClassGenericPassword,
(__bridge id)kSecAttrService: service,
(__bridge id)kSecAttrAccount: keyToSearchFor,
(__bridge id)kSecReturnAttributes: (__bridge id)kCFBooleanTrue,
};
CFDictionaryRef cfAttributes = NULL;
OSStatus found = SecItemCopyMatching((__bridge CFDictionaryRef)query,
(CFTypeRef *)&cfAttributes);
if (found == errSecSuccess){
NSDictionary *attributes =
(__bridge_transfer NSDictionary *)cfAttributes;
NSString *comments = attributes[(__bridge id)kSecAttrComment];
NSLog(@"Comments = %@", comments);
} else {
NSLog(@"Error happened with code: %ld", (long)found);
}
}
— (BOOL) application:(UIApplication *)application
didFinishLaunchingWithOptions:(NSDictionary *)launchOptions{
NSString *keyToSearchFor = @"Full Name";
NSString *service = [[NSBundle mainBundle] bundleIdentifier];
NSDictionary *query = @{
(__bridge id)kSecClass: (__bridge id)kSecClassGenericPassword,
(__bridge id)kSecAttrService: service,
(__bridge id)kSecAttrAccount: keyToSearchFor,
};
OSStatus found = SecItemCopyMatching((__bridge CFDictionaryRef)query,
NULL);
if (found == errSecSuccess){
NSData *newData = [@"Mark Tremonti"
dataUsingEncoding: NSUTF8StringEncoding];
NSDictionary *update = @{
(__bridge id)kSecValueData: newData,
(__bridge id)kSecAttrComment: @"My Comments",
};
OSStatus updated = SecItemUpdate((__bridge CFDictionaryRef)query,
(__bridge CFDictionaryRef)update);
if (updated == errSecSuccess){
[self readExistingValue];
} else {
NSLog(@"Failed to update the value. Error = %ld", (long)updated);
}
} else {
NSLog(@"Error happened with code: %ld", (long)found);
}
self.window = [[UIWindow alloc]
initWithFrame: [[UIScreen mainScreen] bounds]];
self.window.backgroundColor = [UIColor whiteColor];
[self.window makeKeyAndVisible];
return YES;
}
В этом примере важнее всего отметить, что мы включили в обновляющий словарь ключ kSecAttrComment. Как только обновление будет выполнено, мы считаем комментарий с помощью того самого метода считывания, который изучили в разделе 8.3.
См. также
Разделы 8.2 и 8.3.
8.5. Удаление значений из связки ключей
Постановка задачи
Требуется удалить элемент из связки ключей.
Решение
Воспользуйтесь функцией SecItemDelete.
Обсуждение
В разделе 8.2 мы научились сохранять значения в связке ключей. Для удаления этих значений потребуется использовать функцию SecItemDelete. Эта функция принимает всего один параметр: словарь типа CFDictionaryRef. Можно взять обычный словарь и преобразовать его в экземпляр CFDictionaryRef с помощью мостика, как мы поступали в других разделах этой главы. Словарь, передаваемый этому методу, должен содержать следующие ключи:
• kSecClass — тип элемента, который вы собираетесь удалить, например kSecClassGenericPassword;
• kSecAttrService — сервис, к которому привязан элемент. Сохраняя элемент, вы подбираете для него сервис, и этот же сервис вы должны указать здесь. Так, в предыдущих примерах мы задавали в качестве значения этого ключа идентификатор пакета нашего приложения. Если вы поступали так же, то просто задайте идентификатор пакета приложения в качестве значения этого ключа;
• kSecAttrAccount — здесь указывается ключ, который должен быть удален.
Если вы выполнили все указания, приведенные в разделе 8.2, то на данном этапе связка ключей имеет обобщенный пароль (kSecClassGenericPassword) с именем сервиса (kSecAttrService), равным идентификатору пакета приложения, а также имеет ключ (kSecAttrAccount), равный Full Name. Вот что нужно сделать, чтобы удалить этот ключ:
#import «AppDelegate.h»
#import
@implementation AppDelegate
— (BOOL) application:(UIApplication *)application
didFinishLaunchingWithOptions:(NSDictionary *)launchOptions{
NSString *key = @"Full Name";
NSString *service = [[NSBundle mainBundle] bundleIdentifier];
NSDictionary *query = @{
(__bridge id)kSecClass: (__bridge id)kSecClassGenericPassword,
(__bridge id)kSecAttrService: service,
(__bridge id)kSecAttrAccount: key
};
OSStatus foundExisting =
SecItemCopyMatching((__bridge CFDictionaryRef)query, NULL);
if (foundExisting == errSecSuccess){
OSStatus deleted = SecItemDelete((__bridge CFDictionaryRef)query);
if (deleted == errSecSuccess){
NSLog(@"Successfully deleted the item");
} else {
NSLog(@"Failed to delete the item.");
}
} else {
NSLog(@"Did not find the existing value.");
}
self.window = [[UIWindow alloc]
initWithFrame: [[UIScreen mainScreen] bounds]];
self.window.backgroundColor = [UIColor whiteColor];
[self.window makeKeyAndVisible];
return YES;
}
После запуска этой программы (предполагается, что вы выполнили все инструкции из раздела 8.2) вы должны увидеть на консоли NSLog, соответствующий успешному удалению. В противном случае вы в любой момент можете считать значение функции SecItemDelete и узнать, почему возникла проблема.
См. также
Раздел 8.2.
8.6. Совместное использование данных из связки ключей в нескольких приложениях
Постановка задачи
Требуется, чтобы хранилищем данных связки ключей могли пользоваться два ваших приложения.
Решение
Сохраняя данные вашей связки ключей, укажите ключ kSecAttrAccessGroup в словаре, передаваемом функции SecItemAdd. Значением этого ключа должна быть группа доступа, которую вы найдете в разделе Entitlements (Разрешения) вашего профиля инициализации. Об этом мы говорили во введении к данной главе.
Обсуждение
Несколько приложений с одного и того же портала разработки могут совместно использовать область связки ключей. Чтобы не усложнять этот пример, в данном разделе рассмотрим взаимодействие всего двух приложений, но описанные методы применимы для любого количества программ.
Чтобы два приложения могли совместно использовать область связки ключей, должны выполняться следующие требования.
1. Оба приложения должны быть подписаны с помощью профиля инициализации, взятого с одного и того же портала для разработки под iOS.
2. Оба приложения должны иметь в своем профиле инициализации один и тот же групповой идентификатор (Group ID). Обычно это идентификатор команды (Team ID), выбираемый Apple. Рекомендую не менять этот групповой идентификатор при создании собственных профилей инициализации.
3. Первое приложение, сохраняющее значение в связке ключей, должно задавать для сохраняемого элемента связки ключей атрибут kSecAttrAccessGroup. Эта группа доступа должна совпадать с той, которая указана в вашем профиле инициализации. Прочитайте во введении к этой главе, как извлекать это значение из ваших профилей инициализации.
4. Значение, сохраненное в связке ключей, должно быть сохранено с таким атрибутом kSecAttrService, значение которого известно обоим взаимодействующим приложениям. Обычно это идентификатор пакета того приложения, которое сохранило значение в связке ключей. Если вы сами написали оба приложения, то знаете этот идентификатор пакета. Таким образом, в другом вашем приложении можете считать это значение, предоставив идентификатор пакета первого приложения вышеупомянутому ключу.
5. Оба приложения должны обладать идентификацией подписи кода. Это файл настроек. plist, содержимое которого в точности совпадает с информацией из секции Entitlements (Разрешения) вашего профиля инициализации. Затем потребуется указать путь к этому файлу в разделе Code Signing Entitlements (Разрешения для подписи кода) в настройках сборки. Вскоре мы обсудим этот вопрос подробнее.
6. Хотя приложение и подписано профилями инициализации, в которых указаны разрешения (подробнее об этом говорится во введении к данной главе), вам все равно потребуется явно сообщить Xcode об этих разрешениях. Вся информация о разрешениях — это обычный файл. plist, содержимое которого очень напоминает тот файл разрешений, вывод которого я продемонстрировал во введении к этой главе.
Обратите внимание на ключ keychain-access-groups. Значение этого ключа указывает группу связки ключей, к которой имеет доступ данное приложение, а именно: F3FU372W5M.*. Вам потребуется найти в разделе «Разрешения» свою группу доступа к связке ключей и использовать ее в примерах кода в данном разделе. Мы напишем два приложения. Первое будет заносить в связку ключей информацию, касающуюся группы доступа к связке ключей, а второе будет считывать эту информацию. Приложения будут иметь разные идентификаторы пакетов и, в принципе, будут двумя совершенно разными программами. Однако они смогут совместно использовать определенную область в связке ключей.
Группа доступа является общим идентификатором моей команды и соответствует группе, имеющей доступ к нашей общей связке ключей. Разумеется, в вашем случае это значение будет другим. Воспользуйтесь приемами, изученными в разделе 8.0 этой главы, чтобы извлечь разрешения из ваших профилей инициализации.
Для первого из наших iOS-приложений я задам следующие настройки. Вы должны заменить их собственными.
• Идентификатор пакета com.pixolity.ios.cookbook.SecurityApp.
Группа доступа к связке ключей F3FU372W5M.*.
Профиль инициализации, специально созданный для идентификатора пакета данного приложения.
А вот мои настройки для второго приложения — того, которое будет считывать из связки ключей значения, сохраненные там первым приложением.
• Идентификатор пакета com.pixolity.ios.cookbook.SecurityApp.
Группа доступа к связке ключей F3FU372W5M.*.
Профиль инициализации, специально созданный для идентификатора пакета данного приложения, но отличающийся от аналогичного профиля, созданного для первого приложения.
Самая важная деталь, отличающая первое приложение (сохраняющее связку ключей) от второго (считывающего цепочку ключей), — это идентификаторы пакетов. Первое приложение будет использовать свой идентификатор пакета для сохранения значения в связке ключей, а второе — использовать идентификатор пакета первого приложения для считывания того самого значения из связки ключей. Код очень напоминает тот, что мы написали в разделе 8.2. Разница заключается в том, что новый код будет указывать группу доступа к связке ключей при сохранении данных в этой связке ключей:
#import «AppDelegate.h»
#import
@implementation AppDelegate
— (BOOL) application:(UIApplication *)application
didFinishLaunchingWithOptions:(NSDictionary *)launchOptions{
NSString *key = @"Full Name";
NSString *service = [[NSBundle mainBundle] bundleIdentifier];
NSString *accessGroup = @"F3FU372W5M.*";
/* Сначала удаляем имеющееся значение, если оно существует. Этого можно
и не делать, но SecItemAdd завершится с ошибкой, если имеющееся
значение окажется в связке ключей */
NSDictionary *queryDictionary = @{
(__bridge id)kSecClass: (__bridge id)kSecClassGenericPassword,
(__bridge id)kSecAttrService: service,
(__bridge id)kSecAttrAccessGroup: accessGroup,
(__bridge id)kSecAttrAccount: key,
};
SecItemDelete((__bridge CFDictionaryRef)queryDictionary);
/* Затем запишем новое значение в связку ключей */
NSString *value = @"Steve Jobs";
NSData *valueData = [value dataUsingEncoding: NSUTF8StringEncoding];
NSDictionary *secItem = @{
(__bridge id)kSecClass: (__bridge id)kSecClassGenericPassword,
(__bridge id)kSecAttrService: service,
(__bridge id)kSecAttrAccessGroup: accessGroup,
(__bridge id)kSecAttrAccount: key,
(__bridge id)kSecValueData: valueData,
};
CFTypeRef result = NULL;
OSStatus status = SecItemAdd((__bridge CFDictionaryRef)secItem, &result);
if (status == errSecSuccess){
NSLog(@"Successfully stored the value");
} else {
NSLog(@"Failed to store the value with code: %ld", (long)status);
}
self.window = [[UIWindow alloc]
initWithFrame: [[UIScreen mainScreen] bounds]];
self.window.backgroundColor = [UIColor whiteColor];
[self.window makeKeyAndVisible];
return YES;
}
Код начинается с запрашивания связки ключей для нахождения имеющегося элемента с заданным ключом, именем службы и группой доступа к связке ключей. Если такое значение существует, мы удаляем его из связки ключей. Мы делаем это лишь для того, чтобы позже можно было успешно добавить новое значение. Функция SecItemAdd завершится с ошибкой, если вы попытаетесь перезаписать ею имеющееся значение. Поэтому мы удаляем имеющееся значение (если оно существует) и записываем новое. Вы также можете попытаться найти имеющееся значение, обновить его (при наличии такого значения), а если оно отсутствует — записать новое значение. Второй подход более сложен, а поскольку мы выполняем демонстрационное упражнение, обойдемся первым примером.
Однако прежде, чем вы сможете запустить это приложение, необходимо задать в коде разрешения, связанные с подписыванием. Чтобы задать элементы, регламентирующие подписывание кода, выполните следующие шаги.
1. Воспользуйтесь приемами, изученными во введении к этой главе, нужными для извлечения разрешений, находящихся в вашем профиле инициализации.
2. Создайте в вашем приложении новый. plist-файл и назовите его Entitlements.plist. Вставьте содержимое файла с разрешениями из профиля инициализации — в точности как они есть — в ваш файл Entitlements.plist и сохраните.
3. Перейдите в настройки сборки и найдите разрешения подписывания кода (Code Signing Entitlements). Задайте для этого раздела значение $(TARGET_NAME)/Entitlements.plist. Оно означает, что Xcode должна найти файл Entitlements.plist в целевом каталоге, имеющем такое имя.
Опишу причины, по которым нам требуется это значение. Если создать проект под названием MyProject, Xcode создаст корневой каталог (SCROOT) под названием MyProject. В нем среда создаст еще один подкаталог, который также будет называться MyProject, а уже в этом каталоге будет находиться ваш исходный код. В каталоге SCROOT (то есть в вышестоящем каталоге под названием MyProject) будет создан еще один подкаталог под названием MyProjectTests. В этом подкаталоге будут модульные тесты, интеграционные тесты и тесты пользовательского интерфейса. Под этой структурой вы найдете ваш файл Entitlements.plist (он находится по адресу MyProject/MyProject/Entitlements.plist). Программа поиска разрешений подписывания кода ищет указанный. plist-файл в каталоге SRCROOT, поэтому, если вы предоставите ей значение MyProject/MyProject/Entitlements.plist, это ее вполне устроит! $(TARGET_NAME) в Xcode — это переменная, разрешаемая в имя вашей цели, которое по умолчанию совпадает с именем вашего проекта. Следовательно, в случае с MyProject значение $(TARGET_NAME)/Entitlements.plist будет разрешаться в MyProject/Entitlements.plist.
4. Соберите приложение и убедитесь, что все работает правильно.
Если вы получите сообщение об ошибке примерно следующего содержания:
error: The data couldn't be read because it isn't in the correct format [8]«Ошибка: данные не могут быть считаны, так как имеют неверный формат». — Примеч. пер .
это означает, что ваши разрешения записаны в неверном формате. Распространенная ошибка, которую совершают многие iOS-программисты, связана с заполнением файла с разрешениями следующим образом:
Обратите внимание: этот файл с разрешениями оформлен неправильно, так как в верхней его части содержится висячий ключ Entitlements. Этот ключ потребуется удалить, чтобы ваш файл имел следующий вид:
Итак, разработка записывающего приложения закончена. Вплотную займемся приложением, которое считывает данные. Это два совершенно самостоятельных подписанных приложения, каждое из которых обладает собственным профилем инициализации:
#import «AppDelegate.h»
#import
@implementation AppDelegate
— (BOOL) application:(UIApplication *)application
didFinishLaunchingWithOptions:(NSDictionary *)launchOptions{
NSString *key = @"Full Name";
/* Это не ID пакета того приложения, которое записывало информацию
в связку ключей. Это и не ID пакета данного приложения. Идентификатор
пакета данного приложения — com.pixolity.ios.cookbook.SecondSecurityApp */
NSString *service = @"com.pixolity.ios.cookbook.SecurityApp";
NSString *accessGroup = @"F3FU372W5M.*";
NSDictionary *queryDictionary = @{
(__bridge id)kSecClass: (__bridge id)kSecClassGenericPassword,
(__bridge id)kSecAttrService: service,
(__bridge id)kSecAttrAccessGroup: accessGroup,
(__bridge id)kSecAttrAccount: key,
(__bridge id)kSecReturnData: (__bridge id)kCFBooleanTrue,
};
CFDataRef data = NULL;
OSStatus found =
SecItemCopyMatching((__bridge CFDictionaryRef)queryDictionary,
(CFTypeRef *)&data);
if (found == errSecSuccess){
NSString *value = [[NSString alloc]
initWithData:(__bridge_transfer NSData *)data
encoding: NSUTF8StringEncoding];
NSLog(@"Value = %@", value);
} else {
NSLog(@"Failed to read the value with error = %ld", (long)found);
}
self.window = [[UIWindow alloc]
initWithFrame: [[UIScreen mainScreen] bounds]];
self.window.backgroundColor = [UIColor whiteColor];
[self.window makeKeyAndVisible];
return YES;
}
Может потребоваться время, чтобы привыкнуть к тому, как работает связка ключей. Не волнуйтесь, если все не заработает с пол-оборота. Просто внимательно изучите все инструкции, данные в этой главе (особенно в ее введении), чтобы четко понять, что такое группа доступа к связке ключей и как она связана с разрешениями вашего приложения.
См. также
Разделы 8.0 и 8.2.
8.7. Запись и считывание информации связки ключей из iCloud
Постановка задачи
Требуется сохранить информацию в связке ключей, а также обеспечить хранение этой информации в пользовательской связке ключей, расположенной в облаке iCloud. Так пользователь сможет получать доступ к этой связке с любых имеющихся у него устройств с iOS.
Решение
При добавлении вашего элемента к связке ключей с помощью функции SecItemAdd добавьте ключ kSecAttrSynchronizable в словарь, передаваемый этой функции. В качестве значения этого ключа передайте kCFBooleanTrue.
Обсуждение
Когда элементы хранятся в связке ключей с ключом kSecAttrSynchronizable, имеющим значение kCFBooleanTrue, такие элементы сохраняются и в той пользовательской связке ключей, которая расположена в облаке iCloud. Таким образом, эти элементы будут доступны на всех пользовательских устройствах, если пользователь зашел в них под учетной записью для работы с iCloud. Если вы просто хотите считать значение, которое (как вам точно известно) синхронизировано с пользовательской связкой ключей из iCloud, необходимо указать вышеупомянутый ключ, а также задать для этого ключа значение kCFBooleanTrue. В таком случае iOS сможет получить требуемое значение из облака, если еще не сделала этого.
Пример, который мы рассмотрим в этом разделе, на 99 % совпадает с кодом, изученным в разделе 8.6. Разница заключается в следующем: когда мы сохраняем значение в связке ключей или пытаемся его оттуда считать, то указываем в нашем словаре ключ kSecAttrSynchronizable и задаем для этого ключа значение kCFBooleanTrue. Итак, давайте для начала рассмотрим, как сохранить значение в связке ключей:
#import «AppDelegate.h»
#import
@implementation AppDelegate
— (BOOL) application:(UIApplication *)application
didFinishLaunchingWithOptions:(NSDictionary *)launchOptions{
NSString *key = @"Full Name";
NSString *service = [[NSBundle mainBundle] bundleIdentifier];
NSString *accessGroup = @"F3FU372W5M.*";
/* Сначала удаляем имеющееся значение, если оно существует. Этого можно
и не делать, но SecItemAdd завершится с ошибкой, если имеющееся
значение окажется в связке ключей */
NSDictionary *queryDictionary = @{
(__bridge id)kSecClass: (__bridge id)kSecClassGenericPassword,
(__bridge id)kSecAttrService: service,
(__bridge id)kSecAttrAccessGroup: accessGroup,
(__bridge id)kSecAttrAccount: key,
(__bridge id)kSecAttrSynchronizable: (__bridge id)kCFBooleanTrue
};
SecItemDelete((__bridge CFDictionaryRef)queryDictionary);
/* Затем запишем новое значение в связку ключей */
NSString *value = @"Steve Jobs";
NSData *valueData = [value dataUsingEncoding: NSUTF8StringEncoding];
NSDictionary *secItem = @{
(__bridge id)kSecClass: (__bridge id)kSecClassGenericPassword,
(__bridge id)kSecAttrService: service,
(__bridge id)kSecAttrAccessGroup: accessGroup,
(__bridge id)kSecAttrAccount: key,
(__bridge id)kSecValueData: valueData,
(__bridge id)kSecAttrSynchronizable: (__bridge id)kCFBooleanTrue
};
CFTypeRef result = NULL;
OSStatus status = SecItemAdd((__bridge CFDictionaryRef)secItem, &result);
if (status == errSecSuccess){
NSLog(@"Successfully stored the value");
} else {
NSLog(@"Failed to store the value with code: %ld", (long)status);
}
self.window = [[UIWindow alloc]
initWithFrame: [[UIScreen mainScreen] bounds]];
self.window.backgroundColor = [UIColor whiteColor];
[self.window makeKeyAndVisible];
return YES;
}
Пожалуйста, внимательно изучите примечания из раздела 8.6. На данном этапе вы уже должны знать, что группа доступа, упоминаемая во всех приведенных примерах, у каждого разработчика будет называться по-своему. Обычно это командный идентификатор, генерируемый на портале Apple для разработки под iOS для каждого разработчика и обеспечивающий доступ к коду для его команды. Необходимо изменить это значение в ваших примерах и убедиться, что оно совпадает именно с вашим командным идентификатором.
Ранее был приведен код приложения, сохраняющего значения в связке ключей, расположенной в облаке iCloud. Теперь остается написать приложение, считывающее эти данные:
#import «AppDelegate.h»
#import
@implementation AppDelegate
— (BOOL) application:(UIApplication *)application
didFinishLaunchingWithOptions:(NSDictionary *)launchOptions{
NSString *key = @"Full Name";
/* Это не ID пакета того приложения, которое записывало информацию
в связку ключей в iCloud. Это и не ID пакета данного приложения.
Идентификатор пакета данного приложения — com.pixolity.ios.cookbook.SecondSecurityApp*/
NSString *service = @"com.pixolity.ios.cookbook.SecurityApp";
NSString *accessGroup = @"F3FU372W5M.*";
NSDictionary *queryDictionary = @{
(__bridge id)kSecClass: (__bridge id)kSecClassGenericPassword,
(__bridge id)kSecAttrService: service,
(__bridge id)kSecAttrAccessGroup: accessGroup,
(__bridge id)kSecAttrAccount: key,
(__bridge id)kSecReturnData: (__bridge id)kCFBooleanTrue,
(__bridge id)kSecAttrSynchronizable: (__bridge id)kCFBooleanTrue
};
CFDataRef data = NULL;
OSStatus found =
SecItemCopyMatching((__bridge CFDictionaryRef)queryDictionary,
(CFTypeRef *)&data);
if (found == errSecSuccess){
NSString *value = [[NSString alloc]
initWithData:(__bridge_transfer NSData *)data
encoding: NSUTF8StringEncoding];
NSLog(@"Value = %@", value);
} else {
NSLog(@"Failed to read the value with error = %ld", (long)found);
}
self.window = [[UIWindow alloc]
initWithFrame: [[UIScreen mainScreen] bounds]];
self.window.backgroundColor = [UIColor whiteColor];
[self.window makeKeyAndVisible];
return YES;
}
Необходимо отметить еще несколько деталей, связанных с работой со связкой ключей в iCloud.
• В такой связке ключей можно хранить только пароли.
• Связка ключей iCloud работает где угодно — это означает, что она появляется сразу на нескольких устройствах, принадлежащих одному пользователю iCloud. Если вы запишете элемент в связку ключей iCloud, он будет синхронизирован на всех устройствах этого пользователя. Аналогично если вы удалите элемент, то он удалится со всех устройств пользователя, связанных с iCloud. Поэтому в данном случае нужно проявлять особую осторожность.
Также следует отметить, что все остальные приемы, изученные нами в этой главе (например, обновление имеющегося элемента связки ключей — см. раздел 8.4), работают и со связкой ключей, расположенной в iCloud.
См. также
Разделы 8.0 и 8.6.
8.8. Безопасное хранение файлов в песочнице приложения
Постановка задачи
Требуется, чтобы iOS защищала файлы, расположенные в песочнице вашего приложения, от несанкционированного считывания. Такое считывание, в частности, могут выполнять файловые менеджеры для iOS, которые в изобилии встречаются в Интернете.
Решение
Сделайте следующие шаги.
1. Выполните все операции, необходимые для создания профиля инициализации приложения, — об этом см. в разделе 8.0. Приложение должно быть подключено к такому идентификатору, для которого действует защита данных (Data Protection).
2. Подпишите ваше приложение профилем инициализации.
3. Задайте разрешения на подписывание кода вашего приложения (следуйте указаниям, изложенным в разделе 8.6).
4. Используйте метод createFileAtPath: contents: attributes: экземпляра NSFileManager, чтобы сохранить ваш файл. Для свойства attributes передайте словарь, содержащий ключ NSFileProtectionKey. Этот ключ может иметь одно из следующих значений.
• NSFileProtectionNone — при таком значении сохраненный файл не имеет никакой защиты. Файл, сохраненный с таким уровнем защиты, будет доступен в приложении, сохранившем его на диск, а также любым бесплатным или коммерческим файловым менеджером, взятым из Интернета. Такая программа-менеджер может свободно просматривать файловую систему устройства с iOS, даже если это устройство защищено паролем. Указывая этот ключ, вы получаете возможность как считывать ваш файл, так и записывать в этот файл, в том числе и тогда, когда пользовательское устройство заблокировано.
• NSFileProtectionComplete — это наиболее высокий уровень защиты, который вы можете задать для ваших файлов. При наличии данного ключа приложение сможет считывать такой файл и записывать в него информацию, пока устройство разблокировано. Если устройство заблокировано, считывать файл и записывать в него какие-либо данные невозможно. При применении такого уровня защиты коммерческие файловые менеджеры не могут читать содержимое файла, даже если пользовательское устройство разблокировано.
• NSFileProtectionCompleteUnlessOpen — значение очень похоже на NSFileProtectionComplete. Разница понятна уже из названия: вы сможете получить доступ к файлу, если уже открыли его, даже если впоследствии пользователь заблокирует устройство. Итак, открыв файл впервые, вы получаете к нему гарантированный доступ до тех пор, пока существует ваше приложение.
• NSFileProtectionCompleteUntilFirstUserAuthentication — при таком значении ваше приложение сможет считывать файл и заносить в него информацию, как только пользователь в первый раз разблокирует свое устройство. После этого вы можете и далее обращаться к файлу, даже если впоследствии пользователь вновь заблокирует устройство.
Вот пример:
— (NSString *) filePath{
NSFileManager *fileManager = [[NSFileManager alloc] init];
NSError *error = nil;
NSURL *documentFolderUrl = [fileManager URLForDirectory: NSDocumentDirectory
inDomain: NSUserDomainMask
appropriateForURL: nil
create: YES
error:&error];
if (error == nil && documentFolderUrl!= nil){
NSString *fileName = @"MyFile.txt";
NSString *filePath = [documentFolderUrl.path
stringByAppendingPathComponent: fileName];
return filePath;
}
return nil;
}
— (BOOL) application:(UIApplication *)application
didFinishLaunchingWithOptions:(NSDictionary *)launchOptions{
/*
Предпосылки:
1) подписать приложение валидным профилем инициализации;
2) в вашем профиле должна быть активизирована полная защита файла;
3) добавить в проект разрешения на подписывание кода.
*/
NSFileManager *fileManager = [[NSFileManager alloc] init];
if ([self filePath]!= nil){
NSData *dataToWrite = [@"Hello, World"
dataUsingEncoding: NSUTF8StringEncoding];
NSDictionary *fileAttributes = @{
NSFileProtectionKey: NSFileProtectionComplete
};
BOOL wrote = [fileManager createFileAtPath: [self filePath]
contents: dataToWrite
attributes: fileAttributes];
if (wrote){
NSLog(@"Successfully and securely stored the file");
} else {
NSLog(@"Failed to write the file");
}
}
self.window = [[UIWindow alloc]
initWithFrame: [[UIScreen mainScreen] bounds]];
self.window.backgroundColor = [UIColor whiteColor];
[self.window makeKeyAndVisible];
return YES;
}
Обсуждение
Пользователи доверяют вашим приложениям. Поэтому, когда вы запрашиваете у пользователя определенную личную информацию, например имя и фамилию, пользователь рассчитывает, что эта информация будет храниться в хорошо защищенном месте, где до нее не доберутся хакеры или кто-нибудь, кто получает временный доступ к пользовательскому устройству с iOS.
Предположим, вы пишете приложение для редактирования фотографий. Работая с этим приложением, пользователь может подключить камеру к своему устройству с iOS, импортировать свои фотографии в ваше приложение, а потом пользоваться им для редактирования, сохранения фотографий и раздачи их друзьям. Вы могли бы поступить так, как делают очень многие разработчики приложений: импортировать эти фотографии в папку Documents (Документы), где они сразу будут готовы к редактированию. Но с таким подходом связана одна проблема: любой файловый менеджер для iOS, который можно свободно скачать в Интернете, может считывать содержимое папки Documents (Документы) в любом приложении, даже если устройство заблокировано. Чтобы защитить пользовательские данные, следует активизировать защиту тех файлов, которые вы храните в песочнице приложения. Защита файлов — неотъемлемая часть безопасности пользовательского устройства, в частности, пароля к этому устройству. Допустим, пользователь установил на устройстве пароль, без которого устройство нельзя разблокировать (пусть даже этот пароль совсем простой), и такая блокировка произошла. В таком случае после того, как устройство будет заблокировано, все файлы, сохраненные в песочнице вашего приложения и обладающие ключом NSFileProtectionComplete, будут недоступны для посторонних. Прочитать такие файлы не сможет даже файловый менеджер.
Итак, при релизе вашего приложения и даже на этапе его разработки задавайте профили для разработки и распространения программы так, чтобы в них действовала необходимая защита файлов, распространяющаяся на конфиденциальные файлы, находящиеся на диске. Сами определите, какие файлы нуждаются в защите от непрошеных зрителей. Остальные файлы на диске можно и не защищать.
См. также
Разделы 8.0 и 8.6.
8.9. Защита пользовательского интерфейса
Постановка задачи
Необходимо гарантировать, что пользовательский интерфейс соответствует наиболее распространенным правилам безопасности, действующим в iOS.
Решение
Следуйте приведенным далее указаниям.
• Обеспечьте, чтобы вся информация, вводимая пользователем в поля паролей и другие защищенные поля, попадала в экземпляры UITextField, чьим свойствам secureTextEntry присвоено значение YES.
• Если пользователь находится на экране, содержащем персональную информацию, например номер кредитной карточки или домашний адрес, присвойте свойству hidden главного окна вашего приложения значение YES (в методе applicationWillResignActive: делегата вашего приложения). Чтобы само окно отображалось на экране, тому же самому свойству нужно присвоить значение NO в методе applicationDidBecomeActive: делегата приложения. Так вы гарантируете, что на скриншоте пользовательского интерфейса (iOS снимает такой скриншот, когда приложение переходит в фоновый режим) не будет отражаться никакое содержимое вашего окна. Apple рекомендует действовать именно так.
• Обязательно валидируйте пользовательский ввод в текстовых полях/видах перед отправкой этой информации на сервер.
• Пользуясь механизмами, изученными в этой главе, защищайте пользовательские записи, если храните их в файлах на диске или в связке ключей.
• На тех экранах, где вы принимаете пароль или числовой код для аутентификации, очищайте такие поля с кодом/паролем, как только контроллер вида перестает отображаться на экране. Если вы не будете уступать и высвобождать эти контроллеры видов, их содержимое будет сохраняться в памяти. В частности, в памяти будут находиться записи из защищенных текстовых полей, находящихся в этих контроллерах видов. Целесообразно высвобождать память, содержащую конфиденциальные данные, сразу же после того, как исчезает необходимость в этих данных.
Обсуждение
В этом списке лишь второй элемент требует дополнительного объяснения. Когда пользователь видит окно приложения на экране своего устройства с iOS и переводит это приложение в фоновый режим, возвращаясь на главный экран (нажимая Home), iOS помещает приложение в неактивное состояние. Когда приложение в таком состоянии переведено в фоновый режим, iOS делает скриншот пользовательского интерфейса этого приложения (в точном соответствии с изображением на экране) и сохраняет этот файл в каталоге Library/Caches/Snapshots/. Данный каталог находится в песочнице вашего приложения. Как только пользователь вновь переводит приложение в приоритетный режим, iOS сразу же отображает этот скриншот, и он остается на экране до тех пор, пока приложение не «оживет» окончательно и не примет управление экраном. Поэтому переход из фонового в приоритетный режим в iOS получается очень плавным. Но хотя такая практика и очень положительна с точки зрения удобства использования (UX), она привносит определенную проблему в области безопасности. Дело в том, что если на скриншоте была зафиксирована конфиденциальная информация, то она будет сохранена и на диске. Мы не можем полностью отключить эту функцию в iOS, однако можем нейтрализовать ее негативное влияние на безопасность приложения. Чтобы это сделать (кстати, такая практика рекомендуется Apple), нужно накрыть основное окно нашего приложения другим видом либо скрыть содержимое этого окна, устанавливая свойство hidden окна приложения в значение YES, когда приложение становится неактивным. При переходе приложения в активное состояние мы вновь присваиваем этому свойству значение NO (и окно снова становится видимым).
iOS-разработчики, стремящиеся выполнять это требование безопасности, часто совершают одну ошибку. Они пытаются устанавливать в YES или NO значение свойства hidden экземпляра keyWindow. Даже хотя окно keyWindow экземпляра приложения будет валидным окном в момент, когда приложение становится неактивным, в момент «оживления» приложения keyWindow равно nil — то есть указывает в никуда. Следовательно, во избежание возможных ошибок просто пользуйтесь свойством window делегата приложения, отображая или скрывая окно.
Другая проблема, касающаяся безопасности приложения, связана с «оседанием» персональных данных в контроллерах видов. Предположим, у вас есть контроллер вида для входа в систему. Здесь пользователь вводит свои имя и пароль. Как только будет нажата кнопка, которая зачастую называется Login (Вход в систему), вы отправляете учетные данные пользователя на сервер по сетевому HTTPS-соединению. Когда произойдет аутентификация пользователя, вы выдвигаете на экран другой контроллер вида. Проблема, связанная с таким подходом, заключается в том, что имя и пароль, введенные пользователем на предыдущем экране, по-прежнему остаются в памяти (как и сам контроллер вида). Как вы помните, навигационный контроллер включает целый стек контроллеров видов.
Чтобы справиться с этой проблемой и повысить безопасность пользовательского интерфейса, можно присвоить свойству text защищенных текстовых полей значение nil. Это делается в тот самый момент, когда на экран выдвигается второй контроллер вида. Другой способ — переопределить метод экземпляра viewWillDisappear: контроллера вида для входа в систему, а также установить свойство text текстовых полей в nil прямо здесь. Тем не менее такой подход требует известной осторожности, так как упомянутый метод экземпляра контроллера вида вызывается всякий раз, когда контроллер исчезает с экрана — например, когда пользователь покидает вкладку с контроллером вида, уходит на другую вкладку, а потом возвращается на первую. Действительно, при таком переходе контроллер вида успевает и исчезнуть с экрана, и вновь там появиться. Поэтому если пользователь всего лишь перешел на соседнюю вкладку, а вы уже стерли всю информацию, введенную в поля, то при возвращении на первую вкладку пользователь найдет там лишь пустые окошки и будет вынужден заносить всю информацию заново. Разработка всегда ведется в соответствии с поставленными бизнес-требованиями, поэтому не существует однозначного ответа на вопрос о том, как справиться с такой ситуацией.
См. также
Разделы 8.2 и 8.8.