Last active
February 10, 2024 10:45
-
-
Save thomsmed/b76a0b695c1a1816e2c0ef25d151368b to your computer and use it in GitHub Desktop.
A general storage for Cryptographic Key Pairs generated on device.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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