I’m currently working on a project that saves game data stored in a Lua table to a file. Saving the data itself was fairly straightforward, but it got me thinking about how I could better protect the file from being easily read or modified.
That led me down a bit of a rabbit hole, resulting in an extension for Defold that encrypts and decrypts Lua strings using AES-CTR algorithm, built on top of the tiny-AES implementation.
I hope it may be of use to others.
https://github.com/alfowler1976/defold_aes_ctr
Please read the important notes at the bottom of the page if you are interested in using this.
Overview
The current version exposes four functions to Defold, designed to work in pairs.
The encryption functions take a string/data variable, encrypts it, and return a string containing the encrypted data, an automatically generated IV (Initialization Vector), and a checksum to facilitate tamper detection. Their corresponding decryption functions reverse this process, check for tampering, and return an error if verification fails.
Key-Based Functions
-
aes_ctr.encrypt_using_key(data, key) -
aes_ctr.decrypt_using_key(data, key)
These two functions encrypt and decrypt using a supplied key. The key is a table containing 32 integer numbers ranging between 0 and 255. Tables are used instead of strings to help mitigate risks associated with Lua string interning.
Seed-Based Functions
-
aes_ctr.encrypt_using_seed(data, seed) -
aes_ctr.decrypt_using_seed(data, seed)
These require a seed supplied as a string, which the extension uses to generate a key internally. The advantage with these is that the key is never exposed to lua
Example Usage
Here is a truncated example (table based) of how my save and load functions look using the extension:
Lua
local function save_game_data(game_data_table, filename, seed)
-- Serialize the data
local data = sys.serialize(game_data_table)
-- Compress the data
local compressed_data = zlib.deflate(data)
-- Encrypt after compressing (compressing encrypted data yields poor results)
local encrypted_data = aes_ctr.encrypt_using_seed(compressed_data, seed)
-- Open file for saving
local file = io.open(filename, "wb")
if file then
file:write(encrypted_data)
file:close()
pprint("Save successful!")
return true
else
pprint("Save failed")
return false
end
end
local function load_game_data(self, filename, seed)
local data, error
-- Open file
local file = io.open(filename, "rb")
if file then
data = file:read("*a")
file:close()
else
error = "Could not load file from disk at " .. filename
end
if not data then
pprint(error)
return false
end
-- Decrypt data
local decrypted_data, err2 = aes_ctr.decrypt_using_seed(data, seed)
if not decrypted_data then
pprint(err2)
return false
end
-- Inflate data
local uncompressed_data = zlib.inflate(decrypted_data)
-- Deserialise
local level_data = sys.deserialize(uncompressed_data)
-- ...
-- ...
-- ...
return true
end
Just as a side note, while you could use json.encode and json.decode to convert tables to strings, I’ve found them to be much more temperamental. The JSON encoder does not handle Defold’s native engine data types, such as hashes.
Other ideas…Encrypted Bundled Assets
Instead of relying on standard io functions, you could use the sys.load_resource() function to load encrypted custom resource files packaged directly inside your game’s archive - useful for level data.
Important Notes
-
Client-Side Security: LuaJIT is relatively easy to decompile, so storing keys directly in client-side code will make them easy to extract. The seed-based variants offer more protection because the key is generated internally—meaning it never appears in Lua. Even if an attacker discovers the seed, they would have to decompile and reverse-engineer the C++ code to obtain the key, which is significantly harder. Using Prometheus | Defold would in theory help protect against decompiling lua code
-
Customization: If you want even more protection, you can download the code and include it directly in your project rather than using it as a remote dependency. This allows you to customize internal elements, such as modifying the
generate_key_from_seedfunction to create a completely unique implementation. -
Testing & Data Backups (Disclaimer): While this extension works perfectly well for my own project, it has not been exhaustively tested across every possible situation or environment. Always keep a backup of your raw, unencrypted data before running it through encryption functions, just in case you run into any unexpected issues
I would like to stress that this is not a bullet proof solution . The goal of this extension is simply to make tampering significantly more difficult.