You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在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
    }
}

关键修复点说明

  1. 公钥格式问题:原代码中错误地传递pubKeyExternRepData.bytes给P256.KeyAgreement.PublicKey的初始化方法,应该直接传入Data类型的externalPubKeyData。
  2. 共享密钥处理:CryptoKit的SharedSecret通过dataRepresentation获取原始字节数据,对应NodeJS中derive后的结果。
  3. 盐值拼接与哈希:按照NodeJS逻辑拼接前后盐值,再用SHA512哈希,最后转换为十六进制字符串。
  4. 公钥裁剪:将本地生成的公钥(65字节未压缩格式)去掉第一个字节(0x04),转换为十六进制字符串后返回。

内容的提问来源于stack exchange,提问作者alexward1230

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.08 10:30:52