UserDefault always returns empty result[cocos2dx-3.13.1]

UserDefault always returns empty result[cocos2dx-3.13.1]
0.0 0

#1

Hello, guys!
I ran into the problem of writing/reading data from UserDefault. So the instance of the problem is that when I’m trying to read data with
UserDefault::getInstance()->getStringForKey("key")
an result of the operation is always empty string. But is very strange that it’s prefectly works on Windows. Before reading data, of course, I have writed it with
UserDefault::getInstance()->setStringForKey("key", std::string("str")), but before this I used the xxtea encryption. Here is code:

//EncryptEngine.cpp
#include <cstdlib>
#include "cocos2d.h"
#include "EncryptEngine.h"

using std::string;

EncryptEngine* EncryptEngine::getInstance()
{
	static EncryptEngine* instance = new EncryptEngine;
	return instance;
}

string EncryptEngine::encrypt(int number)
{
	string tempStr = patch::to_string(number);
	xxtea_long dataLen = tempStr.length();
	unsigned char* data = new unsigned char[dataLen];
	strcpy((char*) data, tempStr.c_str());

	xxtea_long retLen = 0;
	unsigned char* result = xxtea_encrypt(data, dataLen, key, keyLen, &retLen);

	string retRes = std::string((char*) result);
	return retRes;
}

int EncryptEngine::decrypt(string tempStr)
{
	xxtea_long dataLen = tempStr.length();
	unsigned char* data = new unsigned char[dataLen];
	strcpy((char*) data, tempStr.c_str());

	xxtea_long retLen = 0;
	//temp str is empty
	cocos2d::log("key:%s, keyLen:%d, data:%s, dataLen:%d, tempstr:%s", key, keyLen, data, dataLen, tempStr.c_str());
	unsigned char* result = xxtea_decrypt(data, dataLen, key, keyLen, &retLen);
	cocos2d::log("after");

	if (result == 0)
		return 0;

	int retRes = atoi((char*) result);
	return retRes;
}

patch namespace here it’s just template-wrapper around std::to_string() for avoid ndk compilation error.
And here is using the write/read operations:
Write:

UserDefault::getInstance()->setStringForKey("ofvm5x", encrEn->encrypt(23));//garbage
UserDefault::getInstance()->setStringForKey("h84sza", encrEn->encrypt(45));//garbage
UserDefault::getInstance()->setStringForKey("d8m2wt", encrEn->encrypt(0));//money

Read:

p_instance->money = encrEn->decrypt(UserDefault::getInstance()->getStringForKey("d8m2wt"));
Here is just using obfuscated keys for transfer the values.
So I should describe the process:
Just first run through cocos run -p android is ok and applications works. But when I’m trying to restart application, he is just crashing with error in xxtea_decrypt method because of emptiness of the string value. I have debugging it with cocos2d::log and adb logcat, so I found that UserDefault returns empty string here:
std::string result = UserDefault::getInstance()->getStringForKey("d8m2wt")//money

I’m trying to solve it 3’th day and I’m really asking for help. Maybe @slackmoehrle or @iQD can help me :smiley:
Thank you very much :slight_smile:
Helpful info:
Cocos engine version = cocos2dx-3.13.1
Android NDK = r10e


Best way to storage player-data
#2

are you sure your encrypt and decrypt is working as expected? Can you step over each line to verify?


#3

Yes, I tested it for Windows, and on Windows enc/dec perfectly works. I think that there is not a huge difference between working on Windows/Android.


#4

I found this more. When I’m trying to read data instantly after writing in one application execute, I get empty string too, but there is an default value, i.e. UserDefault see variable with this key, but therein empty string .-.


#5

Sorry. Are you writing to UserDefaults in one application and reading from it in another?


#6

No, no, sorry for my english :smiley:
Just my main idea is save data between launches. On the first launch the game checks the existing of the file(special variable value) by uDef->getStringForKey("somevalue", "-1") and if the value is -1, we should write to the database the deаfault varibale values. But on second launch, the value of previous expression is empty string(but should be some data :smiley:) and because of this game trying to read variables data. And there we catch empty string :frowning:
Thank you very much for helping @slackmoehrle :slight_smile:


#7

You have to use cocos2d::base64Encode(...) & cocos2d::base64Decode(...)
For Encrypt:
after xxtea_encrypt pass result into base64Encode and store that base64 string into userdefault.

For Decrypt:
before xxtea_decrypt use base64Decode and output of that pass into xxtea_decrypt

Hope this helps.
if there is no other mistake then it will work.


#8

Does it work on Android without the encryption?
Do you call the flush function of UserDefault? This will ensure that the buffer is flushed/written to the disk. If you are not calling it, it’s not guaranteed, that the content of the buffer is written to the disk (depending on the OS), and so the value of the key will be empty.

It’s because the value for the key was not flushed to the disk in your first launch.
From the API:
std::string getStringForKey(const char* key); Get string value by key, if the key doesn't exist, will return an empty string.

This would of course ensure to store/move around the data safely to prevent the false handling of special characters, but I guess the problem is just the buffer not being written to the disk.


#9

No, i dont think so.
Because in latest cocos flush has empty method declaration.

I ran into similar problem before thats why i think base64 is need to use, as mostly there is problem storing string in user-default with special characters. and mostly there will be special character if you use xxtea_encrypt .


#10

Not for every implementation. E.g. MACOSX

void UserDefault::flush()
{
    [[NSUserDefaults standardUserDefaults] synchronize];
}

This is just how encryption algorithms work: on binary data. You should never treat encrypted data as strings in the first place. That’s why you need to encode it, as you did, if you want to store the data in text mode.
Storing encrypted data in UserDefault might not be the ideal solution anyway. Encrypting the UserDefault file itself would not lead to such problems and be even more safe. But as UserDefault is implemented differently (internal database vs. xml files), it behaves differently, and using another solution would be even better.

But back to the OT. To get to the issue, more data is needed:
Is it working without the encryption on any platform? If so, the issue is related to the encryption.

Is the key (when using encryption) written to UserDefault?
If not, there is something wrong with I/O. If yes, then the UserDefault function is having trouble with the content and is falling back to writing out an empty string.

Is the getter funtion returning a correct empty string because it is actually empty or is it returning an empty string because the key does not exist?


#11

Hi!
base64Encode and base64Decode just solve my problem! As said @smitpatel88 the problem was in the writting xxtea_encrypt raw data with lots of special ASCII characters and because of this string is not recorded. Thank you very much guys!!! @smitpatel88 @iQD @slackmoehrle


#12

But was something else recorded?

It’s not an issue of xxtea_encrypt, but an issue with the implementation of UserDefault/the underlying implementation of string/data handling.

So was value of the key actually an empty string, a string with special characters in it or was the issue just a missing key?

It would be interesting because of that:

Is it really a problem of not encoding the string, or a problem of the OS. the implementation of the I/O API, the internal database? Does it work on iOS/macOS/Linux/Unix without encoding it?
Maybe there is a bug in the implementation/how data/special characters are treated. They should be treated the same on every platform.


#13

Yes, on side of xxtea all is clear. Application has crashed on the xxtea_decrypt because of emptiness of string, and it is not xxtea fault. Of course it is interesting to know why Android can’t handle strange characters, but Windows can. Btw cocos community is best :slight_smile:


#14

Which data gets generated on Android for UserDefault?

Is the key entry written to it?

When you say “garbage”, what does that mean for the value? Is it NOT written, empty or is neither the key “ofvm5x” written to UserDefault? How does the entry for “ofvm5x” look on Android?

Is something written to UserDefault or nothing at all?

Can you provide your UserDefault file versions for Windows and Android?


#15

As I understood key is written to UserDefault file because since I am getting string by UserDefault::getInstance()->getStringForKey("key", "-1") result is not “-1” well then record with the “key” is exists in file, but she is empty. So here is contents of UserDefault on Windows before using base64 methods with raw xxtea output:

<?xml version="1.0" encoding="UTF-8"?>
<userDefaultRoot>
    <ofvm5x>՝ԩ	;μ/ofvm5x>
    <h84sza>֢ݦgt;��</h84sza>
    <d8m2wt>ѻʦgt;ϤT߼/d8m2wt>
    <xc0bzb>Γx勚:</xc0bzb>
    <ku90b8>ѻʦgt;ϤT߼/ku90b8>
    <3rd8yh>{OFZ2񫳼/3rd8yh>
    <oz4nxh>^dߟu7Ȑ</oz4nxh>
    <bx30qx>ہ뵭 ^|/bx30qx>
    <c56zxv>rjZ$ܘ</c56zxv>
    <mx92w2>E8Q⮂خ</mx92w2>
    <kp6mep>ђ̑ޥ;ڼ/kp6mep>
    <98h79q>ђ̑ޥ;ڼ/98h79q>
    <xajw4a>ђ̑ޥ;ڼ/xajw4a>
    <btle5d>ђ̑ޥ;ڼ/btle5d>
</userDefaultRoot>

Garbage values needs for confuse a hacker and try to prevent change of important data.
Unfortunately I don’t know how to get contents of Android’s UserDefault, could you explain me? :slight_smile:


#16

That does not add that much value to it, as an attacker has to decrypt the data at first and finding the correct value (which is not garbage) is an easy task compared to decrypting it.

Are you familiar with the Linux/Unix command line?
You need to use adb for that.

Assuming you have rooted your device.

adb shell pm path com.example.someapp (change that to your package name).
This gives the output as: package:/data/app/com.example.someapp-2.apk.

Then you need to change into that directory with the adb cd command:
adb cd /data/app/com.example.someapp-2.apk

The UserDefault file (named Cocos2dxPrefsFile.xml) is located under shared_prefs

You can get the file with the pull command: adb pull shared_prefs/Cocos2dxPrefsFile.xml
Or you can also use the full path. E.g. for pulling your apk: adb pull /data/app/com.example.someapp-2.apk

You can also list your files with e.g. adb ls -Ral yourDirectory

If your device is not rooted, you have to use adb exec-out run-as <your package name> <commands>, but keep in mind, that you cannot pull files. You need to cat it to a local file.

E.g.: adb exec-out run-as com.example.someapp cat ./shared_prefs/Cocos2dxPrefsFile.xml > Cocos2dxPrefsFile.xml
Cocos2dxPrefsFile.xml will be created in your local path on your desktop.

As you can see from the filename alone, cocos2d-x is not that consistent throughout various platforms regarding naming conventions, defaults, etc.


#17

Yes, I use linux sometimes :slight_smile: Thanks for nice guide to getting data from Android applications!
So I’ve been surprised when I saw PrefsFile without base64 operations:

<?xml version='1.0' encoding='utf-8' standalone='yes' ?>
<map>
    <string name="oz4nxh"></string>
    <string name="mx92w2"></string>
    <string name="98h79q"></string>
    <string name="ofvm5x"></string>
    <string name="h84sza"></string>
    <string name="bx30qx"></string>
    <string name="xajw4a"></string>
    <string name="c56zxv"></string>
    <string name="ku90b8"></string>
    <string name="xc0bzb"></string>
    <string name="3rd8yh"></string>
    <string name="d8m2wt"></string>
    <string name="btle5d"></string>
    <string name="kp6mep"></string>
</map>

I.e cocos just wrote keys instead of string values with special-characters, because I’ve checked the returned values by encrEn->encrypt(number) and they’re contained data before pass trough uDef->setStringForKey("key", std::string())


#18

You’re welcome.

You may want to check out a GUI oriented tool like this:

So it is really writing an empty string.
Writing the encoded encrypted data leads to the correct outcome also on Android. This concludes, that your encrypt function also works on Android.

What you can do is debugging the functions inside of Cocos2dxHelper.java

public static void setStringForKey(String key, String value) {
    SharedPreferences settings = sActivity.getSharedPreferences(Cocos2dxHelper.PREFS_NAME, 0);
    SharedPreferences.Editor editor = settings.edit();
    editor.putString(key, value);
    editor.commit();
}

Set some breakpoints and check the value of the parameter, which is passed in. If the passed in value is the encrypted contend, the putString function fails at handling special chars.

I guess it is related to the Java String implementation. In Java any character preceeded by a \ is considered an escape character.

What could someone learn from this: stay away and run circles around Java :wink:

Just found a nice quote on stack overflow regarding storing bitmap data in UserDefault:

Base64 is really a pretty ugly hack. It’s used when you’re putting in ugly hacks and trying to store binary data in a text based interface. If you ever need to use it, it means you’re trying to use a technology for something it was never meant to do and should probably stop.

Opinion based, of course :wink:


#19

So now all is clear :slight_smile:
Of course, base64 is terrible solution, but it is nice for saving small amount of data, like me. Now I looking for SQLite + wqsqlite for next projects, I hope he has less problems with saving on android than UserDeafults :wink:
Thank you very much for help with it!


#20

It does, as it supports blobs:

BLOB. The value is a blob of data, stored exactly as it was input.

The question is, if you need SQLite at all. Why add bloat and features, which are not really needed? Do you need dynamic querying? If not, a database is not the ideal solution. Most games use simple binary data as savegames. Nothing fancy.

With “controlled binary serialization” the potential uses are endless. There are many serialization libs out there. Here is a table which compares the “fancy” ones:
https://capnproto.org/news/2014-06-17-capnproto-flatbuffers-sbe.html

Maybe you should look at FlatBuffers (made for games by Google… hmmm ;)):

FlatBuffers is an efficient cross platform serialization library for C++, C#, C, Go, Java, JavaScript, PHP, and Python. It was originally created at Google for game development and other performance-critical applications.

And in case you did not know (well, IIRC for their now canceled CS):

Cocos2d-x, the #1 open source mobile game engine, uses it to serialize all their game data.

You are maybe interested in the following links (despite the mentioning of Java, the information is language agnostic):