Search code examples
javaandroidc++java-native-interfacecrypto++

ECDH Shared Secret does not match between Crypto++ and Android


So I am writing an ECDH implementation on Android using Java and the Crypto++ 5.6.3 Library.

I wrote some C++ JNI code to call Crypto++ functions, I have one function to generate the public/private key pair, and another function to extract the shared secret. There seems to be an issue however with the shared secrets not matching.

Th situation is as follows. Alice and Bob both generate their own Public and Private Key Pairs. They exchange public keys successfully.

To get the shared secret Alice does the following :

byte[] sharedSecret = getSharedSecret(bobPublicKey, alicePrivateKey);

Bob does a similar operation :

byte[] sharedSecret = getSharedSecret(alicePublicKey, bobPrivateKey);

The issue I am seeing is, the two shared secrets do not match each other. Is there some misunderstanding on my part on how this is supposed to work?

I am assuming there is just a specific implementation issue on my side relating to the shared secret but I am not sure. The C++ JNI implementation is below. The retrieveSharedSecret function always outputs "It Worked". Any ideas on what I am doing wrong here?

JNIEXPORT jobject JNICALL Java_com_myproject_test_cryptopp_ECDHLibrary_generateKeyPair
        (JNIEnv *env, jclass)
{
    // Generate a public private key pair using ECDH (Elliptic Curve Diffie Hellman)
    OID CURVE = secp256r1(); // the key is 256 bits (32 bytes) long
    AutoSeededRandomPool rng;

    // Because we are using point compression
    // Private Key 32 bytes
    // Public Key 33 bytes
    // If compression was not used the public key would be 65 bytes long
    ECDH < ECP >::Domain dhA( CURVE );
    dhA.AccessGroupParameters().SetPointCompression(true);

    SecByteBlock privA(dhA.PrivateKeyLength()), pubA(dhA.PublicKeyLength());
    dhA.GenerateKeyPair(rng, privA, pubA);

    jobject publicKeyByteBuffer = (*env).NewDirectByteBuffer(pubA.BytePtr(), pubA.SizeInBytes());
    jobject privateKeyByteBuffer = (*env).NewDirectByteBuffer(privA.BytePtr(), privA.SizeInBytes());

    // Return the ECDH Key Pair back as our custom Java ECDHKeyPair class object
    jclass keyPairClass = (*env).FindClass("com/myproject/test/cryptopp/ECDHKeyPair");
    jmethodID midConstructor = (*env).GetMethodID(keyPairClass, "<init>", "(Ljava/nio/ByteBuffer;Ljava/nio/ByteBuffer;)V");
    jobject keyPairObject = (*env).NewObject(keyPairClass, midConstructor, publicKeyByteBuffer, privateKeyByteBuffer);

    return keyPairObject;
}

JNIEXPORT jobject JNICALL Java_com_myproject_test_cryptopp_ECDHLibrary_retrieveSharedSecret
        (JNIEnv *env, jclass, jbyteArray publicKeyArray, jbyteArray privateKeyArray)
{
    // Use the same ECDH Setup that is specified in the generateKeyPair method above
    OID CURVE = secp256r1();
    DL_GroupParameters_EC<ECP> params(CURVE);
    ECDH<ECP>::Domain dhAgreement(params);
    dhAgreement.AccessGroupParameters().SetPointCompression(true);

    // Figure out how big the public and private keys are
    // Public Key: This belongs to the other user
    // Private Key: This is out personal private key
    int pubLen = (int)(*env).GetArrayLength(publicKeyArray);
    int privLen = (int)(*env).GetArrayLength(privateKeyArray);

    // Convert the keys from a jbyteArray to a SecByteBlock so that they can be passed
    // into the CryptoPP Library functions.
    unsigned char* pubData = new unsigned char[pubLen];
    (*env).GetByteArrayRegion(publicKeyArray, 0, pubLen, reinterpret_cast<jbyte*>(pubData));

    unsigned char* privData = new unsigned char[privLen];
    (*env).GetByteArrayRegion(privateKeyArray, 0, privLen, reinterpret_cast<jbyte*>(privData));

    SecByteBlock pubB(pubData, pubLen) , privA(privData, privLen);

    // Now extract shared secret between the two keys
    SecByteBlock sharedSecretByteBlock(dhAgreement.AgreedValueLength());
    ALOG("Shared Agreed Value Length: %d", dhAgreement.AgreedValueLength());

    bool didWork = dhAgreement.Agree(sharedSecretByteBlock, privA, pubB);

    ALOG("Key Agreement: %s", didWork ? "It Worked" : "It Failed");
    ALOG("Shared Secret Byte Size: %d", sharedSecretByteBlock.SizeInBytes());

    // Return the shared secret as a Java ByteBuffer
    jobject publicKeyByteBuffer = (*env).NewDirectByteBuffer(sharedSecretByteBlock.BytePtr(), sharedSecretByteBlock.SizeInBytes());

    return publicKeyByteBuffer;
}

EDIT: I put my test project up on Github here so that others can take a look and try their own luck. Contains some instructions in the README on how to get it up and running.


Solution

  • I was able to figure it out with some help from a friend. The issue was the retrieveSharedSecret method and the fact that it was directly returning a byte buffer, which was pointing to a memory address that was in scope during the C++ method call, but then out of scope as soon it got back out into the Java code. So I was essentially getting garbage memory as my shared secret.

    I tweaked the code so that the method returns a custom SharedSecret Java object, just like the keyGeneration method does. Doing that allows all the info I need to be copied over properly I and don't have to worry about this scope issue.

    Revised method code is below. I will also update the Github project so that it can exist as a working example of how to use Android Studio with the NDK (non-experimental) and CryptoPP.

    // Use the same ECDH Setup that is specified in the generateKeyPair method above
    OID CURVE = secp256r1();
    DL_GroupParameters_EC<ECP> params(CURVE);
    ECDH<ECP>::Domain dhAgreement(params);
    dhAgreement.AccessGroupParameters().SetPointCompression(true);
    
    // Figure out how big the public and private keys are
    // Public Key: This belongs to the other user
    // Private Key: This is out personal private key
    int pubLen = (int)(*env).GetArrayLength(publicKeyArray);
    int privLen = (int)(*env).GetArrayLength(privateKeyArray);
    
    // Convert the keys from a jbyteArray to a SecByteBlock so that they can be passed
    // into the CryptoPP Library functions.
    unsigned char* pubData = new unsigned char[pubLen];
    (*env).GetByteArrayRegion(publicKeyArray, 0, pubLen, reinterpret_cast<jbyte*>(pubData));
    
    unsigned char* privData = new unsigned char[privLen];
    (*env).GetByteArrayRegion(privateKeyArray, 0, privLen, reinterpret_cast<jbyte*>(privData));
    
    SecByteBlock pubB(pubData, pubLen) , privA(privData, privLen);
    
    // Now extract shared secret between the two keys
    SecByteBlock sharedSecretByteBlock(dhAgreement.AgreedValueLength());
    ALOG("Shared Agreed Value Length: %d", dhAgreement.AgreedValueLength());
    
    bool didWork = dhAgreement.Agree(sharedSecretByteBlock, privA, pubB);
    
    ALOG("Key Agreement: %s", didWork ? "It Worked" : "It Failed");
    ALOG("Shared Secret Byte Size: %d", sharedSecretByteBlock.SizeInBytes());
    
    // Return the shared secret as a Java ByteBuffer
    jobject sharedSecretByteBuffer = (*env).NewDirectByteBuffer(sharedSecretByteBlock.BytePtr(), sharedSecretByteBlock.SizeInBytes());
    
    // Return the ECDH Key Pair back as a Java ECDHKeyPair object
    jclass keyPairClass = (*env).FindClass("com/tcolligan/ecdhtest/SharedSecret");
    jmethodID midConstructor = (*env).GetMethodID(keyPairClass, "<init>", "(Ljava/nio/ByteBuffer;)V");
    jobject sharedSecretObject = (*env).NewObject(keyPairClass, midConstructor, sharedSecretByteBuffer);
    
    return sharedSecretObject;