Search code examples
iosswiftbouncycastleecdhapple-cryptokit

How to generate PEM from Curve25519 public key


I want to use ECC (curve25519) with Diffie-Hellman Key exchange. Backend is using ECC with X9.62 and PKCS#8 encoding. I want to achieve same on iOS so that I can fetch data from backed, decrypt it and show it to user.

I tried this code but didn't work

func getPEM() -> String {
    let keyPair = Curve25519.Signing.PrivateKey()
    let pubKey = keyPair.publicKey
    let pem = "-----BEGIN PUBLIC KEY-----\(pubKey.rawRepresentation.base64EncodedString())-----END PUBLIC KEY-----"
    
    return pem
}

After searching on Google I found that In order to get PEM we need DER and ASN1 from my public key but they are not supported by CryptoKit.

Android is able to get right PEM using bouncycastle. Just for reference I am posting snippet from android code base.

import org.bouncycastle.asn1.x9.X9ECParameters
import org.bouncycastle.crypto.ec.CustomNamedCurves
import org.bouncycastle.jcajce.provider.asymmetric.util.EC5Util
import org.bouncycastle.jce.provider.BouncyCastleProvider
import java.security.*
import java.security.spec.ECParameterSpec
import java.security.spec.InvalidKeySpecException
import java.security.spec.PKCS8EncodedKeySpec
import java.security.spec.X509EncodedKeySpec

object CryptoKeyGenerator {

    init {
        Security.removeProvider(BOUNCY_CASTLE_IDENTIFIER)
        Security.addProvider(BouncyCastleProvider())
    }

    fun getClientKeyMaterial(): String {
        val keyPair = generateEphemeralKeyPair()
        val pemEncodedPublicKey = getPEMEncodedStream(keyPair.public, false)
        return pemEncodedPublicKey
    }

    private fun getPEMEncodedStream(key: Key, privateKey: Boolean): String {
        val pkcS8EncodedKeySpec = PKCS8EncodedKeySpec(key.encoded)
        val stringBuilder = StringBuilder()
        val keyType = if(privateKey) PRIVATE_KEY else PUBLIC_KEY
        stringBuilder.append(KEY_HEADER_START + keyType + KEY_HEADER_END)
        stringBuilder.append(CryptoUtils.getBase64Encoded(pkcS8EncodedKeySpec.encoded))
        stringBuilder.append(KEY_FOOTER_START + keyType + KEY_HEADER_END)
        return stringBuilder.toString()
    }

    /**
    * This method generates an ECC KeyPair with Curve25519 specs
    */
    private fun generateEphemeralKeyPair(): KeyPair {
        val keyPairGenerator = KeyPairGenerator.getInstance(EC_ALGO_IDENTIFIER, BOUNCY_CASTLE_IDENTIFIER)
        val eccParameters: X9ECParameters = CustomNamedCurves.getByName(ECC_CURVE_SPEC)
        val eccSpec: ECParameterSpec = EC5Util.convertToSpec(eccParameters)
        keyPairGenerator.initialize(eccSpec)
        return keyPairGenerator.generateKeyPair()
    }
}

Android generated PEM key look like this MIIBMTCB6gYHKoZIzj0CATCB3gIBATArBgcqhkjOPQEBAiB/////////////////////////////////////////7TBEBCAqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqYSRShRAQge0Je0Je0Je0Je0Je0Je0Je0Je0Je0Je0JgtenHcQyGQEQQQqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq0kWiCuGaG4oIa04B7dLHdI0UySPU1+bXxhsinpxaJ+ztPZAiAQAAAAAAAAAAAAAAAAAAAAFN753qL3nNZYEmMaXPXT7QIBCANCAAQwsdIRTVn2+6rlgqAhVvx7ERj/Oku0wHmZZU1OST617h95ygSP5zJOa9lNiKqZMArjtJh7yQ4rg7kUq08Nv8+Q

This repo has samples for java, c and Nodejs https://github.com/Sahamati/rahasya


Solution

  • With CryptoKit you won't be able to do this. You will need to use SwiftECC library(https://github.com/leif-ibsen/SwiftECC).

    Step 1 - Create a Domain specifically for Curve25519

    struct Curve25519Domain {
        static let domainName = "x25591"
        static func getInstance() -> Domain? {
            guard let p = BInt("7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffed", radix: 16),
                  let a = BInt("2aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa984914a144", radix: 16),
                  let b = BInt("7b425ed097b425ed097b425ed097b425ed097b425ed097b4260b5e9c7710c864", radix: 16),
                  let gx = BInt("2aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaad245a", radix: 16),
                  let gy = BInt("20ae19a1b8a086b4e01edd2c7748d14c923d4d7e6d7c61b229e9c5a27eced3d9", radix: 16),
                  let order = BInt("1000000000000000000000000000000014def9dea2f79cd65812631a5cf5d3ed", radix: 16) else {
                return nil
            }
            // returns nil if fails to create a domain
            return try? Domain.instance(name: domainName,
                                        p: p,
                                        a: a,
                                        b: b,
                                        gx: gx,
                                        gy: gy,
                                        order: order,
                                        cofactor: 8)
        }
    }
    

    Step 2 - Generate Public Key

    if let domain = Curve25519Domain.getInstance() {
        let keyPair = domain.makeKeyPair()
        let privateKey = keyPair.1
        let publicKey = keyPair.0
    }
    

    Step 3 - Encapsulate the public key in PEM format -

    func getPEMofPublicKey(publicKey: ECPublicKey) -> String? {
        let base64Encoded = Data(publicKey.der).base64EncodedString()
        let pemString = "-----BEGIN PUBLIC KEY-----\(base64Encoded)-----END PUBLIC KEY-----"
        return pemString
    }
    

    I have also posted a detailed solution in a medium article, which also includes decryption - https://medium.com/@k.mohsin11/decrypting-financial-data-in-account-aggregator-ios-app-30d672ad4426

    Let me know if you are still stuck.