Encryption & Security
TinySave includes built-in AES-256 encryption to protect save files from tampering and cheating. This guide covers how to enable and configure encryption.
Why Use Encryption?
Without encryption, save files are stored as plain JSON text that can be easily read and modified by players. Encryption is important for:
- Preventing cheating - Players can’t edit save files to give themselves unlimited resources
- Protecting competitive games - Leaderboards and multiplayer games need tamper-proof saves
- Maintaining game balance - Preserve intended difficulty and progression
- Professional polish - Players expect security in commercial games
Quick Setup
Enable encryption in two steps:
Step 1: Configure Encryption Key
using UnityEngine;
using TinySave.Runtime;
public class GameInit : MonoBehaviour
{
void Awake()
{
var config = SaveManagerConfig.CreateDefault();
config.EncryptionEnabled = true;
config.EncryptionKey = "your-secret-key-here";
SaveManager.Configure(config);
}
}Step 2: Use SaveManager Normally
// Save and load work exactly the same
await SaveManager.SaveAsync("slot1");
await SaveManager.LoadAsync("slot1");
// Save files are now encrypted automaticallyThat’s it! All save files are now encrypted with AES-256.
SaveManagerConfig
The SaveManagerConfig class controls encryption settings:
public class SaveManagerConfig
{
public bool EncryptionEnabled { get; set; }
public string EncryptionKey { get; set; }
public string StorageBasePath { get; set; }
public string FileExtension { get; set; } = "tsf";
}Properties
- EncryptionEnabled - Enable/disable encryption (default: false)
- EncryptionKey - The encryption key (required when EncryptionEnabled is true)
- StorageBasePath - Custom save location (optional)
- FileExtension - File extension for save files (default: “tsf”)
Creating a Configuration
// Create default config
var config = SaveManagerConfig.CreateDefault();
// Enable encryption
config.EncryptionEnabled = true;
config.EncryptionKey = "MySecureKey123";
// Optional: Custom save location
config.StorageBasePath = "C:/MyGameSaves";
// Optional: Custom file extension
config.FileExtension = "sav";
// Apply configuration
SaveManager.Configure(config);Validation
SaveManagerConfig validates your settings:
var config = SaveManagerConfig.CreateDefault();
config.EncryptionEnabled = true;
// config.EncryptionKey is null
try
{
SaveManager.Configure(config);
}
catch (InvalidOperationException ex)
{
// Error: "Encryption is enabled but no encryption key was provided"
Debug.LogError(ex.Message);
}TinySaveSettings
For persistent settings across sessions, use TinySaveSettings which stores configuration in PlayerPrefs:
using TinySave.Runtime;
// Access the singleton settings
var settings = TinySaveSettings.defaultSettings;
// Configure encryption
settings.encryptionEnabled = true;
settings.encryptionKey = "MySecureKey123";
// Optional: Custom save path
settings.storageBasePath = "/custom/path";
// Settings are automatically saved to PlayerPrefs
// Apply to SaveManager
var config = settings.ToConfig();
SaveManager.Configure(config);Properties
public class TinySaveSettings
{
public bool encryptionEnabled { get; set; }
public string encryptionKey { get; set; }
public string storageBasePath { get; set; }
}Persistence
TinySaveSettings automatically persists to PlayerPrefs:
// Changes are saved immediately
TinySaveSettings.defaultSettings.encryptionEnabled = true; // Saved to PlayerPrefs
// Manually reload from PlayerPrefs
TinySaveSettings.ReloadFromPrefs();Choosing an Encryption Key
Your encryption key is critical for security. Follow these guidelines:
Good Keys
// Long, random, unique
config.EncryptionKey = "k8Jx2Pq9vM4nW7sZ1bY5cR3dF6hT0gL";
// Mix of characters
config.EncryptionKey = "MyGame_2024_Secure!Key#9876";
// Generated
config.EncryptionKey = System.Guid.NewGuid().ToString();Bad Keys
// Too short
config.EncryptionKey = "123";
// Common words
config.EncryptionKey = "password";
// Predictable
config.EncryptionKey = "MyGameName";Best Practices
- Use a long key - At least 16 characters
- Make it unique - Don’t reuse keys from other projects
- Keep it secret - Never commit keys to public repositories
- Don’t hardcode in shipping builds - Use obfuscation or runtime generation
- Keep it consistent - Same key must be used for save and load
Key Management Strategies
Development Build
For development, hardcode the key for convenience:
#if UNITY_EDITOR
config.EncryptionKey = "dev-key-12345";
#else
config.EncryptionKey = GetProductionKey();
#endifProduction Build
For production, use more secure approaches:
Option 1: Environment Variable
string key = System.Environment.GetEnvironmentVariable("GAME_SAVE_KEY");
if (string.IsNullOrEmpty(key))
{
key = "fallback-key"; // Default for testing
}
config.EncryptionKey = key;Option 2: Runtime Generation
// Generate key based on device-specific data
string deviceId = SystemInfo.deviceUniqueIdentifier;
string gameId = Application.productName;
config.EncryptionKey = $"{gameId}_{deviceId}";Option 3: Obfuscated Key
// Simple obfuscation (use a proper obfuscator for production)
private string GetEncryptionKey()
{
byte[] bytes = System.Convert.FromBase64String("TXlHYW1lS2V5MTIzNDU2Nzg=");
return System.Text.Encoding.UTF8.GetString(bytes);
}
config.EncryptionKey = GetEncryptionKey();IEncryption Interface
TinySave uses the IEncryption interface for encryption operations:
public interface IEncryption
{
string Encrypt(string plaintext);
string Decrypt(string ciphertext);
}The default implementation uses AES-256:
public class AES256Encryption : IEncryption
{
public AES256Encryption(string key) { }
public string Encrypt(string plaintext) { }
public string Decrypt(string ciphertext) { }
}Custom Encryption
You can implement custom encryption:
using TinySave.Runtime;
using System.Security.Cryptography;
using System.Text;
using System;
public class CustomEncryption : IEncryption
{
private string key;
public CustomEncryption(string encryptionKey)
{
key = encryptionKey;
}
public string Encrypt(string plaintext)
{
// Your encryption implementation
byte[] bytes = Encoding.UTF8.GetBytes(plaintext);
// ... apply encryption ...
return Convert.ToBase64String(bytes);
}
public string Decrypt(string ciphertext)
{
// Your decryption implementation
byte[] bytes = Convert.FromBase64String(ciphertext);
// ... apply decryption ...
return Encoding.UTF8.GetString(bytes);
}
}Then use it with custom storage:
// Create custom storage with your encryption
IStorage baseStorage = new JsonFileStore();
IEncryption encryption = new CustomEncryption("my-key");
IStorage encryptedStorage = new EncryptedStorage(baseStorage, encryption);
// You'd need to configure SaveManager to use this storage
// (This requires modifying SaveManager's internal storage)IStorage Interface
TinySave uses the IStorage interface for file operations:
public interface IStorage
{
Task WriteTextAtomicAsync(string path, string content, CancellationToken ct = default);
void WriteTextAtomic(string path, string content);
Task<string> ReadTextAsync(string path, CancellationToken ct = default);
string ReadText(string path);
bool Exists(string path);
void Delete(string path);
string GetFullPath(string relativePath);
}Storage Implementations
TinySave includes two storage implementations:
JsonFileStore
Plain text storage (no encryption):
IStorage storage = new JsonFileStore();
// or
IStorage storage = new JsonFileStore("/custom/path");EncryptedStorage
Encrypted wrapper around any IStorage:
IStorage baseStorage = new JsonFileStore();
IEncryption encryption = new AES256Encryption("my-key");
IStorage encryptedStorage = new EncryptedStorage(baseStorage, encryption);SaveManagerConfig.CreateStorage() handles this automatically:
public IStorage CreateStorage()
{
IStorage baseStorage = new JsonFileStore(StorageBasePath);
if (EncryptionEnabled)
{
IEncryption encryption = new AES256Encryption(EncryptionKey);
return new EncryptedStorage(baseStorage, encryption);
}
return baseStorage;
}Security Considerations
What Encryption Protects
- File content is encrypted (can’t read save data without key)
- Tampering is detectable (decryption fails if file is modified)
- Casual cheating is prevented (players can’t edit JSON directly)
What Encryption Doesn’t Protect
- Memory editing - Cheaters can still modify values in memory at runtime
- Determined attackers - Encryption key can be extracted from your game binary
- Code modification - Players can patch your game to bypass encryption
- Replay attacks - Old save files can still be loaded
Additional Security Measures
For maximum security, combine encryption with:
- Server validation - Validate critical data on your server
- Checksums - Add hash verification to detect tampering
- Obfuscation - Hide encryption keys in compiled code
- Runtime checks - Validate data ranges and relationships
- Anti-cheat - Use professional anti-cheat solutions for multiplayer
Performance Impact
Encryption adds minimal overhead:
// Without encryption
Capture=5ms, Serialize=12ms, IO=8ms, Total=25ms
// With encryption
Capture=5ms, Serialize=12ms, IO=10ms, Total=27msThe encryption/decryption happens during I/O and is negligible compared to serialization.
Migrating to Encrypted Saves
If you’re adding encryption to an existing game:
Option 1: Automatic Migration
Load old unencrypted saves, re-save with encryption:
void MigrateToEncryption()
{
// Disable encryption temporarily
var config = SaveManagerConfig.CreateDefault();
config.EncryptionEnabled = false;
SaveManager.Configure(config);
// Load old save
bool loaded = await SaveManager.LoadAsync("slot1");
if (loaded)
{
// Enable encryption
config.EncryptionEnabled = true;
config.EncryptionKey = "your-new-key";
SaveManager.Configure(config);
// Re-save with encryption
await SaveManager.SaveAsync("slot1");
}
}Option 2: Check File Format
Detect encrypted vs unencrypted files:
bool IsEncrypted(string slot)
{
string path = SaveManager.GetSlotPath(slot);
string content = File.ReadAllText(path);
// Encrypted files are Base64, plain JSON starts with '{'
return !content.TrimStart().StartsWith("{");
}
async Task LoadWithEncryptionDetection(string slot)
{
bool encrypted = IsEncrypted(slot);
var config = SaveManagerConfig.CreateDefault();
config.EncryptionEnabled = encrypted;
if (encrypted)
config.EncryptionKey = "your-key";
SaveManager.Configure(config);
await SaveManager.LoadAsync(slot);
}Troubleshooting
”Decryption failed” Error
Cause: Wrong encryption key or corrupted file.
Solution:
try
{
await SaveManager.LoadAsync("slot1");
}
catch (System.Security.Cryptography.CryptographicException ex)
{
Debug.LogError("Save file is corrupted or wrong encryption key");
// Delete corrupted file and start fresh
SaveManager.DeleteSlot("slot1");
}“No encryption key provided” Error
Cause: EncryptionEnabled is true but EncryptionKey is null.
Solution:
var config = SaveManagerConfig.CreateDefault();
config.EncryptionEnabled = true;
config.EncryptionKey = "your-key"; // Don't forget this!
SaveManager.Configure(config);Save Files Are Plain Text
Cause: Encryption wasn’t configured before saving.
Solution:
// Configure encryption BEFORE first save
void Awake()
{
var config = SaveManagerConfig.CreateDefault();
config.EncryptionEnabled = true;
config.EncryptionKey = "your-key";
SaveManager.Configure(config);
}