Search code examples
androidkotlincryptographybouncycastleelliptic-curve

Not getting the right result in base64 string when I try to convert base64 encoded string EC Public Key generated on iOS to Java PublicKey


As the title suggests I am trying to convert base64 encoded string (EC Public Key) generated on IOS device(Swift) to Java PublicKey which will be used to calculate a Shared Secret Key between two parties. There is neither runtime nor compile time exception/error in the code, it compiles and runs successfully and generates a PublicKey but when I encode the PublicKey (Base64.encodeToString(PublicKey.encoded), Base64.NO_WRAP) back to Base64 string to confirm whether I have gotten the same public key I have passed as an argument, they are not the same.

import android.util.Base64
import org.bouncycastle.asn1.sec.SECNamedCurves
import org.bouncycastle.math.ec.ECCurve
import java.math.BigInteger
import java.nio.charset.StandardCharsets
import java.security.*
import java.security.spec.*
import javax.crypto.*
import javax.crypto.spec.IvParameterSpec
import javax.crypto.spec.SecretKeySpec
    
    
fun iosB64EncodedStrPKToPK(iOSB64EncodedPK: String): PublicKey {
    val decodedPK = Base64.decode(iOSB64EncodedPK, Base64.NO_WRAP)
    val x9ECParamSpec = SECNamedCurves.getByName("secp256r1")
    val curve = x9ECParamSpec.curve
    val point = curve.decodePoint(decodedPK)
    val xBcEC = point.affineXCoord.toBigInteger()
    val yBcEC = point.affineYCoord.toBigInteger()
    val gBcEC = x9ECParamSpec.g
    val xGBcEC = gBcEC.affineXCoord.toBigInteger()
    val yGBcEC = gBcEC.affineYCoord.toBigInteger()
    val hBcEC = x9ECParamSpec.h.toInt()
    val nBcEC = x9ECParamSpec.n
    val jPEC = ECPoint(xBcEC, yBcEC)
    val gJpEC = ECPoint(xGBcEC, yGBcEC)
    val jEllipticCurve = convertECCurveToEllipticCurve(curve, gJpEC, nBcEC, hBcEC)
    val eCParameterSpec = ECParameterSpec(jEllipticCurve, gJpEC, nBcEC, hBcEC)
    val ecPubLicKeySpec = ECPublicKeySpec(jPEC, eCParameterSpec)
    val keyFactorySpec = KeyFactory.getInstance("EC")
    return keyFactorySpec.generatePublic(ecPubLicKeySpec)
}
    
private fun convertECCurveToEllipticCurve(
    curve: ECCurve,
    ecPoint: ECPoint,
    n: BigInteger,
    h: Int
): EllipticCurve {
    val ecField = ECFieldFp(curve.field.characteristic)
    val firstCoefficient = curve.a.toBigInteger()
    val secondCoefficient = curve.b.toBigInteger()
    val ecParams = ECParameterSpec(
        EllipticCurve(ecField, firstCoefficient, secondCoefficient),
            ecPoint,
            n,
            h
        )
    return ecParams.curve
}

The public key I am passing to the iosB64EncodedStrPKToPK() function: BAlWWu46il/ly6Axd/qclmhEVhGth93QN5+h3JBJEKEmhKd1LfqkpCqX1cT1cQDs9nPq9Lq0/FtZitkjr7Rqd94=

The output I get: MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAECVZa7jqKX+XLoDF3+pyWaERWEa2H3dA3n6HckEkQoSaEp3Ut+qSkKpfVxPVxAOz2c+r0urT8W1mK2SOvtGp33g==

val pkIOS = "BAlWWu46il/ly6Axd/qclmhEVhGth93QN5+h3JBJEKEmhKd1LfqkpCqX1cT1cQDs9nPq9Lq0/FtZitkjr7Rqd94="

Log.i("SOME_TAG","PUBLIC_KEY_IOS:${Base64.encodeToString(iosB64EncodedStrPKToPK(pkIOS).encoded,Base64.NO_WRAP)}") 

I am not an expert on the matter, maybe someone may guide me in the right direction and can see the mistake I am making. Cryptography is out of my field expertise.

I have tried Googling, ChatGPT and other insightful resources, if you know a source around the issue I would gladly accept it too.

I am running the code in an Android Environment

The version of BouncyCastle I am using:

def bouncy_castle_version = '1.70'
implementation "org.bouncycastle:bcpkix-jdk15on:$bouncy_castle_version"
implementation "org.bouncycastle:bcprov-jdk15on:$bouncy_castle_version"

Solution

  • The keys are identical, only their formats differ:

    • The input BAlW... is an uncompressed public EC key (Base64 encoded).
    • The output MFkw... is an ASN.1/DER encoded key in X.509/SPKI format (Base64) encoded.

    This can be easily verified by encoding both keys not in Base64 but in hex:

    input :                                                     0409565aee3a8a5fe5cba03177fa9c9668445611ad87ddd0379fa1dc904910a12684a7752dfaa4a42a97d5c4f57100ecf673eaf4bab4fc5b598ad923afb46a77de
    output: 3059301306072a8648ce3d020106082a8648ce3d0301070342000409565aee3a8a5fe5cba03177fa9c9668445611ad87ddd0379fa1dc904910a12684a7752dfaa4a42a97d5c4f57100ecf673eaf4bab4fc5b598ad923afb46a77de
    

    As can be seen, the ASN.1/DER encoded X.509/SPKI key contains the uncompressed public key at the end (the last 65 bytes).


    Background:
    Keep in mind that a public EC key is a point (x, y) on an EC curve (obtained by multiplying the private key by the generator point) and that there are different formats for its representation, e.g. the following two:

    • The uncompressed format, which corresponds to the concatenation of a 0x04 byte, and the x and y coordinates of the point: 04|x|y.
      For the secp256r1 curve (aka prime256v1 aka NIST P-256), x and y are both 32 bytes, so the uncompressed key is 65 bytes.
    • The X.509/SPKI format as defined in RFC 5280. This format is described with ASN.1 and serialized/encoded with DER (s. here).
      PublicKey#getEncoded() returns the ASN.1/DER encoded X.509/SPKI key. With an ASN.1/DER parser the ASN.1/DER can be decoded, e.g. https://lapo.it/asn1js/.