From 65c87c1b8f7afd369253470f971bbc22a748d125 Mon Sep 17 00:00:00 2001 From: Hatter Jiang Date: Sun, 27 Apr 2025 23:27:16 +0800 Subject: [PATCH] feat: swift-secure-enclave-tool-v2 --- .gitignore | 1 + swift-secure-enclave-tool-v2.swift | 329 +++++++++++++++++++++++++++++ 2 files changed, 330 insertions(+) create mode 100644 swift-secure-enclave-tool-v2.swift diff --git a/.gitignore b/.gitignore index ef5422b..7138742 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ swift-secure-enclave-tool +swift-secure-enclave-tool-v2 # ---> Swift # Xcode # diff --git a/swift-secure-enclave-tool-v2.swift b/swift-secure-enclave-tool-v2.swift new file mode 100644 index 0000000..df3744f --- /dev/null +++ b/swift-secure-enclave-tool-v2.swift @@ -0,0 +1,329 @@ +// Reference: +// - https://developer.apple.com/documentation/swift/commandline/arguments +// - https://git.hatter.ink/hatter/card-cli/src/branch/master/swift-lib/src/lib.swift +// - https://developer.apple.com/documentation/security/secaccesscontrolcreateflags +// - https://gist.github.com/monsoir/cc06c298c9c37d629942d87b14a202fc +// - https://developer.apple.com/documentation/foundation/jsonencoder + +import CryptoKit +import Foundation +import LocalAuthentication + +struct ErrorResponse: Codable { + var success: Bool + var error: String +} + +struct VersionResponse: Codable { + var success: Bool + var version: String +} + +struct SupportSecureEnclaveResponse: Codable { + var success: Bool + var supported: Bool +} + +struct GenerateSecureEnclaveP256KeyPairResponse: Codable { + var success: Bool + var publicKeyPointBase64: String + var publicKeyDerBase64: String + var dataRepresentationBase64: String +} + +struct ComputeSecureEnclaveP256EcsignResponse: Codable { + var success: Bool + var signatureDerBase64: String +} + +struct ComputeSecureEnclaveP256EcdhResponse: Codable { + var success: Bool + var sharedSecret: String +} + +func stringify(_ value: T) -> String? { + guard let jsonData = try? JSONEncoder().encode(value) else { return nil } + let result = String(data: jsonData, encoding: .utf8) + return result +} + +func exitError(_ error: String) { + if let response = stringify(ErrorResponse(success: false, error: error)) { + print(response) + } else { + print("{\"success\":false,\"error\":\"JSON stringify erorr, raw error: \(error)\"}") + } + exit(1) +} + +func exitOkWithJson(_ value: T) { + if let response = stringify(value) { + print(response) + exit(0) + } else { + print("{\"success\":false,\"error\":\"JSON stringify erorr\"}") + exit(1) + } +} + +func isSupportSecureEnclave() -> SupportSecureEnclaveResponse { + return SupportSecureEnclaveResponse(success: true, supported: SecureEnclave.isAvailable) +} + +func generateSecureEnclaveP256KeyPair(sign: Bool, controlFlag: String) -> GenerateSecureEnclaveP256KeyPairResponse? { + var error: Unmanaged? = nil + let accessControlCreateFlags: SecAccessControlCreateFlags + if (controlFlag == "none") { + accessControlCreateFlags = [.privateKeyUsage] + } else if (controlFlag == "userPresence") { + accessControlCreateFlags = [.privateKeyUsage, .userPresence] + } else if (controlFlag == "devicePasscode") { + accessControlCreateFlags = [.privateKeyUsage, .devicePasscode] + } else if (controlFlag == "biometryAny") { + accessControlCreateFlags = [.privateKeyUsage, .biometryAny] + } else if (controlFlag == "biometryCurrentSet") { + accessControlCreateFlags = [.privateKeyUsage, .biometryCurrentSet] + } else { + exitError("invalid control flag: \(controlFlag)") + return nil + } + guard let accessCtrl = SecAccessControlCreateWithFlags( + nil, + kSecAttrAccessibleWhenUnlockedThisDeviceOnly, + accessControlCreateFlags, + &error + ) else { + exitError(error.debugDescription) + return nil + } + do { + if (sign) { + let privateKeyReference = try SecureEnclave.P256.Signing.PrivateKey.init( + accessControl: accessCtrl + ) + return signingPrivateKeyToResponse(privateKeyReference) + } else { + let privateKeyReference = try SecureEnclave.P256.KeyAgreement.PrivateKey.init( + accessControl: accessCtrl + ) + return keyAgreementPrivateKeyToResponse(privateKeyReference) + } + } catch { + exitError("\(error)") + return nil + } +} + +func signingPrivateKeyToResponse(_ privateKeyReference: SecureEnclave.P256.Signing.PrivateKey) -> GenerateSecureEnclaveP256KeyPairResponse { + let publicKeyPointBase64 = privateKeyReference.publicKey.x963Representation.base64EncodedString() + let publicKeyDerBase64 = privateKeyReference.publicKey.derRepresentation.base64EncodedString() + let dataRepresentationBase64 = privateKeyReference.dataRepresentation.base64EncodedString() + return GenerateSecureEnclaveP256KeyPairResponse( + success: true, + publicKeyPointBase64: publicKeyPointBase64, + publicKeyDerBase64: publicKeyDerBase64, + dataRepresentationBase64: dataRepresentationBase64 + ) +} + +func keyAgreementPrivateKeyToResponse(_ privateKeyReference: SecureEnclave.P256.KeyAgreement.PrivateKey) -> GenerateSecureEnclaveP256KeyPairResponse { + let publicKeyPointBase64 = privateKeyReference.publicKey.x963Representation.base64EncodedString() + let publicKeyDerBase64 = privateKeyReference.publicKey.derRepresentation.base64EncodedString() + let dataRepresentationBase64 = privateKeyReference.dataRepresentation.base64EncodedString() + return GenerateSecureEnclaveP256KeyPairResponse( + success: true, + publicKeyPointBase64: publicKeyPointBase64, + publicKeyDerBase64: publicKeyDerBase64, + dataRepresentationBase64: dataRepresentationBase64 + ) +} + +func recoverSecureEnclaveP256PublicKeyEcsign(privateKeyDataRepresentation: String) -> GenerateSecureEnclaveP256KeyPairResponse? { + return recoverSecureEnclaveP256PublicKey(privateKeyDataRepresentation: privateKeyDataRepresentation, sign: true) +} + +func recoverSecureEnclaveP256PublicKey(privateKeyDataRepresentation: String, sign: Bool) -> GenerateSecureEnclaveP256KeyPairResponse? { + guard let privateKeyDataRepresentation = Data( + base64Encoded: privateKeyDataRepresentation + ) else { + exitError("private key base64 decode failed") + return nil + } + do { + let context = LAContext() + if (sign) { + let privateKeyReference = try SecureEnclave.P256.Signing.PrivateKey( + dataRepresentation: privateKeyDataRepresentation, + authenticationContext: context + ) + return signingPrivateKeyToResponse(privateKeyReference) + } else { + let privateKeyReference = try SecureEnclave.P256.KeyAgreement.PrivateKey( + dataRepresentation: privateKeyDataRepresentation, + authenticationContext: context + ) + return keyAgreementPrivateKeyToResponse(privateKeyReference) + } + } catch { + exitError("\(error)") + return nil + } +} + +func computeSecureEnclaveP256Ecsign(privateKeyDataRepresentation: String, content: String) -> ComputeSecureEnclaveP256EcsignResponse? { + guard let privateKeyDataRepresentation = Data( + base64Encoded: privateKeyDataRepresentation + ) else { + exitError("private key base64 decode failed") + return nil + } + guard let contentData = Data( + base64Encoded: content + ) else { + exitError("content base64 decode failed") + return nil + } + do { + let context = LAContext() + let p = try SecureEnclave.P256.Signing.PrivateKey( + dataRepresentation: privateKeyDataRepresentation, + authenticationContext: context + ) + + let digest = SHA256.hash(data: contentData) + let signature = try p.signature(for: digest) + + return ComputeSecureEnclaveP256EcsignResponse( + success: true, + signatureDerBase64: signature.derRepresentation.base64EncodedString() + ) + } catch { + exitError("\(error)") + return nil + } +} + +func computeSecureEnclaveP256Ecdh(privateKeyDataRepresentation: String, ephemeraPublicKey: String) -> ComputeSecureEnclaveP256EcdhResponse? { + guard let privateKeyDataRepresentation = Data( + base64Encoded: privateKeyDataRepresentation + ) else { + exitError("private key base64 decode failed") + return nil + } + guard let ephemeralPublicKeyRepresentation = Data( + base64Encoded: ephemeraPublicKey + ) else { + exitError("ephemeral public key base64 decode failed") + return nil + } + do { + let context = LAContext() + let p = try SecureEnclave.P256.KeyAgreement.PrivateKey( + dataRepresentation: privateKeyDataRepresentation, + authenticationContext: context + ) + + let ephemeralPublicKey = try P256.KeyAgreement.PublicKey.init(derRepresentation: ephemeralPublicKeyRepresentation) + + let sharedSecret = try p.sharedSecretFromKeyAgreement( + with: ephemeralPublicKey) + + return ComputeSecureEnclaveP256EcdhResponse( + success: true, + sharedSecret: sharedSecret.description + ) + } catch { + exitError("\(error)") + return nil + } +} + +if (CommandLine.arguments.count == 1) { + exitError("require at least one argument") +} + +let command = CommandLine.arguments[1] + +if (command == "is_support_secure_enclave") { + exitOkWithJson(isSupportSecureEnclave()) +} + +if (command == "generate_p256_ecsign_keypair") { + if (CommandLine.arguments.count != 3) { + exitError("require two arguments") + } + exitOkWithJson(generateSecureEnclaveP256KeyPair(sign: true, controlFlag: CommandLine.arguments[2])) +} + +if (command == "generate_p256_ecdh_keypair") { + if (CommandLine.arguments.count != 3) { + exitError("require two arguments") + } + exitOkWithJson(generateSecureEnclaveP256KeyPair(sign: false, controlFlag: CommandLine.arguments[2])) +} + +if (command == "recover_p256_ecsign_public_key") { + if (CommandLine.arguments.count != 3) { + exitError("require two arguments") + } + let response = recoverSecureEnclaveP256PublicKey( + privateKeyDataRepresentation: CommandLine.arguments[2], sign: true) + exitOkWithJson(response) +} + +if (command == "recover_p256_ecdh_public_key") { + if (CommandLine.arguments.count != 3) { + exitError("require two arguments") + } + let response = recoverSecureEnclaveP256PublicKey( + privateKeyDataRepresentation: CommandLine.arguments[2], sign: false) + exitOkWithJson(response) +} + +if (command == "compute_p256_ecsign") { + if (CommandLine.arguments.count != 4) { + exitError("require three arguments") + } + let response = computeSecureEnclaveP256Ecsign( + privateKeyDataRepresentation: CommandLine.arguments[2], + content: CommandLine.arguments[3] + ) + exitOkWithJson(response) +} + +if (command == "compute_p256_ecdh") { + if (CommandLine.arguments.count != 4) { + exitError("require three arguments") + } + let response = computeSecureEnclaveP256Ecdh( + privateKeyDataRepresentation: CommandLine.arguments[2], + ephemeraPublicKey: CommandLine.arguments[3] + ) + exitOkWithJson(response) +} + +if (command == "version") { + exitOkWithJson(VersionResponse(success: true, version: "2.0.0-20250428")) +} + +if (command == "help") { + print("swift-secure-enclave-tool-v2 ") + print("help - print help") + print("version - print version") + print("is_support_secure_enclave - is Secure Enclave supported") + print("generate_p256_ecsign_keypair [controlFlag] - generate Secure Enclave P256 EC sign key pair") + print("generate_p256_ecdh_keypair [controlFlag] - generate Secure Enclave P256 EC DH key pair") + print("recover_p256_ecsign_public_key - recover Secure Enclave P256 EC sign key pair") + print("recover_p256_ecdh_public_key - recover Secure Enclave P256 EC DH key pair") + print("compute_p256_ecsign - compure Secure Enclave P256 EC sign") + print("compute_p256_ecdh - compure Secure Enclave P256 EC DH") + print() + print("options:") + print("> controlFlag - none, userPresence, devicePasscode, biometryAny, biometryCurrentSet") + print("> privateKey - private key representation (dataRepresentationBase64)") + print("> content - content in base64") + print("> ephemeraPublicKey - public key der in base64") + exit(0) +} + +exitError("invalid command")