Статьи

Защита данных iOS в покое: брелок

Любое приложение, которое сохраняет данные пользователя, должно заботиться о безопасности и конфиденциальности этих данных. Как мы видели в случае недавних утечек данных, могут быть очень серьезные последствия для защиты защищенных данных ваших пользователей. В этом руководстве вы познакомитесь с некоторыми рекомендациями по защите данных ваших пользователей.

В предыдущем посте вы узнали, как защитить файлы с помощью API защиты данных. Защита на основе файлов — это мощная функция для безопасного хранения больших объемов данных. Но это может быть излишним для небольшого количества защищаемой информации, такой как ключ или пароль. Для этих типов предметов брелок является рекомендуемым решением.

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

Существует несколько популярных сторонних библиотек для служб цепочки для ключей, таких как Strongbox (Swift) и SSKeychain (Objective-C). Или, если вы хотите полностью контролировать свой собственный код, вы можете напрямую использовать API-интерфейс Keychain Services, который является C-API.

Я кратко объясню, как работает цепочка для ключей. Вы можете думать о цепочке для ключей как о типичной базе данных, где вы выполняете запросы к таблице. Все функции API цепочки для ключей требуют объекта CFDictionary который содержит атрибуты запроса.

Каждая запись в цепочке для ключей имеет имя сервиса. Имя службы — это идентификатор: ключ для любого значения, которое вы хотите сохранить или получить в цепочке для ключей. Чтобы позволить элементу цепочки для ключей быть сохраненным только для определенного пользователя, вы также часто захотите указать имя учетной записи.

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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
import Security
 
//…
 
class func passwordQuery(service: String, account: String) -> Dictionary<String, Any>
{
    let dictionary = [
        kSecClass as String : kSecClassGenericPassword,
        kSecAttrAccount as String : account,
        kSecAttrService as String : service,
        kSecAttrAccessible as String : kSecAttrAccessibleWhenUnlocked //If need access in background, might want to consider kSecAttrAccessibleAfterFirstUnlock
    ] as [String : Any]
     
    return dictionary
}

Этот код устанавливает Dictionary запроса с именами вашей учетной записи и службы и сообщает цепочке для ключей, что мы будем хранить пароль.

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

Функция SecItemAdd() добавляет данные в SecItemAdd() для ключей. Эта функция принимает объект Data , что делает его универсальным для хранения многих видов объектов. Используя функцию запроса пароля, которую мы создали выше, давайте сохраним строку в цепочке для ключей. Для этого нам просто нужно преобразовать String в Data .

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
@discardableResult class func setPassword(_ password: String, service: String, account: String) -> Bool
{
    var status : OSStatus = -1
    if !(service.isEmpty) && !(account.isEmpty)
    {
        deletePassword(service: service, account: account) //delete password if pass empty string.
         
        if !password.isEmpty
        {
            var dictionary = passwordQuery(service: service, account: account)
            let dataFromString = password.data(using: String.Encoding.utf8, allowLossyConversion: false)
            dictionary[kSecValueData as String] = dataFromString
            status = SecItemAdd(dictionary as CFDictionary, nil)
        }
    }
    return status == errSecSuccess
}

Чтобы предотвратить повторяющиеся вставки, приведенный выше код сначала удаляет предыдущую запись, если она есть. Давайте напишем эту функцию сейчас. Это достигается с помощью функции SecItemDelete() .

01
02
03
04
05
06
07
08
09
10
@discardableResult class func deletePassword(service: String, account: String) -> Bool
{
    var status : OSStatus = -1
    if !(service.isEmpty) && !(account.isEmpty)
    {
        let dictionary = passwordQuery(service: service, account: account)
        status = SecItemDelete(dictionary as CFDictionary);
    }
    return status == errSecSuccess
}

Затем, чтобы получить запись из цепочки для ключей, используйте SecItemCopyMatching() . Он возвратит AnyObject , соответствующий вашему запросу.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
class func password(service: String, account: String) -> String //return empty string if not found, could return an optional
{
    var status : OSStatus = -1
    var resultString = «»
    if !(service.isEmpty) && !(account.isEmpty)
    {
        var passwordData : AnyObject?
        var dictionary = passwordQuery(service: service, account: account)
        dictionary[kSecReturnData as String] = kCFBooleanTrue
        dictionary[kSecMatchLimit as String] = kSecMatchLimitOne
        status = SecItemCopyMatching(dictionary as CFDictionary, &passwordData)
         
        if status == errSecSuccess
        {
            if let retrievedData = passwordData as?
            {
                resultString = String(data: retrievedData, encoding: String.Encoding.utf8)!
            }
        }
    }
    return resultString
}

В этом коде мы устанавливаем для параметра kCFBooleanTrue значение kCFBooleanTrue . kSecReturnData означает, что будут возвращены фактические данные элемента. Другим вариантом может быть возвращение атрибутов ( kSecReturnAttributes ) элемента. Ключ принимает тип CFBoolean который содержит константы kCFBooleanTrue или kCFBooleanFalse . Мы устанавливаем kSecMatchLimit в kSecMatchLimitOne так что будет возвращен только первый элемент, найденный в kSecMatchLimit для ключей, в отличие от неограниченного количества результатов.

Цепочка для ключей также является рекомендуемым местом для хранения объектов открытого и закрытого ключей, например, если ваше приложение работает с объектами EC или RSA SecKey и нуждается в SecKey .

Основное отличие состоит в том, что вместо того, чтобы указывать цепочке для ключей хранить пароль, мы можем сказать, что она хранит ключ. Фактически, мы можем конкретизировать, задав типы хранимых ключей, например, открытые или закрытые. Все, что нужно сделать, — это настроить вспомогательную функцию запроса для работы с тем типом ключа, который вы хотите.

Ключи обычно идентифицируются с использованием обратного доменного тега, такого как com.mydomain.mykey, вместо имен служб и учетных записей (поскольку открытые ключи открыто передаются различным компаниям или организациям). Мы возьмем строки службы и учетной записи и преобразуем их в тег Data object. Например, приведенный выше код, адаптированный для хранения RSA Private SecKey будет выглядеть следующим образом:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
class func keyQuery(service: String, account: String) -> Dictionary<String, Any>
{
    let tagString = «com.mydomain.»
    let tag = tagString.data(using: .utf8)!
    let dictionary = [
        kSecClass as String : kSecClassKey,
        kSecAttrKeyType as String : kSecAttrKeyTypeRSA,
        kSecAttrKeyClass as String : kSecAttrKeyClassPrivate,
        kSecAttrAccessible as String : kSecAttrAccessibleWhenUnlocked,
        kSecAttrApplicationTag as String : tag
        ] as [String : Any]
     
    return dictionary
}
 
@discardableResult class func setKey(_ key: SecKey, service: String, account: String) -> Bool
{
    var status : OSStatus = -1
    if !(service.isEmpty) && !(account.isEmpty)
    {
        deleteKey(service: service, account:account)
        var dictionary = keyQuery(service: service, account: account)
        dictionary[kSecValueRef as String] = key
        status = SecItemAdd(dictionary as CFDictionary, nil);
    }
    return status == errSecSuccess
}
 
@discardableResult class func deleteKey(service: String, account: String) -> Bool
{
    var status : OSStatus = -1
    if !(service.isEmpty) && !(account.isEmpty)
    {
        let dictionary = keyQuery(service: service, account: account)
        status = SecItemDelete(dictionary as CFDictionary);
    }
    return status == errSecSuccess
}
 
class func key(service: String, account: String) -> SecKey?
{
    var item: CFTypeRef?
    if !(service.isEmpty) && !(account.isEmpty)
    {
        var dictionary = keyQuery(service: service, account: account)
        dictionary[kSecReturnRef as String] = kCFBooleanTrue
        dictionary[kSecMatchLimit as String] = kSecMatchLimitOne
        SecItemCopyMatching(dictionary as CFDictionary, &item);
    }
    return item as!
}

Элементы, защищенные флагом kSecAttrAccessibleWhenUnlocked , разблокируются только тогда, когда устройство разблокировано, но в первую очередь это зависит от того, настроен ли для пользователя пароль или Touch ID.

applicationPassword данные applicationPassword позволяют защищать элементы в цепочке для ключей с помощью дополнительного пароля. Таким образом, если у пользователя нет установленного пароля или Touch ID, элементы будут по-прежнему защищены, и это добавляет дополнительный уровень безопасности, если у них есть установленный пароль.

В качестве примера сценария, после того как ваше приложение аутентифицируется на вашем сервере, ваш сервер может вернуть пароль через HTTPS, который требуется для разблокировки элемента цепочки для ключей. Это предпочтительный способ предоставления этого дополнительного пароля. Жесткое кодирование пароля в двоичном коде не рекомендуется.

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

Другое использование пароля приложения — для хранения секретного ключа, например, такого, который вы не хотели бы раскрывать только потому, что пользователь еще не установил пароль.

applicationPassword доступен только на iOS 9 и выше, поэтому вам понадобится запасной вариант, который не использует applicationPassword если вы ориентируетесь на более низкие версии iOS. Чтобы использовать код, вам нужно добавить следующее в заголовок моста:

1
2
#import <LocalAuthentication/LocalAuthentication.h>
#import <Security/SecAccessControl.h>

Следующий код устанавливает пароль для Dictionary запроса.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
if #available(iOS 9.0, *)
{
    //Use this in place of kSecAttrAccessible for the query
    var error: Unmanaged<CFError>?
    let accessControl = SecAccessControlCreateWithFlags(kCFAllocatorDefault, kSecAttrAccessibleWhenUnlocked, SecAccessControlCreateFlags.applicationPassword, &error)
    if accessControl != nil
    {
        dictionary[kSecAttrAccessControl as String] = accessControl
    }
     
    let localAuthenticationContext = LAContext.init()
    let theApplicationPassword = «passwordFromServer».data(using:String.Encoding.utf8)!
    localAuthenticationContext.setCredential(theApplicationPassword, type: LACredentialType.applicationPassword)
    dictionary[kSecUseAuthenticationContext as String] = localAuthenticationContext
}

Обратите внимание, что мы установили kSecAttrAccessControl в Dictionary . Это используется вместо kSecAttrAccessible , который ранее был установлен в нашем методе passwordQuery . Если вы попытаетесь использовать оба, вы получите ошибку OSStatus -50 .

Начиная с iOS 8, вы можете хранить данные в цепочке для ключей, к которым можно получить доступ только после успешной аутентификации пользователя на устройстве с помощью Touch ID или пароля. Когда приходит время для аутентификации пользователя, Touch ID имеет приоритет, если он установлен, в противном случае отображается экран пароля. Сохранение в цепочку для ключей не потребует от пользователя аутентификации, но получение данных будет.

Вы можете установить элемент цепочки для ключей, чтобы требовать аутентификации пользователя, предоставив объект управления доступом, установленный в .userPresence Если пароль не настроен, то любые запросы цепочки для ключей с .userPresence будут выполнены.

1
2
3
4
5
6
7
8
if #available(iOS 8.0, *)
{
    let accessControl = SecAccessControlCreateWithFlags(kCFAllocatorDefault, kSecAttrAccessibleWhenUnlockedThisDeviceOnly, .userPresence, nil)
    if accessControl != nil
    {
        dictionary[kSecAttrAccessControl as String] = accessControl
    }
}

Эта функция хороша, если вы хотите убедиться, что ваше приложение используется правильным человеком. Например, для пользователя было бы важно пройти аутентификацию, прежде чем он сможет войти в банковское приложение. Это защитит пользователей, которые оставили свое устройство незапертым, так что банковские операции не будут доступны.

Кроме того, если у вас нет серверного компонента в вашем приложении, вы можете использовать эту функцию для выполнения аутентификации на стороне устройства.

Для запроса загрузки вы можете предоставить описание того, почему пользователь должен пройти аутентификацию.

1
dictionary[kSecUseOperationPrompt as String] = «Authenticate to retrieve x»

При получении данных с помощью SecItemCopyMatching() функция покажет пользовательский интерфейс аутентификации и подождет, пока пользователь не использует Touch ID или не введет пароль. Поскольку SecItemCopyMatching() будет блокироваться до тех пор, пока пользователь не завершит аутентификацию, вам нужно будет вызвать функцию из фонового потока, чтобы основной поток пользовательского интерфейса оставался отзывчивым.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
DispatchQueue.global().async
{
    status = SecItemCopyMatching(dictionary as CFDictionary, &passwordData)
    if status == errSecSuccess
    {
        if let retrievedData = passwordData as?
        {
            DispatchQueue.main.async
            {
                //… do the rest of the work back on the main thread
            }
        }
    }
}

Опять же, мы устанавливаем kSecAttrAccessControl в Dictionary запроса. Вам нужно будет удалить kSecAttrAccessible , который ранее был установлен в нашем методе passwordQuery . Использование обоих одновременно приведет к ошибке OSStatus -50.

В этой статье вы ознакомились с API-интерфейсом Keychain Services. Наряду с API защиты данных, который мы видели в предыдущем посте , использование этой библиотеки является частью передового опыта по защите данных.

Однако, если у пользователя нет пароля или Touch ID на устройстве, шифрование для обеих платформ отсутствует. Поскольку API-интерфейсы Keychain Services и Data Protection обычно используются приложениями iOS, они иногда становятся объектами атак, особенно на взломанных устройствах. Если ваше приложение не работает с конфиденциальной информацией, это может быть приемлемым риском. В то время как iOS постоянно обновляет безопасность фреймворков, мы все еще находимся во власти пользователя, который обновляет ОС, использует надежный код-пароль и не делает джейлбрейк своего устройства.

Цепочка для ключей предназначена для небольших фрагментов данных, и вы можете иметь больший объем данных для защиты, который не зависит от аутентификации устройства. Несмотря на то, что обновления iOS добавляют некоторые замечательные новые функции, такие как пароль приложения, вам все равно может потребоваться поддержка более низких версий iOS и при этом высокий уровень безопасности. По некоторым из этих причин вы можете захотеть зашифровать данные самостоятельно.

Последняя статья этой серии посвящена самостоятельному шифрованию данных с использованием шифрования AES, и, хотя это более продвинутый подход, он позволяет вам полностью контролировать, как и когда шифруются ваши данные.

Так что следите за обновлениями. А пока, посмотрите другие наши посты о разработке приложений для iOS!

  • iOS SDK
    Защита связи на iOS
    Коллин Штюрт
  • Мобильная разработка
    Back-End как сервис для мобильных приложений
    Сандамаль Сирипати
  • iOS SDK
    Правильный способ поделиться состоянием между контроллерами Swift View
    Маттео Манфердини
  • стриж
    Swift From Scratch: Закрытие
    Барт Джейкобс