Skip to content

Instantly share code, notes, and snippets.

@thomsmed
Last active February 10, 2024 10:45
Show Gist options
  • Save thomsmed/b76a0b695c1a1816e2c0ef25d151368b to your computer and use it in GitHub Desktop.
Save thomsmed/b76a0b695c1a1816e2c0ef25d151368b to your computer and use it in GitHub Desktop.
A general storage for Cryptographic Key Pairs generated on device.
import Foundation
/// A general storage for Cryptographic Key Pairs generated on device.
///
/// The generated and stored keys are protected by Secure Enclave,
/// hence only NIST P-256 elliptic curve key pairs are supported.
///
/// These keys can only be used for creating and verifying cryptographic signatures, or for elliptic curve Diffie-Hellman key exchange (and by extension, symmetric encryption).
public protocol CryptographicKeyStorageProtocol {
/// Generate a NIST P-256 elliptic curve key pair.
/// Protected by Secure Enclave and stored securely in the keychain.
/// - Parameter tag: A tag used to identify the key pair. The tag should be unique per key pair.
/// - Returns: The private key of the newly generated key pair.
func generateAndStoreKeyPair(withTag tag: Data) throws -> SecKey
/// Get the private key of a NIST P-256 elliptic curve key pair.
/// - Parameter tag: The tag that identifies the key pair this private key is part of.
/// - Returns:The private key of the key pair identified with the given tag.
func getPrivateKey(withTag tag: Data) throws -> SecKey
/// Get the public key of a NIST P-256 elliptic curve key pair.
/// - Parameter tag: The tag that identifies the key pair this public key is part of.
/// - Returns: The public key of the key pair identified with the given tag.
func getPublicKey(withTag tag: Data) throws -> SecKey
/// Delete a previously generated NIST P-256 elliptic curve key pair.
/// - Parameter tag: The tag that identifies the key pair.
func deleteKeyPair(withTag tag: Data) throws
}
import CryptoKit
import Foundation
/// Enumeration describing possible errors that might occur when using ``CryptographicKeyStorage``.
public enum CryptographicKeyStorageError: Error {
case secureEnclaveUnavailable
case keyPairExistWithTag
case failedToCreateKeyPairAccessControl
case failedToCreateKeyPair
case failedToGetPrivateKey
case failedToGetPublicKey
case noKeyPairAvailable
case unexpectedError(Int)
}
/// A general storage for generating and storing Cryptographic Keys in the device's Keychain.
/// Generated keys are protected by the Secure Enclave.
public struct CryptographicKeyStorage: CryptographicKeyStorageProtocol {
public init() {}
private func noExistingKeyPair(withTag tag: Data) throws -> Bool {
let query: [String: Any] = [
kSecClass as String: kSecClassKey,
kSecAttrApplicationTag as String: tag,
kSecAttrKeyType as String: kSecAttrKeyTypeECSECPrimeRandom
]
var item: CFTypeRef?
let status = SecItemCopyMatching(query as CFDictionary, &item)
switch status {
case errSecItemNotFound:
// This tag is free. There is no key pair with this tag.
return true
case errSecSuccess:
// This tag is taken. There is already a key pair with this tag.
return false
default:
throw CryptographicKeyStorageError.unexpectedError(Int(status))
}
}
public func generateAndStoreKeyPair(withTag tag: Data) throws -> SecKey {
#if targetEnvironment(simulator)
// Simulators do not have SecureEnclave.
#else
guard SecureEnclave.isAvailable else {
throw CryptographicKeyStorageError.secureEnclaveUnavailable
}
#endif
// It is recommended by Apple to avoid using the same tag for multiple Keychain items.
guard try noExistingKeyPair(withTag: tag) else {
throw CryptographicKeyStorageError.keyPairExistWithTag
}
var error: Unmanaged<CFError>?
guard let accessControl = SecAccessControlCreateWithFlags(
// Allocator.
kCFAllocatorDefault,
// Protection class.
// Ensure key(s) is only available when the device is unlocked,
// and will never move to another device (e.g restoring from backup on another device).
kSecAttrAccessibleWhenUnlockedThisDeviceOnly,
// Flags.
[
// Enable the private key to be used in signing operations inside Secure Enclave.
.privateKeyUsage
],
// Error.
&error
) else {
throw CryptographicKeyStorageError.failedToCreateKeyPairAccessControl
}
#if targetEnvironment(simulator)
// Simulators do not have SecureEnclave.
let attributes = [
kSecAttrKeyType as String: kSecAttrKeyTypeECSECPrimeRandom,
kSecAttrKeySizeInBits as String: 256,
kSecPrivateKeyAttrs as String: [
kSecAttrCanSign as String: true,
kSecAttrIsPermanent as String: true,
kSecAttrApplicationTag as String: tag,
kSecAttrAccessControl as String: accessControl,
] as [String: Any]
] as [String: Any]
#else
let attributes = [
kSecAttrKeyType as String: kSecAttrKeyTypeECSECPrimeRandom,
kSecAttrKeySizeInBits as String: 256,
kSecAttrTokenID as String: kSecAttrTokenIDSecureEnclave,
kSecPrivateKeyAttrs as String: [
kSecAttrCanSign as String: true,
kSecAttrIsPermanent as String: true,
kSecAttrApplicationTag as String: tag,
kSecAttrAccessControl as String: accessControl,
] as [String: Any],
] as [String : Any]
#endif
guard let privateKey = SecKeyCreateRandomKey(attributes as CFDictionary, &error) else {
throw CryptographicKeyStorageError.failedToCreateKeyPair
}
return privateKey
}
public func getPrivateKey(withTag tag: Data) throws -> SecKey {
let query: [String: Any] = [
kSecClass as String: kSecClassKey,
kSecAttrApplicationTag as String: tag,
kSecAttrKeyType as String: kSecAttrKeyTypeECSECPrimeRandom,
kSecReturnRef as String: true,
]
var item: CFTypeRef?
let status = SecItemCopyMatching(query as CFDictionary, &item)
guard status == errSecSuccess else {
if status == errSecUnknownTag {
throw CryptographicKeyStorageError.noKeyPairAvailable
} else {
throw CryptographicKeyStorageError.failedToGetPrivateKey
}
}
// This force cast will always success if status == errSecSuccess
return item as! SecKey
}
public func getPublicKey(withTag tag: Data) throws -> SecKey {
let privateKey = try getPrivateKey(withTag: tag)
guard let publicKey = SecKeyCopyPublicKey(privateKey) else {
throw CryptographicKeyStorageError.failedToGetPublicKey
}
return publicKey
}
public func deleteKeyPair(withTag tag: Data) throws {
let query: [String: Any] = [
kSecClass as String: kSecClassKey,
kSecAttrApplicationTag as String: tag,
kSecAttrKeyType as String: kSecAttrKeyTypeECSECPrimeRandom
]
let status = SecItemDelete(query as CFDictionary)
guard status == errSecSuccess || status == errSecItemNotFound else {
throw CryptographicKeyStorageError.unexpectedError(Int(status))
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment