4.0 CheatDroid is rebuilt from scratch, and a 2.x license carries over. What changed
CheatDroid

Protecting PlayerPrefs and SharedPreferences from Editing

Unity’s PlayerPrefs on Android are an ordinary shared preferences file, /data/data/<package>/shared_prefs/<package>.v2.playerprefs.xml. Ints and floats are stored as they are, strings percent-encoded. A native app’s SharedPreferences sit in the same folder in the same format. Any root file manager opens them, and CheatDroid’s first screen exists for exactly this.

This is Perfect Slices 1.4.55 after one level:

<int name="coinsAmount" value="30" />

Changing 30 to 999999 and starting the game again is the whole Perfect Slices tutorial. No search, no second step.

What doesn’t help

Renaming keys. A key called a7f3 that went from 30 to 144 after a round is the coin counter, whatever it’s called, and finding it takes two snapshots of the folder. Base64 is a step people take next; Ballz keeps a copy of its coins in a base64 string, and that’s one decode away from JSON.

Scrambling the values with a fixed scheme does slow a file editor down. Knife Hit and Dunk Shot store names and values as runs of capital letters, and I didn’t bother. But whoever reads the game’s code once can write a converter, and after that every save is open. What you want is something an editor can’t redo without a secret.

A checksum without a key isn’t that either. AdVenture Capitalist 9.28 zips its save, base64s it, zips every block inside again and stores a SHA-256 of each block next to it. Four layers, and none of them needs anything the phone doesn’t already have: SHA-256 of the new block is as easy to compute as SHA-256 of the old one. The next CheatDroid spots a hash of an edited value elsewhere in the same document and writes the new one in on save. A MAC with a key it doesn’t have is the version it can’t follow.

Nor is a key everyone has. Pizza Ready! 60.4 keeps its stage, unlocks and daily bonuses in Anti-Cheat Toolkit’s ObscuredPrefs and leaves the toolkit’s crypto key at its default, e806f6. That string is the same in every game that doesn’t set its own, so the names and values are one XOR away, and the xxHash the toolkit checks is computed from the plain bytes anyone can recompute. The next CheatDroid opens those entries as ordinary numbers. If you use ObscuredPrefs, set ObscuredPrefs.CryptoKey to something of your own before the first read, and sign what is worth something with a MAC as below.

Sign the value

Store a MAC next to each value you care about and check it on load. An edited value fails the check and the game ignores it. CheatDroid can change the number, but it can’t produce a matching MAC without the key.

using System;
using System.Security.Cryptography;
using System.Text;
using UnityEngine;

public static class SignedPrefs
{
    // Ships in the build. Not a secret from a decompiler, only from a file editor.
    static readonly byte[] Key = Convert.FromBase64String("REPLACE+WITH+32+RANDOM+BYTES+IN+BASE64=");

    public static void SetInt(string name, int value)
    {
        PlayerPrefs.SetInt(name, value);
        PlayerPrefs.SetString(name + ".mac", Mac(name, value));
    }

    public static int GetInt(string name, int fallback = 0)
    {
        int value = PlayerPrefs.GetInt(name, fallback);
        return PlayerPrefs.GetString(name + ".mac") == Mac(name, value) ? value : fallback;
    }

    static string Mac(string name, int value)
    {
        using var hmac = new HMACSHA256(Key);
        return Convert.ToBase64String(hmac.ComputeHash(Encoding.UTF8.GetBytes(name + "=" + value)));
    }
}

The same in Kotlin, for SharedPreferences:

fun mac(name: String, value: Int): String {
    val mac = Mac.getInstance("HmacSHA256").apply { init(SecretKeySpec(KEY, "HmacSHA256")) }
    return Base64.encodeToString(mac.doFinal("$name=$value".toByteArray()), Base64.NO_WRAP)
}

Three things the snippet leaves to you:

  • Players who already have coins have no MAC yet. Accept an unsigned value once, sign it, and enforce from then on, or the update empties every wallet.
  • Decide what a failed check does. Falling back to the default is fair. A silent ban hits the player whose phone died mid-write as hard as the one who cheated.
  • Mixing a device ID into the MAC stops people copying a rich player’s file, and it breaks Android’s Auto Backup: a save restored onto a new phone fails the check. Pick one.

The key ships inside the APK. Against CheatDroid that doesn’t matter, because a file editor doesn’t read your code. If it matters to you, keep the key in the Android Keystore, where it never leaves the phone’s secure hardware, and use it from a native plugin. Or keep the balance on a server, which is the only version a decompiler can’t touch.

What to leave plain

Sound on or off, the last selected skin, whether the tutorial was shown. Sign what is worth something to a cheater: currencies, unlocks, progress, timers that gate rewards.