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.a for modules built with the MSFS 2020 SDK
  • DRMCore2_FS24.a / DRMCore2_FS24_debug.a for 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:

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

  2. Ensure Configuration → All Configurations is selected.

  3. Navigate to:

    • Configuration Properties → VC++ Directories

      • Include Directories: Add the path to the include folder.
    • Linker → General

      • Additional Library Directories: Add the path to the include folder.
    • Linker → Input

      • Additional Dependencies: Add DRMCore2_FS20.a or DRMCore2_FS24.a (Release), or the matching _debug.a library (Debug)

Click OK to apply.

Debug vs Release library: the _debug.a libraries 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 Portal

Security Enhancement: Use the OBF(...) macro from obfusheader.h 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.
    //   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

OptionDescription
url(string)RequiredBase URL of the DRM server. The activation path is appended automatically.
productName(string)RequiredProduct name shown on the activation UI.
productVersion(string)OptionalVersion appended to the product name on the activation UI.
clientApiKey(string)RequiredClient API key created on the DRM Portal.
keyless(bool)Requiredtrue for Marketplace (no activation key), false for activation-key-based auth. Must be set explicitly.
wasmPath(string)Required in key modePackage-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)RequiredCalled on successful activation with an SResponse (expiry, username, Client API key status).
onError(cb)RequiredCalled when activation fails with an FResponse (error message).
onTamper(cb)RequiredCalled when tampering is detected (only if autoAbort(false)).
keyRegex(string)OptionalClient-side regex check of the activation key.
autoAbort(bool)OptionalCrash on tampering (default) or call onTamper.
transparentSuccessScreen(bool)OptionalHide the gauge after successful activation.
eulaUrl(string)OptionalEULA / Terms page the user must accept on first launch.
addFileHash(string)RecommendedAdds a file to the server-side integrity check. Call once per file.
requireLocalWasmSigning(bool)DeprecatedIgnored 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.exe and the generated DRMFileHash.h/DRMFileHash.cpp are 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 .wasm or edited .js/.html never blocks you during development. No manual whitelisting step is needed.

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

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 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): when you want to decide yourself how the aircraft reacts, in your onTamper callback.

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:
    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.
  • Page requirements: the page must allow being shown in a frame (no X-Frame-Options: DENY / SAMEORIGIN and no restrictive CSP frame-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(); // void

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

APIUse 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, or fallback while the formula is unavailable
  • Formula_<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 .wasm and 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 + fallback

Do 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 reports ArityMismatch. 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 requests

ConfirmAuth(): 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_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(): 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 via GetAuthValue() 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 string GetAuthValue() 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 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. Arrays are stored flat: for a table with cols columns, use index = 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_AUTHKEY takes 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;
}