Реализация безопасного хранения ключей в Secure Enclave (iOS) для криптокошелька
Secure Enclave — отдельный процессор внутри Apple SoC, изолированный от основного CPU и RAM. Приватный ключ, сгенерированный в Secure Enclave, физически не покидает чип — даже у вашего кода нет к нему прямого доступа. Операция подписи выполняется внутри SE и наружу возвращается только результат.
Ограничения, которые нужно знать до начала
Secure Enclave поддерживает только P-256 (secp256r1, он же NIST P-256). Это не secp256k1, которую используют Bitcoin и Ethereum. Поэтому SE не подходит для хранения ETH/BTC приватных ключей напрямую. Типичное использование для крипто-кошелька — хранить в SE ключ шифрования, которым зашифрован secp256k1 приватный ключ в Keychain. Или использовать SE для биометрической защиты Keychain-записи через SecAccessControlCreateWithFlags.
Если ваше приложение работает с блокчейнами, использующими P-256 (например, некоторые enterprise-цепочки или NEAR protocol через ed25519 — не путать), SE можно использовать для прямого хранения и подписи.
Создание ключа в Secure Enclave
let accessControl = SecAccessControlCreateWithFlags(
nil,
kSecAttrAccessibleWhenUnlockedThisDeviceOnly,
[.privateKeyUsage, .biometryCurrentSet],
nil
)!
let attributes: [String: Any] = [
kSecAttrKeyType as String: kSecAttrKeyTypeECSECPrimeRandom,
kSecAttrKeySizeInBits as String: 256,
kSecAttrTokenID as String: kSecAttrTokenIDSecureEnclave,
kSecPrivateKeyAttrs as String: [
kSecAttrIsPermanent as String: true,
kSecAttrApplicationLabel as String: "wallet-signing-key-v1",
kSecAttrAccessControl as String: accessControl
]
]
var error: Unmanaged<CFError>?
guard let privateKey = SecKeyCreateRandomKey(attributes as CFDictionary, &error) else {
throw error!.takeRetainedValue()
}
kSecAttrTokenIDSecureEnclave — это и есть указание системе создать ключ в SE. biometryCurrentSet инвалидирует ключ при изменении биометрии (добавление нового отпечатка или смена Face ID). Для кошелька это правильное поведение — нужна явная переаутентификация.
Подпись данных через SE-ключ
let publicKey = SecKeyCopyPublicKey(privateKey)!
let algorithm: SecKeyAlgorithm = .ecdsaSignatureMessageX962SHA256
guard SecKeyIsAlgorithmSupported(privateKey, .sign, algorithm) else {
throw WalletError.algorithmNotSupported
}
var signError: Unmanaged<CFError>?
guard let signature = SecKeyCreateSignature(
privateKey,
algorithm,
dataToSign as CFData,
&signError
) else {
throw signError!.takeRetainedValue()
}
Подпись выполняется асинхронно с точки зрения UI — пока SE обрабатывает запрос (и если нужна биометрия — пока пользователь аутентифицируется), main thread не блокируется. Весь вызов нужно вынести в Task или dispatch queue.
Схема для ETH/BTC кошельков
Раз SE не работает с secp256k1 напрямую, используем следующую схему:
- Генерируем ephemeral P-256 ключ в SE — это «ключ шифрования».
- Генерируем secp256k1 приватный ключ в памяти.
- Шифруем secp256k1 ключ через ECIES с публичным ключом SE:
SecKeyCreateEncryptedDataс алгоритмомeciesEncryptionStandardX963SHA256AESGCM. - Зашифрованный blob сохраняем в Keychain с
kSecAttrAccessibleWhenUnlockedThisDeviceOnly. - При подписи транзакции: расшифровываем через SE (что требует биометрию), используем secp256k1 ключ для подписи, сразу обнуляем из памяти.
Это дороже одного Keychain-хранения по сложности, но ключ никогда не живёт на диске в открытом виде.
Процесс
Аудит требований (P-256 напрямую или схема шифрования для secp256k1), реализация, тестирование на реальном железе — симулятор не поддерживает Secure Enclave. Отдельно тестируем поведение при смене биометрии, при удалении и переустановке приложения.
Сроки — 3–5 дней. Симулятор для большей части разработки достаточен, но финальное тестирование только на устройстве.







