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"
The keys are identical, only their formats differ:
BAlW...
is an uncompressed public EC key (Base64 encoded).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:
04|x|y
.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/.