Онлайн курсы по Swift

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

Сервисы Keychain

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

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

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

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

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

 

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 с именами вашей учетной записи и службы и сообщает связке ключей, что мы будем хранить пароль.

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

 

Добавление пароля

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

 

@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. Could change to pass nil to delete password, etc
         
        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().

 

@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, соответствующий вашему запросу.

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? Data
            {
                resultString = String(data: retrievedData, encoding: String.Encoding.utf8)!
            }
        }
    }
    return resultString
}

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

 

Открытые и закрытые ключи

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

 

@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, соответствующий вашему запросу.

 

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? Data
            {
                resultString = String(data: retrievedData, encoding: String.Encoding.utf8)!
            }
        }
    }
    return resultString
}

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

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

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

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

 

class func keyQuery(service: String, account: String) -> Dictionary<String, Any>
{
    let tagString = "com.mydomain." + service + "." + account
    let tag = tagString.data(using: .utf8)! //Store it as Data, not as a String
    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! SecKey?
}

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

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

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

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

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

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

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

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

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 получит приоритет, если он настроен, в противном случае будет показан экран кода доступа. Сохранение в связке ключей не потребует аутентификации пользователя, а получение данных потребует.

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

if #available(iOS 8.0, *)
{
    let accessControl = SecAccessControlCreateWithFlags(kCFAllocatorDefault, kSecAttrAccessibleWhenUnlockedThisDeviceOnly, .userPresence, nil)
    if accessControl != nil
    {
        dictionary[kSecAttrAccessControl as String] = accessControl
    }
}

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

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

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

dictionary[kSecUseOperationPrompt as String] = "Authenticate to retrieve x"

 

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

DispatchQueue.global().async
{
    status = SecItemCopyMatching(dictionary as CFDictionary, &passwordData)
    if status == errSecSuccess
    {
        if let retrievedData = passwordData as? Data
        {
            DispatchQueue.main.async
            {
                //... do the rest of the work back on the main thread
            }   
        }
    }
}

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

Заключение

В этой статье вы ознакомились с API-интерфейсом Keychain.

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

Пролистать наверх