Why Watermark?
Watermarking serves three primary purposes in Lua and Luau script distribution:Leak Identification
Identify the specific customer or build that a leaked script came from, enabling targeted responses.
Deterrence
If users know their copy is uniquely identifiable, they are less likely to share it publicly or with competitors.
Legal Evidence
A watermark can serve as evidence in copyright disputes, proving the origin of unauthorized copies.
Static Watermarking Techniques
Static watermarks are embedded in the code or bytecode and are present even if the script is never executed. They are the easiest to embed and extract.Dead Code Watermarks
Embed a unique constant in a dead code branch that is never executed but is preserved in the bytecode’s constant table. Decompilers and disassemblers will see the constant, but it does not affect runtime behavior:"CUST-7A3F-9912" appears in the bytecode constants but the branch is never taken. After obfuscation, the dead branch may be flattened into a state machine, making it even harder to distinguish from real logic.
Comment-Based Watermarks
Lua comments are stripped during compilation to bytecode, so they do not survive in.luac files. However, if you distribute source code, embed a comment obfuscated as a string assignment to a dummy variable:
Junk Identifier Watermarks
Use a specific subset of dummy identifier names that encodes a value in the bit pattern of their lengths or first characters:Numeric Watermark in Constants
Embed the customer ID as a constant used in a bogus computation:17493 appears in the bytecode and can be mapped back to a customer. The bogus computation prevents simple dead-code elimination from removing it.
Structural Watermarking
Structural watermarks encode information in the arrangement and ordering of code elements rather than in explicit constants. They are harder to remove because an attacker must restructure the code to strip them.Statement Ordering
For N independent statements, there are N factorial possible orderings. Use a specific ordering that encodes bits of the customer ID:Junk Function Count
Embed bits by generating exactly K junk functions where K encodes part of the watermark:Opaque Predicate Values
Choose specific numeric constants in opaque predicates to encode watermark bits:7, 3, 11, and 5 can encode a portion of the customer ID. Because opaque predicates are designed to look like real conditions, an attacker cannot safely remove them without risking behavioral changes.
Runtime Watermarking
Runtime watermarks are only observable when the script executes. They leave no trace in the static code and are invisible to decompilers.License Key Embedding
The script verifies a license key at startup and embeds the key’s hash into its execution trace. The hash is never stored as a constant; it is derived at runtime:HTTP-Based Tracking
If network access is available and expected, the script can call a server endpoint on first run with a hash of its own source, enabling server-side tracking:Watermark Extraction
Document your watermarking scheme internally so you can extract the customer ID from a leaked script. Without documentation, even you cannot recover the watermark.Simple Extractor Script
Here is an example extractor for a dead-code watermark:A sophisticated attacker can strip watermarks if they know the watermarking scheme. Use multiple watermark types simultaneously (static + structural) for redundancy. If one watermark is removed, the others may survive. Rotate your watermarking scheme periodically so that exposed schemes cannot be used to decode future leaks.
