On QML Code Protection
Sandro Andrade
Qt Champion 2024 ?? | Driving innovation with C++, Qt/QML, and KDE | Senior Software Engineer | Ph.D. in Computer Science
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:
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:
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:
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
This is gold, thanks
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.
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?