Let me start with this: even though I promised to show effective ways to hide information in an app, I strongly recommend you don’t do it—at least not until it’s clear you truly can’t avoid it. No matter how sophisticated your data‑hiding techniques are, the information can still be extracted. Yes, you can add layers of obfuscation, use encryption, or embed hidden files in the app (we’ll talk about all of that), but if someone is determined and skilled enough to uncover your app’s secrets, they will.
So you definitely shouldn’t hardcode passwords, encryption keys, or any other truly sensitive data into your app. Need to let the app access a web service? Use its API to obtain a service token at connection time. Does the app use a private/internal API of your service? Make it fetch the endpoint URL from the service itself, and make that URL unique per app instance. Building a file-encryption app? Ask the user for the encryption password. Bottom line: do whatever it takes to ensure the app contains no information that could lead to the compromise of your accounts, your web service, or user data.
If you’ve decided to embed sensitive data into your application’s code and don’t want it exposed, there are several ways to do it—ranging from simple tricks to genuinely sophisticated techniques.
Store strings in strings.xml
This is probably the simplest way to hide strings. Instead of hardcoding a string as a constant—which makes it easy to find after decompilation—you put it in the res/values/strings.xml file:
<resources> ...
<string name="password">MyPassword</string> ...
</resources>
And in code, access it via getResources():
String password = getResources().getString(R.string.password);
Yes, many app reverse-engineering tools let you view the contents of strings.
, so it’s better to rename the string key (password
) to something innocuous, make the actual password look like a diagnostic message (e.g., Error
), and only use part of that string by splitting it with split(
:
String[] string = getResources().getString(R.string.password).split(" ");
String password = strings[1];
Naturally, it’s also best to give variables innocuous names—or just enable ProGuard, which will shorten them to one- or two-letter identifiers like a
, b
, c
, ab
.
Break strings into pieces
You can not only use substrings, but also split strings up and reassemble them later. Say you want to hide the string “MyLittlePony” in your code. There’s no need to keep it in a single variable—break it into several pieces and scatter them across different methods or even different classes:
String a = "MyLi";
String b = "ttle";
String c = "Pony";
...
String password = a + b + c;
However, there’s a risk that the compiler will optimize by folding the string into a single constant to improve performance. So it’s better to avoid declaring these variables as static
or final
.
Encoding data with XOR
To make a reverse engineer’s job even harder, you can XOR the strings. This is a favorite technique of novice (and not only novice) malware authors. The idea is simple: take a string, generate another string (a key), split them into bytes, and apply the exclusive OR operation. The result is an XOR-encoded string that can be decoded by applying XOR again. In code, it might look something like this (create a StringXOR class and put these methods in it):
// Encode a string
public static String encode(String s, String key) {
return Base64.encodeToString(xor(s.getBytes(), key.getBytes()), 0);
}
// Decode a string
public static String decode(String s, String key) {
return new String(xor(Base64.decode(s, 0), key.getBytes()));
}
// XOR operation
private static byte[] xor(byte[] a, byte[] key) {
byte[] out = new byte[a.length];
for (int i = 0; i < a.length; i++) {
out[i] = (byte) (a[i] ^ key[i%key.length]);
}
return out;
}
Come up with a second string (the key) and use it to encode the strings you want to hide (for example, let the strings be password1
and password2
, and the key 1234
):
String encoded1 = StringXOR.encode("password1", "1234");
String encoded2 = StringXOR.encode("password2", "1234");
Log.e("DEBUG", "encoded1: " + encoded1);
Log.e("DEBUG", "encoded2: " + encoded2);
When you open Android Monitor in Android Studio, you’ll find lines like:
encoded1: RVRCRQ==
encoded2: ACHBDS==
These are the original strings encoded with XOR. Add them to the code in place of the originals, and when accessing the strings, use the decoding function:
String password1 = StringXOR.decode(encodedPassword1, "1234");
With this approach, strings won’t be sitting in plain text in your app’s code, but they’ll still be easy to decode, so don’t rely on it as your only defense. You’ll also need to figure out how to hide the key.
Encrypting data
Okay, XOR is a start. But what if we go further and apply real encryption to strings? I briefly touched on this in the article “How to Protect Your Android App from Reverse Engineering and Debugging,” but now let’s break it down in more detail. First, we’ll need functions to encrypt and decrypt strings:
public static byte[] encryptString(String message, SecretKey secret) throws Exception {
Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
cipher.init(Cipher.ENCRYPT_MODE, secret);
return cipher.doFinal(message.getBytes("UTF-8"));
}
public static String decryptString(byte[] cipherText, SecretKey secret) throws Exception {
Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
cipher.init(Cipher.DECRYPT_MODE, secret);
return new String(cipher.doFinal(cipherText), "UTF-8");
}
Secondly, the random 128-bit key generation function:
public static SecretKey generateKey() throws Exception {
KeyGenerator keyGen = KeyGenerator.getInstance("AES");
keyGen.init(128);
return keyGen.generateKey();
}
Third, functions to convert a key to and from a string:
public static String keyToString(SecretKey secretKey) {
return Base64.encodeToString(secretKey.getEncoded(), Base64.DEFAULT);
}
public static SecretKey stringToKey(String stringKey) {
byte[] encodedKey = Base64.decode(stringKey.trim(), Base64.DEFAULT);
return new SecretKeySpec(encodedKey, 0, encodedKey.length, "AES");
}
Just like with the XOR case, add some code near app startup that generates a key and then logs it to the console using Log
(this example assumes you’ve placed all cryptographic functions in a Crypto class):
try {
SecretKey key = Crypto.generateKey();
Log.e("DEBUG", "key: " + Crypto.keyToString(key));
catch (Exception e) {}
On the screen, you’ll see a key that you can use to encrypt strings and then output them to the console in the same way:
// Your key
String key = "...";
SecretKey secretkey = stringToKey(key);
// String to encrypt
String password = "test";
// Encrypt and print to log
byte[] encrypted = encryptString(password, secretkey);
Log.e("DEBUG", "password: " + Base64.encodeToString(encrypted, Base64.DEFAULT));
This way, you’ll get an encrypted string in the console. You can then insert it into your application code as-is and decrypt it at runtime:
String key = "...";
String encryptedPassword = "...";
SecretKey secretkey = stringToKey(key);
String password = decryptString(Base64.decode(encodedPassword, Base64.DEFAULT), secretkey);
To further confuse a reverse engineer, you can split the key and password into several parts and XOR them together. With ProGuard enabled, this approach will turn your string reconstruction and decryption code into an obfuscated mess that’s hard to untangle at a glance.
Embedding data in native code
Finally, the most hardcore and effective way to hide data is to put it into native code. More precisely, into code that compiles not into easily decompilable Java, but into ARM/ARM64 instructions. This kind of code is much harder to analyze: there’s essentially no convenient decompiler for it, and the disassembled output is difficult to read and understand, demanding solid reverse-engineering skills.
On Android, as with desktop Java, native code is typically written in C or C++. For our task, we’ll use C. First, we’ll write a wrapper class that will call our native code—specifically, an ARM library implementing the getPassword() function:
public class Secrets {
static {
System.loadLibrary("secret");
}
public native String getPassword();
}
The function’s implementation isn’t in the code; it will reside in a C library named secret
. Now create a jni
subdirectory in your project, add a file named secret.
there, and paste the following lines into it:
#include <string.h>#include <jni.h>jstring Java_com_example_secret_Secrets_getPassword(JNIEnv* env, jobject javaThis) { return (*env)->NewStringUTF(env, "password");}
This is, so to speak, a reference version of the library that simply returns the string password
. For Android Studio to know how to compile this library, we need a Makefile
in the same directory:
LOCAL_PATH := $(call my-dir)include $(CLEAR_VARS)LOCAL_MODULE:= secret
LOCAL_SRC_FILES := secret.c
include $(BUILD_SHARED_LIBRARY)
You don’t need to delve into its meaning; it’s just instructions for compiling the file secret.
into the binary (shared library) file secret.
.
That’s pretty much it. With one caveat: while fully reversing the native library would be difficult, you can still extract the password by pulling the library out of the APK and running the strings command on it (in Linux):
$ strings secret.so
...password...
But if you apply all the above techniques—string splitting, XOR, encryption, and so on—things get much more complicated, and you’ll immediately discourage 99% of reverse engineers from poking at your app. The catch is, you’ll have to implement these protection techniques in C/C++.
Conclusions
You can hard-code confidential data into an app, but it should only be a last resort. Even the last technique described can be bypassed by running your app under a debugger and setting a breakpoint on the line that calls getPassword
.