-
Notifications
You must be signed in to change notification settings - Fork 1
Code Generation
Contrary to Lombok that works as an annotation processor and is based on a bug in the Java compiler that lets it modify the existing AST (abstract syntax tree) of your code before actual compilation (conversion of .java
source to .class
bytecode), Access Warden does not interfere with the compilation phase of your application. Instead, it uses ASM to modify the existing, already compiled JVM (Java Virtual Machine) bytecode.
Let's discover how it all works under the hood on the example of the @RestrictedAccess
annotation.
This is what the workflow of Access Warden Core looks like:
- Wait for an input application JAR to be passed (that is, you first build your application executable).
- Create (basically, compile) a new class file in a special dedicated package, we'll refer to it as to checker class. This class is an utility-class — that is, it is
final
(cannot be inherited from) and has aprivate
default constructor (cannot be instantiated). - Walk through all compiled classes (
.class
files) within the application executable archive (.jar
). - Analyze bytecode in each compiled class (that is, in each already existing class of your application, including all of the library classes shaded in your JAR).
- For each method in the analyzed class:
- If the method is annotated with
@RestrictedAccess
, proceed to step 5.ii, else continue to the next method (step 5). - Attempt to form a
RestrictedAccess.Configuration
object based on the annotation parameters. - If an
UnexpectedSetupException
is caught, it is logged, and the method is not transformed (continue to step 5). Otherwise (if the configuration seems to be valid), proceed to step 5.iv. - Delete the annotation from the method (if the
preserveThisAnnotation
parameter is set tofalse
). - Generate a new method with a random name in the checker class, with access flags
public
,static
, andsynthetic
(to outline the fact the method is generated and should never be manually called). We'll refer to this method as to checker method. - Generate bytecode instructions in the checker method that will resolve the current context at runtime, remove the checker class from this context, and check if the context matches the
RestrictedAccess.Configuration
object created earlier from the annotation parameters. - Insert bytecode instructions in the beginning of the currently analyzed method that will call the checker method. That is, now, whenever this (analyzed) method will be invoked, the special checker method dedicated to this particular method located in the checker class will be called to check if the current call stack and environment are valid (otherwise a
java.lang.SecurityException
will be thrown). - Continue to the next method (step 5), if there are still any, or to the next class (step 4), if there are still any, or to step 6 otherwise.
- If the method is annotated with
- Insert the generated checker class with all the generated special checker methods in the input JAR.
- Replace all changed classes (classes with at least one transformed method) in the input JAR (delete old
.class
files, insert new.class
files). - Save the updated input JAR.
- Now the build JAR you provided that previously contained "hint" to Access Warden (annotations and other stuff) is working and runnable. And it is now protected against arbitrary unwanted access to critical parts.
Access Warden attempts to generate as minimal code as possible. For example, it will omit configuration builder methods with empty lists (keeping them null
internally implies Collections.emptyList()
already). Another example is int
generation on stack: for a few smallest values, elementary instructions iconst_0
, iconst_1
, ..., iconst_5
are used; for values in range [Byte.MIN_VALUE; Byte.MAX_VALUE] bipush
is used; for values in range [Short.MIN_VALUE; Short.MAX_VALUE] sipush
is used; finally, for all other values, ldc
is used.
Consider the following code from the Demo module:
@RestrictedCall (
preserveThisAnnotation = true,
prohibitReflectionTraces = true,
prohibitNativeTraces = true,
prohibitArbitraryInvocation = true,
permittedSources = "me.darksidecode.accesswarden.demo.AccessWardenDemo#first"
)
private static void test(int x) {
System.out.println(">>> Successful test call, x=" + x);
}
After transforming the output JAR (in case of the Demo module we're currently looking at, that happens automatically after ./gradlew access-warden-demo:build
), this method will looks somewhat like this (decompiled with Procyon):
@RestrictedCall(preserveThisAnnotation = true, prohibitReflectionTraces = true, prohibitNativeTraces = true, prohibitArbitraryInvocation = true, permittedSources = { "me.darksidecode.accesswarden.demo.AccessWardenDemo#first" })
private static void test(final int x) {
__CheckerClass__.__check__334aaca99fc517c0__();
System.out.println(">>> Successful test call, x=" + x);
}
Note that the annotation was kept because preserveThisAnnotation
was explicitly set to true
— it may just sometimes be useful to keep these annotations. In most cases, however, you'll want to be removing them (simply don't set this option to true
then). You may notice a method call inserted at the beginning of our test
method. This is the call to the checker method generated specifically for this test
method.
Now, here's the new class itself (the checker class) that Access Warden created in our application JAR (decompiled with Procyon):
package __access__warden__generated__;
import java.util.Arrays;
import me.darksidecode.accesswarden.api.FilteredContext;
import me.darksidecode.accesswarden.api.UnexpectedSetupException;
import me.darksidecode.accesswarden.api.ContextResolution;
import java.util.Collections;
import me.darksidecode.accesswarden.api.RestrictedCall;
public final class __CheckerClass__
{
private __CheckerClass__() {
super();
}
public static /* synthetic */ void __check__334aaca99fc517c0__() {
try {
final RestrictedCall.Configuration conf = RestrictedCall.Configuration.newBuilder().prohibitReflectionTraces(true).prohibitNativeTraces(true).prohibitArbitraryInvocation(true).permittedSources(Collections.singletonList("me.darksidecode.accesswarden.demo.AccessWardenDemo#first")).build();
final FilteredContext ctx = ContextResolution.resolve(conf.contextResolutionOptions());
if (ctx.filteredCallStack().size() <= 1) {
throw new UnexpectedSetupException("filtered call stack is unexpectedly small");
}
ctx.filteredCallStack().remove(0);
ContextResolution.ensureCallPermitted(ctx, conf);
}
catch (UnexpectedSetupException ex) {
throw new SecurityException("unexpected setup: " + ex.getMessage());
}
}
public static /* synthetic */ void __check__3ee730bef7c9dfb0__() {
try {
final RestrictedCall.Configuration conf = RestrictedCall.Configuration.newBuilder().prohibitedSources(Arrays.asList("me.darksidecode.accesswarden.demo.AccessWardenDemo#prohibitTest*", "me.darksidecode.accesswarden.demo.AccessWardenDemo#otherProhibitedTest")).build();
final FilteredContext ctx = ContextResolution.resolve(conf.contextResolutionOptions());
if (ctx.filteredCallStack().size() <= 1) {
throw new UnexpectedSetupException("filtered call stack is unexpectedly small");
}
ctx.filteredCallStack().remove(0);
ContextResolution.ensureCallPermitted(ctx, conf);
}
catch (UnexpectedSetupException ex) {
throw new SecurityException("unexpected setup: " + ex.getMessage());
}
}
}
This class contains checker methods for all methods that we annotated with @RestrictedAccess
(in our Demo application, there are two such methods — hence the two checker methods). These methods make use of the low-level API module to inspect the current call stack and environment at runtime and decide whether the current invocation should be allowed (passed), or prohibited, according to the configuration we provided for this particular method in @RestrictedAccess
parameters.
Access Warden can work fine with obfuscated applications. To avoid messing with class names transformations and make everything as easy as possible, it is recommended that you apply Access Warden after you obfuscate your program. Since Access Warden itself generates random, meaningless names, it will not make your code any more vulnerable or disclose any private information.
However, if for some reason you have to use Access Warden before obfuscating your application executable, make sure that your obfuscation software can handle class and method renaming in Strings, or set it up the way it is capable of doing so. If you don't ensure that, Access Warden will be unable to inspect the call stacks properly at runtime, since classes that it was configured to work with, say, "com.mycompany.MyApp"
turn into "a.b.c"
at runtime (which means that Access Warden will check strack traces for classes that no longer exist because they were renamed by the obfuscator).