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.
Activation is online-only (L3DRM 3.0 or newer): the aircraft activates against the DRM server every time it is loaded. There is no offline or cached activation. If the network request fails, the module retries 3 times (after 10, 30 and 60 seconds) before reporting the error through
onError.
Setting Up Visual Studio
The DRM module ships as a static library, one per MSFS SDK:
DRMCore2_FS20.a/DRMCore2_FS20_debug.afor modules built with the MSFS 2020 SDKDRMCore2_FS24.a/DRMCore2_FS24_debug.afor modules built with the MSFS 2024 SDK
Link the library that matches the MSFS SDK (platform toolset) of your module, 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_FS20.aorDRMCore2_FS24.a(Release), or the matching_debug.alibrary (Debug)
Click OK to apply.
Debug vs Release library: the
_debug.alibraries provide enhanced console logging for diagnosing integration issues. The release libraries have 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" // OBF(...) string obfuscation
#include "L3DRM_Formulas.h" // optional: formula wrappers generated by the DRM PortalSecurity Enhancement: Use the
OBF(...)macro fromobfusheader.hto 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.
// response.expires expiry date of the activation key (time_t)
// response.username registered username
// response.vStatus status of the Client API key used:
// SResponse::DRMvStatus::DRMvStatus_Active,
// DRMvStatus_Deprecated or DRMvStatus_Inactive.
// Use it to show update info to the user.
std::cout << "DRM Success";
}
void DrmErrorEvent(FResponse response)
{
// Called when an error occurs during activation.
// response.errorMessage message to show to the user
std::cerr << "DRM FAIL";
}
void DrmTamperEvent()
{
// Called when DRM tampering is highly likely (only if autoAbort is false).
// It's recommended to immediately terminate MSFS or disable the aircraft.
std::cerr << "DRM Tamper";
}DRM Initialization
Initialize the DRM using the Builder pattern. build() creates the DRM instance and starts activation in the background; the result arrives through onSuccess or onError. Guard it with IsInitialized() so it is safe to reach more than once (e.g., from a callback that may fire more than once).
if (!DRMPublish::IsInitialized())
{
DRMPublish::Builder builder = DRMPublish::Builder()
// Required: server endpoint, product info. Recommended: WASM path
.url(OBF("https://sapi.drm.land3simulations.com/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
// Optional: behavior toggles
.autoAbort(false)
.transparentSuccessScreen(false)
.eulaUrl(OBF("https://www.example.com/privacy-policy"));
// File-hash integrity checks (path only; hashes computed at activation
// and verified server-side, see File Hash Verification)
builder.addFileHash(OBF(".\html_ui\Pages\VCockpit\Instruments\MyCompany\ModuleDrmGauge.html"));
builder.addFileHash(OBF(".\html_ui\Pages\VCockpit\Instruments\MyCompany\ModuleDrmGauge.js"));
builder.addFileHash(OBF(".\SimObjects\Airplanes\MyCompany_Aircraft\panel\panel.cfg"));
builder.build();
}Builder Options
| Option | Description | |
|---|---|---|
url(string) | Required | Base URL of the DRM server. The activation path is appended automatically. |
productName(string) | Required | Product name shown on the activation UI. |
productVersion(string) | Optional | Version appended to the product name on the activation UI. |
clientApiKey(string) | Required | Client API key created on the DRM Portal. |
keyless(bool) | Required | true for Marketplace (no activation key), false for activation-key-based auth. Must be set explicitly. |
wasmPath(string) | Required in key mode | Package-relative path of your .wasm. The server checks its hash at activation, like the addFileHash() files. In key mode (keyless(false)), activation fails with AUTHZ_006 when no .wasm hash is sent and the Client API key has "require hash" on (the default); other file hashes alone do not count. Recommended for Marketplace builds too. |
onSuccess(cb) | Required | Called on successful activation with an SResponse (expiry, username, Client API key status). |
onError(cb) | Required | Called when activation fails with an FResponse (error message). |
onTamper(cb) | Required | Called when tampering is detected (only if autoAbort(false)). |
keyRegex(string) | Optional | Client-side regex check of the activation key. |
autoAbort(bool) | Optional | Crash on tampering (default) or call onTamper. |
transparentSuccessScreen(bool) | Optional | Hide the gauge after successful activation. |
eulaUrl(string) | Optional | EULA / Terms page the user must accept on first launch. |
addFileHash(string) | Recommended | Adds a file to the server-side integrity check. Call once per file. |
requireLocalWasmSigning(bool) | Deprecated | Ignored since 3.0.0 (local WASM signing was removed). Remove the call. |
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. On each activation the DRM module computes a hash of every registered file (and of the .wasm set with wasmPath()) and sends them to the DRM server. Verification happens server-side against the hashes whitelisted on the DRM Portal. If a file has been tampered with, activation will fail.
Changed in 2.0.X: You no longer precompute hashes.
HashGenerator.exeand the generatedDRMFileHash.h/DRMFileHash.cppare gone.addFileHash()now takes only the file path. See the upgrade guide for migration details.
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. The .wasm module is checked through wasmPath(); do not list it via addFileHash().
Register protected files (path only):
builder.addFileHash(OBF(".\html_ui\Pages\VCockpit\Instruments\MyCompany\ModuleDrmGauge.html"));
builder.addFileHash(OBF(".\html_ui\Pages\VCockpit\Instruments\MyCompany\ModuleDrmGauge.js"));
builder.addFileHash(OBF(".\SimObjects\Airplanes\MyCompany_Aircraft\panel\panel.cfg"));The path is the file's location as seen from the package root at runtime (the path the DRM runtime uses to locate the file). Use OBF(...) to obfuscate it.
How verification works
When hash validation is required, a hash must be marked valid on the DRM Portal to pass, with two exceptions: a hash first submitted by a whitelisted activation key or by a whitelisted username is accepted automatically. The first username that submitted each hash is recorded, so you can see who introduced a tampered file.
Developing with hash validation on: Use a whitelisted activation key (or whitelist your own username) for your builds. Hashes first seen from a whitelisted key or username are accepted automatically and marked valid, so a new
.wasmor edited.js/.htmlnever blocks you during development. No manual whitelisting step is needed.
Whitelisting an activation key
To mark an activation key as whitelisted:
- Open the Developer Portal and go to Activation Keys.
- Search for the key you want to whitelist.
- Tick the WL (whitelist) checkbox on that key's row.
From then on, any file hash first submitted by that key is accepted automatically and marked valid on the Portal. No manual per-hash whitelisting is needed.
Use whitelisted keys carefully. A whitelisted key is not restricted to a single user: it can be activated by multiple users, and every one of them auto-accepts whatever hashes they submit. If a whitelisted key leaks, a tampered
.wasmor.js/.htmlfrom an attacker would be whitelisted automatically and pass verification. Whitelist only keys you fully control (your own development/build machines) and revoke a whitelisted key the moment it is no longer needed.
Whitelisting a username
A username can be whitelisted the same way as an activation key: new file hashes first submitted by a whitelisted username are accepted automatically and marked valid. This is useful for developers and testers who activate with changing keys, or with Marketplace (keyless) builds.
- Manage whitelisted usernames in the Users area of the Developer Portal.
- Or use the quick action in the First seen by column of the Client API key Settings dialog to whitelist the user who submitted a hash.
Trust: a whitelisted username means you trust every hash that user reports. Whitelist only people you trust (your own team) and remove the entry when it is no longer needed.
Manage hashes on the Portal: The Client API Keys settings dialog lists every submitted hash, which file it belongs to, and the user that first submitted it. Mark a hash valid to whitelist it for non-whitelisted (end-user) keys and users.
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): when you want to decide yourself how the aircraft reacts, in youronTampercallback.
Builder Option: transparentSuccessScreen()
When enabled, the DRM gauge will be transparent instead of continuously showing the activation success message.
builder.transparentSuccessScreen(true);- 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: 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
- Page requirements: the page must allow being shown in a frame (no
X-Frame-Options: DENY / SAMEORIGINand no restrictive CSPframe-ancestors), otherwise the EULA screen stays blank. Scripts do not run in the frame, so the text must be in the delivered HTML (a static or server-rendered page, not a client-rendered single-page app).
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.
Deprecated: requireLocalWasmSigning()
Local WASM signing was removed in 3.0.0. requireLocalWasmSigning() is ignored and VerifySignature() does nothing; remove both calls. The .wasm is still checked server-side at activation through wasmPath().
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();The five variants are separate implementations of the same check, so a single patch cannot disable all of them. Each returns true when the check passes.
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(); // returns bool
DRMPublish::AssureAuthenticated2(); // void
DRMPublish::AssureAuthenticated3(); // void
DRMPublish::AssureAuthenticated4(); // void
DRMPublish::AssureAuthenticated5(); // voidLike the integrity checks, the five variants are separate implementations; use different ones in different places. AssureAuthenticated1() also returns whether the aircraft is activated (a false result is only reachable with autoAbort(false)).
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.
| API | Use it for |
|---|---|
Formulas (L3DRM_Formulas.h, DRM_EVAL) | Recommended. Calculations: thrust, fuel flow, gauge scaling, lookup tables. The math itself is delivered by the server; your .wasm only contains the call. |
GetAuthValue() | Configuration values and strings (limits, colors, texts). The value is delivered by the server and returned whole. |
ConfirmAuth() | Passing a value you computed yourself only when activated. |
GetAuthMultiplier() | Older multiplier API; prefer formulas. |
Formulas (L3DRM 3.0 or newer)
A formula is a calculation you define in the Formulas tab of the DRM Portal (keys start with VM_). It is delivered encrypted after activation, and your aircraft evaluates it with 1 to 4 inputs. Because the calculation is not in your .wasm, a cracked module without activation has nothing to compute with. This makes formulas stronger than GetAuthValue(), which returns a whole value that can be copied once it is known.
Put a whole calculation into one formula: every step you leave in C++ is visible in your .wasm. See the Formula Reference for what a formula can use.
Generated header: L3DRM_Formulas.h
In the Formulas tab, click Download C++ header and add L3DRM_Formulas.h to your project. Download it again after adding or changing formulas. For every formula VM_<KEY> it contains:
Formula_<KEY>(inputs..., fallback): the result, orfallbackwhile the formula is unavailableFormula_<KEY>_Ready(): whether this formula is delivered and valid
plus L3DRM_FormulasReady(), which is true once every formula in the header is ready.
#include "L3DRM_Formulas.h"
// Formula VM_FUEL_FLOW_R1 with inputs throttle, rpm
double ff = Formula_FUEL_FLOW_R1(throttle, rpm, 0.0); // with a degraded fallback
// Show "not activated" instead of fallback values
if (!L3DRM_FormulasReady())
{
DrawNotReady();
}A formula is unavailable until the aircraft is activated, and also when it is missing or rejected, or when an input or intermediate result is not finite. Formula calls never return NaN: while unavailable they return your fallback. Use L3DRM_FormulasReady() or Formula_<KEY>_Ready() to tell real values from fallbacks.
Fallback: use a degraded fallback (e.g. reduced output), never the correct value. The fallback is visible in your
.wasmand is what runs without activation.
Inputs are positional, in the order shown in the formula editor. Changing the input order, units or meaning of a shipped formula needs a new key (e.g.
_R2), because released aircraft call it with a fixed argument order.
Without the header: DRM_EVAL
The wrappers call DRM_EVAL, which you can also use directly. Pass the formula's inputs, then the fallback as the last argument. It returns the fallback while the formula is unavailable (for example before activation).
double ff = DRM_EVAL("VM_FUEL_FLOW_R1", throttle, rpm, 0.0); // 2 inputs + fallbackDo not forget the fallback. Without it, your last input is taken as the fallback and the formula gets one input too few, so it always returns that input.
GetProgramStatus()then reportsArityMismatch. The generated wrappers cannot make this mistake.
Diagnostics
If a formula stays unavailable after activation, ask why (also works in Release builds):
DrmProgramStatus s = DRMPublish::GetProgramStatus(DRM_AUTHKEY("VM_FUEL_FLOW_R1"), 2); // 2 = input count
// Ok, NotAuthenticated, Missing, Rejected, Duplicate, ArityMismatch
int reason = DRMPublish::GetProgramRejectReason(DRM_AUTHKEY("VM_FUEL_FLOW_R1"));
// Include the reason code in support requestsConfirmAuth(): Value Passthrough
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(): Multiplier
Multiply it into an existing calculation. It keeps your calculation correct only when the aircraft is activated. Prefer formulas for new code.
double epr = computeEPR(n1, n2, ambientTemp);
epr *= DRMPublish::GetAuthMultiplier();GetAuthValue(): Online Protected Values
Protected values are defined on the DRM Portal, not in your code. The server delivers them encrypted at activation time, so they never exist as literals in your shipped binary. Use them for configuration values and strings (limits, colors, texts). For calculations, use formulas: GetAuthValue() returns the value whole, while a formula keeps the calculation itself off the client.
Changed in 2.0.X: The Builder
.addAuthValue()method was removed. Define each value on the DRM Portal for your project instead. Runtime retrieval viaGetAuthValue()is unchanged. See the upgrade guide for migration details.
Runtime retrieval (using the recommended DRM_AUTHKEY() form):
int r = DRMPublish::GetAuthValue(DRM_AUTHKEY("FILL_COLOR_R"), 0);
float maxMach = DRMPublish::GetAuthValue(DRM_AUTHKEY("MAX_MACH"), 0.0f);Available scalar overloads (each also exists with a DRM_AUTHKEY id instead of a string key):
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 const 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 value defined on the Portal. When not authenticated (or cracked), returns the
defaultValue. A key not defined on the Portal also returns the default silently (with a warning in the debug log). String results point into an internal buffer that is valid until the next stringGetAuthValue()call; copy them if you need to keep them.
Array auth values: a Portal value can hold multiple numeric elements, retrieved by index:
static int GetAuthValue(const std::string& key, size_t index, int defaultValue);
static float GetAuthValue(const std::string& key, size_t index, float defaultValue);
static double GetAuthValue(const std::string& key, size_t index, double defaultValue);
static size_t GetAuthValueArrayCount(const std::string& key);// Portal key "FILL_COLOR_RGB" holds [10, 190, 60]
int r = DRMPublish::GetAuthValue(DRM_AUTHKEY("FILL_COLOR_RGB"), 0, 250);
int g = DRMPublish::GetAuthValue(DRM_AUTHKEY("FILL_COLOR_RGB"), 1, 0);
int b = DRMPublish::GetAuthValue(DRM_AUTHKEY("FILL_COLOR_RGB"), 2, 0);
// Iterate without hardcoding the size
size_t n = DRMPublish::GetAuthValueArrayCount(DRM_AUTHKEY("GEAR_LIMITS"));
for (size_t i = 0; i < n; ++i)
{
double v = DRMPublish::GetAuthValue(DRM_AUTHKEY("GEAR_LIMITS"), i, 0.0);
}Array overloads return the element at
indexwhen authenticated; thedefaultValuewhen unauthenticated, the key is missing, orindexis out of range.GetAuthValueArrayCount()returns0when unauthenticated or the key is not found. Arrays are stored flat: for a table withcolscolumns, useindex = row * cols + col.
DRM_AUTHKEY(): Compile-Time Key
DRM_AUTHKEY("NAME") turns a name into a compile-time id; the name is not in your .wasm. It is also the fastest form and safe to call every frame. Use it everywhere; the OBF("NAME") string form is only needed for names computed at runtime.
// string key (slower, fine for occasional reads):
float w = DRMPublish::GetAuthValue(OBF("MFD_STROKE_WIDTH"), 3.0f);
// compile-time key (recommended):
float w = DRMPublish::GetAuthValue(DRM_AUTHKEY("MFD_STROKE_WIDTH"), 3.0f);Notes:
- The name must match the name defined on the DRM Portal exactly (case-sensitive).
DRM_AUTHKEYtakes a string literal.- "Key" here is the name of an auth value or formula, not the activation key, client API key, or any cryptographic key.
Shutdown
Call Shutdown() in PANEL_SERVICE_PRE_KILL (when your module is unloaded or the aircraft is removed):
case PANEL_SERVICE_PRE_KILL:
{
DRMPublish::Shutdown();
// ... your own cleanup
return true;
}