0

I have a problem verifying key exchange with the server using Elliptic Curve Diffie Hellman. The key I sent doesn't work for some reason, the server can not use it or recognize it, and I don't know why, even though I have followed the documentation. Key exchange works from iOS, though. So only the Android is the problem. Here is my code for generating key pairs using AndroidKeystore:

private fun getKeyPair(): KeyPair? {

    val keyPair: KeyPair?
    val keyPairGenerator: KeyPairGenerator = KeyPairGenerator.getInstance(
        KeyProperties.KEY_ALGORITHM_EC,
        "AndroidKeyStore"
    )
    keyPairGenerator.initialize(
        KeyGenParameterSpec.Builder(alias, KeyProperties.PURPOSE_AGREE_KEY)
            .setAlgorithmParameterSpec(ECGenParameterSpec("secp256r1"))
            .build()
    )
    keyPair = keyPairGenerator.generateKeyPair()
    return keyPair
}

Generating public key and send to server:

fun getPublicKey(): String? {
    val keyPair = getKeyPair()
    return Base64.encodeToString(keyPair?.public?.encoded, Base64.NO_WRAP)
}

Even though I would like to stick with the AndroidKeyStore, I have also tried BouncyCastle but the same thing happens. I would greatly appreciate any help.

5
  • 1
    Post an example key that causes problems. Maybe the key is OK and you are making a mistake when importing on the iOS side. Commented Feb 25, 2025 at 13:55
  • 2
    On my machine, the code generates a valid Base64, ASN.1/DER encoded public key in X.509/SPKI format. Commented Feb 25, 2025 at 14:22
  • 1
    Note that getPublicKey() creates a new key pair (as alias). If a key pair was previously generated, this will be overwritten, so that the private and public keys may not be associated. Commented Feb 25, 2025 at 14:44
  • @Topaco here is the example of the generated key: MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEZ5DrInTCSTjtozTfIhLXIl5FARD+the3I4rqa+NiQzXttruqhEoWwsd+xl9HG09sVwD7Sc1ckwoTmsUPsFeHzQ== Commented Feb 25, 2025 at 15:01
  • The key is fine, s. lapo.it/asn1js/…. I suspect that the generation of a new key pair when getting the public key results in key inconsistencies and is the cause of your problem, see my answer. Commented Feb 25, 2025 at 17:03

1 Answer 1

0

Your getKeyPair() generates a new key pair each time it is executed, which is referenced with alias (i.e. the previous key pair gets lost).
Since your getPublic() calls getKeyPair() internally, a new key pair is generated when it is called. This design increases the risk of unrelated private and public keys, which could be a possible cause of the problem.

Example: If getKeyPair() is called first, then the key agreement is generated with the private key (and the public key of the other side) and finally the public key is exported with getPublic(), a new key pair is generated in the last step, so that the private key used for the key agreement is not related to the exported public key for the other side so that a different key agreement is generated on the other side.

Whether this or something similar applies cannot be said with certainty, since it is not clear from your post in which order key generation, generation of the key agreement and export of the public key take place.

But to avoid such problems you have to make sure related keys are used. To do this, the key generation should be removed from getPublicKey(). The public key should be determined in getPublicKey() from the AndroidKeyStore using the alias (as in the following example).


Sample implementation (both sides Android):

// create A-side keys and export public key
val privateKeyA = getKeyPair("a-side")?.private
val exportedPublicKeyA = getPublicKey("a-side")

// create B-side keys and export public key
val privateKeyB = getKeyPair("b-side")?.private
val exportedPublicKeyB = getPublicKey("b-side")

// import pubic B-side key and create A-side key agreement
val keyFactory = KeyFactory.getInstance("EC")
val publicKeyB = keyFactory.generatePublic(X509EncodedKeySpec(Base64.decode(exportedPublicKeyB, Base64.NO_WRAP)));
println(Base64.encodeToString(getKeyAgreement(privateKeyA, publicKeyB), Base64.NO_WRAP));

// import pubic A-side key and create B-side key agreement
val publicKeyA = keyFactory.generatePublic(X509EncodedKeySpec(Base64.decode(exportedPublicKeyA, Base64.NO_WRAP)));
println(Base64.encodeToString(getKeyAgreement(privateKeyB, publicKeyA), Base64.NO_WRAP));

In this use case, getPublicKey() reads the public key from the AndroidKeyStore using the alias for which there must be a key pair. getKeyPair() is essentially unchanged (only the alias is additionally passed). getKeyAgreement() calculates the key agreement (to demonstrate that both sides do indeed generate the same key agreement).

fun getPublicKey(alias: String): String? {
    val ks: KeyStore = KeyStore.getInstance("AndroidKeyStore").apply {
        load(null)
    }
    val entry = ks.getEntry(alias, null) as? KeyStore.PrivateKeyEntry
    val key = entry?.certificate?.publicKey;
    return Base64.encodeToString(key?.encoded, Base64.NO_WRAP)
}
fun getKeyPair(alias: String): KeyPair? {
    val keyPair: KeyPair?
    val keyPairGenerator: KeyPairGenerator = KeyPairGenerator.getInstance(
        KeyProperties.KEY_ALGORITHM_EC,
        "AndroidKeyStore"
    )
    keyPairGenerator.initialize(
        KeyGenParameterSpec.Builder(alias, KeyProperties.PURPOSE_AGREE_KEY)
            .setAlgorithmParameterSpec(ECGenParameterSpec("secp256r1"))
            .build()
    )
    keyPair = keyPairGenerator.generateKeyPair()
    return keyPair
}
fun getKeyAgreement(priv1: PrivateKey?, pub2: PublicKey?): ByteArray {
    val ka = KeyAgreement.getInstance("ECDH")
    ka.init(priv1)
    ka.doPhase(pub2, true)
    return ka.generateSecret("ECDH").encoded
}
Sign up to request clarification or add additional context in comments.

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.