Статьи

Защита данных iOS в состоянии покоя: шифрование

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

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

  • iOS SDK
    Защита данных iOS в покое: брелок
    Коллин Штюрт

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

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

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

AES — это стандарт, который шифрует данные с помощью ключа. Тот же ключ, который используется для шифрования данных, используется для дешифрования данных. Имеются ключи разных размеров, и AES256 (256 бит) является предпочтительной длиной для использования с конфиденциальными данными.

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

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

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

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

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

Очень распространенная ошибка в шифровании AES — использование пароля пользователя непосредственно в качестве ключа шифрования. Что если пользователь решит использовать общий или слабый пароль? Как заставить пользователей использовать случайный и достаточно сильный (достаточно энтропийный) ключ для шифрования, а затем заставить их запомнить его?

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

Без соли можно использовать словарь слов для определения общих ключей, которые затем можно использовать для атаки на пользовательские данные. Это называется «атака по словарю». Для этого используются таблицы с общими ключами, которые соответствуют несоленным паролям. Они называются «радужными столами».

Еще одна ловушка при создании соли — использование функции генерации случайных чисел, которая не была разработана для безопасности. Примером является функция rand() в C, доступ к которой можно получить из Swift. Этот результат может оказаться очень предсказуемым!

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

Чтобы использовать код, вам нужно добавить следующее в заголовок моста:
#import <CommonCrypto/CommonCrypto.h>

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

1
2
3
4
var salt = Data(count: 8)
salt.withUnsafeMutableBytes { (saltBytes: UnsafeMutablePointer<UInt8>) -> Void in
    let saltStatus = SecRandomCopyBytes(kSecRandomDefault, salt.count, saltBytes)
    //…

Теперь мы готовы сделать растяжение ключа. К счастью, у нас уже есть функция для фактического растяжения: функция получения ключа на основе пароля ( PBKDF2 ). PBKDF2 выполняет функцию много раз для получения ключа; Увеличение количества итераций увеличивает время, необходимое для работы с набором ключей во время атаки методом перебора. Для генерации вашего ключа рекомендуется использовать PBKDF2.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
var setupSuccess = true
var key = Data(repeating:0, count:kCCKeySizeAES256)
var salt = Data(count: 8)
salt.withUnsafeMutableBytes { (saltBytes: UnsafeMutablePointer<UInt8>) -> Void in
    let saltStatus = SecRandomCopyBytes(kSecRandomDefault, salt.count, saltBytes)
    if saltStatus == errSecSuccess
    {
        let passwordData = password.data(using:String.Encoding.utf8)!
        key.withUnsafeMutableBytes { (keyBytes : UnsafeMutablePointer<UInt8>) in
            let derivationStatus = CCKeyDerivationPBKDF(CCPBKDFAlgorithm(kCCPBKDF2), password, passwordData.count, saltBytes, salt.count, CCPseudoRandomAlgorithm(kCCPRFHmacAlgSHA512), 14271, keyBytes, key.count)
            if derivationStatus != Int32(kCCSuccess)
            {
                setupSuccess = false
            }
        }
    }
    else
    {
        setupSuccess = false
    }
}

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

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

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

Теперь, когда у нас есть ключ, давайте зашифруем некоторые данные. Существуют разные режимы шифрования, но мы будем использовать рекомендуемый режим: цепочка блоков шифрования (CBC). Это работает с нашими данными по одному блоку за раз.

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

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

Мы будем использовать ту же безопасную функцию SecRandomCopyBytes для создания IV.

1
2
3
4
5
6
7
8
var iv = Data.init(count: kCCBlockSizeAES128)
iv.withUnsafeMutableBytes { (ivBytes : UnsafeMutablePointer<UInt8>) in
    let ivStatus = SecRandomCopyBytes(kSecRandomDefault, kCCBlockSizeAES128, ivBytes)
    if ivStatus != errSecSuccess
    {
        setupSuccess = false
    }
}

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

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

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
69
class func encryptData(_ clearTextData : Data, withPassword password : String) -> Dictionary<String, Data>
{
    var setupSuccess = true
    var outDictionary = Dictionary<String, Data>.init()
    var key = Data(repeating:0, count:kCCKeySizeAES256)
    var salt = Data(count: 8)
    salt.withUnsafeMutableBytes { (saltBytes: UnsafeMutablePointer<UInt8>) -> Void in
        let saltStatus = SecRandomCopyBytes(kSecRandomDefault, salt.count, saltBytes)
        if saltStatus == errSecSuccess
        {
            let passwordData = password.data(using:String.Encoding.utf8)!
            key.withUnsafeMutableBytes { (keyBytes : UnsafeMutablePointer<UInt8>) in
                let derivationStatus = CCKeyDerivationPBKDF(CCPBKDFAlgorithm(kCCPBKDF2), password, passwordData.count, saltBytes, salt.count, CCPseudoRandomAlgorithm(kCCPRFHmacAlgSHA512), 14271, keyBytes, key.count)
                if derivationStatus != Int32(kCCSuccess)
                {
                    setupSuccess = false
                }
            }
        }
        else
        {
            setupSuccess = false
        }
    }
     
    var iv = Data.init(count: kCCBlockSizeAES128)
    iv.withUnsafeMutableBytes { (ivBytes : UnsafeMutablePointer<UInt8>) in
        let ivStatus = SecRandomCopyBytes(kSecRandomDefault, kCCBlockSizeAES128, ivBytes)
        if ivStatus != errSecSuccess
        {
            setupSuccess = false
        }
    }
     
    if (setupSuccess)
    {
        var numberOfBytesEncrypted : size_t = 0
        let size = clearTextData.count + kCCBlockSizeAES128
        var encrypted = Data.init(count: size)
        let cryptStatus = iv.withUnsafeBytes {ivBytes in
            encrypted.withUnsafeMutableBytes {encryptedBytes in
            clearTextData.withUnsafeBytes {clearTextBytes in
                key.withUnsafeBytes {keyBytes in
                    CCCrypt(CCOperation(kCCEncrypt),
                            CCAlgorithm(kCCAlgorithmAES),
                            CCOptions(kCCOptionPKCS7Padding),
                            keyBytes,
                            key.count,
                            ivBytes,
                            clearTextBytes,
                            clearTextData.count,
                            encryptedBytes,
                            size,
                            &numberOfBytesEncrypted)
                    }
                }
            }
        }
        if cryptStatus == Int32(kCCSuccess)
        {
            encrypted.count = numberOfBytesEncrypted
            outDictionary[«EncryptionData»] = encrypted
            outDictionary[«EncryptionIV»] = iv
            outDictionary[«EncryptionSalt»] = salt
        }
    }
 
    return outDictionary;
}

И вот код расшифровки:

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
class func decryp(fromDictionary dictionary : Dictionary<String, Data>, withPassword password : String) -> Data
{
    var setupSuccess = true
    let encrypted = dictionary[«EncryptionData»]
    let iv = dictionary[«EncryptionIV»]
    let salt = dictionary[«EncryptionSalt»]
    var key = Data(repeating:0, count:kCCKeySizeAES256)
    salt?.withUnsafeBytes { (saltBytes: UnsafePointer<UInt8>) -> Void in
        let passwordData = password.data(using:String.Encoding.utf8)!
        key.withUnsafeMutableBytes { (keyBytes : UnsafeMutablePointer<UInt8>) in
            let derivationStatus = CCKeyDerivationPBKDF(CCPBKDFAlgorithm(kCCPBKDF2), password, passwordData.count, saltBytes, salt!.count, CCPseudoRandomAlgorithm(kCCPRFHmacAlgSHA512), 14271, keyBytes, key.count)
            if derivationStatus != Int32(kCCSuccess)
            {
                setupSuccess = false
            }
        }
    }
     
    var decryptSuccess = false
    let size = (encrypted?.count)!
    var clearTextData = Data.init(count: size)
    if (setupSuccess)
    {
        var numberOfBytesDecrypted : size_t = 0
        let cryptStatus = iv?.withUnsafeBytes {ivBytes in
            clearTextData.withUnsafeMutableBytes {clearTextBytes in
            encrypted?.withUnsafeBytes {encryptedBytes in
                key.withUnsafeBytes {keyBytes in
                    CCCrypt(CCOperation(kCCDecrypt),
                            CCAlgorithm(kCCAlgorithmAES128),
                            CCOptions(kCCOptionPKCS7Padding),
                            keyBytes,
                            key.count,
                            ivBytes,
                            encryptedBytes,
                            (encrypted?.count)!,
                            clearTextBytes,
                            size,
                            &numberOfBytesDecrypted)
                    }
                }
            }
        }
        if cryptStatus!
        {
            clearTextData.count = numberOfBytesDecrypted
            decryptSuccess = true
        }
    }
     
    return decryptSuccess ?
}

Наконец, вот тест, чтобы убедиться, что данные шифруются правильно после шифрования:

1
2
3
4
5
6
7
8
class func encryptionTest()
{
    let clearTextData = «some clear text to encrypt».data(using:String.Encoding.utf8)!
    let dictionary = encryptData(clearTextData, withPassword: «123456»)
    let decrypted = decryp(fromDictionary: dictionary, withPassword: «123456»)
    let decryptedString = String(data: decrypted, encoding: String.Encoding.utf8)
    print(«decrypted cleartext result — «, decryptedString ?? «Error: Could not convert data to string»)
}

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

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

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

Все затронутые темы используют каркасы Apple. Я оставлю вам идею подумать. Что происходит при атаке библиотеки шифрования Apple?

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

Тем не менее, статическая библиотека, которая связана с двоичным файлом вашего приложения, защищена от такого рода атак, потому что если вы попытаетесь ее исправить, то в конечном итоге вы измените двоичный файл приложения. Это нарушит сигнатуру кода приложения и не позволит запустить его. Если вы импортируете и используете, например, OpenSSL для шифрования, ваше приложение не будет уязвимо для широко распространенной атаки Apple API. Вы можете самостоятельно скомпилировать OpenSSL и статически связать его с вашим приложением.

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

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

  • Безопасность
    Как взломать ваше собственное приложение
    Таня Янка
  • IOS
    Идите дальше со Swift: анимация, работа в сети и пользовательские элементы управления
    Маркус Мюльбергер
  • стриж
    Swift From Scratch: Делегирование и Свойства
    Барт Джейкобс