Custom encryption#
An Encryption implementation transforms serialized save data before writing and reverses that transformation when reading. Visual Script includes none, Caesar, and XOR implementations; a custom subclass adds another option to Save settings.
AES256 already provides a cipher
The free AES256 module implements this interface. Install it when the project needs AES instead of maintaining another implementation.
What this can and cannot do#
Client-side encryption is obfuscation
The key ships inside your game, so a player with access to the game can extract it.
Client-side encryption discourages casual edits, such as opening a save file and replacing "gold": 100 with "gold": 999999. It doesn't make player-controlled data trustworthy.
If save data affects a leaderboard, a shared economy, or purchases, validate it on a server.
Choose an implementation based on the edits you want to discourage and any format or platform requirements. A stronger local cipher doesn't replace server validation.
The interface#
Derive from EncryptionBase and implement a symmetric pair. This outline omits the encryption algorithm:
[Title("My Encryption")]
[Serializable]
public class EncryptionMyScheme : EncryptionBase
{
[SerializeField] private string m_Key = "…";
public override string Encrypt(string data) { /* … */ }
public override string Decrypt(string data) { /* … */ }
}Decrypt(Encrypt(x)) must return exactly x, for every input the serializer can produce.
Requirements#
Check that your implementation preserves the serializer's output and fits the save operation:
| Requirement | Why |
|---|---|
| Exact round trip | A single altered character can make the whole save unparseable. |
| Text-safe output | The result is stored as a string. Binary output needs base64 encoding. |
| Unicode-safe | Player-entered names and localized text pass through it. |
| Bounded runtime cost | It runs on the whole save on the main thread, so its execution time contributes to the save pause. |
Account for key-derivation time when measuring that cost. Deliberately slow password key derivation can add a visible pause to a main-thread save.
Handling failure#
Return null when decryption receives data from another key, a truncated file, or corrupted input. This partial method shows the failure path; the decryption logic is omitted:
public override string Decrypt(string data)
{
try
{
// …
}
catch
{
return null;
}
}Returning null lets the save system treat the data as an invalid save. An exception that escapes Decrypt aborts the load.
Changing the key makes existing saves unreadable
Saves written with the old key can't be read using the new key, so players can no longer load that progress.
Choose the key before release. If you change it afterward, keep the old scheme available and fall back to it when the new one fails to decrypt.
Testing#
- Round-trip the largest realistic save.
- Round-trip text with quotes, newlines, and non-ASCII characters.
- Decrypt data encrypted with a different key, and confirm it fails cleanly.
- Decrypt truncated data, and confirm it fails cleanly.
- Decrypt plain unencrypted text, and confirm it fails cleanly.
The failure cases verify that invalid data does not prevent the game from launching.
Choose encryption by threat#
For a single-player game, the built-in XOR obscures plain text without adding another package. It does not protect data against a determined player.
Write a custom scheme for a specific requirement, such as a platform mandate or service interoperability. Define that requirement before choosing the algorithm so you can verify that the implementation meets it.
Where to go next#
- Custom serializers — control the text passed into encryption.
- Custom storage — receive the encrypted result.
- Save settings — select an encryption implementation.