0

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"
codeasus
  • 3
  • 2

2 Answers2

0

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/.
Topaco
  • 40,594
  • 4
  • 35
  • 62
0

Based on Topaco and this answer, you can fix the issue from the iOS side by adding the ASN.1/DER header to the SecKeyCopyExternalRepresentation result.

func pubKeyDataForAndroid(keyData: CFData) -> CFData {
    guard let mutableData = CFDataCreateMutable(kCFAllocatorDefault, CFIndex(0)) else {
        fatalError("Can not create key data")
    }
    var headerBytes = [0x30, 0x59, 0x30, 0x13, 0x06, 0x07, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x02, 0x01, 0x06, 0x08, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x03, 0x01, 0x07, 0x03, 0x42, 0x00] as [UInt8]
    let headerSize = 26
    CFDataAppendBytes(mutableData, &headerBytes, headerSize)
    CFDataAppendBytes(mutableData, CFDataGetBytePtr(keyData), CFDataGetLength(keyData))
    return mutableData as CFData
}

After calling this method, you can convert the result to base64 and send it to Android. Finally, convert the base64 string to a Java PubKey, which should produce identical keys.

  • You can take this shortcut (i.e. putting the leading byte sequence in front of the uncompressed key) in the Kotlin code as well. But this is rather a workaround, since it only works for *secp256r1* and an *uncompressed* key. More robust is the *calculation* as in the Kotlin code (which is probably also supported on the iOS side via corresponding libraries). – Topaco Mar 06 '23 at 14:13