On QML Code Protection

On QML Code Protection

Developing proprietary (closed-source) software systems presents unique challenges regarding intellectual property and unauthorized access to source code, sensitive data, and other development assets. Although advanced disassembly and reverse engineering techniques can still be applied, C++ code generally enjoys sufficient protection through compiler optimizations and other standard techniques. However, this is often not the case for QML code. Despite its primary use in UI development, QML can inadvertently expose strategic or sensitive information about UI workflows and interactions with other components implemented in C++.

The deployment and execution of QML code have evolved significantly over time, transitioning from on-the-fly interpretation of plain source code to just-in-time compilation, and ultimately to ahead-of-time compilation in accordance with the requirements of current QML compilers (qmlcachegen, qmlsc, and qmltc). Typically, QML code is deployed either as plain .qml files, which are easily accessible for inspection, or bundled within the library or application's resource system. In the case of the latter, although the QML code is not stored as plain files on the file system, it remains readily accessible through tools like KDAB's GammaRay. For example, observe how GammaRay's resources inspector can quickly reveal the contents of QML code embedded within the resource system:

The issue of securing QML code has been the focus of numerous Qt bug reports (like QTBUG-118196 and QTBUG-89292). Qt 6.6 introduced QT_DISCARD_FILE_CONTENTS: a feature that specifies certain files should appear empty within the resource system:

Now, the contents of .qml files are properly suppressed within the resource system:

However, as described in the documentation, QT_DISCARD_FILE_CONTENTS introduces an issue that manifests in two distinct aspects:

  1. Since the QML source code is no longer accessible, execution now relies directly on compilation units created by qmlcachegen or qmlsc. These units are tied to the private details of the specific Qt version used during their generation. Consequently, if the QML module is utilized by an application compiled with a different version of Qt, an error message may appear stating: "File was compiled ahead of time with an incompatible version of Qt and the original file cannot be found. Please recompile" (assuming you have no cached .qmlc files for this module and Qt version at QStandardPaths::CacheLocation directory). As a result, such QML object types will not be available for use.
  2. For closed-source projects using Qt Open Source, this setup poses a clear violation of the LGPL clause which mandates that vendors must allow users to operate the application with a modified/different Qt version.

Alternative approaches, such as using qmltc (QML type compiler) to enable ahead-of-time generation of native (C++) code from .qml files, remain heavily reliant on certain assumptions—most of which identifiable by the qmllint tool. Additionally, these methods are limited to working only with QML types defined in C++, as opposed to those defined within QML documents.

A QML code protection approach

Given that, let's develop a custom QML protection approach that avoids the aforementioned drawbacks. We'll proceed with the following steps:

  1. We'll store encrypted versions of the .qml files within the QML module's resource system. This process will be automated by CMake, eliminating the need for manual intervention.
  2. We will attempt to obfuscate the encryption key to prevent it from being easily extracted from the QML module or application binaries.
  3. The encrypted .qml files should be decrypted, and their corresponding QML object types should be registered with the QML engine in a way that is straightforward for developers.
  4. The import and usage of these QML object types in applications should remain unchanged.

Encrypting QML files with CMake

The first step in our approach involves generating a random encryption key and an initialization vector (IV). Moreover, to prevent these values from being easily extracted using utilities likes strings and nm, we opt not to store them in plain text within the QML module's libraries. Instead, we utilize a simple compile-time XOR encryption for both the key and IV values:

Decryption is managed in a similar manner. While this method is not completely secure, it introduces a significant hurdle for those attempting to access security credentials. We generate new key and IV values for each build using the following CMake code snippet:

The keys.h.in file creates these values at compile-time by utilizing the described XOR encryption function.

The encryption of the module's .qml files is handled in CMake as follows:

Here we generate encrypted versions of the module's .qml files (named .qml.enc) and store them in the ENCRYPTED_FILES set variable. encrypt_decrypt is a command-line tool we developed to encrypt and decrypt files using the AES256 algorithm, which is provided by the libgcrypt library. Notice how QT_RESOURCE_ALIAS is used to make the encrypted files accessible within the resource system by their names only, rather than by the full path specified in OUTPUT_FILE.

Defining our secure QML module

Once all the encrypted files have been generated, we define the QML module as usual with qt_add_qml_module. It's important to note, however, that we do not declare any QML_FILES. Instead, the encrypted files are added using the RESOURCE parameter. To automatically manage decryption and the registration of QML types, we disable the generation of the standard Qt-provided module plugin with NO_GENERATE_PLUGIN_SOURCE and instead define our own custom module plugin implementation.

Let's now examine our custom module plugin implementation. Typically, qt_add_qml_module (when NO_GENERATE_PLUGIN_SOURCE is not used) automatically generates a module plugin that primarily handles the registration of the QML object types provided by the module. However, for specific scenarios such as utilizing custom image providers or our need to decrypt content at module loading/importing time, it becomes necessary to provide custom module plugin implementations.

This is how our custom plugin implementation looks like:

This code calls MyQmlModule::registerQmlTypes() whenever this QML module is imported into applications that do not directly link against the QML module—a scenario we'll discuss in more detail shortly. Specifically, it applies to applications that dynamically discover and load this QML module at runtime using paths defined by QML_IMPORT_PATH. Here is what the registration function looks like:

Here are some key points to note ??: after decrypting a QML source file, we need to register its type in the QML engine to enable the usual creation of objects of this type. Unfortunately, qmlRegisterType (and related functions) do not allow for registering types directly from their source code; instead, they require the URL/URI of the source file that defines the type. To manage this, we save the decrypted QML code into temporary files and register their types from these files. Notice how we delay the removal of temporary files using QTimer::singleShot() at line 26. It appears that qmlRegisterType operates asynchronously (likely to accommodate remote QML documents), and creating temporary files on the stack causes the type registrations to fail. We acknowledge potential performance penalties due to IO operations and the security risks of intercepting those temporary files, but for now, it serves our purposes ??.

Using this secure QML module in applications

Now, let's explore how to use or import this secure module in applications. QML modules can typically be utilized in three different ways:

  1. Directly baked into the application's executable. This occurs when the first parameter of qt_add_qml_module (the module target) is the same as the application target. Since our goal is to enable QML module reusability across different applications, we won't focus on this case here.
  2. Imported at run-time using the QML module's plugin. In this scenario, applications search for the QML module's qmldir files at locations defined by the QML_IMPORT_PATH environment variable. Once located, the application uses the module's plugin (described at qmldir file) to load and register the QML object types provided by the module.
  3. Directly linked by the application at build time. This method does not utilize the QML module's plugin and does not provide run-time capabilities to replace the module implementation in use, such as changing Qt Quick Controls style implementations.

Let's see how to use our secure module for option 2: importing the QML module at run-time by using its plugin:

This is a straightforward use of our module, similar to how you would use any other QML module. The only requirement is that our module must be available at any location defined by the QML_IMPORT_PATH environment variable. Note that while GammaRay executions for this application do present the encrypted version of the QML files, it is highly unlikely that malicious users would be able to decrypt them. Additionally, no encryption/decryption key or IV values are exposed when inspecting the application's executable or the QML module's libraries with tools like strings or nm.

For applications using our secure module with option 3 (direct module linking), here's how the application's CMakeLists.txt file should be structured:

Notice how our module's development assets are located with find_package on line 8 (we've properly developed our QML module to support this feature). Additionally, we explicitly link our application against the QML module's library on line 26. Since there is no QML module plugin involved, the registration of the QML module's object types must be explicitly handled by the developer:

Observe how we specifically call MyQmlModule::registerQmlTypes() on line 11.

That concludes our journey into creating secure QML modules ??. I hope you enjoyed reading this and would love to hear any feedback, suggestions, or concerns about the solution presented here. See you next time!

#Qt #QML #cplusplus

Cover Photo by Annabel P on Pixabay

Marcel Petrick

Head of Application Software at DATA MODUL AG

11 个月

Thank you for the detailed walkthrough! Sandro Andrade Lukas Kosinski You might have a look at that.

Miguel ángel Pons

senior software engineer (Qt / C++17 / QML)

11 个月

This is a very interesting article. I used a different approach to achieve this with qmake scripts in Qt 5.9: -I generated a resource file containing all the QML files. -I encrypted this resource file. -I added this encrypted resource file to my executable, as if it were a JPG image. -At runtime, in the main function, I read this resource file (QFile data_file(":/tmp/extresources.rcc.z")) -I decrypted it and registered it with the system using QResource::registerResource. With modern versions of Qt that include the Qt Quick Compiler, it's now possible to avoid including them as plain text in resources (CONFIG+=qtquickcompiler , so encryption should no longer be necessary) Indeed, this approach results in incompatibility when relinking with different versions of Qt (though it does work with the same version of Qt, even if modified). Could it be the case that using the Qt Quick Compiler, which prevents relinking with another version of Qt, does not violate the LGPL? You can always modify the same version of Qt and relink the executable. Burkhard Stubert, what do you think about this?

要查看或添加评论,请登录

Sandro Andrade的更多文章

  • Creating reusable FTUE components with QML

    Creating reusable FTUE components with QML

    Developing successful software applications requires a well-crafted strategy that covers the entire user experience…

    2 条评论
  • #QtStories - The "Qt effect" in South America

    #QtStories - The "Qt effect" in South America

    Let me share a story with you – not just about how Qt has shaped my professional journey over the last 24 years but…

    4 条评论

社区洞察

其他会员也浏览了