Реалізація безпечного зберігання ключів у 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 протокол через 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), реалізація, тестування на реальному залізі — симулятор не підтримується. Окремо тестуємо поведінку при зміні біометрії, при видаленні та переустановці програми.
Часова шкала — 3–5 днів. Симулятор для більшої частини розробки достатній, але кінцеве тестування тільки на пристрої.







