Search code examples
javaswiftencryptionecdhpublic-key-exchange

Reading Public Key Sent by Java Server in Swift


I'm trying to read a public key (x509 format encoded) from a Java Server to finalize my Elliptic Curve Diffie Hellman Exchange. I can send the Public Key to the server with no problem, but now I want to read the public Key that the server has sent to the iOS Client.

byte[] serverPubKeyEnc = serverKpair.getPublic().getEncoded(); (This is on the server)

This is what I return to the iOS side. For me to deal with it, I need to read it from the input stream and then turn it into a usable public key. This is what I have right now on the iOS side of things to read the key:

 var error: Unmanaged<CFError>? = nil
    
    let mutableData = CFDataCreateMutable(kCFAllocatorDefault, CFIndex(0))
        if mutableData != nil
        {
            let headerSize = 26
         
            //For importing Java key data
            CFDataAppendBytes(mutableData, CFDataGetBytePtr(data as CFData), CFDataGetLength(data as CFData))
            CFDataDeleteBytes(mutableData, CFRangeMake(CFIndex(0), headerSize))

            //Use the mutableData here (SecKeyCreateWithData)
            let publicKey = SecKeyCreateWithData(
                mutableData!,
                [
                    kSecAttrKeyType: kSecAttrKeyTypeEC,
                    kSecAttrKeyClass: kSecAttrKeyClassPublic,
                ] as NSDictionary,
                &error)
            
            let fullKey = SecKeyCopyExternalRepresentation(publicKey!, &error)
            
            return fullKey!
        }

Here I can read the "publicKey", I know it has some value inside it. How can I turn this into a usable key for me to generate the shared secret?

TLDR: I want to read a public key that comes from the Java server (ECDH) to generate a symmetric key for encryption in the iOS client.


Solution

  • The complete flow would look like:

    • receiving 91 bytes with the public key from server side
    • create a SecKey with SecKeyCreateWithData
    • create a key pair on iOS with SecKeyCreateRandomKey
    • send own public key to server side
    • server side can compute the shared secret with that information
    • client computes a shared secret with SecKeyCopyKeyExchangeResult
    • if everything is correct, it should give the same shared secret on iOS and Java side

    Therefore, to get a complete test case, one can write a Java program that generates a key pair. For simplicity, one can copy/paste the public key between the Java and iOS app for a test instead of using a network connection. The Java program writes the public key to the console. This key is copied into the Swift source code. The Swift program is compiled and generates a key pair as well. The public key is copied / pasted to the Java program, which reads it on the console. Both programs then output the calculated shared secret, which should be the same for obvious reasons, since it is used for further symmetric encryption.

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

    iOS Swift Code

    The following code assumes that a secp256r1 curve is used with a key size of 256 bit.

    The described flow could be implemented as follows:

        let otherKey = "3059301306072a8648ce3d020106082a8648ce3d03010703420004df96b3c0c651707c93418781b91782319f6e798550d954c46ac7318c7eac130f96380991a93049059e03e4190dd147b64d6ebc57320938f026844bda3de22352".hexadecimal!
        
        guard let otherPublicKey = otherPublicKey(data: otherKey) else { return }
        guard let ownPrivateKey = createOwnKey() else { return }
        guard let ownPublicKey = SecKeyCopyPublicKey(ownPrivateKey) else { return }
        
        send(ownPublicKey: ownPublicKey)
        
        if let sharedSecret = computeSharedSecret(ownPrivateKey: ownPrivateKey, otherPublicKey: otherPublicKey) {
            print("shared secret: \(sharedSecret.hexadecimal)")
        } else {
            print("shared secret computation failed")
        }
        
    

    The used functions:

    private func otherPublicKey(data: Data) -> SecKey? {
        var error: Unmanaged<CFError>? = nil
        
        let cfData = data.dropFirst(26) as CFData
        
        let attributes =  [
            kSecAttrKeyType: kSecAttrKeyTypeEC,
            kSecAttrKeyClass: kSecAttrKeyClassPublic,
        ] as CFDictionary
        
        if let publicKey = SecKeyCreateWithData(cfData, attributes, &error) {
            return publicKey
        }
        print("other EC public: \(String(describing: error))")
        return nil
    }
    
    private func createOwnKey() -> SecKey? {
        var error: Unmanaged<CFError>? = nil
        let keyPairAttr: [String : Any] = [kSecAttrKeySizeInBits as String: 256,
                                           kSecAttrKeyType as String: kSecAttrKeyTypeECSECPrimeRandom,
                                           kSecPrivateKeyAttrs as String: [kSecAttrIsPermanent as String: false]
        ]
        guard let key = SecKeyCreateRandomKey(keyPairAttr as CFDictionary, &error) else {
            print("key creation: \(String(describing: error))")
            return nil
        }
        return key
    }
    

    This function send only outputs the key in hex on the debug console. For the test it can be transferred to the Java program via copy/paste. In a real program it would be transferred to the server via a network connection.

    private func send(ownPublicKey: SecKey) {
        guard let data = SecKeyCopyExternalRepresentation(ownPublicKey, nil) as Data? else {
            print("SecKeyCopyExternalRepresentation failed")
            return
        }
        let secp256r1Header = "3059301306072a8648ce3d020106082a8648ce3d030107034200"
        let pkWithHeader = secp256r1Header + data.hexadecimal
        print("ownPublicKeyHexWithHeader \(pkWithHeader.count / 2) bytes: " + pkWithHeader)
    }
    

    With the own private key and the public key of the server, the shared secret can be computed.

    private func computeSharedSecret(ownPrivateKey: SecKey, otherPublicKey: SecKey) -> Data? {
        let algorithm:SecKeyAlgorithm = SecKeyAlgorithm.ecdhKeyExchangeStandard
        let params = [SecKeyKeyExchangeParameter.requestedSize.rawValue: 32, SecKeyKeyExchangeParameter.sharedInfo.rawValue: Data()] as [String: Any]
        
        var error: Unmanaged<CFError>? = nil
        if let sharedSecret: Data = SecKeyCopyKeyExchangeResult(ownPrivateKey, algorithm, otherPublicKey, params as CFDictionary, &error) as Data? {
            return sharedSecret
        } else {
            print("key exchange: \(String(describing: error))")
        }
        return nil
    }
    

    Test

    In the upper area you can see the Xcode console and in the lower area the output of the Java program. The common secret is the same. So the test was successful.

    Test ECDH Java/Swift