Добрый день, Хабр! Представляю вашему вниманию перевод статьи про базовые основы безопасности конфиденциальных данных в iOS приложениях «Application Security Musts for every iOS App» автора Arlind Aliu.

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

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

Хранение данных не в том месте


Я провел исследование нескольких приложений из AppStore и во многих допускается одна и та же ошибка: конфиденциальная информация хранится там, где её не должно быть.
Если вы храните данные личного характера в UserDefaults, то вы подвергаете её риску. UserDefaults хранятся в файле со списком свойств, который находится внутри папки «Настройки» в вашем приложении. Данные сохраняются в приложении без малейшего намека на шифрование.

Установив на mac стороннюю программу, как например iMazing, можно даже не взламывать телефон, а сразу увидеть все данные UserDefaults, из приложения установленного из AppStore. Такие программы позволяют смотреть и управлять данными из приложений, установленных на айфоне. Можно легко получить UserDefaults любого приложения.
В этом и заключается главная причина, по которой я решил написать статью – я нашел кучу приложений в AppStore, которые хранят данные в UserDefaults, такие как: токены, активные и возобновляемые подписки, число доступных денег и так далее. Все эти данные можно легко получить и использовать со злым умыслом, начиная от управления платными подписками в приложении и заканчивая взломом на сетевом уровне и хуже.

А теперь о том, как нужно хранить данные.

Запомните, в UserDefaults следует хранить только небольшой объём информации, такой как настройки внутри приложения, то есть данные, которые не являются конфиденциальными для пользователя.

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

API сервис Keychain


Ниже приведен пример того, как можно сохранить пароль пользователя в Связке ключей.

class KeychainService {
	    func save(_ password: String, for account: String) {
	        let password = password.data(using: String.Encoding.utf8)!
	        let query: [String: Any] = [kSecClass as String: kSecClassGenericPassword,
	                                    kSecAttrAccount as String: account,
	                                    kSecValueData as String: password]
	        let status = SecItemAdd(query as CFDictionary, nil)
	        guard status == errSecSuccess else { return print("save error")
	    }
	}

Часть словаря kSecClass:kSecClassGenericPassword означает что информацией, нуждающейся в шифровании является пароль. Затем мы добавляем новый пароль в связку ключей вызывая метод SecItemAdd. Получение данных из связки аналогично сохранению.

func retrivePassword(for account: String) -> String? {
	    let query: [String: Any] = [kSecClass as String: kSecClassGenericPassword,
	                                kSecAttrAccount as String: account,
	                                kSecMatchLimit as String: kSecMatchLimitOne,
	                                kSecReturnData as String: kCFBooleanTrue] 
	    
	    var retrivedData: AnyObject? = nil
	    let _ = SecItemCopyMatching(query as CFDictionary, &retrivedData) 
	    
	    guard let data = retrivedData as? Data else {return nil}
	    return String(data: data, encoding: String.Encoding.utf8)
	}

Давайте напишем небольшую проверку корректности сохранения и получения данных.

func testPaswordRetrive() {
	    let  password = "123456"
	    let account = "User"
	    keyChainService.save(password, for: account)
	    XCTAssertEqual(keyChainService.retrivePassword(for: account), password)
	}

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

Если хотите узнать больше про этот паттерн, а также о том, как создать простенькую обертку для сложных подсистем, тогда вот эта статья поможет вам. Так же в интернете полно открытых библиотек, помогающих использовать Keychain API, например, SAMKeychain и SwiftKeychainWrapper.

Сохранение пароля и Авторизация


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

Если вы храните данные в UserDefault, то прочитав информацию из первой части статьи, вы уже понимаете, как сильно вы рискуете. Сохраняя пароли в Связке ключей вы серьезно повышаете уровень безопасности вашего приложения, но опять же, прежде чем сохранять где-либо конфиденциальную информацию, нужно её предварительно зашифровать.

Предположим, что некий хакер может атаковать нас через нашу сеть. Таким образом он получит пароли в виде сырого текста. Лучше, конечно, хэшировать все пароли.

Шифрование личных данных


Хэширование может показаться чересчур сложным занятием, если делать все самому, поэтому в данной статье мы воспользуемся библиотекой CryptoSwift. В ней собрано много стандартных надежных алгоритмов шифрования, применяемых в Swift.

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

func saveEncryptedPassword(_ password: String, for account: String) {
	    let salt = Array("salty".utf8)
	    let key = try! HKDF(password: Array(password.utf8), salt: salt, variant: .sha256).calculate().toHexString()
	    keychainService.save(key, for: account)
	}

Приведенная выше функция записывает логин и пароль и сохраняет их в Keychain в виде зашифрованной строки.

Давайте разберемся, что же происходит внутри:

— В переменную salt записываются логин и пароль в виде строки
— sha256 заполняет хэш SHA-2
— HKDF это функция формирования ключа (KDF), основанная на коде аутентификации сообщений (HMAC)

Мы создали переменную salt для того, чтобы усложнить хакерам задачу. Мы могли бы зашифровать только пароль, но в таком случае у злоумышленника может быть список самых часто используемых паролей, он без проблем зашифрует их и сравнит с нашим зашифрованным паролем. После чего найти пароль к конкретному аккаунту не составит труда.
Теперь же мы можем авторизоваться, используя наш аккаунт и созданный ключ.

authManager.login(key, user)

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

В качестве завершения


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

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

Комментарии (6)


  1. agee
    20.11.2018 22:44

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


    Судя по всему Вы тоже просто прочли оригинальную статью и поверили ей на слово.


    1.


    Разработчики… создают запрос на сервер, который напрямую посылает логин и пароль.

    Автор будет чрезвычайно удивлен, узнав, что это… не влияет ни на что! Хэширование, о котором он говорит, должно происходить на сервере при сохранении паролей в базу. И необходимо оно для того, чтобы злые хацкеры не смогли восстановить пароль, если они взломают сервак.


    При посылке пароля, как и всех остальных запросов можно и нужно (сорвем же покровы!) использовать TLS. Нет разницы, будете вы посылать пароль в чистом или хэшированом виде. Если канал будет скомпрометирован, то человечек посередине в любом случае его перехватит и получит доступ к вашему аккаунту, хэшируй ты этот пароль на клиенте хоть миллиард раз.


    2.


    HKDF это функция формирования ключа

    Почему использовался HKDF, а не PBKDF2, не scrypt? Просто в образовательных целях: это функции формирования ключа шифрования. То есть прямое назначение этих функций — это из короткого и неслучайного массива байт сделать более длинный и выглядящий как случайный. И "соль" играет здесь огромную роль.


    3.


    let salt = Array("salty".utf8)
    сервер должен знать, что зашифровано в нашей переменной salt. Бэкэнд сможет сравнить ключи используя тот же алгоритм, чтобы идентифицировать пользователя.

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


    f(s, P, a) -> k

    P — это пароль. Секретное, но НЕ случайное значение.
    s — соль. НЕ секретное, но псевдослучайное значение.
    a — алгоритм хэширования, используемый внутри функции формирования ключа.
    k — псевдослучайное значение на выходе.


    Обатите внимание, что функция вернет псевдослучайное значение, если ну хотя бы один из ее параметров будет псевдослучайным. Откуда эта случайность вообще появится, если все ее параметры — это "salty" и (допустим) "mysuperpass"? Когда соль случайна, то на выходе каждый раз будет новое значение, даже для одних и тех же паролей.


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


    Как же сгенерировать по-настоящему псевдослучайное значение? Нужно где-то взять генератор, сгенерить это значение определенной длины, как того требует выбранный Вами алгоритм. Автор использует какой-то CryptoSwift? Что ж посмотрим, где у него там генератор, заходим на https://github.com/krzyzanowskim/CryptoSwift, на главной странице нет ничего про это. Смотрим исходники, видим файл RandomBytesSequence.swift, смотрим содержимое:


    struct RandomBytesSequence: Sequence {
        let size: Int
    
        func makeIterator() -> AnyIterator<UInt8> {
            var count = 0
            return AnyIterator<UInt8>.init { () -> UInt8? in
    //...
                #if os(Linux) || os(Android) || os(FreeBSD)
    //...
                #else
                    return UInt8(arc4random_uniform(UInt32(UInt8.max) + 1))
                #endif
            }
        }
    }

    Я даже не знаю, стоит ли этот arc4random_uniform комментировать. Использование? Например, файл Cryptors.swift:


    extension Cryptors {
        public static func randomIV(_ blockSize: Int) -> Array<UInt8> {
            var randomIV: Array<UInt8> = Array<UInt8>()
            randomIV.reserveCapacity(blockSize)
            for randomByte in RandomBytesSequence(size: blockSize) {
                randomIV.append(randomByte)
            }
            return randomIV
        }

    Вот такой вот у нас "рэндомный" IV получился.
    Не буду вдаваться в подробности, просто не используйте это либу никогда и ни за что. Почитайте лучше про CSPRNG.


    4.
    Ну и финальное. Если Вы будете хранить пароль в Keychain, этого будет вполне достаточно, чтобы знать, что он уже зашифрован. Эппловская "свзяка ключей" как раз и использует KDF для того, чтобы зашифровать ВСЕ ваши данные, которые вы в этой связке храните.


    Поэтому все действия, описанные в этой статье (кроме того, что не нужно хранить пароли в NSUserDefaults), лишние. И, судя по всему, даже вредные.


    1. kosyakus Автор
      21.11.2018 01:58

      Ого, спасибо большое за комментарий, очень интересно! Я действительно переводила статью потому что она мне показалась достаточно понятной начинающим разработчикам (как я), но в который раз убеждаюсь, что копать нужно гораздо глубже и каждый момент разбирать тщательнее.


    1. Funbit
      21.11.2018 09:41

      По поводу п.1 позвольте не согласиться, разница все же есть. Многие используют один и тот же пароль на нескольких сервисах, и если кто-то взломает конкретный TLS канал и узнает пароль в открытом виде — это даёт хакеру потенциальную возможность зайти с этим же паролем в другие сервисы.


    1. GDXRepo
      21.11.2018 09:53

      Про пункт 1 не соглашусь, как и Funbit. Все действия по шифрованию передаваемых данных будут «лишними» только при условии 100% гарантии, что канал передачи данных не взломан. А если MITM? А если подмена сертификатов? В случае отсутствия дополнительных ступеней защиты, после взлома канала передачи данных все данные будут скомпрометированы. А вот если данные зашифрованы, то вскрытие канала взломщику не то чтобы прям сильно поможет. Безопасности много не бывает, чем больше замков на двери — тем меньше шанс, что ее вскроют за разумное время и деньги.


      1. agee
        21.11.2018 18:17

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


  1. agee
    20.11.2018 23:30

    Upd: оказывается, arc4random_uniform кое-где заявляется как все еще криптографически стойкий, так что его использование — наверное не такая уж и дикая ошибка. В любом случае, в iOS для таких целей использование SecRandomCopyBytes является рекомендованным.