When we say "PHP bytecode encryption," we're describing a multi-stage process that transforms readable PHP source into an encrypted binary format. This article breaks down each stage of that pipeline.
Stage 1: Parsing and Compilation
The first step is compiling PHP source code into a custom bytecode format. This is fundamentally different from PHP's own opcache:
- PHP's opcache stores Zend Engine opcodes — a well-documented format with known decompilers (like php-decompiler). It's meant for performance, not protection.
- Custom bytecode uses a proprietary instruction set. The compiler translates PHP constructs (variables, functions, classes, control flow) into custom opcodes that only our VM understands.
The compiler handles the full PHP language surface: classes with inheritance, interfaces, traits, closures, generators, try/catch, namespaces, and all the constructs modern PHP applications use.
// Original PHP
function calculateDiscount(float $price, int $tier): float {
return match(true) {
$tier >= 3 => $price * 0.20,
$tier >= 2 => $price * 0.10,
default => $price * 0.05,
};
}
// Becomes custom bytecode (simplified representation)
LOAD_CONST 0 ; function metadata
LOAD_ARG 0 ; $price
LOAD_ARG 1 ; $tier
PUSH_CONST 3
CMP_GTE
JMP_FALSE @L1
LOAD_ARG 0
PUSH_CONST 0.20
MUL
RET
...
Stage 2: AES-256-CBC Encryption
Once compiled, the bytecode is encrypted using AES-256-CBC:
- A random IV (initialization vector) is generated for each file
- The encryption key is derived from the project's unique key stored in the CompileLock cloud
- The encrypted payload is embedded in a PHP file using
__halt_compiler()
The resulting file looks like this on disk:
<?php
require_once __DIR__ . '/compilelock-bootstrap.php';
$__k = compilelock_gate(__FILE__);
if ($__k !== null) compilelock_exec(__FILE__, $__k);
__halt_compiler();
[binary encrypted bytecode data]
The actual bytecode is completely unreadable. There's no PHP source to extract — only encrypted binary data that's meaningless without the decryption key.
Stage 3: Runtime Decryption and Execution
When the protected file is loaded by PHP, the following happens:
- The bootstrap validates the license (locally and via phone-home)
- If valid, the license returns the encryption key
- The C extension (
compilelock_exec) reads the encrypted payload after__halt_compiler() - It decrypts the bytecode in memory using AES-256-CBC
- The custom VM executes the bytecode instruction by instruction
- The decrypted bytecode never touches disk — it exists only in process memory
The Custom VM
The virtual machine is implemented as a PHP C extension. It maintains:
- Register file — 256 general-purpose registers for fast value access
- Call stack — Function call frames with local scope isolation
- Constant pool — Strings, numbers, and other literals
- Instruction pointer — Current execution position in the bytecode
The VM executes a fetch-decode-execute loop, processing each instruction and interacting with PHP's Zend Engine through the extension API. This means protected code can seamlessly call any PHP function, use any class, and interact with frameworks like Laravel or Symfony.
Performance Characteristics
The overhead of this approach is surprisingly low:
- Startup — License validation adds ~2ms on cache hits (local validation) or ~50-100ms on phone-home (once per 24 hours)
- Decryption — AES-256 decryption of a typical file takes <1ms (hardware-accelerated on modern CPUs via AES-NI)
- Execution — The VM runs at roughly 60-80% of native PHP speed for computation-heavy code, but for typical web applications (which are I/O bound), the difference is negligible
Why Not Just Use Zend Opcache Encoding?
PHP's opcache format is standardized and well-understood. Tools exist to decompile opcache files back into readable PHP. Using Zend opcodes for protection is like using ROT13 for encryption — it changes the representation but doesn't provide real security.
A custom bytecode format means there's no existing decompiler. An attacker would need to reverse-engineer the C extension (which can be stripped and obfuscated) to understand the instruction set, then build a custom decompiler from scratch. This raises the effort from "download a tool" to "months of specialized reverse engineering."