Files
swift-secure-enclave-tool/swift-secure-enclave-tool-v2.swift
2025-04-28 22:54:29 +08:00

414 lines
14 KiB
Swift

// 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 public_key_point_base64: String
var public_key_base64: String
var data_representation_base64: String
}
struct ComputeSecureEnclaveP256EcsignResponse: Codable {
var success: Bool
var signature_base64: String
}
struct ComputeSecureEnclaveP256EcdhResponse: Codable {
var success: Bool
var shared_secret: String
}
struct ExternalSpecResponse: Codable {
var success: Bool
var agent: String
var specification: String
}
func stringify<T: Encodable>(_ 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<T: Encodable>(_ 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<CFError>? = 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,
public_key_point_base64: publicKeyPointBase64,
public_key_base64: publicKeyDerBase64,
data_representation_base64: 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,
public_key_point_base64: publicKeyPointBase64,
public_key_base64: publicKeyDerBase64,
data_representation_base64: 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,
signature_base64: 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,
shared_secret: sharedSecret.description
)
} catch {
exitError("\(error)")
return nil
}
}
func externalSpec() -> ExternalSpecResponse {
return ExternalSpecResponse(
success: true,
agent: "swift-secure-enclave-external-provider/2.0.0-alpha",
specification: "External/1.0.0-alpha"
)
}
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 == "external_spec") {
exitOkWithJson(externalSpec())
}
if (command == "version") {
exitOkWithJson(VersionResponse(success: true, version: "2.0.0-20250428"))
}
struct GenerateSecureEnclaveP256KeyPairRequest {
var controlFlag: String
}
func parseGenerateSecureEnclaveP256KeyPairRequest() -> GenerateSecureEnclaveP256KeyPairRequest? {
var controlFlagOpt: String?
let len = CommandLine.arguments.count;
if CommandLine.arguments.count > 2 {
var i = 2
while i < len {
let k = CommandLine.arguments[i];
if (k == "--control-flag") {
controlFlagOpt = CommandLine.arguments[i + 1]
i += 1
}
}
}
guard let controlFlag = controlFlagOpt else {
exitError("parameter --control-flag required.")
return nil
}
return GenerateSecureEnclaveP256KeyPairRequest(
controlFlag: controlFlag
)
}
struct ComputeP256EcSignRequest {
var dataRepresentationBase64: String
var messageBase64: String
}
func parseComputeP256EcSignRequest() -> ComputeP256EcSignRequest? {
var dataRepresentationBase64Opt: String?
var messageBase64Opt: String?
let len = CommandLine.arguments.count;
if CommandLine.arguments.count > 2 {
var i = 2
while i < len {
let k = CommandLine.arguments[i];
if (k == "--data-representation-base64" || k == "--private-key") {
dataRepresentationBase64Opt = CommandLine.arguments[i + 1]
i += 1
} else if (k == "--message-base64") {
messageBase64Opt = CommandLine.arguments[i + 1]
i += 1
}
}
}
guard let dataRepresentationBase64 = dataRepresentationBase64Opt else {
exitError("parameter --data-representation-base64 or --private-key required.")
return nil
}
guard let messageBase64 = messageBase64Opt else {
exitError("parameter --message-base64 required.")
return nil
}
return ComputeP256EcSignRequest(
dataRepresentationBase64: dataRepresentationBase64,
messageBase64: messageBase64
)
}
if (command == "help") {
print("swift-secure-enclave-tool-v2 <command>")
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 <privateKey> - recover Secure Enclave P256 EC sign key pair")
print("recover_p256_ecdh_public_key <privateKey> - recover Secure Enclave P256 EC DH key pair")
print("compute_p256_ecsign <privateKey> <content> - compure Secure Enclave P256 EC sign")
print("compute_p256_ecdh <privateKey> <ephemeraPublicKey> - compure Secure Enclave P256 EC DH")
print("external_spec - external specification")
print("external_public_key --parameter <parameter> - external public key")
print("external_sign --parameter <parameter> --alg <alg> --message-base64 <message-in-base64> - external sign")
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")