← All documentation

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:

  1. Right-click the project containing the DRM integration → Properties

  2. Ensure ConfigurationAll Configurations is selected.

  3. Navigate to:

    • Configuration PropertiesVC++ Directories

      • Include Directories: Add the path to the include folder.
    • LinkerGeneral

      • Additional Library Directories: Add the path to the include folder.
    • LinkerInput

      • Additional Dependencies: Add DRMCore2.a (Release) or DRMCore2_debug.a (Debug)

Click OK to apply.

Debug vs Release library:DRMCore2_debug.a provides 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

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://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();
}

See the dedicated sections for keyRegex(), autoAbort(), transparentSuccessScreen(), eulaUrl(), and GetAuthValue() 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. On each activation the DRM module computes a hash of every registered file (and the .wasm itself) 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.exe, drm_files.txt, and the generated DRMFileHash.h/DRMFileHash.cpp are gone. addFileHash() now takes only the file path — see the 2.0.X 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 always checked automatically — 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 one exception: a hash first submitted by a whitelisted activation key 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 for your own builds. Hashes first seen from a whitelisted key are accepted automatically and marked valid, so a new .wasm or edited .js/.html never blocks you during development — no manual whitelisting step.

Whitelisting an activation key

To mark an activation key as whitelisted:

  1. Open the Developer Portal and go to Activation Keys.
  2. Search for the key you want to whitelist.
  3. 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 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 .wasm or .js/.html from 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.

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.

DRM validation will begin automatically after build(). Wait for DrmSuccessEvent() 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: 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:
    1. On first launch, the DRM gauge checks for a marker file in the \work\ folder.
    2. 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.
    3. All interaction with the page content is blocked (links, forms, etc.) — the user can only scroll and accept.
    4. When the user clicks Accept, the marker file is written and the normal DRM authentication flow begins.
    5. On subsequent launches, the marker file is found and the EULA is skipped entirely.

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_ACTIVATION to skip when you want to test without DRM. Define BYPASS_ACTIVATION in 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() — Online Protected Values

The strongest anti-crack mechanism the library offers. Protected values are defined on the DRM Portal, not in your code — the server delivers them encrypted at activation time. They never exist as literals in your shipped binary, so there is nothing for an attacker to find, patch, or copy.

Changed in 2.0.X: The Builder .addAuthValue() method was removed. Define each value on the DRM Portal for your project instead. Runtime retrieval via GetAuthValue() is unchanged. See the 2.0.X upgrade guide for migration details.

Runtime retrieval — the examples below use the compile-time-keyed DRM_AUTHKEY("KEY") macro, which is the recommended form and safe to call every frame (see DRM_AUTHKEY() below for why and how):

int r = DRMPublish::GetAuthValue(DRM_AUTHKEY("FILL_COLOR_R"), 0);
float maxMach = DRMPublish::GetAuthValue(DRM_AUTHKEY("MAX_MACH"), 0.0f);

Available scalar overloads:

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 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).

Array auth values (new in 2.0.X): 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("eng_fan_cr"));
for (size_t i = 0; i < n; ++i)
{
    double v = DRMPublish::GetAuthValue(DRM_AUTHKEY("eng_fan_cr"), i, 0.0);
}

Array overloads return the element at index when authenticated; the defaultValue when unauthenticated, the key is missing, or index is out of range. GetAuthValueArrayCount() returns 0 when unauthenticated or the key is not found. See Step 4 of the 2.0.X upgrade guide for multi-dimensional (flat) array indexing.

DRM_AUTHKEY() — Per-Frame Fast Path (new in 2.0.4)

All the examples above use DRM_AUTHKEY("KEY"). You can also look a value up with a string key via OBF("KEY") — but that form is not free: on every call it builds and decrypts an std::string, re-hashes it (FNV), and walks a std::map to find the value. That is fine for occasional reads, but too costly to call once per frame for many values.

2.0.4 adds the DRM_AUTHKEY("KEY") macro. It resolves the key at compile time to a single uint64_t id, so the lookup becomes one O(1) index probe with no per-call string build, OBF decode, or hashing. The key string is consumed by the compiler and never ships in your binary. Prefer it everywhere; reach for OBF(key) only if you need a key computed at runtime.

Migrate existing hot call sites by replacing OBF(key) with DRM_AUTHKEY(key):

// before (fine occasionally, too slow per frame):
float w = DRMPublish::GetAuthValue(OBF("MFD_STROKE_WIDTH"), 3.0f);

// after (cheap, safe to call every frame):
float w = DRMPublish::GetAuthValue(DRM_AUTHKEY("MFD_STROKE_WIDTH"), 3.0f);

Works for every overload — scalars, strings, and arrays:

int    r  = DRMPublish::GetAuthValue(DRM_AUTHKEY("FILL_COLOR_RGB"), 0, 250);
size_t n  = DRMPublish::GetAuthValueArrayCount(DRM_AUTHKEY("eng_fan_cr"));
const std::string& ed =
    DRMPublish::GetAuthValue(DRM_AUTHKEY("edition"), std::string("demo"));

Notes:

  • Non-breaking: the existing GetAuthValue(std::string, ...) overloads are kept unchanged. Migrate only the hot, per-frame call sites; everything else keeps working as-is.
  • The name passed to DRM_AUTHKEY("...") must match the name defined on the DRM Portal exactly (case-sensitive); it is hashed identically on both sides. No Portal change is needed — same names, same values.
  • DRM_AUTHKEY takes a string literal (it is evaluated at compile time).
  • "Key" here is the lookup key into the auth-value registry — not the activation key, client API key, or any cryptographic key.

Shutdown

Call Shutdown() when your module is unloaded or the aircraft is removed:

DRMPublish::Shutdown();