L3S DRM Developer Guide
All required files for the C++/WASM module are located in the include folder.
Copy this folder to a known location of your choice.
Security Note: Make sure you're using the latest
include/files for enhanced security features. See the WASM Build & Security guide for important security improvements.
How It Works
Your C++/WASM code must pass a few parameters to the L3S DRM module using the Builder pattern.
This includes a DrmSuccessEvent() callback function.
Only when this function is called should you allow usage of your aircraft.
How you implement this is up to you.
Setting Up Visual Studio
The DRM Module is compiled as a static .wasm module in DRMCore2.a.
You need to link this file and reference the header files in Visual Studio:
Right-click the project containing the DRM integration → Properties
Ensure Configuration → All Configurations is selected.
Navigate to:
Configuration Properties → VC++ Directories
Include Directories: Add the path to theincludefolder.
Linker → General
Additional Library Directories: Add the path to theincludefolder.
Linker → Input
Additional Dependencies: AddDRMCore2.a(Release) orDRMCore2_debug.a(Debug)
Click OK to apply.
Debug vs Release library:
DRMCore2_debug.aprovides enhanced console logging for diagnosing integration issues. The release version (DRMCore2.a) has very limited logging to make it harder for attackers to reverse-engineer the DRM flow. Use the debug library during development and switch to the release library for public builds.
Initializing DRM
At the top of your .cpp file:
#include "DRMCommon.h"
#include "DRMPublish.h"
#include "obfusheader.h" // For string obfuscation
#include "DRMFileHash.h" // For auto-generated file hashes (see File Hash Verification)Security Enhancement: Use the
OBF(...)macro to obfuscate sensitive strings. See the WASM Build & Security guide for details.
Callback Functions
Define the following DRM event handlers:
void DrmSuccessEvent(SResponse response)
{
// Called only when activation is successful.
std::cout << "DRM Success";
}
void DrmErrorEvent(FResponse response)
{
// Called when an error occurs during activation.
std::cerr << "DRM FAIL";
}
void DrmTamperEvent()
{
// Called when DRM tampering is highly likely.
// It's recommended to immediately terminate MSFS or disable the aircraft.
std::cerr << "DRM Tamper";
}DRM Initialization
Initialize the DRM using the Builder pattern. The initialization is guarded by IsInitialized(), so it is safe to call multiple times (e.g., from a callback that may fire more than once).
if (!DRMPublish::IsInitialized())
{
DRMPublish::Builder builder = DRMPublish::Builder()
// Required: server endpoint, product info, WASM path
.url(OBF("https://kdlkczwmscovjiftqlwq.supabase.co/functions/v1/"))
.productName(OBF("DRM Demo"))
.productVersion(OBF("Version X"))
.keyRegex("^[A-Za-z0-9]{10}$") // Client-side validation before sending to server
.wasmPath(OBF(".\SimObjects\Airplanes\MyCompany_CommBus_Aircraft\panel\SampleCommBus.wasm"))
.onSuccess(DrmSuccessEvent)
.onError(DrmErrorEvent)
.onTamper(DrmTamperEvent)
#ifdef _MARKETPLACE_PKG
.keyless(true) // MSFS Marketplace (incl. XBOX). No activation key needed
.clientApiKey(OBF("YOUR_MARKETPLACE_CLIENT_API_KEY"))
#else
.keyless(false) // Standalone activation-key-based auth
.clientApiKey(OBF("YOUR_DIRECT_SALES_CLIENT_API_KEY"))
#endif
#ifdef _DEBUG
.debug(true)
#endif
// Optional: behavior toggles
.autoAbort(false)
.transparentSuccessScreen(false)
.eulaUrl(OBF("https://www.example.com/privacy-policy"))
// Optional: register protected constants for GetAuthValue() (see Anti-Crack Protection)
.addAuthValue(OBF("FILL_COLOR_R"), 10)
.addAuthValue(OBF("MAX_MACH"), 0.82f);
// File-hash integrity checks (auto-generated by prebuild-msfs.bat and HashGenerator.exe)
for (const auto& [path, hash] : DRMFileHash::values) {
builder.addFileHash(path, hash);
}
builder.build();
}See the dedicated sections for keyRegex(), autoAbort(), transparentSuccessScreen(), eulaUrl(), and addAuthValue() below for details on the optional builder options.
Both Marketplace and Direct Sales builds require a Client API Key. You can create separate keys for each distribution method in the developer portal.
File Hash Verification
File hash verification detects whether an attacker has modified your aircraft files after release. The DRM module computes a hash of each registered file at runtime and compares it against the expected hash compiled into your WASM. If any file has been tampered with, activation will fail.
At a minimum, you should verify ModuleDrmGauge.html, ModuleDrmGauge.js, and panel.cfg. Any other files that contain critical code (e.g. custom .js files) should be added as well.
Option 1: Auto-generated hashes (recommended)
A pre-build script auto-generates file hashes at compile time, so you never have to look them up manually.
1. Add a Pre-Build Event in Visual Studio:
- Right-click your project → Properties
- Navigate to Build Events → Pre-Build Event
- Set the Command Line to:
$(ProjectDir)scripts\prebuild-msfs.bat $(ProjectDir) $(SolutionDir)2. How the pre-build script works:
- Reads a configuration file called
drm_files.txtthat lists the files to hash. - Invokes
HashGenerator.exeon each listed file to compute its hash. - Generates a
DRMFileHash.cppsource file with all hashes embedded as obfuscated strings.
The generated DRMFileHash.cpp is compiled into your WASM module automatically.
3. Required file placement: Place HashGenerator.exe (from the release Tools/ folder), prebuild-msfs.bat, and drm_files.txt in your project's scripts/ folder.
4. Configure drm_files.txt:
Each line contains two paths separated by a pipe (|):
reference_path|actual_path_relative_to_PackageSources- Reference path (left) — The path as seen from the package root at runtime. This is the path the DRM runtime uses to locate the file.
- Actual path (right) — The path to the source file relative to your
PackageSourcesfolder, where the file actually lives during development.
Example drm_files.txt:
.\html_ui\Pages\VCockpit\Instruments\MyCompany\ModuleDrmGauge.html|Copys\aircraft-wasm\MyCompany\ModuleDrmGauge.html
.\html_ui\Pages\VCockpit\Instruments\MyCompany\ModuleDrmGauge.js|Copys\aircraft-wasm\MyCompany\ModuleDrmGauge.js
.\SimObjects\Airplanes\MyCompany_Aircraft\panel\panel.cfg|SimObjects\Airplanes\MyCompany_Aircraft\panel\panel.cfg5. Pass hashes to the builder:
for (const auto& [path, hash] : DRMFileHash::values) {
builder.addFileHash(path, hash);
}Hashes are regenerated automatically on every build, so you never need to update them manually. See the demo project for a complete working example.
Option 2: Manual hashes
You can also add file hashes manually using OBF(...) for obfuscation:
builder.addFileHash(OBF(".\html_ui\Pages\VCockpit\Instruments\MyCompany_CommBus_Aircraft_HtmlGauge\ModuleDrmGauge.html"), OBF("hash1"));
builder.addFileHash(OBF(".\html_ui\Pages\VCockpit\Instruments\MyCompany_CommBus_Aircraft_HtmlGauge\ModuleDrmGauge.js"), OBF("hash2"));
builder.addFileHash(OBF(".\SimObjects\Airplanes\MyCompany_Wasm_Aircraft\common\panel\panel.cfg"), OBF("hash3"));Obtaining manual hash values:
Use the included GenerateHashes.bat script (found in the html folder of the release package). Place it alongside your gauge files and HashGenerator.exe, then run it. It will output the hash for each file in the folder.
- Copy
GenerateHashes.batandHashGenerator.exeinto the folder containing the files you want to hash. - Run
GenerateHashes.bat. The hash for each file will be printed in the console. - Copy the hash values into your code.
Important Note: If you modify a file after obtaining its hash, you will need to re-run the script to get the new hash. The auto-generated approach (Option 1) avoids this issue entirely.
DRM validation will begin automatically after
build(). Wait forDrmSuccessEvent()before unlocking aircraft features.
Builder Option: keyRegex()
Sets a regex pattern for client-side validation of activation keys before they are sent to the server. If the key does not match the pattern, the request is rejected locally without making a server call.
builder.keyRegex("^[A-Za-z0-9]{10}$");- Default: No validation (all keys are sent to the server)
- Use case: Prevents unnecessary server requests when the user enters an obviously invalid key (e.g., wrong length or invalid characters).
Builder Option: autoAbort()
When enabled, tampering detection will immediately abort/crash the WASM module. When disabled, the onTamper callback is called instead, giving you control over how to handle it.
builder.autoAbort(false); // Use onTamper callback instead of crashing- Default: Enabled (
true) — tampering causes an immediate crash - Use case:
autoAbort(true)— For aircraft with complex logic within WASM. Crashing the module effectively disables the aircraft.autoAbort(false)— For aircraft without much WASM logic. The DRM module will then try to block flight control inputs after some time.
Builder Option: transparentSuccessScreen()
When enabled, the DRM gauge will be transparent instead of continuously showing the activation success message.
builder.transparentSuccessScreen();- Default: Disabled (success message shown continuously)
- Use case: Enable this if you want the DRM gauge to become invisible after activation, allowing you to overlay it in front of your normal cockpit screen without it blocking the view.
Builder Option: debug()
When enabled, debug mode prints diagnostic information in the MSFS console, including file hashes if mismatched.
Do NOT enable in public releases!
The recommended approach is to add .debug(true) to the Builder, wrapped in an #ifdef _DEBUG block so it is automatically excluded from release builds:
#ifdef _DEBUG
.debug(true)
#endifWhen enabled, the MSFS console will display file hashes and other diagnostic information prefixed with [DRM].
For even more verbose console logging, you can also link against DRMCore2_debug.a instead of DRMCore2.a. This provides detailed logging of all DRM operations to the MSFS console.
Builder Option: eulaUrl()
Sets a URL for a EULA, Terms & Conditions, or Privacy Policy page that is shown on first launch. The user must accept before DRM authentication proceeds.
builder.eulaUrl(OBF("https://www.example.com/privacy-policy"));- Default: Disabled (empty string — no EULA shown)
- How it works:
- On first launch, the DRM gauge checks for a marker file in the
\work\folder. - If the marker file does not exist, the gauge displays the configured URL in a read-only iframe with scroll buttons and an Accept button at the bottom.
- All interaction with the page content is blocked (links, forms, etc.) — the user can only scroll and accept.
- When the user clicks Accept, the marker file is written and the normal DRM authentication flow begins.
- On subsequent launches, the marker file is found and the EULA is skipped entirely.
- On first launch, the DRM gauge checks for a marker file in the
The URL should point to a publicly accessible page that renders well in an iframe (no login required). The page is displayed read-only — users cannot click links or interact with the content.
IntervalUpdate()
This method should be called from your _system_update or _gauge_callback function:
if (DRMPublish::IsInitialized())
{
DRMPublish::IntervalUpdate(1);
}This function must be called regularly.
It's very lightweight and won't affect performance.
Integrity Checks
Integrity checks verify that your WASM module has not been tampered with at runtime. Unlike AssureAuthenticated, these can be placed anywhere in your code — even before DRM initialization or authentication. Place them in as many locations as possible to make bypassing more difficult.
DRMPublish::Integrity1();
DRMPublish::Integrity2();
DRMPublish::Integrity3();
DRMPublish::Integrity4();
DRMPublish::Integrity5();Example:
void OnStartup() {
DRMPublish::Integrity1();
// ... runs before DRM is even initialized
}
void OnGearSelected() {
DRMPublish::Integrity2();
// ... gear logic
}
void OnFlapsChanged(int position) {
DRMPublish::Integrity3();
// ... flaps logic
}Placement guidance: Spread these calls across different functions and systems in your codebase — startup routines, gauge callbacks, user actions like gear/flaps selection or button presses, etc. The more spread out they are, the harder it is for an attacker to bypass all of them.
AssureAuthenticated Checks
These checks will crash or block the aircraft if executed without the aircraft being activated first. They should only be placed in code paths where it's certain that the aircraft must have been activated (e.g., behind DRMPublish::IsAuthenticated() or in functions enabled via DrmSuccessEvent).
DRMPublish::AssureAuthenticated1();
DRMPublish::AssureAuthenticated2();
DRMPublish::AssureAuthenticated3();
DRMPublish::AssureAuthenticated4();
DRMPublish::AssureAuthenticated5();Example:
void DrmSuccessEvent(SResponse response) {
// Aircraft is now activated — enable protected systems
EnableAvionics();
EnableFlightModel();
}
void UpdateAvionics() {
DRMPublish::AssureAuthenticated1();
// ... avionics logic (only reachable after activation)
}
void UpdateFlightModel() {
DRMPublish::AssureAuthenticated2();
// ... flight model logic (only reachable after activation)
}Warning: Do not place these in code paths that can be reached before activation — this will crash or block the aircraft. Ensure they are only called after successful authentication.
Anti-Crack Protection
The following APIs make critical calculations depend on DRM state, so that removing DRM checks from the WASM binary breaks aircraft behavior instead of producing a working crack. See the Upgrade Guide 1.0 → 1.1 for migration details.
ConfirmAuth() — Value Passthrough Gate
Returns the real value when authenticated, or a developer-specified default when not. Use this to protect any runtime-computed value.
static int ConfirmAuth(int value, int defaultValue);
static float ConfirmAuth(float value, float defaultValue);
static double ConfirmAuth(double value, double defaultValue);
static bool ConfirmAuth(bool value, bool defaultValue);
static const char* ConfirmAuth(const char* value, const char* defaultValue);Example:
double iasHoldValue = DRMPublish::ConfirmAuth(getIASHoldSpeed(), 0.0);Development bypass: Wrap in
#ifndef BYPASS_ACTIVATIONto skip when you want to test without DRM. DefineBYPASS_ACTIVATIONin the preprocessor definitions of any build configuration where you want to bypass activation.
GetAuthMultiplier() — Floating-Point Multiplier
Returns 1.0f when authenticated, 0.0f when not (or garbage if the DRM state is tampered with). Multiply it into existing calculations to silently break them when cracked.
static float GetAuthMultiplier();Example:
double epr = computeEPR(n1, n2, ambientTemp);
epr *= DRMPublish::GetAuthMultiplier();Apply to multiple independent calculations (thrust, fuel flow, hydraulic pressure, etc.) so that cracking requires patching every single one.
GetAuthValue() — Protected Constant Registry
The most secure method for protecting static or constant values. Values are registered at initialization via .addAuthValue() on the Builder, stored in an internal registry, and retrieved at runtime by key. The real values never appear as plain literals in your code.
Builder registration:
builder
.addAuthValue(OBF("FILL_COLOR_R"), 10)
.addAuthValue(OBF("MAX_MACH"), 0.82f);Runtime retrieval:
int r = DRMPublish::GetAuthValue(OBF("FILL_COLOR_R"), 0);
float maxMach = DRMPublish::GetAuthValue(OBF("MAX_MACH"), 0.0f);Available overloads for retrieval:
static int GetAuthValue(const std::string& key, int defaultValue);
static float GetAuthValue(const std::string& key, float defaultValue);
static double GetAuthValue(const std::string& key, double defaultValue);
static std::string GetAuthValue(const std::string& key, const std::string& defaultValue);
static const char* GetAuthValue(const std::string& key, const char* defaultValue);When authenticated, returns the registered value. When not authenticated (or cracked), returns the
defaultValue. A typo in the key name also returns the default silently (with a warning in the debug log).
Shutdown
Call Shutdown() when your module is unloaded or the aircraft is removed:
DRMPublish::Shutdown();