Skip to main content
Watermarking embeds a unique, hidden identifier in each copy of your script. If a script is leaked, shared without authorization, or redistributed, the watermark reveals which customer, build, or distribution channel the leaked copy originated from. When combined with obfuscation, watermarks become harder to strip because they blend into the transformed code structure. This page covers static, structural, and runtime watermarking techniques, extraction methods, and strategies for building redundant, resilient fingerprinting systems.

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.
Watermarking is not a substitute for obfuscation. It is a complementary layer that adds accountability to protection.

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:
The string "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:
Then obfuscate the string with your string encoder. The comment becomes an opaque byte array that looks like any other encoded string.

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:
By controlling the lengths and starting letters of junk identifiers, you encode a binary watermark into the identifier table. This survives renaming obfuscation because the identifiers are the watermark.

Numeric Watermark in Constants

Embed the customer ID as a constant used in a bogus computation:
The constant 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:
If these assignments are independent, their order can encode a permutation index. With 4 statements, there are 24 orderings, enough to encode roughly 4.5 bits. Scale this to larger blocks for more capacity.

Junk Function Count

Embed bits by generating exactly K junk functions where K encodes part of the watermark:
An attacker removing junk functions may alter the count and corrupt the watermark.

Opaque Predicate Values

Choose specific numeric constants in opaque predicates to encode watermark bits:
The constants 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:
An analyst tracing execution will see the hash value in use, but cannot determine what it represents without knowing the license key.

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:
This requires network access and a legitimate reason for the request, or it will appear suspicious to analysts.

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:
For structural watermarks, the extractor must understand the encoding scheme:
Maintain a mapping table that links extracted watermark values to customer records.
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.
Rotate the watermarking scheme itself periodically. If your scheme is exposed, previous leaks cannot be decoded with the new scheme, but future leaks use a harder scheme. Maintain a version identifier in each build so you know which scheme to apply during extraction.