Answers
It is recommended that you initially use the GUI Build Helper tool to get a feeling for the processing sequence
and then to create your first ZKM Script.
It is also highly recommended that you at least read the basic exclusions part of the ZKM Script Exclusions Tutorial
before modifying your first ZKM Scripts.
See the Getting Started page of the documentation for more detail.
There could be a number of problems.
- You cannot run Zelix KlassMaster using a Java 7 or earier JVM. You need Java 8 (JDK 1.8 or better).
- Zelix KlassMaster may not run on your operating system. See the answer to Q4.
If you are not getting a useful error message, try running with the -verify and -verbose switches of the JVM.
Yes. Although, Zelix KlassMaster requires only a recent Java 8 JVM to run, it can handle Java 9 through to Java 23 bytecode including modules.
Of course Zelix KlassMaster will also open and process Java 1.1 through to Java 8 bytecode.
All you need to do is set the Zelix KlassMaster classpath to point to the appropriate bootstrap classes.
For Java 2 through to Java 8 that is the corresponding rt.jar . For Java 9 to Java 23 it is the corresponding lib\jrt-fs.jar file system.
The major differences between the evaluation version and the commercial version are as follows:
- The evaluation version will flow obfuscate only one or two methods in each class.
- The evaluation version is time limited to thirty days.
Zelix KlassMaster is written entirely in Java 8 (i.e. JDK 1.8) and technically should run on any platform that supports a Java 8 or better Virtual Machine.
However, note that Zelix KlassMaster version 10 or better requires a newer version of a Java 8 JVM to run. If you use an Oracle JVM earlier than 1.8.0_152 then you may get a JVM crash due to HotSpot bugs.
Also note that Zelix KlassMaster version 23 currently may not run successfully on older versions (e.g. pre-Aug 2020) of the Eclipse OpenJ9 JVM due to a JVM bug.
More generally, differences in the file systems and GUIs can also cause problems. Further, Zelix KlassMaster's flow obfuscation technology can expose bugs in some Just in Time (JIT) compilers.
Zelix KlassMaster has been tested successfully using the following environments.
- Windows 11 64bit using
- java version "1.8.0_421" Java HotSpot(TM) 64-Bit Server VM (build 25.421-b09, mixed mode)
- IBM Semeru Runtime Open Edition (build 1.8.0_422-b05) Eclipse OpenJ9 VM (build openj9-0.46.1, amd64-64-Bit Compressed References 20240909_1048 (JIT enabled, AOT enabled))
- java version "11.0.24" 2024-07-16 LTS Java HotSpot(TM) 64-Bit Server VM 18.9 (build 11.0.24+7-LTS-271, mixed mode)
- java version "17.0.12" 2024-07-16 LTS Java HotSpot(TM) 64-Bit Server VM (build 17.0.12+8-LTS-286, mixed mode, sharing)
- IBM Semeru Runtime Open Edition 17.0.12.1 (build 17.0.12+7) Eclipse OpenJ9 VM 17.0.12.1 (build openj9-0.46.1, amd64-64-Bit Compressed References 20240716_793 (JIT enabled, AOT enabled))
- java version "21.0.4" 2024-07-16 LTS Java HotSpot(TM) 64-Bit Server VM (build 21.0.4+8-LTS-274, mixed mode, sharing)
- openjdk version "21.0.4" 2024-07-16 LTS Temurin-21.0.4+7 OpenJDK 64-Bit Server VM Temurin-21.0.4+7 (build 21.0.4+7-LTS, mixed mode)
- IBM Semeru Runtime Open Edition 21.0.4.1 (build 21.0.4+7-LTS) Eclipse OpenJ9 VM 21.0.4.1 (build openj9-0.46.1,amd64-64-Bit Compressed References 20240716_260 (JIT enabled, AOT enabled))
- java version "23" 2024-09-17 Java HotSpot(TM) 64-Bit Server VM (build 23+37-2369, mixed mode, sharing)
- openjdk version "23" 2024-09-17 Temurin-23+37 (build 23+37) OpenJDK 64-Bit Server VM Temurin-23+37 (build 23+37, mixed mode))
- IBM Semeru Runtime Open Edition 23.0.0.0 (build 23+37) Eclipse OpenJ9 VM 23.0.0.0 (build openj9-0.47.0, amd64-64-Bit Compressed References 20240917_41 (JIT enabled, AOT enabled))
- IBM Semeru Runtime Open Edition 21.0.2.0 (build 21.0.2+13-LTS) Eclipse OpenJ9 VM 21.0.2.0 (build openj9-0.43.0, amd64-64-Bit 20240116_95 (JIT enabled, AOT enabled)
- Linux (Xubuntu 22.04) using
- java version "1.8.0_421" (build 1.8.0_421-b09)Java HotSpot(TM) 64-Bit Server VM (build 25.421-b09, mixed mode)
- openjdk version "1.8.0_422" (Temurin)(build 1.8.0_422-b05) OpenJDK 64-Bit Server VM (Temurin)(build 25.422-b05, mixed mode)
- IBM Semeru Runtime Open Edition (build 1.8.0_422-b05) Eclipse OpenJ9 VM (build openj9-0.46.1, amd64-64-Bit Compressed References 20240909_1031 (JIT enabled, AOT enabled))
- java version "11.0.24" 2024-07-16 LTS (build 11.0.24+7-LTS-271) Java HotSpot(TM) 64-Bit Server VM 18.9 (build 11.0.24+7-LTS-271, mixed mode)
- java version "17.0.12" 2024-07-16 LTS (build 17.0.12+8-LTS-286) Java HotSpot(TM) 64-Bit Server VM (build 17.0.12+8-LTS-286, mixed mode, sharing)
- openjdk version "17.0.12" 2024-07-16 Temurin-17.0.12+7 (build 17.0.12+7) OpenJDK 64-Bit Server VM Temurin-17.0.12+7 (build 17.0.12+7, mixed mode, sharing)
- IBM Semeru Runtime Open Edition 17.0.12.1 (build 17.0.12+7) Eclipse OpenJ9 VM 17.0.12.1 (build openj9-0.46.1, amd64-64-Bit Compressed References 20240716_840 (JIT enabled, AOT enabled))
- java version "21.0.4" 2024-07-16 LTS build 21.0.4+8-LTS-274) Java HotSpot(TM) 64-Bit Server VM (build 21.0.4+8-LTS-274, mixed mode, sharing)
- openjdk version "21.0.4" 2024-07-16 LTS Temurin-21.0.4+7 (build 21.0.4+7-LTS) OpenJDK 64-Bit Server VM Temurin-21.0.4+7 (build 21.0.4+7-LTS, mixed mode, sharing)
- IBM Semeru Runtime Open Edition 21.0.4.1 (build 21.0.4+7-LTS) Eclipse OpenJ9 VM 21.0.4.1 (build openj9-0.46.1, amd64-64-Bit Compressed References 20240716_264 (JIT enabled, AOT enabled))
- java version "23" 2024-09-17 (build 23+37-2369) Java HotSpot(TM) 64-Bit Server VM (build 23+37-2369, mixed mode, sharing)
- openjdk version "23" 2024-09-17 Temurin-23+37 (build 23+37) OpenJDK 64-Bit Server VM Temurin-23+37 (build 23+37, mixed mode, sharing)
- IBM Semeru Runtime Open Edition 23.0.0.0 (build 23+37) Eclipse OpenJ9 VM 23.0.0.0 (build openj9-0.47.0, amd64-64-Bit Compressed References 20240917_35 (JIT enabled, AOT enabled)>
- MacOS (Monterey 12.7.6) using
- java version "1.8.0_421" (build 1.8.0_421-b09)Java HotSpot(TM) 64-Bit Server VM (build 25.421-b09, mixed mode)
- java version "17.0.12" 2024-07-16 LTS (build 17.0.12+8-LTS-286) Java HotSpot(TM) 64-Bit Server VM (build 17.0.12+8-LTS-286, mixed mode, sharing)
- java version "21.0.4" 2024-07-16 LTS build 21.0.4+8-LTS-274) Java HotSpot(TM) 64-Bit Server VM (build 21.0.4+8-LTS-274, mixed mode, sharing)
- java version "23" 2024-09-17 (build 23+37-2369) Java HotSpot(TM) 64-Bit Server VM (build 23+37-2369, mixed mode, sharing)
- IBM Semeru Runtime Open Edition 23.0.0.0 (build 23+37) Eclipse OpenJ9 VM 23.0.0.0 (build openj9-0.47.0, amd64-64-Bit Compressed References 20240917_36 (JIT enabled, AOT enabled))
Yes. Zelix KlassMaster supports JEE.
Its default exclusions and its SmartSave technology provide automatic support for servlets,
the EJB 1 specification and the EJB 3 specification to the extent that you have used annotations rather than XML.
If your application is an EJB 2 application that uses advanced features such as container managed persistence
then we recommend that, for all classes directly related to the EJBs, you exclude
- public class names and their containing package names,
- public field names,
- public method names.
If your EJB related classes are in specific packages then it should be quite easy to specify these exclusions in ZKM Script. Flow obfuscation and String encryption should cause no JEE problems.
Yes. Zelix KlassMaster supports Spring and Hibernate by automatically updating the XML to reflect any obfuscated names. However, there are some minor qualifiactions.
Yes. Zelix KlassMaster supports Java ME.
There is a plugin for the Sun Java ME Wireless Toolkit.
If you are not using the Sun Java ME Wireless Toolkit then see the Java ME obfuscation tutorial for more detail on the steps invloved.
Yes. Zelix KlassMaster supports Google Android by obfuscating the Java class files before they are converted into an APK file. Depending on your build environment you may need to use of Zelix KlassMaster's ProGuard compatibility.
There are a number of reasons why all class, field or method names don't change when you obfuscate.
- The name is public or protected and your obfuscate options say that
public or protected names shouldn't be changed.
- The name of a package scoped method is overridden to be protected or public in a subclass.
If your obfuscate options say that protected or public method names shouldn't
be changed then the overridden method name can't be changed without breaking the polymorphic
link.
- The name of a method overrides a method in a superclass or implements a method in an interface
but the superclass or interface hasn't been opened inside Zelix KlassMaster. In such cases the
method name cannot be changed without breaking the polymorphic link.
Encrypted strings are decrypted at runtime using code that has been added to your classes.
This means that the classes will run slightly slower and will be bigger.
How much slower and bigger depends on how many String literals there are in your bytecode and the option selected.
Typically the performance impact is insignificant.
The bytecode size increase if all Strings are encrypted is typically in the range of 10% to 30%.
The enhanced setting will result in the biggest increase in bytecode size but it will also provide the best protection.
It is recommended that you measure the impact on your classes.
When you change a method name, Zelix KlassMaster automatically changes all method names that are polymorphically linked to that method. So overridden methods in superclasses and overriding methods in subclasses must also be renamed. If the method is an implementation of an interface method then the method in the interface and the corresponding methods in all classes that implement that interface must also be renamed. So the change can ripple across to different branches of the class hierarchy.
The answer to Q8 explains how a method name change can ripple across to different branches of the class hierarchy. So Zelix KlassMaster must check that the new name wont clash with existing names in these other classes. A name clash occurs if the proposed new signature
- already exists in a class
- already exists in a superclass or subclass so that a new polymorphic relationship would be created.
This will happen if you obfuscate your bytecode using Java illegal identifiers and you have run you code with verification switched on.
Verification is on:
- in JDK 1.1.7 or less, for applications run with the
-verify switch
- in Java 2 or better by default unless you use the
-Xverify:none switch
- is always switched on for Applets.
You cannot use illegal identifiers if your classes must run after being verified.
Alternatively, you will get such messages even when not runing with verification if you have renamed your highest level
package qualifiers so that class names are Java illegal when fully qualified.
For example a class named com.1.0 will cause no problems when running without verification but
a class named 1.1.0 will cause problems.
The short answer is yes. Zelix KlassMaster's AutoReflection functionality can automatically handle Java Reflection API calls.
However, it is highly recommended that you also explicitly specify the subset of classes, fields and methods that are accessed by unresolved Reflection API calls by using the
accessedByReflection and/or accessedByReflectionExclude statements.
If you choose not to use the AutoReflection functionality,
Zelix KlassMaster can still automatically handle many simple Java Reflection API calls.
When Zelix KlassMaster opens your classes, it detects Java Reflection API calls that access classes, fields or methods and it automatically looks for the source of the class, field or method name which is being passed to the Reflection API method.
If Zelix KlassMaster can find a String literal which contains the name of the class, field or method then it is said to have "resolved" that call.
Zelix KlassMaster automatically handles such resolved Reflection API calls by updating the String literal that holds the class, field or method name to reflect the corresponding obfuscated name.
Regardless of whether or not you use the the AutoReflection functionality,
if Zelix KlassMaster cannot fully resolve a Reflection API call then it lists the unresolved call in the Class Open Warnings window if you are using the GUI or
in the Zelix KlassMaster log (which is named "ZKM_log.txt" by default) under the heading "API calls detected that may not be handled automatically...".
Zelix KlassMaster may still be able to automatically process classes that appear in the Class Warnings list (even if not you don't use the AutoReflection functionality).
For example, calls to Class.getName() are often made purely for debugging purposes.
Also, in certain situations, Zelix KlassMaster can change the contents of a string containing a class name to match the new class name.
In any case, the user must look at the way the call is used to determine if the class can be reliably processed automatically.
The bottom line is that
- If you choose not to use the AutoReflection functionality and
- If a particular Reflection API call is not handled automatically by Zelix KlassMaster and
- That will break the obfuscated application
then the package, class, field or method that is accessed by name by that call must be explicitly excluded from being renamed.
When Zelix KlassMaster opens a class that it might not be able to trim or obfuscate reliably it reports the fact using the
"Class Open Warnings" window.
You may get the Class Open Warnings window when an opened class contains a Java Reflection API call to a method like one of the following:
- Class.forName(String)
- Class.getName()
- Class.getField(String)
- Class.getMethod(String)
- Class.getDeclaredField(String)
- Class.getDeclaredMethod(String)
- Field.getName()
- Method.getName()
- Constructor.getName()
- ClassLoader.loadClass(String,boolean)
- ClassLoader.defineClass()
- ClassLoader.findSystemClass(String)
- ClassLoader.findLoadedClass(String)
- RMIClassLoader.loadClass(String)
- RMIClassLoader.loadClass(URL, String)
- LoaderHandler.loadClass(String)
- LoaderHandler.loadClass(URL, String)
- EventSetDescriptor(Class, String, Class, String)
- EventSetDescriptor(Class, String, Class, String[], String, String)
- EventSetDescriptor(Class, String, Class, String[], String, String, String)
Note that the Class Open Warnings window lists
- the class in which the Reflection API method call appears
- the signature of the method in which the Reflection API method call appears
- the signature of the Reflection API method being called.
Note that it does not name the class, field or method that is being accessed by name.
Of course it cannot do that because the Reflection API call could not be fully analyzed.
Please see the answer to Q11 for detail on what to do if you get "Class Open Warnings".
Some obfuscators add corrupt information to bytecode files in the hope of confusing or crashing decompilers.
Zelix KlassMaster detects this corrupt information and reports the fact using the Class Open Errors window?
Corrupt Local Variable Table and Inner Class information is automatically removed.
Zelix KlassMaster will also report the existence of duplicate classes if the classes
- appear in the same archive or
- are not absolutely identical.
Obfuscation of even a mildly complicated application is not trivial. For this reason it is highly recommended that you start by using minimal obfuscation.
Once you have your minimally obfuscated application working satisfactorily then you can iteratively increase the level of obfuscation.
Please see the Getting Started for some preliminary advice.
Trim function
It is highly recommended that you do not use the Trim function until you have your obfuscated application working.
Once your obfuscated application is working you will find that your ZKM Script exclude statement exclusion parameters
will provide an excellent guide to the necessary ZKM Script trimExclude statement parameters.
Inner classes
Generally, you don't need to keep the inner class information so you select "false" in the "Keep inner class information" list.
This will reduce the size of your bytecode. Typically, JVMs make no use of inner class information at runtime.
However, compilers do make use of inner class information at compile time.
So, if your classes form a library which third party classes will be compiled against and if some of your inner classes are
exposed to the user then you will need to select either "true" or "if name not obfuscated" from the "Keep inner class information" list.
Selecting "true" will preserve all inner class information.
Generics
Similarly, you generally don't need to keep the generics information of your classes so you select "false" in the "Keep generics information" list.
Typically, JVMs make no use of generics information at runtime and removing the generics information will reduce the size of your bytecode.
However, compilers do make use of generics information at compile time.
So, if your classes form a library which third party classes will be compiled against then you should select "true" from the "Keep generics information" list.
String Encryption
The String Encryption "encrypt string literals" option is generally safe. However, use of the "aggressive", "flowObfuscate" or "enhanced" settings
can, in certain unusual circumstances, result in the contents of public static final String fields of some interfaces not being accessible to classes outside of the set that
you have obfuscated. If this happens, you will get a warning message in the Zelix KlassMaster log.
Also, note that String encryption does make your bytecode larger and slightly slower (see the answer to Q7).
For maximum resistance to deobfuscation, it is recommended that you use the "enhanced" setting along with the Method Parameter Changes functionality.
Flow Obfuscation
If you are obfuscating an applet note that there are known bugs in the JITs of the early browsers that can be exposed by flow obfuscation.
If you flow obfuscate using the "light" option then the risk of JIT problems will be greatly reduced.
Nonetheless, if you flow obfuscate your applet then test it thoroughly.
Reference Obfuscation
The Reference Obfuscation functionality when used in conjunction with the the Method Parameter Changes functionality
can make decompiled bytecode very difficult to understand. However, its use of the Reflection API can slow your application down.
The performance impact can be quite dramatic in the case of pre-Java 8 bytecode where the JVM's invokedynamic instruction is not available and the Reflection API must be used instead.
It is recommended that you obfuscate only those references that are not accessed many times and which do not appear in performance critical areas of your code.
For example, as a rule of thumb, we would suggest that you do not obfuscate a reference which will be used more than 75,000 times (1,000 times for pre-Java 8) within a short spell of processing.
Typically you could safely obfuscate those "high level" references which occur only a limited number of times as a user enters sensitive parts of your application.
Otherwise, it is generally recommended that you do not use Reference Obfuscation functionality until after you have your obfuscated application
working satisfactorily using the more straightforward features.
Method Parameter Obfuscation
The Method Parameter Obfuscation functionality obfuscates method parameter lists by replacing them with a single Object[] parameter.
Note that only methods that will have their names obfuscated will be eligible for method parameter obfuscation.
The combination of method Name Obfuscation and Method Parameter Obfuscation reduces the comprehensibility of the Java source code produced by a decompiler.
Method Parameter Obfuscation also obscures the changes made by the Method Parameter Changes functionality.
Note that the Method Parameter Obfuscation functionality can have a significant performance impact.
For this reason it is recommended that you exclude any performance critical methods from having their parameter lists obfuscated.
Method Parameter Changes
The Method Parameter Changes functionality is essential if you want maximum protection against deobfuscation.
As mentioned above, the functionality significantly hardens the String Encryption and Reference Obfuscation
functions against being reversed.
You could consider using the flowObfuscate setting which results in a special kind of Flow Obfuscation which is more difficult to reverse than the classic Flow Obfuscation.
Application types
If your classes make up a self-contained application then it should be safe to change all package, class, field and method names other than
those of the public static main(String[]) method and the class that contains it. You can achieve this by selecting the
"Self contained application or applet" application type and by selecting your entry paint class in the "Don't change main class name" list.
If you are obfuscating a set of classes that will be used by third parties as a non-extensible class library then you should
- not obfuscate public class names
- not obfuscate public field or method names
You can achieve this is the GUI by selecting the "Non-extensible class library" application type.
If you are obfuscating a set of classes that will be used by third parties as an extensible framework then you should
- not obfuscate public class names
- not obfuscate public or protected field or method names
You can achieve this is the GUI by selecting the "Extensible framework" application type.
In either case you should
- limit String encryption by either
- using the "normal" setting or
- using the "aggressive", "flowObfuscate" or "enhanced" settings but excluding all interfaces from String encryption
using the ZKM Script
stringEncryptionExclude statement.
(e.g. "stringEncryptionExclude interface *.*;" )
- consider choosing the line numbers "scramble" option
If your classes form a complex EJB 2 application or (an EJB 3 application where you have used XML rather than annotations) then the easiest solution is to exclude all public classes, fields and methods that are directly related to the EJBs.
Otherwise it should be safe to use light flow obfuscation and String encryption.
Line numbers
If the size of your bytecode is critical then select the "delete" line numbers option.
However, if you are obfuscating for a beta release or if the size of your code isn't
critical then you can select the "scramble" line numbers option.
The code will be larger but you will find crashes much easier to track down.
Local variables
By default, Zelix KlassMaster deletes all local variable information. This information is primary used for debugging purposes and is not essential at runtime.
However, the localVariables parameter of the ZKM Script obfuscate statement allows you to keep all or some of the local variable information or to obfuscate that information.
When using ZKM Script, you generally should use the "delete" local variable setting (which is the default). This is particularly the case if the size of your bytecode is important.
However, if you are obfuscating a class library and need the method signatures to be previewable in IDEs then you should use the "keepVisibleMethodParameters" or "keepVisibleMethodParametersIfNotObfuscated"
settings. (If you are using Java 8 then see the "Method parameters" section below.) See the documentation for the obfuscate functionality for more detail.
You can use the "obfuscate" setting to slightly complicate any attempts at decompilation but this will result in increased bytecode size when compared to the use of the "delete" setting.
Generally it is recommended that you never use the "keep" setting outside of your development environment.
Method parameters
Java 8 introduced the optional MethodParameters attribute which stores the names of your method parameters and some access flag information.
These attributes are created only if you specify the -parameters compiler option.
When using ZKM Script, you generally should use the "delete" local variable setting (which is the default). This is particularly the case if the size of your bytecode is important.
However, if you are obfuscating a class library and need the method signatures to be previewable in IDEs then you should use the "keepVisible" or "keepVisibleIfNotObfuscated" settings.
See the documentation for the obfuscate statement for more detail.
Legal identifiers
It is highly recommended that you always use the "legal identifiers" option.
This option is now available only in ZKM Script and only for the purposes of backwards compatibility.
If you are obfuscating an application that doesn't have to run with verification switched on then you can get extra protection with this option but they will cause problems with modern JVMs.
The bottom line
Please note that whatever options you choose it remains your responsibility to fully test or retest your classes
after they have been processed by Zelix KlassMaster.
Zelix KlassMaster holds a lot of information in memory for each class that it opens.
So, the more classes you open and the more complicated those classes are the more memory you need.
If Zelix KlassMaster tells you that there was insufficient memory to open your classes or if it crashes with an OutOfMemoryError then you can do one of the following:
- Give Java more memory to work with by using the
-Xmx??m option where ?? represents the number of megabytes.
(For example, the option -Xmx512m gives Java 512MB of heap space.)
-
When you are running Zelix KlassMaster via Ant then you need to give more memory to Ant itself. An easy way of doing this is by setting the "ANT_OPTS" environment variable.
e.g. set ANT_OPTS=-Xmx512m
- Use the "Tools/Garbage Collect" menu option or the gc ZKM Script statement after opening your classes and before obfuscating.
- Install more physical memory.
It is not recommended that you make use of virtual memory (by giving Java more memory to work with than is physically installed.)
Performance will be unreasonably degraded as memory is swapped to and from the disk.
There are a number of reasons Zelix KlassMaster may run slowly.
- Your computer may have insufficient real memory installed. If your computer has to
use virtual memory performance will be very poor. You will notice that the hard disk
is being very heavily utilized.
- You are obfuscating while viewing a very large class or method. This
results in reduced performance due to the overhead of updating the class or method
details in the View Panel.
Zelix KlassMaster's default exclusions will automatically handle any native methods by ensuring that
- the names of the native methods themselves and
- the fully qualified names of the classes that contain them
will not be changed.
However, Zelix KlassMaster has no way of knowing if your native code accesses
your Java fields or methods. If your native code does access any of your Java fields or methods then you will need to explicitly exclude
the accessed member names and the fully qualified names of the classes that contain them from obfuscation.
The current version of the ZKM Script Helper does not validate all of your choices to ensure they are not contradictory. For example, it will allow you to exclude final abstract methods even though such methods cannot exist. The parser will give you a warning in this case.
Zelix KlassMaster cannot obfuscate JSP (Java Server Page) source but it can obfuscate the servlet that is generated from a JSP.
However, problems can arise if a JSP servlet is automatically regenerated at runtime replacing the class produced by
Zelix KlassMaster. If this can happen in your environment then you should specify your JSP servlet classes as "fixed" by using the
ZKM Script fixedClasses statement. For example you could say:
fixedClasses *.* implements javax.servlet.jsp.JspPage;
Generally, Zelix KlassMaster's flow obfuscation will slightly increase the size of your bytecode and it will reduce its performance.
It is a trade off between the degree of protection against decompilation and bytecode size and speed.
The table below shows the approximate bytecode size increase in a compressed JAR file that could be expected for typical applications.
Note that the size increase will vary from application to application and you should measure the impact on your bytecode.
flow obfuscation option used |
approximate size increase |
light |
2% |
normal |
3% |
aggressive |
7% |
extraAggressive |
10% |
The performance impact of flow obfuscation varies depending upon
- the bytecode that is being obfuscated,
- the JVM used to execute the bytecode,
- the flow obfuscation option used.
When the HotSpot 1.8.0 JVMs are used, the performance impact for general purpose applications is insignificant.
However, the performance impact on extremely CPU intensive applications will be greater.
Also, the the impact when other JVMs (e.g IBM) using JIT compiler technology will be greater.
Again, it is recommended that you measure the impact on your classes. If you experience an unacceptable performance impact then you should
- use "light" flow obfuscation,
- use the ZKM Script obfuscateFlowExclude statement to exclude performance critical methods from flow obfuscation.
You can use the ZKM Script "obfuscateFlowExclude" statement to specify methods that you don't want flow obfuscated.
This functionality is not yet available in the GUI interface. (Note that the name exclusion functionality has no impact at all on flow obfuscation.)
Zelix KlassMaster only attempts to obfuscate the flow of methods that already have some control flow complexity.
So methods without if , switch , while or for constructs will not be flow
obfuscated. Also there are small percentage of methods with some control flow complexity that Zelix KlassMaster cannot yet
flow obfuscate. Finally, note that the evaluation version will only flow obfuscate one or two methods in each opened class.
"Normal" String encryption encrypts all String literals that are accessed by methods in the defining class.
This can mean that the values of some final String fields were not obfuscated. For example, String
constants in interfaces would typically not be encrypted.
"Aggressive" String encryption uses a more sophisticated approach that can normally encrypt Strings even in interfaces.
The "flow obfuscate" option is the same as the "aggressive" option but causes the runtime decryption instructions, that is inserted into your bytecode,
to be flow obfuscated. This makes the decrypt instructions harder to decompile but it could cause problems with some JITs.
Note that the limitations mentioned above for the "aggressive" option also apply for the "flow obfuscate" option.
Note also that the String Encryption "flow obfuscate" option is quite independent of the general Flow Obfuscation functionality.
You can choose to have one without the other.
The "enhanced" option is the same as the "flow obfuscate" option but uses an extra decrypt key.
This makes the String encryption harder to reverse.
For maximum resistance against deobfuscation, it is recommended that you use the "enhanced" setting along with the Method Parameter Changes functionality.
If you get any of the runtime errors "ClassFormatError", "VerifyError", "NoClassDefFoundError" or "Can't find class Classname" see
the answer to Q10.
Another reasons for errors such as "NoClassDefFoundError", "Can't find class Classname" or "NoSuchMethodError" are:
- You didn't open all your classes inside Zelix KlassMaster before obfuscating
- Your application
- Uses the Java Reflection API and
- Zelix KlassMaster has reported that there are some Reflection API calls in your bytecode that it may not be able to automatically handle and
- You didn't exclude the names of the classes, fields or methods that are accessed by those Reflection API calls.
Please see the answer to Q12 for more detail.
Finally, if you used Flow Obfuscation and your classes are causing the VM to crash (typically with a memory access error) or if you are getting an unexpected
NullPointerException, you may be running foul of a bug in the JIT. To test this, run your application with the JIT switched off.
If the problems occur only when the JIT is being used you should:
- Upgrade to a more recent JIT. Of course it isn't always practical to ask your users to upgrade their JITs. JITs that are known to cause bugs with
flow obfuscated bytecode are:
- Microsoft VMs earlier than 5.00.3167
- Symantec JITs earlier than 3.00.072b.
- some of the IBM 1.3 JVMs
- If you obfuscated with "aggressive" flow obfuscation, try the "normal" setting.
- If you obfuscated with "normal" flow obfuscation, try the "light" setting. The "light" setting should eliminate most problems.
- If you can identify the flow obfuscated method that is triggering the JIT bug then you can use the ZKM Script obfuscateFlowExclude statement
to exclude that method from flow obfuscation.
- As a last resort you can switch off flow obfuscation altogether by using the "none" option.
The warning message indicates that the method name mappings in the Input Change Log are inconsistent for the opened classes. This can happen if
- you have directly edited the change log file and make some mistakes or
- your application has changed so that there are now polymorphic relationships between two or more methods that where previously unrelated.
In either case, Zelix KlassMaster handles the situation automatically to ensure the obfuscated bytecode is OK.
If you are using the GUI or if you specified an input change log using the changeLogFileIn parameter of the ZKM Script obfuscate statement then
Zelix KlassMaster will always try to rename packages, classes, fields and methods to the names specified by the
input change log. All the other statements such as the exclude and obfuscate statements remain fully active but generally they will only have impact on the names of
packages, classes, fields and methods that do not already appear in the input change log.
So, for example, if the input change log clashes with the exclusions that have been set (or haven't been set) then warnings will be generated but the input change log will be given priority. The only exceptions are where:
- the input change log contains inconsistent or conflicting information. For example if it
- contains duplicate new package, class, field or method names
- contains fully qualified new class names that conflict with the corresponding new package name
- the input change log contains information that cannot be validly applied to the opened classes. For example if it
- would incorrectly make or break a polymorphic relationship between methods.
However, if you specified an input change log using the looseChangeLogFileIn parameter of the ZKM Script obfuscate statement
then your exclusions will take precedence over the change log mappings.
However, this could mean that the newly obfuscated classes will not be fully consistent with your previous release.
Yes. If you specify a class in the exclude statement or using the GUI's Obfuscate Advanced Name Exclusion Options Dialog, you are excluding only the class name from obfuscation.
Excluding a class name from obfuscation has no effect
- on the obfuscation of that class's field and method names or
- on the flow obfuscation or String encryption of any of its methods.
To exclude methods from flow obfuscation you must use the ZKM Script
obfuscateFlowExclude statement.
To exclude String literals from encryption you must use the ZKM Script
stringEncryptionExclude statement.
Yes but be careful. If you are using a change log that was generated when you obfuscated a release of your application or applet then make sure you edit a copy.
You may still need the original to interpret stack dumps from the released bytecode.
Also, be careful when you choose you predetermined names. It's a complicated business - especially if you are predetermining method names.
Zelix KlassMaster will perform a large number of cross checks on your input change log and on your opened classes but
it may not check for every possible conflict. If Zelix KlassMaster gives you warnings then read them carefully.
Note that Zelix KlassMaster's ZKM Script language allows you to specify more than one input change log. So you could specify an small, edited input change log
that specifies only your changes followed by the full, original input change log. This can be a safer approach.
It causes problems when different applets (or different classes used by different applets) are given the same name. Browsers and proxy servers can get confused. The recommended solution is to put your applets in different packages that are unique to your organisation and to exclude those package names from obfuscation. If you then obfuscate the package as a whole, then each class will be given a unique name within the name space of the package.
However, it is recognized that, for various reasons, not everyone wants to do this. The best alternative way of handling this situation is to use Zelix KlassMaster's Input Change Log functionality. Perform a preliminary obfuscation with all of your applet classes opened inside Zelix KlassMaster. Don't bother saving the obfuscated classes. All you want is the "cross applet" change log with each class being given a unique name. Now obfuscate each of the individual applets using the "cross applet" change log as an input change log so that each class again gets its unique name. Retain the "cross applet" change log for use as an input change log in any subsequent releases of your applets. You only need save the individual applet change logs if you have used line number scrambling.
Using the ZKM Script interface you can accomplish this easily with a statement like:
exclude *.* implements java.io.Serializable !static !transient *;
Using the GUI interface's Advanced Exclusion Options dialog, you can select "!static" and "!transient" from the drop down lists.
Obfuscate your application as a whole in one pass. Zelix KlassMaster version 3 or better automatically handles issues relating to obfuscating multiple JAR files.
If you will sometimes need to release just a changed client or server jar file then you should use Zelix KlassMaster's Input Change Log functionality to ensure consistent
renaming across your releases. The steps are
- Initially obfuscate your client and server jar files together in one pass generating an overall change log.
- If you make changes to only one of the jar files then obfuscate both your client and server jar files together in one pass once more
using the change log generated in the step above as an input change log. Release just the changed jar.
- Use the change log generated in the second step as the new overall change log.
Yes. Zelix KlassMaster's Input Change Log functionality ensures
- consistent package, class, field and method renaming and
- consistent flow obfuscation
across releases. It is generally recommended that you always open ALL the classes of the application inside Zelix KlassMaster.
This gives Zelix KlassMaster the opportunity to ensure that all references are consistent.
This is the case even if you are planning to distribute just the changed class subset in the form of some kind of patch.
Zelix KlassMaster does provide some limited ability to obfuscate a subset of your application provided the mappings for the full application appears in an input change log.
To do this you should set the allClassesOpened parameter of the obfuscate statement to false
and set the classpath so that the unobfuscated versions of all the unopened classes of the application can been located.
However, if you are using the Trim function then you must still open your application as a whole.
Yes. Zelix KlassMaster exposes a simple, generic Java API for use by build tools. It also provides an Ant task called ZKMTask.
See the online documentation for a full description of the API and for extra detail on the Ant task definition.
To access an enviroment variable such as PATH from within your ZKM script you should firstly
add the environment variable to the System properties when you start Zelix KlassMaster. Use this syntax for Windows:
java -D"PATH=%PATH%" -jar ZKM.jar
and this syntax for Unix
java -DPATH=$PATH -jar ZKM.jar
If you running Zelix KlassMaster through Apache Ant then you can either add the environment variable to the Ant project properties or
use the Zelix KlassMaster Ant task (ZKMTask) to add the environment variable to the System propeties. (See the online documentation for more detail.)
Then access the variable in your ZKM Script by using the %...% delimiters. For example %PATH% .
The Zelix KlassMaster preprocessor will perform the substitution.
If you never have to deserialize a non-obfuscated class with a obfuscated class then Serialization presents no problems.
Zelix KlassMaster will automatically handle the necessary exclusions using its defaults and
its Input Change Log functionality can be used to ensure consistent renaming across releases.
However, if you need to deserialize already existing (ie. unobfuscated) serialized instances using your obfuscated classes
then your obfuscated classes need to be compatible for Serialization purposes with your unobfuscated classes.
To achieve this compatibility you need to use the ZKM Script existingSerializedClasses statement to specify the classes that
have existing serialized instances.
For each class matched by the statement, Zelix KlassMaster will
- add name exclusions for
- the fully qualified name of the class
- the name of every field contained or inherited by the class
- the fully qualified class name of the type of each non-primitive field contained or inherited by the class
- add a
serialVersionUID field to the class (if it doesn't already exist) set to a value based upon the non-obfuscated class definition.
The issue here is whether Zelix KlassMaster's obfuscation of class names will effect ResourceBundle.getBundle(String, Locale) calls.
PropertyResourceBundles
In the case of PropertyResourceBundles , because they are backed by properties files and don't involve Java class files, they are generally unaffected by obfuscation.
For example, a properties file with the file name "pack1/pack2/dir3/MyProperties.properties" will be moved to be "a/a/dir3/MyProperties.properties"
if the packages "pack1.pack2" are obfuscated to be "a.a".
However, if a property file backing a PropertyResourceBundle
- Has a name containing an unqualified class name (e.g. "pack1.pack2.MyClass0.properties") and
- The name of the class is used to get the name of the resource bundle (e.g. "
ResourceBundle.getBundle(getClass().getName()) ")
then the code will be broken if the unqualified class name is obfuscated.
This because the properties file will be moved to reflect obfuscated package names but it will not be renamed to reflect an obfuscated unqualified class name.
In such a case you must explicitly exclude the unqualified class name from being obfuscated.
ListResourceBundles
On the other hand, ListResourceBundles are backed by class files. If you create your ListResourceBundles by specifying the
fully qualified base class name in a String literal and if the base class is within a package then Zelix KlassMaster will automatically change the value
of the String literal to match the new obfuscated base class name. So the following code would be automatically handled (as far as the base class is concerned).
ResourceBundle myBundle = ResourceBundle.getBundle("package1.Class1", myLocale);
However, some additional work would still have to be done to handle the subsidiary ListResourceBundle classes such as
package1.Class1_de ,
package1.Class1_de_DE ,
package1.Class1_fr and
package1.Class1_fr_CH
Basically, a class suffix exclusion statement must be used for each language code, country code and locale variant combination to ensure that
the subsidiary classes are renamed in synchronization with the base class with the suffixes being retained.
An example ZKM Script exclusion statement would be:
exclude *.<link>_de and
*.<link>_de_DE and
*.<link>_fr and
*.<link>_fr_CH;
If your organization uses a standard set of localizations, you could consider adding such an exclusion to your defaultExclude.txt file
so that it is automatically applied.
Otherwise, if your ListResourceBundle class names are not stored as fully qualified names in String literals or if the classes are not
within packages then you would have to explicitly exclude the base class and subsidiary class names from being obfuscated.
Another issue with ListResourceBundles is that they must be explicitly from being trimmed if you use the trim function. This can be done as follows.
trimExclude *.* extends java.util.ListResourceBundle
A ClassCastException can occur if you obfuscate bytecode compiled with the JDK 1.5 or better with Zelix KlassMaster 4.2 or earlier.
It is due to the fact that the JDK 1.5 changed the JVM specification for the loading of constants in a fundamental way.
Zelix KlassMaster (version 4.3 and better) has been enhanced to support bytecode created with the JDK 1.5.
However, if you compile with the JDK 1.5 using the "-source 1.4 -target 1.4" parameters then you may still be able to use earlier versions of Zelix KlassMaster without further difficulties.
Inner classes are compiled to be top level classes.
If you have an inner class Inner within the class com.mycompany.Outer then that inner class Outer.Inner will be compiled to the class file com/mycompany/Outer$Inner.class .
At the level of the bytecode, the name of that inner class will be com.mycompany.Outer$Inner . Note the presence of the '$' character in the unqualified class name.
When you refer to inner classes in ZKM Script statements, you need to use the bytecode level name. So you would use the name com.mycompany.Outer$Inner and not com.mycompany.Outer.Inner .
Because all inner classes will contain the '$' character in their unqualified class name, you could exclude all inner classes with the following ZKM Script exclude statement.
However, the statement would also match any non-inner classes that have been given a name including an embedded '$' character.
exclude *.*$*;
You can get this error when you use Zelix KlassMaster to take the place of the ProGuard obfuscator in the Google Android Eclipse plugin.
You should first look in the ZKM_PG_log.txt file.
This is a special log file which Zelix KlassMaster uses to write messages relating to the translation of a ProGuard configuration file.
The ZKM_PG_log.txt file will also give you the location of the Zelix KlassMaster log file which is named ZKM_log.txt by default.
You should look in the Zelix KlassMaster log file for any warnings or errors.
See the ProGuard Configuration File Translation Tutorial Troubleshooting section for more detail.
Note that Android Studio 3.6 no longer allows you to use ProGuard. You must use R8. So unfortunately you cannot use Zelix KlassMaster in place of ProGuard in this version.
Everything set out below assumes that you are using a version of Android Studio prior to 3.6.
Below is an example ZKM Script for the obfuscation of Java classes before they are compiled into an APK. It should be adequate for most Android Apps. Obviously it would have to be modified for your environment.
classpath "/usr/local/java/android-sdk/platforms/android-9/android.jar"
;
open "bin/classes\*"
"libs\*"
;
trimExclude
public *.^*^ extends android.app.Activity and
public *.^*^ extends android.app.Application and
public *.^*^ extends android.app.Service and
public *.^*^ extends android.content.BroadcastReceiver and
public *.^*^ extends android.content.ContentProvider and
public *.^*^ extends android.view.View public <init>(android.content.Context) and
public *.^*^ extends android.view.View public <init>(android.content.Context,android.util.AttributeSet) and
public *.^*^ extends android.view.View public <init>(android.content.Context,android.util.AttributeSet,int) and
public *.^*^ extends android.view.View public set*(*) and
*.^*^ containing {public <init>(android.content.Context,android.util.AttributeSet)}
public <init>(android.content.Context,android.util.AttributeSet) and
*.^*^ containing {public <init>(android.content.Context,android.util.AttributeSet,int)}
public <init>(android.content.Context,android.util.AttributeSet,int) and
*.* extends android.content.Context public *(android.view.View) and
*.* extends android.content.Context public *(android.view.MenuItem) and
*.* implements android.os.Parcelable static android.os.Parcelable$Creator CREATOR and
*.R$* public static * and
*.* @android.webkit.JavascriptInterface *(*)
;
trim deleteDeprecatedAttributes=true
deleteSourceFileAttributes=true
deleteAnnotationAttributes=false
deleteExceptionAttributes=true
//deleteUnknownAttributes=true
;
exclude
public *.^*^ extends android.app.Activity and
public *.^*^ extends android.app.Application and
public *.^*^ extends android.app.Service and
public *.^*^ extends android.content.BroadcastReceiver and
public *.^*^ extends android.content.ContentProvider and
public *.^*^ extends android.view.View public <init>(android.content.Context) and
public *.^*^ extends android.view.View public <init>(android.content.Context,android.util.AttributeSet) and
public *.^*^ extends android.view.View public <init>(android.content.Context,android.util.AttributeSet,int) and
public *.^*^ extends android.view.View public set*(*) and
*.^*^ containing {public <init>(android.content.Context,android.util.AttributeSet)}
public <init>(android.content.Context,android.util.AttributeSet) and
*.^*^ containing {public <init>(android.content.Context,android.util.AttributeSet,int)}
public <init>(android.content.Context,android.util.AttributeSet,int) and
*.* extends android.content.Context public *(android.view.View) and
*.* extends android.content.Context public *(android.view.MenuItem) and
*.* implements android.os.Parcelable static android.os.Parcelable$Creator CREATOR and
*.R$* public static * and
*.* @android.webkit.JavascriptInterface *(*)
;
obfuscate keepGenericsInfo=false
keepInnerClassInfo=false
obfuscateFlow=none //can change from none to light, normal or aggressive
encryptStringLiterals=none //can change from none to normal, aggressive, flowObfuscate or enhanced
exceptionObfuscation=none //can change from none to light or heavy
autoReflectionHandling=none //can change from none to normal
randomize=false //can change from false to true
collapsePackagesWithDefault=""
preverify=false //don't need to preverify if only used for Android
mixedCaseClassNames=ifInArchive
keepBalancedLocks=true //ensures compatibility with ART verifier
;
saveAll archiveCompression=all
"bin/classes-processed.jar"
;
By default, Zelix KlassMaster will exclude certain classes, fields and methods from being trimmed by the Trim function or renamed by the Name Obfuscation function.
However, you may override these defaults with your own.
In the case of the Trim function, Zelix KlassMaster looks for a file in the current user directory named "defaultTrimExclude.txt".
If it finds the file then it will use its contents as the default trim exclusions.
You can tell Zelix KlassMaster to use a different file for default trim exclusions by using the -dte command line option.
In the case of Name Obfuscation, Zelix KlassMaster looks for a file in the current user directory named "defaultExclude.txt".
If it finds the file then it will use its contents as the default exclusions.
You can tell Zelix KlassMaster to use a different file for default exclusions by using the -de command line option.
If the default exclusion file cannot be found then Zelix KlassMaster applies its predefined internal default exclusions. If the defaultExclude.txt file exists but is is empty then there there will be no default exclusions.
The same applies for the default trim exclusion file.
Yes it will.
Although you could obfuscate ALL of the field and method references in your application, it would have a dramatic and unacceptable impact on your application's performance.
Reflection API calls are much slower than direct references.
The Reference Obfuscation functionality is intended to be used sparingly to obscure references in and to sensitive parts of your application.
It can make decompiled code quite incomprehensible. It can also obscure key API calls which otherwise would allow a hacker to "zero in" on a particular part of your bytecode.
When you use the Reference Obfuscation functionality to obscure sensitive parts of your application you should be careful to also obscure a significant number non-sensitive parts.
Otherwise you will be drawing attention to the sensitive parts which would be counter productive.
However, you should be careful not to unnecessarily obfuscate references in performance sensitive parts of your code such as within deeply nested loops.
For maximum resistance against deobfuscation, it is recommended that Reference Obfuscation be used along with the Method Parameter Changes functionality.
Zelix KlassMaster will perform some processing in parallel by default and where possible to reduce obfuscation time.
The size of the reduction in obfuscation time could be more than 30% but it will very much depend upon
- The obfuscation options that have been selected,
- The number of processors that are available and
- The amount of heap space that is available.
Parallel processing can be disabled by setting the ZKM_USE_PARALLEL special configuration option to false .
Note that these Reflection API call warnings are just warnings rather than errors. Zelix KlassMaster detects Reflection API calls and attempts to automatically resolve the object that is being accessed by name.
If it isn't able to 100% resolve an access, then it outputs a warning to the ZKM_log.txt file (if you are using ZKM Script) which starts with the line "API calls detected that may not be handled automatically...".
(If you are using the "-v" parameter, then resolved accesses are also listed but are marked as "RESOLVED".) These warnings indicate
- The name of the class being obfuscated which contains the unresolved Reflection API call,
- The name of the method being obfuscated which contains the unresolved Reflection API call and
- The signature the Reflection API call.
Note that the warning cannot specify the name of the class, field or method being accessed by the Reflection API call if it is unresolved.
If you get such an unresolved warning, you should go to the method concerned and satisfy yourself that the Reflection API call will not be broken by name obfuscation.
So you need to work out which class, field or method is being accessed by the Reflection API call and then determine whether the accessed class, field or method is being renamed.
If a Reflection API call would be broken by the obfuscation of a class, field or method name then that class, field or method name should be explicitly excluded from name obfuscation.
If you decide to use Zelix KlassMaster's Trim functionality then the same exclusions should also appear in
a ZKM Script trimExclude statement so that Zelix KlassMaster's can satisfactorily trace all accesses.
However, also note that any warnings pertaining to java.lang.Class.getName() calls will only be a problem if
- The String returned by java.lang.Class.getName() is compared to another String containing a class name AND
- The second String is not effectively a String literal containing a full class name AND
- The class which has its named stored in the second String will have its name obfuscated.
So, if the value returned by java.lang.Class.getName() is simply output for debugging purposes (which is normally the case) then there will be no problem.
However, if the returned value is used in a way that satisfies the above three conditions then the name of the class or classes that the return value is being compared to will need to be excluded from obfuscation.
You can suppress these warnings by setting the ZKM_REFLECTION_WARNINGS system property to "false". One way of doing this is to use a command line like the following.
java -D"ZKM_REFLECTION_WARNINGS=false" -jar ZKM.jar MyScript.txt
Note that you can also use Zelix KlassMaster's AutoReflection functionality to automatically handle Java Reflection API calls.
See the answer to FAQ Question 11 above.
Zelix KlassMaster by default provides only limited support for the obfuscation of constructs that the Kotlin compiler puts into the Java bytecode that it generates.
Java bytecode generated from relatively simple Kotlin code might be obfuscated safely by adding ZKM Script removeMethodCallsInclude and removeMethodCalls statements and an exclusion like the following.
//Specify removal of non-essential validity check methods
removeMethodCallsInclude
kotlin.jvm.internal.Intrinsics throwUninitializedPropertyAccessException(java.lang.String) and
kotlin.jvm.internal.Intrinsics checkExpressionValueIsNotNull(java.lang.Object, java.lang.String) and
kotlin.jvm.internal.Intrinsics checkFieldIsNotNull(java.lang.Object, java.lang.String) and
kotlin.jvm.internal.Intrinsics checkFieldIsNotNull(java.lang.Object, java.lang.String, java.lang.String) and
kotlin.jvm.internal.Intrinsics checkNotNull(java.lang.Object) and
kotlin.jvm.internal.Intrinsics checkNotNull(java.lang.Object, java.lang.String) and
kotlin.jvm.internal.Intrinsics checkNotNullExpressionValue(java.lang.Object, java.lang.String) and
kotlin.jvm.internal.Intrinsics checkNotNullParameter(java.lang.Object, java.lang.String) and
kotlin.jvm.internal.Intrinsics checkParameterIsNotNull(java.lang.Object, java.lang.String) and
kotlin.jvm.internal.Intrinsics checkReturnedValueIsNotNull(java.lang.Object, java.lang.String) and
kotlin.jvm.internal.Intrinsics checkReturnedValueIsNotNull(java.lang.Object,java.lang.String,java.lang.String)
;
//"removeMethodCalls" statement should precede any "trim" or "obfuscate" statements.
removeMethodCalls;
exclude kotlin.* + and //exclude all classes, fields and methods in kotlin package
kotlin.*.* + and //exclude all classes, fields and methods in kotlin sub-packages
org.jetbrains.kotlin.* + and //exclude all classes, fields and methods in package
org.jetbrains.kotlin.*.* + //exclude all classes, fields and methods in packages
;
If your Java bytecode is generated from very simple Kotlin code you might even be able to safely remove the Kotlin annotations entirely using the ZKM Script trim statement
with the deleteAnnotationAttributes=true setting. However by default this setting would remove ALL annotations.
So you may need to specify annotation exclusions on your trimExclude statement to pevent some annotations from being removed
as a result of this setting.
|