Building a Tool for Encrypting Strings in an Android APK: A Step-by-Step Guide

Hi, I'm Adly!
I am passionately interested in Leadership, iOS, Android, Flutter, app security, and Managing teams.
I work as an Engineering Manager in PayTabs a leading fintech company in the MENA market.
Through my articles, I share what I feel is important for developers to know, technical tips and tricks, and managerial insights.
My content is for anyone interested in mobile app development or engineering management — Whether you're a professional or an enthusiast!
Overview
This demo protection tool demonstrates a tool designed to encrypt sensitive data such as URLs, keys, and other static strings within your APK. Our goal is to make extracting these strings as plain text significantly more challenging. While it is still possible, it requires considerable effort. You can consider this POC as a foundational step towards developing a comprehensive tool by adding further complexities to enhance its robustness and professionalism.

The roadmap for building the tool :
Create a sample Android app.
Prerequisites.
Collect inputs.
Decompile the APK.
Generate the secret key and build JNI libs.
Create the custom application class.
Encrypting Strings in Smali Files.
Rebuild the APK.
Sign the APK.
Run our Tool.
Test our output.
Create a sample Android app
To test our tool, we will begin by creating a simple app that displays a text message. Using JADX-GUI, we will observe that the text message is decompiled as plain text.
Now, create an Android app and modify the onCreate method to display a text message as shown below, or download the starter project from this link.
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
String str1 = "A Secured ";
Toast.makeText(this, str1 + "Content Here", Toast.LENGTH_SHORT).show();
}
}

When you build a release of the app and open the APK using JADX-GUI, you will observe that the text message "A Secured Content Here" is decompiled as plain text.

Prerequisites
Install the JDK.
Install the Android SDK.
Install the NDK.
Configure the environment variables.
export JAVA_HOME={/path/to}/java_home export ANDROID_HOME={/path/to}/Android/sdk export ANDROID_BUILD_TOOLS={/path/to}/build-tools/{version} export ANDROID_NDK_HOME={/path/to}/sdk/ndk/{version}Install JADX.
Collect inputs
Now it's time to start building our tool. Create a Java file named APKProtector.java and add the main method that will handle the input arguments and initiate the encryption process. First, we need to gather the APK path and the list of file paths to be protected.
import java.util.*;
import java.io.*;
public class APKProtector {
public static void main(String[] args) throws Exception {
String apkPath = null;
List<String> files = new ArrayList<>();
for (int i = 0; i < args.length; i++) {
if (args[i].equals("--apk") && i + 1 < args.length) {
apkPath = args[i + 1];
i++;
} else if (args[i].equals("--files")) {
while (i + 1 < args.length && !args[i + 1].startsWith("--")) {
String filePath = args[++i].replace(".", "/");
files.add(filePath);
}
}
}
if (apkPath == null || files.isEmpty()) {
System.out.println("Usage: java APKProtector --apk <path-to-apk> --files <filePath1> [<filePath2> ...]");
return;
}
File apkFile = new File(apkPath);
if (!apkFile.exists()) {
System.out.println("APK file does not exist.");
return;
}
}
}
Decompile the APK
After collecting the inputs and checking the APK file exists, we will decompile the APK to extract its contents using the apktool, I created executeCommand method to call commands and log outputs.
import java.util.*;
import java.util.regex.*;
import java.security.SecureRandom;
import java.util.stream.Collectors;
import java.io.*;
public class APKProtector {
private static byte[] secretKey;
public static void main(String[] args) throws Exception {
String apkPath = null;
List<String> files = new ArrayList<>();
for (int i = 0; i < args.length; i++) {
if (args[i].equals("--apk") && i + 1 < args.length) {
apkPath = args[i + 1];
i++;
} else if (args[i].equals("--files")) {
while (i + 1 < args.length && !args[i + 1].startsWith("--")) {
String filePath = args[++i].replace(".", "/");
files.add(filePath);
}
}
}
if (apkPath == null || files.isEmpty()) {
System.out.println("Usage: java APKProtector --apk <path-to-apk> --files <filePath1> [<filePath2> ...]");
return;
}
File apkFile = new File(apkPath);
if (!apkFile.exists()) {
System.out.println("APK file does not exist.");
return;
}
// Decompile the APK
String apkDir = apkFile.getName().replace(".apk", "");
executeCommand(new String[]{"apktool", "d","-f", apkPath});
}
private static String executeCommand(String[] command) throws IOException, InterruptedException {
ProcessBuilder pb = new ProcessBuilder(command);
pb.redirectErrorStream(true); // Merge stdout and stderr
Process process = pb.start();
// Read the output
String commandOutput;
try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) {
commandOutput = reader.lines().collect(Collectors.joining("\n"));
}
// Check the exit code
int exitCode = process.waitFor();
if (exitCode != 0) {
throw new RuntimeException("Command failed with exit code " + exitCode);
}
return commandOutput;
}
}
Generate the secret key and build JNI libs
In this step, we will generate a secret key using generateSecretKey method for encrypting the strings.
import java.util.*;
import java.util.regex.*;
import java.security.SecureRandom;
import java.util.stream.Collectors;
import java.io.*;
public class APKProtector {
private static byte[] secretKey;
public static void main(String[] args) throws Exception {
String apkPath = null;
List<String> files = new ArrayList<>();
for (int i = 0; i < args.length; i++) {
if (args[i].equals("--apk") && i + 1 < args.length) {
apkPath = args[i + 1];
i++;
} else if (args[i].equals("--files")) {
while (i + 1 < args.length && !args[i + 1].startsWith("--")) {
String filePath = args[++i].replace(".", "/");
files.add(filePath);
}
}
}
if (apkPath == null || files.isEmpty()) {
System.out.println("Usage: java APKProtector --apk <path-to-apk> --files <filePath1> [<filePath2> ...]");
return;
}
File apkFile = new File(apkPath);
if (!apkFile.exists()) {
System.out.println("APK file does not exist.");
return;
}
// Decompile the APK
String apkDir = apkFile.getName().replace(".apk", "");
executeCommand(new String[]{"apktool", "d","-f", apkPath});
String bundleId = getAppBundleId(apkPath);
String customAppClassName = "application";
// Generate a secret key
secretKey = generateSecretKey(32); // Generating a 32-byte key
// Generate the C++ file that will embed the secret key, build and inject libs into the decompiled app
JNIHelper.generateAndCompileJNI(bundleId,apkDir,secretKey,customAppClassName);
}
private static byte[] generateSecretKey(int length) {
SecureRandom secureRandom = new SecureRandom();
byte[] key = new byte[length];
secureRandom.nextBytes(key);
return key;
}
private static String executeCommand(String[] command) throws IOException, InterruptedException {
ProcessBuilder pb = new ProcessBuilder(command);
pb.redirectErrorStream(true); // Merge stdout and stderr
Process process = pb.start();
// Read the output
String commandOutput;
try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) {
commandOutput = reader.lines().collect(Collectors.joining("\n"));
}
// Check the exit code
int exitCode = process.waitFor();
if (exitCode != 0) {
throw new RuntimeException("Command failed with exit code " + exitCode);
}
return commandOutput;
}
}
After generating the secret key that will be used to encrypt the strings, the JNIHelper class will handle generating the C++ code, writing it into encryptor-lib.cpp, and embedding the secret key into the getKey method. It will also generate the Android.mk and Application.mk files required for ndk-build to compile the C++ file. Hiding the key in a JNI library makes it harder to decompile bytecode back to Java code because part of the code is moved to native code. Adding multiple complexities to the key retrieval process, such as using XOR operations, array reversing, implementing encryption/decryption layers, further increases the time required for hackers to access the raw secret key.
After building the libraries, the JNIHelper copies them to the decompiled app directory to be ready for the next step.
import java.io.*;
import java.util.Arrays;
public class JNIHelper {
public static void generateAndCompileJNI(String bundleId, String decompiledDir, byte[] secretKey, String customAppClass) {
try {
// Retrieve ANDROID_NDK_HOME environment variable
String ndkPath = System.getenv("ANDROID_NDK_HOME");
if (ndkPath == null || ndkPath.isEmpty()) {
throw new RuntimeException("ANDROID_NDK_HOME environment variable is not set.");
}
// Path to store the generated C++ file
String cppFilePath = "src/main/jni/encryptor-lib.cpp";
// Create the directory for the generated C++ file if it doesn't exist
new File(cppFilePath).getParentFile().mkdirs();
// Create directories for jni and jniLibs
String jniDirPath = "src/main/jni";
String jniLibsDirPath = "src/main/jniLibs";
new File(jniDirPath).mkdirs();
new File(jniLibsDirPath).mkdirs();
// Write getkey C++ code to a file with the generated secret key and bundle ID
writeCppFile(cppFilePath, secretKey, bundleId, customAppClass);
// Generate the Android.mk and Application.mk files
writeAndroidMkFile(jniDirPath);
writeApplicationMkFile(jniDirPath);
// Compile the C++ code using ndk-build
compileWithNdkBuild(ndkPath);
// Copy the jniLibs to the decompiled directory
copyJniLibraries(jniLibsDirPath,decompiledDir);
System.out.println("Native libraries generated and compiled successfully.");
} catch (IOException | InterruptedException e) {
e.printStackTrace();
}
}
private static void writeCppFile(String filePath, byte[] secretKey, String bundleId, String customAppClass) throws IOException {
// Convert byte array to a C++ array initializer
StringBuilder keyArrayStr = new StringBuilder();
for (byte b : secretKey) {
keyArrayStr.append(String.format("0x%02X, ", b));
}
// Remove the last comma and space
if (keyArrayStr.length() > 0) {
keyArrayStr.setLength(keyArrayStr.length() - 2);
}
String cppCode = "#include <jni.h>\n" +
"#include <vector>\n" +
"\n" +
"extern \"C\" JNIEXPORT jbyteArray JNICALL\n" +
"Java_" + bundleId.replace('.', '_') + "_"+ customAppClass +"_getKey(\n" +
" JNIEnv* env,\n" +
" jobject /* this */) {\n" +
" std::vector<uint8_t> key = {" + keyArrayStr.toString() + "};\n" +
" jbyteArray jKey = env->NewByteArray(key.size());\n" +
" env->SetByteArrayRegion(jKey, 0, key.size(), reinterpret_cast<jbyte*>(key.data()));\n" +
" return jKey;\n" +
"}\n";
try (FileWriter writer = new FileWriter(filePath)) {
writer.write(cppCode);
}
}
private static void writeAndroidMkFile(String jniDirPath) throws IOException {
String androidMk = "LOCAL_PATH := $(call my-dir)\n" +
"include $(CLEAR_VARS)\n" +
"LOCAL_MODULE := encryptor-lib\n" +
"LOCAL_SRC_FILES := encryptor-lib.cpp\n" +
"include $(BUILD_SHARED_LIBRARY)\n";
try (FileWriter writer = new FileWriter(jniDirPath + "/Android.mk")) {
writer.write(androidMk);
}
}
private static void writeApplicationMkFile(String jniDirPath) throws IOException {
String applicationMk = "APP_STL := c++_static\n" +
"APP_ABI := armeabi-v7a arm64-v8a x86 x86_64\n" +
"APP_PLATFORM := android-21\n";
try (FileWriter writer = new FileWriter(jniDirPath + "/Application.mk")) {
writer.write(applicationMk);
}
}
private static void compileWithNdkBuild(String ndkPath) throws IOException, InterruptedException {
String projectDir = System.getProperty("user.dir");
String command = ndkPath + "/ndk-build NDK_PROJECT_PATH=" + projectDir + "/src/main" +
" NDK_OUT=" + projectDir + "/src/main/obj" +
" NDK_LIBS_OUT=" + projectDir + "/src/main/jniLibs";
Process process = Runtime.getRuntime().exec(command);
int exitCode = process.waitFor();
// Capture and log output
try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()));
BufferedReader errorReader = new BufferedReader(new InputStreamReader(process.getErrorStream()))) {
String line;
while ((line = reader.readLine()) != null) {
System.out.println(line);
}
while ((line = errorReader.readLine()) != null) {
System.err.println(line);
}
}
if (exitCode != 0) {
throw new RuntimeException("Failed to compile native code using ndk-build");
}
}
private static void copyJniLibraries(String jniLibsPath, String decompiledApkPath) throws IOException {
File jniLibsDir = new File(jniLibsPath);
File decompiledDir = new File(decompiledApkPath);
if (!jniLibsDir.exists() || !jniLibsDir.isDirectory()) {
throw new IllegalArgumentException("Invalid JNI libs directory: " + jniLibsPath);
}
if (!decompiledDir.exists() || !decompiledDir.isDirectory()) {
throw new IllegalArgumentException("Invalid decompiled APK directory: " + decompiledApkPath);
}
// Iterate through each ABI directory (e.g., armeabi-v7a, x86, ...)
for (File abiDir : jniLibsDir.listFiles()) {
if (abiDir.isDirectory()) {
File targetAbiDir = new File(decompiledApkPath, "lib/" + abiDir.getName());
targetAbiDir.mkdirs(); // Create target ABI directory if not exists
// Copy .so files from src/main/jniLibs/<ABI> to decompiled_apk/lib/<ABI>
for (File libFile : abiDir.listFiles()) {
if (libFile.isFile() && libFile.getName().endsWith(".so")) {
copyFile(libFile, new File(targetAbiDir, libFile.getName()));
}
}
}
}
}
private static void copyFile(File source, File destination) throws IOException {
try (InputStream inputStream = new FileInputStream(source);
OutputStream outputStream = new FileOutputStream(destination)) {
byte[] buffer = new byte[1024];
int length;
while ((length = inputStream.read(buffer)) > 0) {
outputStream.write(buffer, 0, length);
}
}
}
}
Create a custom application class
Now, we need to link the JNI library with our app. To do this, we will create a class that includes the getKey method signature and a decryptString method, which will decrypt any encrypted string to its original form. Finally, we will add this class's smali file to the smali directory.
In our case, since we generated a custom application class, we need to update the app's manifest file. We will add a new attribute, android:name, and assign it to our generated application class.
import java.io.*;
import javax.xml.parsers.*;
import org.w3c.dom.*;
import javax.xml.transform.*;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
public class ApplicationGenerator {
public static void writeApplicationSmaliFile(String decodedDir, String bundleId, String customAppClass) throws IOException {
File smaliFile = new File(new File(decodedDir, "smali/" + bundleId.replace('.', '/')), customAppClass + ".smali");
smaliFile.getParentFile().mkdirs();
String smaliCode =
".class public L"+bundleId.replace('.', '/')+"/"+ customAppClass +";\n" +
".super Landroid/app/Application;\n" +
".source \"application.java\"\n" +
"\n" +
"# direct methods\n" +
".method static constructor <clinit>()V\n" +
" .registers 1\n" +
" const-string v0, \"encryptor-lib\"\n" +
" invoke-static {v0}, Ljava/lang/System;->loadLibrary(Ljava/lang/String;)V\n" +
" return-void\n" +
".end method\n" +
"\n" +
".method public constructor <init>()V\n" +
" .registers 1\n" +
"\n" +
" .line 10\n" +
" invoke-direct {p0}, Landroid/app/Application;-><init>()V\n" +
"\n" +
" return-void\n" +
".end method\n" +
"\n" +
".method public static decryptString(Ljava/lang/String;)Ljava/lang/String;\n" +
" .registers 4\n" +
"\n" +
" const-string v0, \"AES\"\n" +
"\n" +
" .line 25\n" +
" invoke-static {}, L"+bundleId.replace('.', '/')+"/"+ customAppClass +";->getKey()[B\n" +
"\n" +
" move-result-object v1\n" +
"\n" +
" if-eqz v1, :cond_35\n" +
"\n" +
" .line 26\n" +
" array-length v2, v1\n" +
"\n" +
" if-eqz v2, :cond_35\n" +
"\n" +
" .line 30\n" +
" :try_start_b\n" +
" new-instance v2, Ljavax/crypto/spec/SecretKeySpec;\n" +
"\n" +
" invoke-direct {v2, v1, v0}, Ljavax/crypto/spec/SecretKeySpec;-><init>([BLjava/lang/String;)V\n" +
"\n" +
" .line 31\n" +
" invoke-static {v0}, Ljavax/crypto/Cipher;->getInstance(Ljava/lang/String;)Ljavax/crypto/Cipher;\n" +
"\n" +
" move-result-object v0\n" +
"\n" +
" const/4 v1, 0x2\n" +
"\n" +
" .line 32\n" +
" invoke-virtual {v0, v1, v2}, Ljavax/crypto/Cipher;->init(ILjava/security/Key;)V\n" +
"\n" +
" .line 33\n" +
" new-instance v1, Ljava/lang/String;\n" +
"\n" +
" const/4 v2, 0x0\n" +
"\n" +
" invoke-static {p0, v2}, Landroid/util/Base64;->decode(Ljava/lang/String;I)[B\n" +
"\n" +
" move-result-object p0\n" +
"\n" +
" invoke-virtual {v0, p0}, Ljavax/crypto/Cipher;->doFinal([B)[B\n" +
"\n" +
" move-result-object p0\n" +
"\n" +
" const-string v0, \"UTF-8\"\n" +
"\n" +
" invoke-direct {v1, p0, v0}, Ljava/lang/String;-><init>([BLjava/lang/String;)V\n" +
" :try_end_28\n" +
" .catch Ljava/lang/Exception; {:try_start_b .. :try_end_28} :catch_29\n" +
"\n" +
" return-object v1\n" +
"\n" +
" :catch_29\n" +
" move-exception p0\n" +
"\n" +
" .line 35\n" +
" invoke-virtual {p0}, Ljava/lang/Exception;->printStackTrace()V\n" +
"\n" +
" .line 36\n" +
" new-instance v0, Ljava/lang/RuntimeException;\n" +
"\n" +
" const-string v1, \"Decryption failed\"\n" +
"\n" +
" invoke-direct {v0, v1, p0}, Ljava/lang/RuntimeException;-><init>(Ljava/lang/String;Ljava/lang/Throwable;)V\n" +
"\n" +
" throw v0\n" +
"\n" +
" .line 27\n" +
" :cond_35\n" +
" new-instance p0, Ljava/lang/RuntimeException;\n" +
"\n" +
" const-string v0, \"Secret key is not available\"\n" +
"\n" +
" invoke-direct {p0, v0}, Ljava/lang/RuntimeException;-><init>(Ljava/lang/String;)V\n" +
"\n" +
" throw p0\n" +
".end method\n" +
"\n" +
".method public static native getKey()[B\n" +
".end method\n";
try (BufferedWriter writer = new BufferedWriter(new FileWriter(smaliFile))) {
writer.write(smaliCode);
}
}
public static void updateAndroidManifest(String path, String bundleId, String customAppClass) {
try {
// Construct the path to the AndroidManifest.xml file
String manifestPath = path + "/AndroidManifest.xml"; // Adjust this path as necessary
// Load the AndroidManifest.xml file
DocumentBuilderFactory dbFactory = DocumentBuilderFactory.newInstance();
DocumentBuilder dBuilder = dbFactory.newDocumentBuilder();
Document doc = dBuilder.parse(new File(manifestPath));
doc.getDocumentElement().normalize();
// Get the <application> element
NodeList applicationNodes = doc.getElementsByTagName("application");
if (applicationNodes.getLength() > 0) {
Element applicationElement = (Element) applicationNodes.item(0);
// Set the android:name attribute to the custom application class
applicationElement.setAttribute("android:name", bundleId + "." + customAppClass);
// Add or update the android:extractNativeLibs attribute to true
applicationElement.setAttribute("android:extractNativeLibs", "true");
// Save the updated document back to the file
TransformerFactory transformerFactory = TransformerFactory.newInstance();
Transformer transformer = transformerFactory.newTransformer();
DOMSource source = new DOMSource(doc);
StreamResult result = new StreamResult(new File(manifestPath));
transformer.transform(source, result);
System.out.println("Updated AndroidManifest.xml successfully.");
} else {
System.out.println("No <application> element found in AndroidManifest.xml.");
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
Encrypting Strings in Smali Files
Since our helper classes are ready, it's time to process the smali files that require string encryption. We will loop through these files line by line, wrapping each encrypted string with a decryptString call to decrypt it at runtime. This process operates at the smali code level.
import java.io.*;
import java.util.*;
import java.util.regex.*;
import javax.crypto.Cipher;
import javax.crypto.spec.SecretKeySpec;
public class Encryptor {
public static void encryptStrings(String dir, String bundleId, byte[] secretKey, String customAppClass, List<String> files) throws Exception {
File smaliDir = new File(dir);
for (File file : Objects.requireNonNull(smaliDir.listFiles())) {
if (file.isDirectory()) {
encryptStrings(file.getAbsolutePath(), bundleId, secretKey, customAppClass, files);
} else {
String relativePath = file.getAbsolutePath().replace(File.separator, "/");
if (relativePath.endsWith(".smali")) {
relativePath = relativePath.substring(0, relativePath.length() - ".smali".length());
}
boolean isTargetFile = false;
for (String targetFilePath : files) {
if (relativePath.endsWith(targetFilePath)) {
System.out.println(relativePath);
isTargetFile = true;
break;
}
}
if (isTargetFile) {
System.out.println("Processing file: " + file.getAbsolutePath());
List<String> lines = new ArrayList<>();
try (BufferedReader reader = new BufferedReader(new FileReader(file))) {
String line;
while ((line = reader.readLine()) != null) {
line = encryptStringsInLine(line, bundleId, secretKey, customAppClass);
lines.add(line);
}
}
try (BufferedWriter writer = new BufferedWriter(new FileWriter(file))) {
for (String line : lines) {
writer.write(line);
writer.newLine();
}
}
}
}
}
}
private static String encryptStringsInLine(String line, String bundleId,byte[] secretKey, String customAppClass) throws Exception {
StringBuilder sb = new StringBuilder();
int start = 0;
if (!line.trim().startsWith("const-string")) {
return line;
}
while (true) {
int pos = line.indexOf('"', start);
if (pos == -1) break;
int end = line.indexOf('"', pos + 1);
if (end == -1) break;
String original = line.substring(pos + 1, end);
String encrypted = encrypt(original,secretKey);
String register = extractRegister(line);
String decryptionCall = "\n\tinvoke-static {"+register+"}, L"+bundleId.replace(".","/")+"/"+customAppClass+";->decryptString(Ljava/lang/String;)Ljava/lang/String;\n" +
"\tmove-result-object " + register ;
// Replace the original string with the encrypted string
sb.append(line, start, pos + 1).append(encrypted).append(line, end, end + 1).append(decryptionCall);
start = end + 1;
}
sb.append(line.substring(start));
return sb.toString();
}
private static String extractRegister(String line) {
String regex = "\\s*const-string\\s+([vp]\\d+),\\s*\"([^\"]*)\"";
Pattern pattern = Pattern.compile(regex);
Matcher matcher = pattern.matcher(line);
if (matcher.find()) {
return matcher.group(1); // Group 1 is the register (e.g., p1, v0)
} else {
return "";
}
}
private static String encrypt(String strToEncrypt,byte[] secretKey) throws Exception {
Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
final SecretKeySpec secretKeySpec = new SecretKeySpec(secretKey, "AES");
cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec);
return Base64.getEncoder().encodeToString(cipher.doFinal(strToEncrypt.getBytes()));
}
}
Rebuild the APK
Rebuild the entire decompiled directory of the app after applying our modifications using apktool.
executeCommand(new String[]{"apktool", "b","-f", apkDir});
Sign the APK
Sign the resulting APK using apksigner.
private static void signApk(String inputApk, String outputApk) throws Exception {
String buildToolsPath = System.getenv("ANDROID_BUILD_TOOLS");
String cmd = buildToolsPath + "/apksigner sign --ks testkey.jks --ks-key-alias test --ks-pass pass:test123 --key-pass pass:test123 --out " + outputApk + " " + inputApk;
Runtime.getRuntime().exec(cmd).waitFor();
}
String unsignedApk = apkDir + "/dist/" + apkDir + ".apk";
String signedApk = apkDir + "-signed.apk";
signApk(unsignedApk, signedApk);
APKProtector full code
Here is the complete APKProtector class
import java.util.*;
import java.util.regex.*;
import java.security.SecureRandom;
import java.util.stream.Collectors;
import java.io.*;
public class APKProtector {
private static byte[] secretKey;
public static void main(String[] args) throws Exception {
String apkPath = null;
List<String> files = new ArrayList<>();
for (int i = 0; i < args.length; i++) {
if (args[i].equals("--apk") && i + 1 < args.length) {
apkPath = args[i + 1];
i++;
} else if (args[i].equals("--files")) {
while (i + 1 < args.length && !args[i + 1].startsWith("--")) {
String filePath = args[++i].replace(".", "/");
files.add(filePath);
}
}
}
if (apkPath == null || files.isEmpty()) {
System.out.println("Usage: java APKProtector --apk <path-to-apk> --files <filePath1> [<filePath2> ...]");
return;
}
File apkFile = new File(apkPath);
if (!apkFile.exists()) {
System.out.println("APK file does not exist.");
return;
}
// Decompile the APK
String apkDir = apkFile.getName().replace(".apk", "");
executeCommand(new String[]{"apktool", "d","-f", apkPath});
String bundleId = getAppBundleId(apkPath);
String customAppClassName = "application";
// Generate a secret key
secretKey = generateSecretKey(32); // Generating a 32-byte key
// Generate the C++ file that will embed the secret key
JNIHelper.generateAndCompileJNI(bundleId,apkDir,secretKey,customAppClassName);
// Generate the custom application class and update the manifest file.
ApplicationGenerator.writeApplicationSmaliFile(apkDir,bundleId,customAppClassName);
ApplicationGenerator.updateAndroidManifest(apkDir,bundleId,customAppClassName);
//Encrypt strings in Smali files
Encryptor.encryptStrings(apkDir+"/"+"smali", bundleId,secretKey,customAppClassName,files);
// Rebuild APK
executeCommand(new String[]{"apktool", "b","-f", apkDir});
// Sign the APK
String unsignedApk = apkDir + "/dist/" + apkDir + ".apk";
String signedApk = apkDir + "-signed.apk";
signApk(unsignedApk, signedApk);
System.out.println("Encrypted APK: " + signedApk);
}
private static byte[] generateSecretKey(int length) {
SecureRandom secureRandom = new SecureRandom();
byte[] key = new byte[length];
secureRandom.nextBytes(key);
return key;
}
private static String executeCommand(String[] command) throws IOException, InterruptedException {
ProcessBuilder pb = new ProcessBuilder(command);
pb.redirectErrorStream(true); // Merge stdout and stderr
Process process = pb.start();
// Read the output
String commandOutput;
try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) {
commandOutput = reader.lines().collect(Collectors.joining("\n"));
}
// Check the exit code
int exitCode = process.waitFor();
if (exitCode != 0) {
throw new RuntimeException("Command failed with exit code " + exitCode);
}
return commandOutput;
}
private static void signApk(String inputApk, String outputApk) throws Exception {
String buildToolsPath = System.getenv("ANDROID_BUILD_TOOLS");
String cmd = buildToolsPath + "/apksigner sign --ks testkey.jks --ks-key-alias test --ks-pass pass:test123 --key-pass pass:test123 --out " + outputApk + " " + inputApk;
Runtime.getRuntime().exec(cmd).waitFor();
}
private static String getAppBundleId(String apkPath) {
try {
String sdkPath = getAndroidSdkPath();
if (sdkPath != null) {
String aaptPath = findAapt(sdkPath);
if (aaptPath != null) {
String output = executeCommand(new String[]{aaptPath, "dump", "badging", apkPath});
String bundleId = parsePackageName(output);
return bundleId;
} else {
System.out.println("aapt not found in the Android SDK.");
}
} else {
System.out.println("ANDROID_HOME or ANDROID_SDK_ROOT environment variable is not set.");
}
} catch (Exception e) {
e.printStackTrace();
}
return "cann't find the bundle Id";
}
private static String parsePackageName(String output) {
Pattern pattern = Pattern.compile("package: name='([^']+)'");
Matcher matcher = pattern.matcher(output);
if (matcher.find()) {
return matcher.group(1);
}
return "Package name not found";
}
private static String getAndroidSdkPath() {
String sdkPath = System.getenv("ANDROID_HOME");
if (sdkPath == null) {
sdkPath = System.getenv("ANDROID_SDK_ROOT");
}
return sdkPath;
}
private static String findAapt(String sdkPath) throws IOException {
File buildToolsDir = new File(sdkPath, "build-tools");
if (!buildToolsDir.exists()) {
throw new IOException("build-tools directory not found in SDK path: " + sdkPath);
}
List<File> aaptFiles = new ArrayList<>();
findAaptInDirectory(buildToolsDir, aaptFiles);
if (!aaptFiles.isEmpty()) {
// Return the first found aapt tool
return aaptFiles.get(0).getAbsolutePath();
}
return null;
}
private static void findAaptInDirectory(File dir, List<File> aaptFiles) {
if (dir.isDirectory()) {
for (File file : dir.listFiles()) {
if (file.isDirectory()) {
findAaptInDirectory(file, aaptFiles);
} else if (file.getName().equals("aapt") || file.getName().equals("aapt.exe")) {
aaptFiles.add(file);
}
}
}
}
}
Run our Tool
Compile all classes:
javac JNIHelper.java ApplicationGenerator.java Encryptor.java APKProtector.java
Run the Tool:
java APKProtector --apk path/to/app.apk --files "com/example/protectstringsexample/MainActivity"
Console output:

Test our output
Let's open the signed APK using JADX-GUI and observe how the strings appear.
jadx-gui signed-app.apk

Congratulations 🥳 , the text message "A Secured Content Here" is encrypted.
The full source code of the tool can be downloaded from here.

