Статьи

Защита связи на iOS

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

При разработке приложения рассмотрите возможность ограничения сетевых запросов теми, которые необходимы. Для этих запросов убедитесь, что они выполняются по HTTPS, а не по HTTP — это поможет защитить данные вашего пользователя от «атак посредника», когда другой компьютер в сети выступает в роли ретранслятора для вашего соединения, но прослушивает или изменяет данные, которые он передает. В последние несколько лет наблюдается тенденция к созданию всех соединений через HTTPS. К счастью для нас, новые версии Xcode уже применяют это.

Чтобы создать простой запрос HTTPS на iOS, все, что нам нужно сделать, это добавить « s » в раздел « http » URL. Пока хост поддерживает HTTPS и имеет действительные сертификаты, мы получим безопасное соединение. Это работает для таких API, как URLSession , NSURLConnection и CFNetwork, а также для популярных сторонних библиотек, таких как AFNetworking .

На протяжении многих лет HTTPS имел несколько атак на него. Поскольку важно правильно настроить HTTPS, Apple создала App Transport Security (для краткости ATS). ATS гарантирует, что сетевые соединения вашего приложения используют стандартные отраслевые протоколы, чтобы вы случайно не отправили данные пользователя небезопасно. Хорошей новостью является то, что ATS по умолчанию включен для приложений, созданных с использованием текущих версий XCode.

ATS доступен начиная с iOS 9 и OS X El Capitan. Текущие приложения в магазине не будут внезапно требовать ATS, но приложения, созданные на основе более новых версий Xcode и его SDK, будут включать его по умолчанию. Некоторые из лучших практик, применяемых ATS, включают использование TLS версии 1.2 или выше, прямую секретность посредством обмена ключами ECDHE, шифрование AES-128 и использование как минимум сертификатов SHA-2.

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

Поскольку ATS обеспечивает использование HTTPS и других безопасных протоколов, вы можете задаться вопросом, сможете ли вы по-прежнему устанавливать сетевые подключения, которые не поддерживают HTTPS, например при загрузке изображений из кэша CDN. Не волнуйтесь, вы можете управлять настройками ATS для определенных доменов в файле plist вашего проекта. В Xcode найдите файл info.plist , щелкните его правой кнопкой мыши и выберите « Открыть как»> « Исходный код» .

Вы найдете раздел NSAppTransportSecurity . Если его там нет, вы можете добавить код самостоятельно; формат выглядит следующим образом.

01
02
03
04
05
06
07
08
09
10
11
12
13
<key>NSAppTransportSecurity</key>
<dict>
    <key>NSExceptionDomains</key>
    <dict>
        <key>yourdomain.com</key>
        <dict>
            <key>NSIncludesSubdomains</key>
            <true/>
            <key>NSThirdPartyExceptionRequiresForwardSecrecy</key>
            <false/>
        </dict>
    </dict>
</dict>

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

  • NSAllowsArbitraryLoads : отключает ATS. Не используйте это! В следующих версиях Xcode этот ключ будет удален.
  • NSAllowsArbitraryLoadsForMedia : NSAllowsArbitraryLoadsForMedia загрузку мультимедиа без ограничений ATS для платформы AV Foundation. Вы должны разрешать небезопасную загрузку, только если ваш носитель уже зашифрован другим способом. (Доступно в iOS 10 и macOS 10.12.)
  • NSAllowsArbitraryLoadsInWebContent : Может использоваться для отключения ограничений ATS для объектов веб-просмотра в вашем приложении. Прежде чем отключать это, подумайте, так как это позволяет пользователям загружать произвольный небезопасный контент в ваше приложение. (Доступно в iOS 10 и macOS 10.12.)
  • NSAllowsLocalNetworking : это можно использовать для загрузки ресурсов локальной сети без ограничений ATS. (Доступно в iOS 10 и macOS 10.12.)

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

  • NSExceptionAllowsInsecureHTTPLoads : позволяет конкретному домену использовать не HTTPS-соединения.
  • NSIncludesSubdomains : Указывает, NSIncludesSubdomains ли текущие правила субдоменам.
  • NSExceptionMinimumTLSVersion : Используется для указания более старых, менее безопасных версий TLS, которые разрешены.

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

Для предотвращения такого рода компрометации Perfect Forward Secrecy (PFS) генерирует сеансовый ключ это уникально для каждого сеанса связи. Если ключ для определенного сеанса скомпрометирован, он не скомпрометирует данные других сеансов. ATS реализует PFS по умолчанию, и вы можете управлять этой функцией, используя NSExceptionRequiresForwardSecrecy ключ NSExceptionRequiresForwardSecrecy . Отключение этого параметра позволит использовать шифры TLS, которые не поддерживают идеальную секретность пересылки.

Прозрачность сертификатов — это новый стандарт, предназначенный для проверки или аудита сертификатов, представленных во время установки HTTPS-соединения.

Когда ваш хост устанавливает HTTPS-сертификат, он выдается так называемым центром сертификации (CA). Прозрачность сертификатов направлена ​​на то, чтобы обеспечить мониторинг в режиме реального времени, чтобы выяснить, был ли сертификат выдан злонамеренно или был скомпрометирован центром сертификации.

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

Ключом NSRequiresCertificateTransparency для этой функции является NSRequiresCertificateTransparency . Включение этого параметра обеспечит прозрачность сертификатов. Это доступно на iOS 10 и macOS 10.12 и более поздних версиях.

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

Когда HTTPS-соединение установлено, эти сертификаты представляются клиенту. Эта цепочка доверия оценивается, чтобы убедиться, что сертификаты правильно подписаны центром сертификации, которому iOS уже доверяет. (Есть способы обойти эту проверку и принять собственный самозаверяющий сертификат для тестирования, но не делайте этого в производственной среде.)

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

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

Прикрепление сертификата приходит на помощь, сверяя сертификат сервера с копией ожидаемого сертификата.

Чтобы реализовать закрепление, должен быть реализован следующий делегат. Для URLSession используйте следующее:

1
2
3
optional func urlSession(_ session: URLSession,
             didReceive challenge: URLAuthenticationChallenge,
      completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void)

Или для NSURLConnection , вы можете использовать:

1
2
optional func connection(_ connection: NSURLConnection,
             didReceive challenge: URLAuthenticationChallenge)

Оба метода позволяют вам получить объект SecTrust из challenge.protectionSpace.serverTrust . Поскольку мы переопределяем делегаты аутентификации, мы теперь должны явно вызвать функцию, которая выполняет стандартные проверки цепочки сертификатов, которые мы только что обсудили. Сделайте это, вызвав функцию SecTrustEvaluate . Затем мы можем сравнить сертификат сервера с ожидаемым.

Вот пример реализации.

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
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
import Foundation
import Security
 
class URLSessionPinningDelegate: NSObject, URLSessionDelegate
{
    func urlSession(_ session: URLSession, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Swift.Void)
    {
        var success: Bool = false
        if let serverTrust = challenge.protectionSpace.serverTrust
        {
            if (challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust)
            {
                //Set policy to validate domain
                let policy: SecPolicy = SecPolicyCreateSSL(true, «yourdomain.com» as CFString)
                let policies = NSArray.init(object: policy)
                SecTrustSetPolicies(serverTrust, policies)
                 
                let certificateCount: CFIndex = SecTrustGetCertificateCount(serverTrust)
                if certificateCount > 0
                {
                    if let certificate = SecTrustGetCertificateAtIndex(serverTrust, 0)
                    {
                        let serverCertificateData = SecCertificateCopyData(certificate) as NSData
                         
                        //for loop over array which may contain expired + upcoming certificate
                        let certFilenames: [String] = [«CertificateRenewed», «Certificate»]
                        for filenameString: String in certFilenames
                        {
                            let filePath = Bundle.main.path(forResource: filenameString, ofType: «cer»)
                            if let file = filePath
                            {
                                if let localCertData = NSData(contentsOfFile: file)
                                {
                                    //Set anchor cert to your own server
                                    if let localCert: SecCertificate = SecCertificateCreateWithData(nil, localCertData)
                                    {
                                        let certArray = [localCert] as CFArray
                                        SecTrustSetAnchorCertificates(serverTrust, certArray)
                                    }
                                     
                                    //validates a certificate by verifying its signature plus the signatures of the certificates in its certificate chain, up to the anchor certificate
                                    var result = SecTrustResultType.invalid
                                    SecTrustEvaluate(serverTrust, &result);
                                    let isValid: Bool = (result == SecTrustResultType.unspecified || result == SecTrustResultType.proceed)
                                    if (isValid)
                                    {
                                        //Validate host certificate against pinned certificate.
                                        if serverCertificateData.isEqual(to: localCertData as Data)
                                        {
                                            success = true
                                            completionHandler(.useCredential, URLCredential(trust:serverTrust))
                                            break //found a successful certificate, don’t need to continue looping
                                        } //end if serverCertificateData.isEqual(to: localCertData as Data)
                                    } //end if (isValid)
                                } //end if let localCertData = NSData(contentsOfFile: file)
                            } //end if let file = filePath
                        } //end for filenameString: String in certFilenames
                    } //end if let certificate = SecTrustGetCertificateAtIndex(serverTrust, 0)
                } //end if certificateCount > 0
            } //end if (challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust)
        } //end if let serverTrust = challenge.protectionSpace.serverTrust
         
        if (success == false)
        {
            completionHandler(.cancelAuthenticationChallenge, nil)
        }
    }
}

Чтобы использовать этот код, установите делегата URLSession при создании вашего соединения.

01
02
03
04
05
06
07
08
09
10
11
12
13
if let url = NSURL(string: «https://yourdomain.com»)
{
    let session = URLSession(
        configuration: URLSessionConfiguration.ephemeral,
        delegate: URLSessionPinningDelegate(),
        delegateQueue: nil)
     
    let dataTask = session.dataTask(with: url as URL, completionHandler: { (data, response, error) -> Void in
        //…
    })
     
    dataTask.resume()
}

Убедитесь, что сертификат включен в комплект вашего приложения. Если ваш сертификат представляет собой файл .pem, вам необходимо преобразовать его в файл .cer в терминале macOS:

openssl x509 -inform PEM -in mycert.pem -outform DER -out certificate.cer

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

Обратите внимание, что некоторые сторонние библиотеки, такие как AFNetworking, уже поддерживают закрепление.

С учетом всех средств защиты ваши соединения должны быть достаточно безопасными для атак посредников. Тем не менее, одно важное правило, касающееся сетевых коммуникаций, никогда не должно слепо доверять данным, которые вы получаете. На самом деле, это хорошая практика программирования для проектирования по контракту.
входы и выходы ваших методов имеют контракт, который определяет конкретные ожидания интерфейса; если интерфейс сообщает, что вернет номер NSNumber , он должен это сделать. Если ваш сервер ожидает строку из 24 символов или менее, убедитесь, что интерфейс вернет только до 24 символов.

Это помогает предотвратить невинные ошибки, но, что более важно, оно также может снизить вероятность различных атак внедрения и повреждения памяти. Обычные синтаксические анализаторы, такие как класс JSONSerialization , преобразуют текст в типы данных Swift, где можно выполнять такие тесты.

1
2
3
4
5
if let dictionary = json as?
               {
                   if let count = dictionary[«count»] as?
                   {
                       //…

Другие парсеры могут работать с эквивалентными объектами Objective-C. Вот способ проверить, что объект имеет ожидаемый тип в Swift.

1
if someObject is NSArray

Прежде чем отправить делегату метод, убедитесь, что объект имеет правильный тип, чтобы он отвечал на метод — в противном случае приложение завершится с ошибкой «нераспознанный селектор».

1
if someObject.responds(to: #selector(getter: NSNumber.intValue)

Кроме того, вы можете увидеть, соответствует ли объект протоколу, прежде чем пытаться отправлять ему сообщения:

1
if someObject.conforms(to: MyProtocol.self)

Или вы можете проверить, что он соответствует типу объекта Core Foundation.

1
if CFGetTypeID(someObject) != CFNullGetTypeID()

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

Кроме того, убедитесь, что вы кодируете свои URL, чтобы они содержали только допустимые символы. Будет работать stringByAddingPercentEscapesUsingEncoding NSString stringByAddingPercentEscapesUsingEncoding . Он не кодирует некоторые символы, такие как амперсанды и знаки плюс, но функция CFURLCreateStringByAddingPercentEscapes позволяет настраивать то, что будет кодироваться.

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

1
2
3
4
5
6
var mutableString: String = string
mutableString = mutableString.replacingOccurrences(of: «%», with: «»)
mutableString = mutableString.replacingOccurrences(of: «\»», with: «»)
mutableString = mutableString.replacingOccurrences(of: «\’», with: «»)
mutableString = mutableString.replacingOccurrences(of: «\t», with: «»)
mutableString = mutableString.replacingOccurrences(of: «\n», with: «»)

Хорошей практикой является ограничение длины пользовательского ввода. Мы можем ограничить количество символов, набираемых в текстовом поле, установив UITextField , а затем реализовав его shouldChangeCharactersInRange делегата shouldChangeCharactersInRange .

01
02
03
04
05
06
07
08
09
10
11
12
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool
{
    let newLength: Int = textField.text!.characters.count + string.characters.count — range.length
    if newLength > maxSearchLength
    {
        return false
    }
    else
    {
        return true
    }
}

Для UITextView метод делегата, который реализует это:

1
2
3
optional func textField(_ textField: UITextField,
shouldChangeCharactersIn range: NSRange,
      replacementString string: String) -> Bool

Пользовательский ввод может быть дополнительно проверен, так что ввод имеет ожидаемый формат. Например, если пользователь хочет ввести адрес электронной почты, мы можем проверить действительный адрес:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
class func validateEmail(from emailString: String, useStrictValidation isStrict: Bool) -> Bool
{
    var filterString: String?
    if isStrict
    {
        filterString = «[A-Z0-9a-z._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,4}»
    }
    else
    {
        filterString = «.+@.+\\.[A-Za-z]{2}[A-Za-z]*»
    }
    let emailPredicate = NSPredicate(format: «SELF MATCHES %@», filterString!)
    return emailPredicate.evaluate(with: emailString)
}

Если пользователь загружает изображение на сервер, мы можем проверить, является ли оно действительным изображением. Например, для файла JPEG первые два байта и два последних байта всегда FF D8 и FF D9.

01
02
03
04
05
06
07
08
09
10
11
12
class func validateImageData(_ data: Data) -> Bool
{
    let totalBytes: Int = data.count
    if totalBytes < 12
    {
        return false
    }
         
    let bytes = [UInt8](data)
    let isValid: Bool = (bytes[0] == UInt8(0xff) && bytes[1] == UInt8(0xd8) && bytes[totalBytes — 2] == UInt8(0xff) && bytes[totalBytes — 1] == UInt8(0xd9))
    return isValid
}

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

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

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

1
URLCache.shared.removeAllCachedResponses()

Чтобы отключить кэширование на глобальном уровне, используйте:

1
2
let theURLCache = URLCache(memoryCapacity: 0, diskCapacity: 0, diskPath: nil)
URLCache.shared = theURLCache

И если вы используете URLSession , вы можете отключить кеш для сессии следующим образом:

1
2
3
4
let configuration = URLSessionConfiguration.default
configuration.requestCachePolicy = .reloadIgnoringLocalCacheData
configuration.urlCache = nil
let session = URLSession.init(configuration: configuration)

Если вы используете объект NSURLConnection с делегатом, вы можете отключить кэш для каждого соединения с помощью этого метода делегата:

1
2
3
4
func connection(_ connection: NSURLConnection, willCacheResponse cachedResponse: CachedURLResponse) -> CachedURLResponse?
{
    return nil
}

И для создания запроса URL, который не будет проверять кэш, используйте:

1
var request = NSMutableURLRequest(url: theUrl, cachePolicy: .reloadIgnoringLocalCacheData, timeoutInterval: urlTimeoutTime)

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

Важно понимать ограничения HTTPS для защиты сетевых коммуникаций.

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

Я не могу закончить эту статью, не охватив еще одну концепцию, которая является недавней тенденцией — так называемое «сквозное шифрование». Хорошим примером является зашифрованное приложение чата, в котором два мобильных устройства взаимодействуют друг с другом через сервер. Два устройства создают открытый и закрытый ключи — они обмениваются открытыми ключами, в то время как их закрытые ключи никогда не покидают устройство. Данные по-прежнему отправляются через HTTPS через сервер, но сначала они шифруются открытым ключом другой стороны таким образом, что только устройства, хранящие закрытые ключи, могут дешифровать сообщения друг друга.

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

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

Если вы хотите узнать больше об этом подходе, начните с репозитория GitHub для Open Whisper System , проекта с открытым исходным кодом.

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

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

И пока вы здесь, ознакомьтесь с некоторыми из наших популярных учебных пособий и курсов по iOS-приложениям!

  • iOS 10
    Использование API распознавания речи в iOS 10
    Патрик Балестра
  • iOS SDK
    Как использовать Apple CloudKit для push-уведомлений
    Дэвис Элли
  • Мобильная разработка
    Back-End как сервис для мобильных приложений
    Сандамаль Сирипати
  • IOS
    Идите дальше со Swift: анимация, работа в сети и пользовательские элементы управления
    Маркус Мюльбергер