Description of the intended goal
I'm trying to implement OpenSSL-generated public/private key pairs in Android/Kotlin using JNI, in order to implement user encryption on the information stored to the cloud server. I've compiled all OpenSSL source code and generated all .so files correcly.
The code (C++)
The C++ code to use OpenSSL is shown below. CmakeLists.txt and NDK configuration is working fine.
extern "C"
JNIEXPORT jobjectArray JNICALL
Java_eu_joober_ui_entry_SplashFragment_generateRSAKeyPair(JNIEnv *env, jobject thiz) {
int ret = 0;
RSA *r = nullptr;
BIGNUM *bne = nullptr;
BIO *bp_public = nullptr, *bp_private = nullptr;
int bits = 2048;
unsigned long e = RSA_F4;
jstring public_key_text;
jstring private_key_text;
jobjectArray returnPair = env->NewObjectArray(2, env->FindClass("java/lang/String"),nullptr);
// 1. generate rsa key
bne = BN_new();
ret = BN_set_word(bne,e);
if(ret != 1){
goto free_all;
}
r = RSA_new();
ret = RSA_generate_key_ex(r, bits, bne, nullptr);
if(ret != 1){
goto free_all;
}
// 2. get public key
bp_public = BIO_new(BIO_s_mem());
ret = PEM_write_bio_RSAPublicKey(bp_public, r);
BIO_get_mem_data(bp_public, &public_key_text);
if(ret != 1){
goto free_all;
}
// 3. get private key
bp_private = BIO_new(BIO_s_mem());
ret = PEM_write_bio_RSAPrivateKey(bp_private, r, nullptr, nullptr, 0, nullptr, nullptr);
BIO_get_mem_data(bp_private, &private_key_text);
// Check public and private keys were generated correctly
__android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, "Public key is: \n%s",public_key_text);
__android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, "Private key is: \n%s",private_key_text);
// 4. free
free_all:
BIO_free_all(bp_public);
BIO_free_all(bp_private);
RSA_free(r);
BN_free(bne);
// 5. Return strings using jobjectArray
if (ret == 1) {
env->SetObjectArrayElement(returnPair, 0, public_key_text);
env->SetObjectArrayElement(returnPair, 1, private_key_text);
return returnPair;
}
else {
return nullptr;
}
}
The error
If I check the Android Logcat, both public and private key seem to be generated correctly (as per __android_log_print output) but the app crashes with the following error when env->SetObjectArrayElement(returnPair, 0, public_key_text); is called:
JNI DETECTED ERROR IN APPLICATION: use of invalid jobject
The IDE (Android Studio) does not complain on any error, and the log suggests that key pair is being generated correctly, but I don't know why the keys are not being stored in the jobjectArray correctly. In fact, if I just simply put:
env->SetObjectArrayElement(returnPair, 0, env->NewStringUTF("Hello"));
env->SetObjectArrayElement(returnPair, 1, env->NewStringUTF("World"));
the code works completely fine, my Kotlin code gets the Strings correctly ("Hello" and "World"), and the app does not crash, which makes me think that problem is only on the C++ side.
The question
What I am doing wrong? I have checked other SO questions like JNI converting jstring to char * or jstring return in JNI program with slight modifications and combinations with no luck.
SIDE NOTE: I'm using OpenSSL implementation with C++ because Android/Kotlin code does not provide the private key generated using KeyPairGenerator.getInstance() and generatePair() (only public key can be retrieved from Keystore), which I need to be stored in a different place so that user information can be retrieved even if the app is uninstalled, as every subsequent call to generatePair() will lead to a different key pair. If you know a different approach to this problem I am more than welcome to any suggestions you may provide.
You never created a java string out of public_key_text
Try
char * public_key_text;
...
BIO_get_mem_data(bp_public, &public_key_text);
...
env->SetObjectArrayElement(returnPair, 0, env->NewStringUTF(public_key_text));
I call a function in C++ from java. In java I have an array of Strings that I want to use in my C++-function.
I have in C++:
std::string names[6]; // Global variable
extern "C"
JNIEXPORT void JNICALL
Java_com_erikbylow_mycamera3_JNIUtils_updateStandingBoard(JNIEnv *env, jobject type, std::string *names, jint nbrElements){
memcpy(standingText, names, 6* sizeof(std::string));
nbrStandText = nbrElements;
}
In `Java`:
public static void updateStanding( String resultArray[]){
updateStandingBoard(resultArray, resultArray.length);
}
What is the simplest way of achieving what I want? When I try this and different variants it either crashes or yields nonsense data.
JNI is a primarily a C API, it doesn't know anything about std::string as you can validate by calling javah on the Java source file contained the native methods declaration.
Also Java isn't C, there is no need to pass the array size as additional parameter.
So your native void updateStandingBoard(String[] result, int size) should actually be native void updateStandingBoard(String[] result)
With this in mind, the JNI code should be
std::vector<std::string> names; // much safer or use std::array as alternative
extern "C"
JNIEXPORT void JNICALL
Java_com_erikbylow_mycamera3_JNIUtils_updateStandingBoard(JNIEnv *env, jobject type, jobjectArray names) {
jint nbrElements = env->GetArrayLength(names);
// now copy the strings into C++ world
for (int i = 0; i < nbrElements ; i++) {
// access the current string element
jobject elem = env->GetObjectArrayElement(names, i);
jsize length = env->GetStringLength(elem);
// pin it to avoid GC moving it around during the copy
const jchar *str = env->GetStringChars(elem, nullptr);
names.push_back(std::string(str, length));
// make it available again to the VM
env->ReleaseStringChars(elem, str);
}
}
This was just for the basic strings, if you are interested in UTF strings, then you should make use of std::wstring and the UTF variants of the above JNI functions.
Does anyone know how to solve the error?
JNIEXPORT jstring JNICALL JAVA_com_pfc_camera_ndkmain_MainActivity_compresion(JNIEnv* env, jobject obj, jobjectArray jargv){
//jargv is a Java array of Java strings
int argc = env->GetArrayLength(jargv);
typedef char *pchar;
pchar *argv = new pchar[argc];
int i;
for(i=0; i<argc; i++)
{
jstring js = env->GetObjectArrayElement(jargv, i); //A Java string
const char *pjc = env->GetStringUTFChars(js); //A pointer to a Java-managed char buffer
size_t jslen = strlen(pjc);
argv[i] = new char[jslen+1]; //Extra char for the terminating null
strcpy(argv[i], pjc); //Copy to *our* buffer. We could omit that, but IMHO this is cleaner. Also, const correctness.
env->ReleaseStringUTFChars(js, pjc);
}
//Call main
Principal *pa=Principal::CreateInstance(argc,argv);
pa->Run();
pa->FreeInstance();
//Now free the array
for(i=0;i<argc;i++)
delete [] argv[i];
delete [] argv;
}
I understand that the error can come from not doing a casting but I do not have it very clear
[ https://i.stack.imgur.com/bOWKZ.png][1]
It seems that the problem has been solved, now I get another error but I do not understand if I'm passing two arguments js and pjc
[ https://i.stack.imgur.com/UHCAR.png][1]
In C++ you have to use explicit conversion to your desired type.
jstring js = (jstring)env->GetObjectArrayElement(jargv, i);
You can learn about jni programming here
Regarding your other question do the following:
const jbyte *pjc = env->GetStringUTFChars(js, NULL);
I have a method in JNI C/C++ which takes jstring and returns back jstring some thing like as below,
NATIVE_CALL(jstring, method)(JNIEnv * env, jobject obj, jstring filename)
{
// Get jstring into C string format.
const char* cs = env->GetStringUTFChars (filename, NULL);
char *file_path = new char [strlen (cs) + 1]; // +1 for null terminator
sprintf (file_path, "%s", cs);
env->ReleaseStringUTFChars (filename, cs);
reason_code = INTERNAL_FAILURE;
char* info = start_module(file_path);
jstring jinfo ;
if(info==NULL)
{
jinfo = env->NewStringUTF(NULL);
}
else
{
jinfo = env->NewStringUTF(info);
}
delete info;
info = NULL;
return jinfo;
}
The code works perfectly with prior android 4.0 versions like 2.2,2.3 and so on. With ICS 4.0 check JNI is on by default and because of it the app crashes throwing the following error
08-25 22:16:35.480: W/dalvikvm(24027): **JNI WARNING: input is not valid Modified UTF-8: illegal continuation byte 0x40**
08-25 22:16:35.480: W/dalvikvm(24027):
08-25 22:16:35.480: W/dalvikvm(24027): ==========
08-25 22:16:35.480: W/dalvikvm(24027): /tmp/create
08-25 22:16:35.480: W/dalvikvm(24027): ==========
08-25 22:16:35.480: W/dalvikvm(24027): databytes,indoorgames,drop
08-25 22:16:35.480: W/dalvikvm(24027): ==========���c_ag����ϋ#�ډ#�����#'
08-25 22:16:35.480: W/dalvikvm(24027): in Lincom/inter /ndk/comNDK;.rootNDK:(Ljava/lang/String;)Ljava/lang/String; **(NewStringUTF)**
08-25 22:16:35.480: I/dalvikvm(24027): "main" prio=5 tid=1 NATIVE
08-25 22:16:35.480: I/dalvikvm(24027): | group="main" sCount=0 dsCount=0 obj=0x40a4b460 self=0x1be1850
08-25 22:16:35.480: I/dalvikvm(24027): | sysTid=24027 nice=0 sched=0/0 cgrp=default handle=1074255080
08-25 22:16:35.490: I/dalvikvm(24027): | schedstat=( 49658000 26700000 48 ) utm=1 stm=3 core=1
08-25 22:16:35.490: I/dalvikvm(24027): at comrootNDK(Native Method)
I am clueless as to where i am wrong. If you see above NewStringUTF is adding some garbage value to the c Char* bytes .
Any idea about why this is happening
Any alternative solution to achieve the above is welcome
I really appreciate if one of you can help me in . Thanks in advance
regds
me
The cause of this problem is directly related to a known UTF-8 bug in the NDK/JNI GetStringUTFChars() function (and probably related functions like NewStringUTF). These NDK functions do not convert supplementary Unicode characters (i.e., Unicode characters with a value of U+10000 and above) correctly. This leads to incorrect UTF-8 and subsequent crashes.
I encountered the crash when handling user input text that contained emoticon characters (see the corresponding Unicode chart). Emoticon characters lie in the Supplementary Unicode character range.
Analysis of the Problem
The Java client passes a string containing a supplementary Unicode
character to JNI/NDK.
JNI uses the NDK function GetStringUTFChars() to extract the contents of the Java string.
GetStringUTFChars() returns the string data as incorrect and invalid UTF-8.
There is a known NDK bug whereby GetStringUTFChars() incorrectly converts supplementary Unicode characters, producing an incorrect and invalid UTF-8 sequence.
In my case, the resulting string was a JSON buffer. When the buffer was passed to the JSON parser, the parser promptly failed because one of the UTF-8 characters of the extracted UTF-8 had an invalid UTF-8 prefix byte.
Possible Workaround
The solution I've used can be summarized as follows:
The goal is to prevent GetStringUTFChars() from performing the
incorrect UTF-8 encoding of the supplementary Unicode character.
This is done by the Java client encoding the request string as
Base64.
The Base64-encoded request is passed to JNI.
JNI calls GetStringUTFChars(), which extracts the Base64-encoded
string without performing any UTF-8 encoding.
The JNI code then decodes the Base-64 data, producing the original
UTF-16 (wide char) request string, including the supplementary
Unicode character.
In this way we circumvent the problem of extracting supplementary Unicode characters from the Java string. Instead, we convert the data to Base-64 ASCII before calling GetStringUTFChars(), extract the Base-64 ASCII characters using GetStringUTFChars(), and convert the Base-64 data back to wide characters.
This is how I done this.
1- Char Array to JByteArray.
2- JByteArray to JString.
3- Return jstring to java side.
JNI Code; (.c) format
jstring Java_com_x_y_z_methodName(JNIEnv *env, jobject thiz) {
int size = 16;
char r[] = {'P', 'K', 'd', 'h', 't', 'X', 'M', 'm', 'r', '1', '8', 'n', '2', 'L', '9', 'K'};
jbyteArray array = (*env)->NewByteArray(env, size);
(*env)->SetByteArrayRegion(env, array, 0, size, r);
jstring strEncode = (*env)->NewStringUTF(env, "UTF-8");
jclass cls = (*env)->FindClass(env, "java/lang/String");
jmethodID ctor = (*env)->GetMethodID(env, cls, "<init>", "([BLjava/lang/String;)V");
jstring object = (jstring) (*env)->NewObject(env, cls, ctor, array, strEncode);
return object;
}
Java Code;
native String methodName();
Other Approach Not Works For Me;
I also tried return (*env)->NewStringUTF(env, r) but returns some characters that are not in the char array, at the end of the string where with the warning of JNI WARNING: input is not valid Modified UTF-8: illegal continuation byte 0x40.
Example; PKdhtXMmr18n2L9K�ؾ�����-DL
Edit:
C++ version
jstring clientStringFromStdString(JNIEnv *env,const std::string &str){
// return env->NewStringUTF(str.c_str());
jbyteArray array = env->NewByteArray(str.size());
env->SetByteArrayRegion(array, 0, str.size(), (const jbyte*)str.c_str());
jstring strEncode = env->NewStringUTF("UTF-8");
jclass cls = env->FindClass("java/lang/String");
jmethodID ctor = env->GetMethodID(cls, "<init>", "([BLjava/lang/String;)V");
jstring object = (jstring) env->NewObject(cls, ctor, array, strEncode);
return object;
}
I resolved this issue by returning byte array instead of String. On the Java side i am now converting the Byte array to Strings .Works fine! Stay away from using NewStringUTF() for Android 4.0 and above as there is already a bug reported on Google Android NDK.
I had this problem when I change the file Application.mk
From this line:
APP_STL := stlport_static
To:
APP_STL := gnustl_static
Once I changed it back again it fixed the issue.
Strings that you pass to NewStringUTF() need to be valid Modified UTF-8. It looks like the string returned by your start_Inauthroot() function is in some other encoding, or is just returning an invalid string. You need to convert the string to UTF-8 before passing it to JNI functions. Or you could use one of the charset-aware String constructors to build the String object instead.
In my opinion, its not a bug.
NewStringUTF Constructs a new java.lang.String object from an array of characters in modified UTF-8 encoding.
Modified UTF-8 is not standard UTF-8. See Modified UTF-8
In most cases, UTF-8 encoded string is valid Modified UTF-8. Because Modified UTF-8 and UTF-8 are quite similar. However when it comes to Unicode string beyond Basic Multilingual Plane, they are not compatible.
Solution:
pass UTF-8 bytes to Java layer and new String(bytes, "UTF-8") then pass jstring to JNI.
For me, the solution was to place the content on a const char*:
const char* string = name_sin.c_str();
jstring utf8 = env_r->NewStringUTF(string);
and the function:
jclass cls_Env = env_r->FindClass(CLASS_ACTIVITY_NAME);
jmethodID mid = env_r->GetMethodID(cls_Env, "Delegate",
"(Ljava/lang/String;)V");
//todo importante hacerlo asi, si pasas directamente c_str a veces da error de carater no UTF 8
const char* string = name_sin.c_str();
jstring utf8 = env_r->NewStringUTF(string);
env_r->CallVoidMethod(*object_r, mid, utf8);
env_r->DeleteLocalRef(utf8);
I also struggled with the same problem from the last day. Finally figured out a solution after a day .. I hope this reply may save someone's day..
The problem was I was calling another function within the native function, used the returned string directly and which caused crash in android older versions
So firstly I saved the string returned from another function to a variable then used it, and the problem gone :D
The below example may clear your concept
//older code with error
//here key_ is the string from java code
const char *key = env->GetStringUTFChars(key_, 0);
const char *keyx = getkey(key).c_str();
return env->NewStringUTF(keyx);
And here is how I solved this error
//newer code which is working
//here key_ is the string from java code
const char *key = env->GetStringUTFChars(key_, 0);
string k = getkey(key);
const char *keyx = k.c_str();
return env->NewStringUTF(keyx);
Happy coding :D
This works for me in c++
extern "C" JNIEXPORT
jstring Java_com_example_ndktest_MainActivity_TalkToJNI(JNIEnv* env, jobject javaThis, jstring strFromJava)
{
jboolean isCopy;
const char* szHTML = env->GetStringUTFChars(strFromJava, &isCopy);
std::string strMine;
strMine = szHTML;
strMine += " --- Hello from the JNI!!";
env->ReleaseStringUTFChars(strFromJava, szHTML);
return env->NewStringUTF(strMine.c_str());
}
c android ndk is working as follows
JNIEXPORT jstring JNICALL
Java_com_example_hellojni_HelloJni_stringFromJNI( JNIEnv* env,
jobject thiz,jstring str )
{
jboolean isCopy;
const char* szHTML = (*env)->GetStringUTFChars(env, str, &isCopy);
return (*env)->NewStringUTF(env, szHTML);
}
I want to access an array that is created and updated in the native C code efficiently. If need be, i could send a pointer or reference from Java code to the native side and have the C-code populate it so that i can read it from SDK side when it's ready to be consumed.
Currently, this is how i am doing it. But i think there can be better ways to do it, since i am doing one copy in C-side and then there an object that is created every time i issue a read on the Java-side.
My Java code:
double[] valuesFromNative = getValues();
public static native double[] getValues();
static { System.loadLibrary("test-jni"); }
My native (C and not C++) code:
#define LEN 18
double testDoubleArr[LEN];
jdoubleArray Java_com_test_testActivity_getValues(JNIEnv *env, jclass clazz) {
jboolean isCopy;
int i;
jdoubleArray result = (*env)->NewDoubleArray(env, LEN);
jdouble* destArrayElems = (*env)->GetDoubleArrayElements(env, result, &isCopy);
for (i = 0; i < LEN; i++) {
destArrayElems[i] = testDoubleArr[i];
}
if(isCopy == JNI_TRUE) {
// isCopy should NEVER be JNI_TRUE in this case, right?
// so, i could as well replace this condition with
// assert(isCopy == JNI_FALSE)?
}
return result;
}
This code snippet works - so, i am looking at more efficient or rather correct way to achieve the same thing.
Thanks for sharing your thoughts.
I think SetDoubleArrayRegion() would be faster. Less code and less JNI calls, that's for sure.
jdoubleArray result = (*env)->NewDoubleArray(env, LEN);
(*env)->SetDoubleArrayRegion(env, result, 0, LEN, testDoubleArr);
You don't even have to create the array on the C++ side. Declare the method like this:
public static native void getValues(double[] a);
Implement like this:
void Java_com_test_testActivity_getValues(JNIEnv *env, jclass clazz, jdoubleArray a)
{//...
Create the array on the Java side, cache it in an instance variable or something, and pass it to JNI to be filled whenever needed. Make sure the assumptions about array size are the same on the Java side and on the C side.