javanode.jsswiftcryptographyshared-secret

Derive Shared Secret From ECDH with existing foreign public key - Swift


I'm converting a method that I have working in both NodeJS/Java to Swift but I'm having trouble getting it to work.

Hoping someone can help me covert this to Swift

NodeJS Code:

//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')  
        }
    }

The same code is also working in Java: See the answer to a similar question I asked a few years ago here if you would like to see the java implementation. This was what originally helped me port it to java.

How do I add the same logic in Swift (I am a Swift beginner). So far I have tried

Step 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

Step 2 Next I have to generate a new random public/private keypair, so I tried this

        // 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

Step 3 Next I have to derive the shared secret given the random keypair above the the public external cert. I am pretty lost at this point. I tried something like

        // 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)

The above will crash with CryptoKit.CryptoKitError.incorrectParameterSize.

Anyway, hoping someone can help me convert the function that's in NodeJS/Java above into Swift. Thanks! :)


Solution

  • The result should be a Swift method that gets a public key in hex, internally generates its own key pair, and determines the shared secret in the same way as the Node program.

    Test Scenario

    A test could then look like the following:

    If the two determined shared secrets match, the test was successful.

    This fine answer https://stackoverflow.com/a/26502285/2331445 provides utility methods for converting a hex String to Data and back and are used in the test.

    Swift

    Step 1 could be the calculation of the common secret (without pre- and post-salting):

        private static func computeSharedSecret(ownPrivateKey: P256.KeyAgreement.PrivateKey, otherPublicKeyHex: String) -> Data? {
            guard let otherPK = otherPublicKeyHex.hexadecimal else { return nil }
            let rawOtherPublicKey = otherPK.dropFirst()
            guard let otherPublicKey = try? P256.KeyAgreement.PublicKey(rawRepresentation: rawOtherPublicKey) else { return nil }
            let sharedSecret = try? ownPrivateKey.sharedSecretFromKeyAgreement(with: otherPublicKey)
            return sharedSecret?.data
        }
    

    Since sharedSecretFromKeyAgreement return a struct SharedSecret that conforms to the protocol ContiguousBytes we can define an extension like this:

    extension ContiguousBytes {
        var data: Data {
            var result = Data()
            self.withUnsafeBytes {
                result = Data($0)
            }
            return result
        }
    }
    

    The private key could be created like this:

    let ownPrivateKey = P256.KeyAgreement.PrivateKey(compactRepresentable: false)
    

    And here we call it and apply pre- and postSaltValue, convert it to data and calculate the SHA512 digest:

    func onComputeSecret() {
            guard let otherPublicKeyHex = otherPublicKeyTextField.text else { return }
            guard let sharedSecret = Self.computeSharedSecret(ownPrivateKey: ownPrivateKey, otherPublicKeyHex: otherPublicKeyHex) else {
                print ("failed")
                return
            }
    
            let preSaltValue = "f0f0f0f0f0"
            let postSaltValue = "0101010101"
            if let salted = (preSaltValue + sharedSecret.hexadecimal + postSaltValue).hexadecimal {
                var sha512 = SHA512()
                sha512.update(data: salted)
                print(sha512.finalize())
            }
        }
    

    Finally the whole program for an easy test on iOS:

    import UIKit
    import CryptoKit
    
    class ViewController: UIViewController {
    
        private let ownPrivateKey = P256.KeyAgreement.PrivateKey(compactRepresentable: false)
        private let otherPublicKeyTextField = UITextField()
        private let computeSecretButton = UIButton()
    
        override func viewDidLoad() {
            super.viewDidLoad()
            buildUI()
            printPublicKeyHex()
        }
    
        @objc func onComputeSecret() {
            guard let otherPublicKeyHex = otherPublicKeyTextField.text else { return }
            guard let sharedSecret = Self.computeSharedSecret(ownPrivateKey: ownPrivateKey, otherPublicKeyHex: otherPublicKeyHex) else {
                print ("failed")
                return
            }
    
            let preSaltValue = "f0f0f0f0f0"
            let postSaltValue = "0101010101"
            if let salted = (preSaltValue + sharedSecret.hexadecimal + postSaltValue).hexadecimal {
                var sha512 = SHA512()
                sha512.update(data: salted)
                print(sha512.finalize())
            }
        }
    
        private func printPublicKeyHex() {
            let ownPublicKey = ownPrivateKey.publicKey
            //to get 65 byte result with uncompressed indicator add "04" prefix
            let ownPublicKeyHex = "04\(ownPublicKey.rawRepresentation.hexadecimal)"
            print ("own public key: \(ownPublicKeyHex)")
        }
    
        private static func computeSharedSecret(ownPrivateKey: P256.KeyAgreement.PrivateKey, otherPublicKeyHex: String) -> Data? {
            guard let otherPK = otherPublicKeyHex.hexadecimal else { return nil }
            let rawOtherPublicKey = otherPK.dropFirst()
            guard let otherPublicKey = try? P256.KeyAgreement.PublicKey(rawRepresentation: rawOtherPublicKey) else { return nil }
            let sharedSecret = try? ownPrivateKey.sharedSecretFromKeyAgreement(with: otherPublicKey)
            return sharedSecret?.data
        }
    
        private func buildUI() {
            otherPublicKeyTextField.translatesAutoresizingMaskIntoConstraints = false
            otherPublicKeyTextField.borderStyle = .bezel
            view.addSubview(otherPublicKeyTextField)
    
            computeSecretButton.translatesAutoresizingMaskIntoConstraints = false
            view.addSubview(computeSecretButton)
            computeSecretButton.setTitle("Compute Shared Secret", for: .normal)
            computeSecretButton.addTarget(self, action: #selector(onComputeSecret), for: .touchUpInside)
            computeSecretButton.setTitleColor(.blue, for: .normal)
    
            let layout = view.safeAreaLayoutGuide
            NSLayoutConstraint.activate([
                otherPublicKeyTextField.topAnchor.constraint(equalTo: layout.topAnchor, constant: 24),
                otherPublicKeyTextField.leadingAnchor.constraint(equalTo: layout.leadingAnchor, constant: 24),
                otherPublicKeyTextField.trailingAnchor.constraint(equalTo: layout.trailingAnchor, constant: -24),
    
                computeSecretButton.topAnchor.constraint(equalTo: otherPublicKeyTextField.bottomAnchor, constant: 24),
                computeSecretButton.centerXAnchor.constraint(equalTo: view.centerXAnchor)
            ])
        }
    
    }
    
    extension ContiguousBytes {
        var data: Data {
            var result = Data()
            self.withUnsafeBytes {
                result = Data($0)
            }
            return result
        }
    }
    

    Test

    In the upper area is the Node program, in the lower area you can see the console of Xcode with the Swift program:

    test run

    The shared secrets are identical, so the test was successful.

    Please note: Like the Node program, the Swift program expects an "04" prefix at the beginning of the other public key, indicating that it is an uncompressed key. This must be added if required.