Перейти к содержанию

Небезопасный класс защиты данных для элемента KeyChain

Критичность: ИНФО
Способ обнаружения: DAST, SENSITIVE INFO

Описание

KeyChain — небольшое защищенное хранилище данных, которое предоставляет операционная система iOS. Фактически представляет собой SQLite-базу с зашифрованными данными (/private/var/Keychains/keychain-2.db) и является одним из рекомендуемых способов хранения конфиденциальной информации пользователя на устройстве. Несмотря на то, что KeyChain зашифровано, есть несколько способов получения его содержимого с устройства:

  1. Зашифрованный локальный бэкап устройства. При установке пароля при создании локальной резервной копии устройства в него также попадают и элементы KeyChain, которые хранятся на устройстве, в том числе и данные приложений.
  2. Jailbreak. При возможности осуществить процедуру Jailbreak благодаря специальным инструментам появляется возможность просмотреть и выгрузить все элементы KeyChain в открытом виде.
  3. Облачное хранилище KeyChain. Благодаря опции синхронизации элементов хранилища между несколькими устройствами, например между компьютером под управлением MacOS и устройством с iOS, данные попадают в облачные хранилища, а при компрометации последних — в руки злоумышленников.
  4. Данные KeyChain не очищаются после удаления приложения. При удалении приложения данные KeyChain сохраняются, в отличие от информации, находящейся в «песочнице» приложения (файловой системе). Если пользователь продаст устройство без сброса к заводским настройкам, покупатель может получить доступ к учетным записям приложений и данным предыдущего пользователя, просто установив приложение и посмотрев, что оно хранит в KeyChain (при условии, что разработчик не имплементировал очистку хранилища после переустановки приложения).

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

Уровень защиты данных в KeyChain Доступность
kSecAttrAccessibleWhenUnlocked Только когда устройство разблокировано
kSecAttrAccessibleWhenUnlockedThisDeviceOnly Только когда устройство разблокировано (только для заданного устройства)
kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly Когда задан пароль для разблокировки (только для заданного устройства)
kSecAttrAccessibleAfterFirstUnlock Всегда доступно после первой разблокировки устройства
kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly Всегда доступно после первой разблокировки устройства (только для заданного устройства)
kSecAttrAccessibleAlways Когда задан пароль для разблокировки (только для заданного устройства)
kSecAttrAccessibleAlwaysThisDeviceOnly Доступен всегда (только для заданного устройства)

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

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

Флаг Описание
kSecAccessControlDevicePasscode Доступ по код-паролю
kSecAccessControlTouchIDAny Доступ к элементу осуществляется с использованием отпечатков пальцев, зарегистрированных в Touch ID.
Добавление или удаление отпечатка пальца не аннулирует элемент Keychain
kSecAccessControlTouchIDCurrentSet Доступ к элементу осуществляется с использованием отпечатков пальцев, зарегистрированных в Touch ID.
Добавление или удаление отпечатка пальца аннулирует элемент Keychain
kSecAccessControlUserPresence Доступ к элементу осуществляется с использованием отпечатков пальцев, зарегистрированных в Touch ID, или же при вводе код-пароля

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

Именно поэтому не рекомендуется сохранять чувствительные данные пользователя в открытом виде в KeyChain даже при условии его защищенности.

Рекомендации

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

Но, несмотря на эти флаги, перед сохранением данных в KeyChain их рекомендуется шифровать или хэшировать в зависимости от способа дальнейшего использования и необходимости получения исходного значения. При этом для работы с шифрованием логично воспользоваться сервисами Security Enclave. Но есть и другой способ — применение алгоритмов усиления ключа (Key Stretching). Такой подход позволяет получить ключ шифрования из достаточно простого пароля, применяя к нему несколько раз функцию хеширования вместе с солью. Соль — это некая последовательность случайных данных. Распространенной ошибкой является исключение соли из алгоритма. Соль придает ключу намного большую энтропию — без нее намного проще получить/восстановить/подобрать ключ. Тем более без использования соли два одинаковых пароля будут иметь одинаковые значения хэша и, соответственно, одинаковые окончательные значения ключа шифрования.

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

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

#import <CommonCrypto/CommonCrypto.h>

Ниже приведен код, создающий соль.

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

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

PBKDF2

Выполнить процедуру расширения ключа позволит функция его определения на основе пароля (Password-Based Key Derivation Function, PBKDF2). PBKDF2 выполняет функцию усиления при получении ключа в несколько итераций. Обычно это около 10 тысяч итераций. Рост количества итераций увеличивает время, необходимое для успешной атаки с использованием полного перебора (brute force).

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
    }
}

Режимы и вектор инициализации

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

  • Электронная кодовая книга (Electronic Code Book, ECB).
  • Сцепление блоков (Cipher Block Chaining, CBC).
  • Обратная связь по шифротексту (Cipher Feedback, CFB).
  • Обратная связь по выходу (Output Feedback, OFB).
  • Режим счетчика (Counter Mode, CM, CTR).

Режим «электронная кодовая книга» (ECB) — самый простой вариант, при котором все блоки шифруются независимо друг от друга.

Режим «электронная кодовая книга» (ECB)

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

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

Режим сцепления блоков (CBC)

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

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

Чтобы избежать подобных проблем, вводится понятие вектора инициализации (IV).

Initialization vector (IV) — вектор инициализации, представляющий собой произвольное число, которое может применяться вместе с ключом для шифрования данных. Использование IV предотвращает повторение шифрования данных в первом блоке.

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

var iv = Data.init(count: kCCBlockSizeAES128)
iv.withUnsafeMutableBytes { (ivBytes : UnsafeMutablePointer<UInt8>) in
    let ivStatus = SecRandomCopyBytes(kSecRandomDefault, kCCBlockSizeAES128, ivBytes)
    if ivStatus != errSecSuccess
    {
        setupSuccess = false
    }
}

Дополнение (Padding)

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

Этот параметр указывает, каким именно способом необходимо провести дополнение блока меньшей длины. Существуют различные варианты, но предпочтительным методом дополнения блоков шифротекста является PKCS7. В нем значение каждого дополняемого байта устанавливается равным количеству дополняемых байтов. Так, если блок состоит из 12 символов, он будет дополнен до стандартного размера (16 байт) четырьмя байтами — [04, 04, 04, 04]. Если размер блока 15 байт, он будет дополнен одним байтом [01]. Если блок 16 байт, добавляется новый блок состоящий из [16]*16.

Операции шифрования и дешифрования

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

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

Encrypt

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 + kCCModeCBC),
                            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;
}

И, соответственно, функция расшифровки.

Decrypt

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)! + kCCBlockSizeAES128
    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 + kCCModeCBC),
                            keyBytes,
                            key.count,
                            ivBytes,
                            encryptedBytes,
                            (encrypted?.count)!,
                            clearTextBytes,
                            size,
                            &numberOfBytesDecrypted)
                    }
                }
            }
        }
        if cryptStatus! == Int32(kCCSuccess)
        {
            clearTextData.count = numberOfBytesDecrypted
            decryptSuccess = true
        }
    }

    return decryptSuccess ? clearTextData : Data.init(count: 0)
}

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

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")
}

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

Ссылки

  1. https://support.apple.com/ru-ru/guide/security/secb0694df1a/web
  2. https://darthnull.org/security/2018/05/31/secure-enclave-ecies/
  3. https://en.wikipedia.org/wiki/Key_stretching
  4. https://en.wikipedia.org/wiki/PBKDF2
  5. https://en.wikipedia.org/wiki/Padding_(cryptography)#PKCS7
  6. https://habr.com/ru/post/247527/
К началу