> ## Documentation Index
> Fetch the complete documentation index at: https://starcryptdocs.starsolenterprise.com/lua-docs/llms.txt
> Use this file to discover all available pages before exploring further.

# String Encoding and Encryption in Lua/Luau

> Eliminate plaintext string literals from your Lua and Luau scripts using byte arrays, XOR ciphers, RC4, compression encoding, and runtime decoding wrappers.

String literals are one of the highest-value targets for reverse engineers. A single plaintext string can reveal the purpose of an entire function, expose API endpoints, leak encryption keys, or disclose file paths and error messages. This guide covers the complete pipeline for removing every readable string from your Lua and Luau scripts: encode at build time, embed the encoded form, and decode at runtime through a small decoder function. You will learn byte array encoding, XOR ciphers, RC4 stream encryption, Base64 encoding, compression strategies, and the runtime decoder pattern that ties everything together.

## Why Strings Are a High-Value Target

Strings leak intent more directly than any other code element. Consider what an attacker learns from these snippets:

```lua theme={"theme":{"light":"ayu-dark","dark":"night-owl"}}
local url = "https://api.example.com/v2/auth"
local apiKey = "sk-live-abc123xyz"
local errorMsg = "Invalid player credentials"
local secretPath = "ReplicatedStorage.GameConfig"
```

Even if every variable name is obfuscated, the string contents remain readable in the source. An attacker can grep for `https://`, `sk-live`, or `ReplicatedStorage` and immediately locate sensitive logic. Encoding every string closes this attack vector.

## The Encoding Pipeline

Every string encoding technique follows the same three-stage pattern:

1. **Encode at build time**: transform the plaintext string into an encoded representation using a script or tool
2. **Embed the encoded form**: replace the original string literal in your source with the encoded data
3. **Decode at runtime**: include a small decoder function that reconstructs the original string when needed

The decoder function itself becomes a target, so you will also learn how to obfuscate the decoder.

## Byte Array Encoding

The simplest encoding technique represents a string as a sequence of byte values passed to `string.char`.

### Basic Byte Array

```lua theme={"theme":{"light":"ayu-dark","dark":"night-owl"}}
-- Original
local greeting = "Hello, World!"
```

```lua theme={"theme":{"light":"ayu-dark","dark":"night-owl"}}
-- Encoded
local greeting = string.char(72, 101, 108, 108, 111, 44, 32, 87, 111, 114, 108, 100, 33)
```

### Offline Encoder Script

You can generate these calls automatically with a small Lua script:

```lua theme={"theme":{"light":"ayu-dark","dark":"night-owl"}}
function encodeString(s)
    local bytes = {}
    for i = 1, #s do
        bytes[i] = string.byte(s, i)
    end
    return "string.char(" .. table.concat(bytes, ", ") .. ")"
end

print(encodeString("Hello, World!"))
-- Output: string.char(72, 101, 108, 108, 111, 44, 32, 87, 111, 114, 108, 100, 33)
```

### Table-Based Unpacking

For long strings, passing dozens of arguments to `string.char` may hit implementation limits or produce unwieldy code. Store bytes in a table and unpack:

```lua theme={"theme":{"light":"ayu-dark","dark":"night-owl"}}
local _t = {72, 101, 108, 108, 111, 44, 32, 87, 111, 114, 108, 100, 33}
local greeting = string.char(table.unpack(_t))
```

In Luau (Roblox), `table.unpack` is available and efficient. In standard Lua 5.1, use `unpack` instead of `table.unpack`.

> [!NOTE]
>
> Very long strings may exceed the maximum number of function arguments in some Lua implementations. The table-based approach avoids this limit entirely.

## XOR Cipher

XOR encoding provides lightweight encryption. Each byte of the plaintext is XORed with a byte from a key. The same operation decrypts the data, making it symmetric and easy to embed.

### How XOR Encoding Works

For a single-byte key `K` and plaintext byte `P`, the ciphertext byte `C` is:

```text theme={"theme":{"light":"ayu-dark","dark":"night-owl"}}
C = P XOR K
P = C XOR K
```

For multi-byte keys, cycle through the key bytes:

```text theme={"theme":{"light":"ayu-dark","dark":"night-owl"}}
C[i] = P[i] XOR K[(i - 1) % #K + 1]
```

### Full XOR Encoder and Decoder

```lua theme={"theme":{"light":"ayu-dark","dark":"night-owl"}}
-- Encoder (run offline to generate encoded data)
function xorEncrypt(plaintext, key)
    local result = {}
    for i = 1, #plaintext do
        local keyByte = string.byte(key, ((i - 1) % #key) + 1)
        result[i] = string.byte(plaintext, i) ~ keyByte
    end
    return result
end

-- Example usage
local key = "mysecret"
local encoded = xorEncrypt("https://api.example.com", key)
-- encoded is now a table of integers
```

```lua theme={"theme":{"light":"ayu-dark","dark":"night-owl"}}
-- Decoder (embedded in your obfuscated script)
local _k = {109, 121, 115, 101, 99, 114, 101, 116}  -- "mysecret" as bytes

function _d(_t, _l)
    local _r = {}
    for _i = 1, #_t do
        local _b = _k[((_i - 1) % _l) + 1]
        _r[_i] = string.char(_t[_i] ~ _b)
    end
    return table.concat(_r)
end

-- Encoded data for "https://api.example.com"
local _e = {60, 16, 16, 3, 70, 51, 70, 3, 84, 16, 0, 3, 68, 20, 0, 84, 3, 68, 20, 0, 84, 3, 68, 20}
local url = _d(_e, 8)
print(url)  -- https://api.example.com
```

### Multi-Byte Rolling Key Example

Using a longer key increases entropy and makes pattern detection harder:

```lua theme={"theme":{"light":"ayu-dark","dark":"night-owl"}}
local key = "ComplexKey123!"
local encoded = xorEncrypt("Sensitive operation data", key)
```

The key itself can also be stored as a byte array to avoid exposing it as a plaintext string:

```lua theme={"theme":{"light":"ayu-dark","dark":"night-owl"}}
local _k = string.char(67, 111, 109, 112, 108, 101, 120, 75, 101, 121, 49, 50, 51, 33)
```

> [!TIP]
>
> Layer XOR with byte array encoding so both the ciphertext and the key are stored as numeric tables. An attacker must first recognize the XOR pattern, then locate and reconstruct the key before recovering any strings.

## ROT-N / Caesar Cipher on Byte Values

A ROT-N cipher adds a fixed offset to each byte value, wrapping around modulo 256. It is weaker than XOR but faster and useful as one layer in a multi-layer encoding scheme.

### Encoder and Decoder

```lua theme={"theme":{"light":"ayu-dark","dark":"night-owl"}}
-- Offline encoder
function rotNEncode(s, n)
    local result = {}
    for i = 1, #s do
        result[i] = (string.byte(s, i) + n) % 256
    end
    return result
end

-- Runtime decoder
function rotNDecode(t, n)
    local result = {}
    for i = 1, #t do
        result[i] = string.char((t[i] - n) % 256)
    end
    return table.concat(result)
end

-- Example
local encoded = rotNEncode("Hello", 42)
-- encoded = {114, 147, 150, 150, 153}
local decoded = rotNDecode(encoded, 42)
-- decoded = "Hello"
```

ROT-N is trivial to break with frequency analysis on common characters like spaces, parentheses, and quotes. Always combine it with another technique rather than using it alone.

## RC4 Stream Cipher

RC4 is a well-known stream cipher that generates a pseudorandom keystream from a variable-length key. It is symmetric: the same key setup and stream generation encrypts and decrypts.

### RC4 Implementation in Lua

```lua theme={"theme":{"light":"ayu-dark","dark":"night-owl"}}
function rc4crypt(data, key)
    local S = {}
    for i = 0, 255 do
        S[i] = i
    end

    local j = 0
    for i = 0, 255 do
        j = (j + S[i] + string.byte(key, (i % #key) + 1)) % 256
        S[i], S[j] = S[j], S[i]
    end

    local i = 0
    j = 0
    local result = {}

    for k = 1, #data do
        i = (i + 1) % 256
        j = (j + S[i]) % 256
        S[i], S[j] = S[j], S[i]
        local K = S[(S[i] + S[j]) % 256]
        result[k] = string.char(string.byte(data, k) ~ K)
    end

    return table.concat(result)
end
```

### Usage for String Encoding

```lua theme={"theme":{"light":"ayu-dark","dark":"night-owl"}}
local key = "MyRC4Key2024"
local plaintext = "Authorization: Bearer secret-token"

-- Encrypt at build time
local encrypted = rc4crypt(plaintext, key)
-- encrypted is a binary string; convert to byte table for embedding

-- Embed and decrypt at runtime
local _e = { /* byte values of encrypted string */ }
local _k = "MyRC4Key2024"
local decrypted = rc4crypt(string.char(table.unpack(_e)), _k)
```

RC4 provides stronger protection than XOR or ROT-N but adds \~25 lines of decoder code to your script. Obfuscate the RC4 function itself with variable renaming and control flow flattening to protect the key schedule logic.

## Base64 Encoding

Base64 converts binary data to ASCII-safe text. It provides no security on its own but is essential for embedding binary ciphertexts (from XOR, RC4, or compression) into Lua source files.

### Full Lua Base64 Encoder and Decoder

```lua theme={"theme":{"light":"ayu-dark","dark":"night-owl"}}
local b64chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"

function b64encode(data)
    local result = {}
    local padding = (3 - (#data % 3)) % 3
    local padded = data .. string.rep("\0", padding)

    for i = 1, #padded, 3 do
        local a, b, c = string.byte(padded, i, i + 2)
        local n = a * 65536 + b * 256 + c
        table.insert(result, string.sub(b64chars, math.floor(n / 262144) % 64 + 1, math.floor(n / 262144) % 64 + 1))
        table.insert(result, string.sub(b64chars, math.floor(n / 4096) % 64 + 1, math.floor(n / 4096) % 64 + 1))
        table.insert(result, string.sub(b64chars, math.floor(n / 64) % 64 + 1, math.floor(n / 64) % 64 + 1))
        table.insert(result, string.sub(b64chars, n % 64 + 1, n % 64 + 1))
    end

    if padding > 0 then
        for i = 1, padding do
            result[#result] = "="
        end
    end

    return table.concat(result)
end

function b64decode(data)
    local decode = {}
    for i = 1, #b64chars do
        decode[string.byte(b64chars, i)] = i - 1
    end

    local result = {}
    local padding = 0

    for i = #data, #data - 1, -1 do
        if string.sub(data, i, i) == "=" then
            padding = padding + 1
        end
    end

    for i = 1, #data - padding, 4 do
        local a = decode[string.byte(data, i)] or 0
        local b = decode[string.byte(data, i + 1)] or 0
        local c = decode[string.byte(data, i + 2)] or 0
        local d = decode[string.byte(data, i + 3)] or 0
        local n = a * 262144 + b * 4096 + c * 64 + d

        table.insert(result, string.char(math.floor(n / 65536) % 256))
        if i + 2 <= #data - padding then
            table.insert(result, string.char(math.floor(n / 256) % 256))
        end
        if i + 3 <= #data - padding then
            table.insert(result, string.char(n % 256))
        end
    end

    return table.concat(result)
end
```

### Embedding Base64-Encoded Strings

```lua theme={"theme":{"light":"ayu-dark","dark":"night-owl"}}
local _s = "SGVsbG8sIFdvcmxkIQ=="
local decoded = b64decode(_s)
print(decoded)  -- Hello, World!
```

> [!WARNING]
>
> Base64 alone provides zero security. It is an encoding, not an encryption. Always combine Base64 with an actual cipher like XOR or RC4.

## Compression Plus Encoding

For scripts containing many large strings, compress before encrypting to reduce size. The standard pattern is compress, then encrypt, then Base64-encode for embedding.

### Luau Buffer Library (Roblox)

Luau on Roblox provides a `buffer` library for binary data manipulation. While there is no built-in zlib binding in base Luau, some environments offer `zlib` through native modules or the `HttpService` pipeline.

### Standard Lua Compression

In standard Lua, you can use `lua-zlib` or similar bindings:

```lua theme={"theme":{"light":"ayu-dark","dark":"night-owl"}}
local zlib = require("zlib")

-- Compress
local compressed = zlib.deflate("Large amount of text data...")
local encoded = b64encode(compressed)

-- Embed
local _data = "eJzLSM3JyQcABmwCFQ=="

-- Decompress at runtime
local compressed = b64decode(_data)
local decompressed = zlib.inflate(compressed)
```

If your target environment lacks zlib bindings, you can implement a simple LZ77-style compressor in pure Lua, though the decoder will be larger.

### Full Pipeline Example

```lua theme={"theme":{"light":"ayu-dark","dark":"night-owl"}}
-- Build-time pipeline (pseudocode)
-- 1. plaintext = "Very long configuration JSON..."
-- 2. compressed = zlib.deflate(plaintext)
-- 3. encrypted = rc4crypt(compressed, key)
-- 4. embedded = b64encode(encrypted)

-- Runtime decoding
local _k = string.char(77, 121, 75, 101, 121)  -- "MyKey"
local _e = "a1b2c3d4e5f6..."  -- base64 string

local function _d(s)
    local _b = b64decode(s)
    return rc4crypt(_b, _k)
end

local configJSON = zlib.inflate(_d(_e))
```

## The Runtime Decoder Pattern

Instead of scattering decode logic throughout your script, define a single decoder function at the top and replace every string literal with a call to that function.

### Single Decoder Function

```lua theme={"theme":{"light":"ayu-dark","dark":"night-owl"}}
local _D = function(_s)
    local _t = {}
    for _i = 1, #_s do
        _t[_i] = string.char(_s[_i] ~ 0xAB)
    end
    return table.concat(_t)
end

local url = _D({195, 197, 197, 206, 143, 171, 188, 197, 206, 200, 197, 200, 143, 195, 206, 200, 143, 195, 206, 200})
local msg = _D({219, 197, 200, 200, 206, 215, 143, 219, 197, 200, 200, 206, 215})
local path = _D({212, 197, 200, 200, 206, 215, 143, 212, 197, 200, 200, 206, 215})
```

### Obfuscating the Decoder

The decoder function is now the weakest link. Protect it by:

1. Renaming all internal variables
2. Using opaque arithmetic instead of constants
3. Splitting the function into multiple nested closures

```lua theme={"theme":{"light":"ayu-dark","dark":"night-owl"}}
local _D = (function()
    local _a = 0xAB
    local _b = string.char
    local _c = table.concat
    return function(_s)
        local _t = {}
        for _i = 1, #_s do
            _t[_i] = _b(_s[_i] ~ _a)
        end
        return _c(_t)
    end
end)()
```

### Full Example File

Here is a complete obfuscated script with five strings replaced by decoder calls:

```lua theme={"theme":{"light":"ayu-dark","dark":"night-owl"}}
local _D = (function()
    local _a = 0xAB
    local _b = string.char
    local _c = table.concat
    return function(_s)
        local _t = {}
        for _i = 1, #_s do
            _t[_i] = _b(_s[_i] ~ _a)
        end
        return _c(_t)
    end
end)()

local _A = _D({195, 197, 197, 206, 143, 171, 188, 197, 206, 200, 197, 200})
local _B = _D({219, 197, 200, 200, 206, 215, 143, 219, 197, 200, 200, 206, 215})
local _C = _D({212, 197, 200, 200, 206, 215, 143, 195, 206, 200, 200, 197, 203, 212})
local _E = _D({188, 206, 215, 215, 197, 200, 143, 195, 206, 200, 200, 197, 203, 212})
local _F = _D({188, 206, 215, 215, 197, 200, 143, 219, 197, 200, 200, 206, 215})

print(_A)
print(_B)
print(_C)
print(_E)
print(_F)
```

## String Table Indirection

For maximum obfuscation, collect all encoded strings into a single table at the top of the file and access them by numeric index.

```lua theme={"theme":{"light":"ayu-dark","dark":"night-owl"}}
local _S = {
    {195, 197, 197, 206, 143, 171, 188, 197, 206, 200, 197, 200},
    {219, 197, 200, 200, 206, 215, 143, 219, 197, 200, 200, 206, 215},
    {212, 197, 200, 200, 206, 215, 143, 195, 206, 200, 200, 197, 203, 212},
    {188, 206, 215, 215, 197, 200, 143, 195, 206, 200, 200, 197, 203, 212},
    {188, 206, 215, 215, 197, 200, 143, 219, 197, 200, 200, 206, 215},
}

local _D = function(_t)
    local _r = {}
    for _i = 1, #_t do
        _r[_i] = string.char(_t[_i] ~ 0xAB)
    end
    return table.concat(_r)
end

local url = _D(_S[1])
local msg = _D(_S[2])
local path = _D(_S[3])
local err1 = _D(_S[4])
local err2 = _D(_S[5])
```

This pattern has two advantages. First, it centralizes all string data, making it harder to correlate a specific encoded block with its usage site. Second, it reduces the decoder call sites to a uniform `_D(_S[N])` pattern that resists simple pattern matching.

> [!WARNING]
>
> Any runtime decoder can be hooked by a debugger or a modified interpreter. An attacker can set a breakpoint on `string.char`, `table.concat`, or your decoder function and read the output. Combine string encoding with anti-debug checks and environment verification to raise the bar for dynamic analysis.

> **Should I encode every string?**
>
> Encoding every string provides maximum protection but increases script size and startup cost. Each encoded string adds overhead: the byte table, the decoder call, and the runtime computation to reconstruct the plaintext. For large scripts with hundreds of strings, this overhead is significant.
>
> The pragmatic approach is to classify strings by sensitivity:
>
> **Always encode:**
>
> * API endpoints and URLs
> * Authentication tokens, keys, and secrets
> * File paths and resource names
> * Error messages that reveal internal logic
> * Debug strings and logging formatters
>
> **Optionally encode:**
>
> * User-facing UI text (if it reveals feature names)
> * Internal state names and event identifiers
> * Protocol message types and command strings
>
> **Usually skip:**
>
> * Common format strings like `"%s"`, `"%d"`, `"%.2f"`
> * Single-character delimiters and separators
> * Empty strings and trivial constants
> * Standard library names that are already public knowledge
>
> A good obfuscator should allow you to tag strings by category and apply the appropriate encoding level to each. For Luau on Roblox, also consider the bytecode size limit: excessively large scripts may fail to load if every 10-character string becomes a 50-character byte table.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.