Использование файлового хранилища ключей
![]() |
Критичность: НИЗКИЙ |
| Способ обнаружения: 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
}
