如何在Swift中基于ECDH利用已有外部公钥派生共享密钥?
将NodeJS/Java的ECDH密钥协商逻辑转换为Swift
我有一个在NodeJS和Java中均可正常运行的ECDH密钥协商+哈希处理方法,现在需要将其转换为Swift,但遇到了问题,希望能完成转换。
NodeJS原代码
//the public_key param here is from a different device. sign: function(public_key) { //dummy values var PRE_SALT_VALUE = 'f0f0f0f0f0'; var POST_SALT_VALUE = '0101010101'; const crypto = require('crypto'); var sha512 = crypto.createHash("sha512"); var EC = require('elliptic').ec; var ec = new EC('p256'); // Generate keys var key1 = ec.genKeyPair(); //key1 is gen before pub key var key2 = ec.keyFromPublic(public_key, 'hex') //pub key gen from saved cert var derived_secret = key1.derive(key2.getPublic()); var derived_secret = Buffer.from(derived_secret.toString(16), 'hex') var public_key_client = key1.getPublic('hex') var pre_salt = Buffer.from(PRE_SALT_VALUE, 'hex') var post_salt = Buffer.from(POST_SALT_VALUE, 'hex') derived_secret = Buffer.from(pre_salt.toString('hex')+derived_secret.toString('hex')+post_salt.toString('hex'), 'hex') // finalyze shared secret // Hash shared secret var sha = sha512.update(derived_secret); derived_secret = sha.digest(); return { public_key: public_key_client.toString('hex').slice(2), //dropping first byte of compression bits secret: derived_secret.toString('hex') } }
该逻辑在Java中同样可正常运行。
我尝试的Swift实现步骤
作为Swift初学者,我已经完成了以下尝试:
步骤1:加载外部证书并提取公钥
// certData will be the external cert byte array in der format let certData = Data(bytes: self.cert!, count: self.cert_length!) // load the cert so I can read the data guard let certificate = SecCertificateCreateWithData(nil, certData as CFData) else { // todo handle error print("explode"); return; } // log public key from cert let publicKey = SecCertificateCopyKey(certificate)! let pubKeyExternRep = SecKeyCopyExternalRepresentation(publicKey, nil) let pubKeyExternRepData:Data = pubKeyExternRep! as Data print(pubKeyExternRepData.bytes) // printed pub key looks reasonable
步骤2:尝试生成本地EC密钥对
// Attempt 1 - generate our own random public/private keypair let attributes: [String: Any] = [ kSecAttrType as String: kSecAttrKeyTypeECSECPrimeRandom, kSecAttrKeySizeInBits as String: 256, kSecPrivateKeyAttrs as String: [ kSecAttrIsPermanent as String: true ] ] var error: Unmanaged<CFError>? let ouPrivateKeyAttempt1 = SecKeyCreateRandomKey(attributes as CFDictionary, &error)! let ourPubKeyAttempt1 = SecKeyCopyPublicKey(ouPrivateKeyAttempt1) let ourPubKeyExternRep = SecKeyCopyExternalRepresentation(ourPubKeyAttempt1!, &error) print("our generated EC public key (65 bytes)") print("\(pubKeyExternRep!)") // =-=-=--=-=-=-=-==--==-=-=-=- End attempt1
步骤3:派生共享密钥时出错
这段代码运行时崩溃,报错为CryptoKit.CryptoKitError.incorrectParameterSize:
// our private key agreement (does this generate a new private key, guess we dont need 'Attempt1' above?) let ourPrivateKey = P256.KeyAgreement.PrivateKey() let ourPubKey = ourPrivateKey.publicKey // external device public key agreement given our generated private agreement let externalPubKeyAg = try! P256.KeyAgreement.PublicKey( rawRepresentation: pubKeyExternRepData.bytes) // <-- var from step 1, crashes here // get shared secret let sharedSecret = try! ourPrivateKey.sharedSecretFromKeyAgreement( with: externalPubKeyAg) print(sharedSecret)
正确的Swift转换实现
以下是完全对应NodeJS逻辑的Swift代码,解决了公钥格式不匹配、密钥协商及哈希处理的问题:
import CryptoKit func sign(withExternalCertData certData: Data) throws -> (publicKey: String, secret: String) { // 定义盐值,对应NodeJS中的常量 let PRE_SALT_VALUE = Data(hex: "f0f0f0f0f0") let POST_SALT_VALUE = Data(hex: "0101010101") // 步骤1:从证书提取外部公钥并转换为CryptoKit可识别的格式 guard let certificate = SecCertificateCreateWithData(nil, certData as CFData), let secPublicKey = SecCertificateCopyKey(certificate), let externalPubKeyData = SecKeyCopyExternalRepresentation(secPublicKey, nil) as Data? else { throw NSError(domain: "ECDHError", code: 0, userInfo: [NSLocalizedDescriptionKey: "Failed to extract public key from certificate"]) } // 将SecKey的外部表示转换为P256公钥(注意:直接传入Data而非bytes数组) let externalPubKey = try P256.KeyAgreement.PublicKey(rawRepresentation: externalPubKeyData) // 步骤2:生成本地P256密钥对 let ourPrivateKey = P256.KeyAgreement.PrivateKey() let ourPubKey = ourPrivateKey.publicKey // 步骤3:执行ECDH密钥协商,获取共享密钥 let sharedSecret = try ourPrivateKey.sharedSecretFromKeyAgreement(with: externalPubKey) // 提取共享密钥的原始数据(对应NodeJS中derive的结果) var sharedSecretData = sharedSecret.dataRepresentation // 步骤4:拼接前后盐值 let combinedData = PRE_SALT_VALUE + sharedSecretData + POST_SALT_VALUE // 步骤5:SHA512哈希处理 let hashedData = SHA512.hash(data: combinedData) let secretHex = hashedData.compactMap { String(format: "%02x", $0) }.joined() // 步骤6:处理本地公钥,去掉第一个字节(对应NodeJS的slice(2)) let ourPubKeyRaw = ourPubKey.rawRepresentation // 原始公钥是65字节(0x04 + 32字节x + 32字节y),去掉第一个字节后取64字节 let trimmedPubKeyData = ourPubKeyRaw.dropFirst() let pubKeyHex = trimmedPubKeyData.compactMap { String(format: "%02x", $0) }.joined() return (publicKey: pubKeyHex, secret: secretHex) } // 给Data添加十六进制初始化的扩展(方便处理盐值) extension Data { init(hex: String) { let filteredHex = hex.replacingOccurrences(of: " ", with: "") var data = Data() var index = filteredHex.startIndex while index < filteredHex.endIndex { let nextIndex = filteredHex.index(index, offsetBy: 2) let byteString = String(filteredHex[index..<nextIndex]) if let byte = UInt8(byteString, radix: 16) { data.append(byte) } index = nextIndex } self = data } }
关键修复点说明
- 公钥格式问题:原代码中错误地传递
pubKeyExternRepData.bytes给P256.KeyAgreement.PublicKey的初始化方法,应该直接传入Data类型的externalPubKeyData。 - 共享密钥处理:CryptoKit的
SharedSecret通过dataRepresentation获取原始字节数据,对应NodeJS中derive后的结果。 - 盐值拼接与哈希:按照NodeJS逻辑拼接前后盐值,再用SHA512哈希,最后转换为十六进制字符串。
- 公钥裁剪:将本地生成的公钥(65字节未压缩格式)去掉第一个字节(0x04),转换为十六进制字符串后返回。
内容的提问来源于stack exchange,提问作者alexward1230
相关产品推荐
相关产品推荐

