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

Доступное на чтение файловое хранилище ключей

Критичность: СРЕДНИЙ
Способ обнаружения: DAST, КЛЮЧЕВАЯ ИНФОРМАЦИЯ

Описание

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

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

Наиболее безопасным вариантом, безусловно является хранение ключей в Keychain.

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

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

Создание нового ключа в Keychain

Для локального шифрования данных достаточно симметричного ключа. Сгенерируйте 256-битный ключ криптографически стойким генератором — на современных iOS для этого удобно использовать SymmetricKey из CryptoKit (внутри он опирается на тот же SecRandomCopyBytes).

import Foundation
import Security
import CryptoKit

// 256-битный симметричный ключ
func computeSymmetricKey() -> SymmetricKey {
    return SymmetricKey(size: .bits256)
}
let secretKey = computeSymmetricKey()

Сохранение нового ключа в Keychain

Сырые байты ключа — это бинарные данные (Data), а не объект SecKey/SecCertificate. Поэтому их нужно класть в атрибут kSecValueData, а не в kSecValueRef (последний ожидает ссылку на ключ или сертификат). Дополнительно задайте класс данных (kSecAttrAccessibleWhenUnlockedThisDeviceOnly), чтобы ключ был доступен только на этом устройстве и только когда оно разблокировано, и не выгружался в резервные копии.

enum KeychainErrors: Error {
    case COULDNOTINSERT
    case COULDNOTREAD
}
func store(key: SymmetricKey, withTag tag: String) throws {
    let keyData = key.withUnsafeBytes { Data($0) }

    // на случай повторного запуска удаляем прежнее значение
    let attributes: [String: Any] = [
        kSecClass as String: kSecClassKey,
        kSecAttrApplicationTag as String: tag
    ]
    SecItemDelete(attributes as CFDictionary)

    let query: [String: Any] = [
        kSecClass as String: kSecClassKey,
        kSecAttrApplicationTag as String: tag,
        kSecAttrAccessible as String: kSecAttrAccessibleWhenUnlockedThisDeviceOnly,
        kSecValueData as String: keyData
    ]

    let status = SecItemAdd(query as CFDictionary, nil)
    guard status == errSecSuccess else { throw KeychainErrors.COULDNOTINSERT }
}
do {
    try store(key: secretKey, withTag: "com.myapp.keys.localStore")
} catch {
    print(error)
}

Чтение ключа из Keychain

Поскольку ключ сохранён как kSecValueData, читаем его обратно как данные (kSecReturnData) и восстанавливаем SymmetricKey из полученных байтов.

func read(tag: String) throws -> SymmetricKey {
    let readQuery: [String: Any] = [
        kSecClass as String: kSecClassKey,
        kSecAttrApplicationTag as String: tag,
        kSecReturnData as String: true
    ]

    var item: CFTypeRef?
    let readStatus = SecItemCopyMatching(readQuery as CFDictionary, &item)
    guard readStatus == errSecSuccess, let keyData = item as? Data else {
        throw KeychainErrors.COULDNOTREAD
    }

    return SymmetricKey(data: keyData)
}
do {
    let keyReadFromKeychain = try read(tag: "com.myapp.keys.localStore")
    print(keyReadFromKeychain)
} catch {
    print(error)
}

Применение ключа для шифрования и расшифровки

Для симметричного ключа применяется симметричный алгоритм. AES.GCM из CryptoKit обеспечивает и конфиденциальность, и контроль целостности (аутентифицированное шифрование), а одноразовый вектор (nonce) генерируется автоматически на каждую операцию.

let plainText = "this is our golden secret. Encrypt it!".data(using: .utf8)!

// Шифрование
let sealedBox = try AES.GCM.seal(plainText, using: secretKey)
let cipherText = sealedBox.combined!   // nonce + ciphertext + tag

// Расшифровка
let reopened = try AES.GCM.SealedBox(combined: cipherText)
let decrypted = try AES.GCM.open(reopened, using: secretKey)

Если действительно нужна асимметричная криптография

kSecAttrKeyTypeRSA и алгоритм rsaEncryptionOAEPSHA256 применимы к RSA-ключам, а не к сырым симметричным байтам. Если требуется именно RSA, ключевую пару следует генерировать штатной функцией SecKeyCreateRandomKey, а не подменять симметричный ключ:

let attributes: [String: Any] = [
    kSecAttrKeyType as String: kSecAttrKeyTypeRSA,
    kSecAttrKeySizeInBits as String: 2048,
    kSecAttrIsPermanent as String: true,
    kSecAttrApplicationTag as String: "com.myapp.keys.localStore"
]
var error: Unmanaged<CFError>?
guard let privateKey = SecKeyCreateRandomKey(attributes as CFDictionary, &error) else {
    throw error!.takeRetainedValue() as Error
}
let publicKey = SecKeyCopyPublicKey(privateKey)!
let cipher = SecKeyCreateEncryptedData(publicKey, .rsaEncryptionOAEPSHA256, plainText as CFData, &error) as Data?

Отображение сертификатов

func showCertificateInfo() -> String {
    var resultString = "--- Certificates in Keychain ---\n"
    var outputACert = false
    let query = [kSecMatchLimit: kSecMatchLimitAll,
                kSecReturnRef: true,
                kSecClass: kSecClassCertificate] as CFDictionary
    var result: CFTypeRef?
    let resultCode = SecItemCopyMatching(query, &result)
    if resultCode == errSecSuccess {
        if CFArrayGetTypeID() == CFGetTypeID(result) {
            let array = (result as? NSArray) as? [SecCertificate]
            array?.forEach { (item) in
                resultString += self.displayCertificate(item)
                outputACert = true
            }
        } else {
            // swiftlint:disable force_cast
            resultString += self.displayCertificate(result as! SecCertificate)
            // swiftlint:enable force_cast
            outputACert = true
        }
    }
    if !outputACert {
        resultString += "None\n"
    }
    resultString += "-------------------------------"
    return resultString
}

Ссылки

  1. https://developer.apple.com/
  2. CertificateSDK
  3. Certificates
  4. Keychain Services
  5. Basic iOS Security: Keychain and Hashing
К началу