Intro — A Gradle plugin and native JNI runtime that removes selected classes from your JAR and materializes them once at startup. No per-method guards, no decryption loops, no steady-state overhead.
The problem with shipping bytecode
Java applications ship with their executable representation fully intact. That's a feature, not a bug, most of the time. The JVM specification makes .class files portable, structured, and easy to validate. But those same properties make them trivially easy to inspect.
A class file contains a constant pool, symbolic references, field and method descriptors, bytecode instructions, exception tables, annotations, and often line number or local variable metadata. Even when names are obfuscated, the artifact still contains executable bytecode that can be copied, disassembled, decompiled, patched, and re-packaged. Open a JAR with any decompiler and you'll get source-quality output for most of the codebase.
This matters when your business logic, licensing checks, or proprietary algorithms live in those classes. Obfuscation raises the cost of reverse engineering, but it doesn't actually remove the plaintext bytecode from the delivered artifact. It's still there, waiting for someone with enough patience and a good decompiler.
What Grace does
Grace is a Gradle plugin and native JNI runtime that takes a different approach: it removes selected classes from the output JAR entirely and stores them in an encrypted bundle. At startup, a generated bootstrap loads a native library, decrypts the bundle, and defines the protected classes through JNI DefineClass. After that one-time materialization step, protected classes are ordinary JVM classes. Normal execution proceeds with zero Grace-specific overhead.
The core tradeoff is explicit and intentional. Grace does not make client-side Java code impossible to recover from a running process. It does make static extraction and ordinary decompilation of protected classes fail by design. And it does so without imposing any post-init runtime cost on protected application logic.
The user configures protectedPaths to select packages or classes to encrypt and skipPackages to exclude public APIs. During graceEncrypt, matched class files are removed from the output JAR and appended to a bundle entry list. Non-class resources and non-protected classes pass through unchanged.
At runtime, the generated bootstrap selects a platform directory from os.name and os.arch, extracts the native library to a temporary directory, calls System.load, reads the configured bundle resource, and invokes:
GraceNative.init(String key, byte[] bundleData, String entrypointClass)
The native method returns a newly constructed entrypoint object. Java code wraps that object in EncryptedClassLoader, which only provides a typed getEntrypoint() accessor. Despite the class name, the current runtime is not a persistent custom class loader. It's a one-shot materializer.
The threat model
Before diving into implementation details, it's worth being precise about what Grace protects against and what it does not. This is a bytecode-at-rest protection system, not a full trusted execution environment.
What Grace fixes:
- Offline decompilation of protected
.classfiles from the JAR - Static extraction of protected class bytecode from ordinary ZIP entries
- Java-level patching of a visible decryption loop, because the decryption lives in native code
- Untrusted direct invocation of the bootstrap when callsite metadata is enabled and the caller class is not in the configured allowlist
- Tampering with the expected bootstrap or allowed caller class bytes, as detected by runtime SHA-256 comparison against encrypted metadata
- Selected runtime conditions when configured to abort, including debuggers, startup JVM agents, preload injection, and Linux executable-segment integrity failure
What Grace does not fix:
- Dynamic attach agents, JVMTI agents not visible in startup JVM arguments, or tools that observe class loading after
DefineClass - A modified JVM, a custom launcher, or an emulator that records JNI inputs
- Process memory capture, debugger inspection when not configured to abort, core dumps when not disabled, or kernel-level instrumentation
- Native API hooking, replacement of
libgracewith a malicious library, or binary patching outside the portions checked by the current Linux self-integrity implementation - Disclosure of the user passphrase through logs, command-line arguments, environment variables, heap inspection, or application code
- Logical abuse of an allowed caller. If an authorized caller exposes a path that accepts attacker-controlled input and then invokes the bootstrap, the callsite check has done exactly what it was configured to do
That last one is worth highlighting. Callsite verification does not prove user intent, does not authenticate a human, and does not make an allowed caller safe by itself. It verifies that the call chain contains specific class identities and class bytes at startup. If an allowed class is itself under attacker control, callsite verification will not save the application. It is a startup path constraint, not a policy engine.
Build-time transformation
The Gradle plugin registers two tasks. graceSetup generates the bootstrap source file. graceEncrypt produces the protected output JAR.
Class selection
GraceEncryptTask scans the input JAR produced by Gradle's jar task. For each .class entry, the path is converted from slash notation to a dotted fully qualified class name. The class is then classified as follows:
- If it exactly matches a configured
skipPackagesentry, or starts with that entry plus a dot, it remains as a plaintext passthrough class - If it exactly matches a configured
protectedPathsentry, or starts with that entry plus a dot, it is added to the encrypted bundle - Otherwise it remains as a plaintext passthrough class
This model supports a common pattern: keep a small public API visible while encrypting the implementation package.
Callsite metadata generation
If bootstrapClassName is configured, the plugin builds a metadata entry named __grace_callsite__. The metadata contains:
- Format version
u16, currently1 - Bootstrap class name length, class name, and SHA-256 hash of its class bytes
- Caller count
u16 - For each allowed caller, class name length, class name, and SHA-256 hash of the caller class bytes
The plugin finds these class bytes from passthrough entries, encrypted entries, or runtime classpath JARs. If the bootstrap or any allowed caller cannot be found, callsite verification is disabled for that build and a warning is logged. If metadata is generated, it is stored as another encrypted bundle entry and is not written as a plaintext JAR resource.
In normal configurations, the bootstrap and allowed caller classes should remain passthrough classes. The runtime verifier hashes class resources through the application class loader before protected classes are defined.
Guard metadata generation
The plugin also writes a guard policy entry named __grace_guards__. It is stored inside the encrypted bundle, not as a plaintext JAR resource. The payload is currently six bytes:
| Offset | Field | Meaning |
|---|---|---|
| 0 | lock_sensitive_mem | request key-buffer memory locking |
| 1 | abort_on_debugger | abort if a debugger is detected |
| 2 | abort_on_jvmti | abort on startup JVM agent arguments |
| 3 | abort_on_ld_preload | abort on preload environment variables |
| 4 | disable_core_dumps | request process core dump suppression |
| 5 | integrity_check | 0 off, 1 warn, 2 abort |
Current Gradle defaults are conservative for compatibility: key-buffer locking is enabled, self-integrity is warn-only, and debugger, JVM agent, preload, and core dump abort policies are disabled. This is intentional because production server environments may legitimately use preload allocators, APM agents, profilers, or debug tooling.
The encrypted bundle format
The current bundle format is a nested encrypted container. It is not an indexed random-access archive. Grace decrypts the full bundle during startup, parses all entries sequentially, runs encrypted guard policy when present, verifies callsite metadata when present, filters metadata entries, and defines the remaining entries as classes.
The key schedule is intentionally simple and identical in Java and C:
The Java bundle writer constructs plaintext entries, appends the GRACEOK trailer, encrypts that plaintext with K_inner, prefixes the inner IV and key digest, then encrypts the resulting inner container with K_outer. The final bundle starts with a 24-byte outer header:
| Offset | Size | Field |
|---|---|---|
| 0 | 4 | magic 0x47524345, string GRCE |
| 4 | 2 | version, currently 1 |
| 6 | 2 | flags, currently 0 |
| 8 | 16 | outer AES-CBC IV |
| 24 | n | outer ciphertext |
After outer decryption, native code verifies the embedded key digest before attempting inner decryption. After inner decryption, native code checks the GRACEOK trailer and then parses entries. Each plaintext entry has:
| Field | Size | Meaning |
|---|---|---|
| name length | 4 B | UTF-8 class name byte length |
| name | variable | dotted class name, not null-terminated |
| data length | 8 B | class or metadata payload length |
| data | variable | raw .class bytes or metadata |
The format currently uses AES-256-CBC with PKCS padding. It includes key digest and trailer checks, but it is not an authenticated-encryption construction. Corruption and wrong keys are expected to fail through padding, digest, trailer, or later class validation. Future versions should consider an AEAD mode such as AES-GCM or a separate MAC to give formal ciphertext integrity.
The native layer
The native library is written in C and linked against OpenSSL's libcrypto. It exposes one JNI entry point:
JNIEXPORT jobject JNICALL Java_dev_pixelib_grace_api_GraceNative_init( JNIEnv *env, jclass clazz, jstring key, jbyteArray bundle_data, jstring entrypoint_class)
The implementation performs the following steps:
- Allocate a small locked native buffer when the platform allows it. This is done before guard metadata can be read because the metadata is itself encrypted. If allocation fails, the implementation falls back to ordinary native storage.
- Convert the Java passphrase to UTF-8 and derive
K_outerwith SHA-256, using the locked buffer when available. - Access the Java byte array containing the encrypted bundle.
- Call
bundle_load, which decrypts both AES layers and parses all entries into native heap allocations. - Release the Java bundle byte array and wipe
K_outer. - Parse
__grace_guards__when present and callgrace_guard_check. This runs before protected classes are defined. - Obtain the class loader associated with
GraceNativefor resource lookups and class definition. - Call
grace_verify_callsite. This runs after bundle decryption because metadata is encrypted, but before any protected class is defined. - Filter metadata entries out of the parsed entry array.
- Call
loader_define_classesto define every protected class via JNIDefineClass. - Call
loader_create_entrypoint, which resolves the configured entrypoint class and invokes its no-argument constructor. - Free decrypted bundle entries from native memory and return the entrypoint object to Java.
Class definition semantics
The native loader converts dotted class names to slash names and calls JNI DefineClass with the selected class loader, class name, byte pointer, and byte length. The preferred class loader is the loader associated with GraceNative. If that lookup fails, the implementation falls back through the java.lang.Object lookup path, which can yield a null bootstrap-loader reference. This means protected classes become JVM classes in the normal sense. They can be referenced by other classes after they are defined, initialized according to JVM rules, optimized by the JIT, and garbage-collected like ordinary loaded classes.
This is also the key security boundary to state accurately. Plaintext bytecode does cross into JVM internals as the input to DefineClass. Grace does not keep plaintext class files off the machine in an absolute sense; it keeps protected class files out of the artifact and avoids a Java-visible decryption implementation. Any attacker able to observe DefineClass, instrument the JVM, or dump class bytes after definition can recover protected classes.
Runtime guard policy
Guard checks are startup policy controls. They are configured at build time, stored as encrypted bundle metadata, parsed after bundle decryption, and executed before callsite verification and class definition. They do not add a hook to protected method calls after initialization.
The current native implementation supports the following checks:
- Debugger detection. Linux reads
TracerPidfrom/proc/self/status; macOS usessysctlandP_TRACED; Windows usesIsDebuggerPresentandCheckRemoteDebuggerPresent. IfabortOnDebuggeris enabled, startup fails withSecurityException. If the integrity level is nonzero and a debugger is detected, the runtime warns when abort is not enabled. - Preload detection. Linux checks
LD_PRELOAD; macOS checksDYLD_INSERT_LIBRARIES. IfabortOnLdPreloadis enabled, startup fails. Otherwise the current implementation still warns when either environment variable is present. - Startup JVM agent detection. When
abortOnJvmtiAgentis enabled, the runtime readsRuntimeMXBean.getInputArguments()and scans for-javaagent:,-agentpath:, and-agentlib:. This does not cover every dynamic attach path. - Core dump suppression. When
disableCoreDumpsis enabled, Linux usesprctl(PR_SET_DUMPABLE, 0)and macOS usesptrace(PT_DENY_ATTACH)when available. Windows uses a best-effort unhandled-exception filter path and does not claim complete minidump suppression. - Native self-integrity. When
integrityCheckis warn or abort, Linux finds the mapping containing the integrity-check function, derives the ELF base from the mapping start and file offset, parses program headers, hashes the executablePT_LOADsegment, and compares it with a build-time embedded SHA-256 hash. macOS and Windows currently report the check as unavailable and skip it. - Sensitive memory locking. The native entry point tries to allocate a small locked buffer before deriving
K_outer. Linux first attemptsmmapwithMAP_LOCKED, then falls back toposix_memalignplusmlock; macOS usesposix_memalignplusmlock; Windows usesVirtualAllocplusVirtualLock. This protects the derived native key buffer when it succeeds. Parsed class entries are ordinary native allocations that are securely wiped before free, not locked allocations.
These checks deliberately default to compatibility-first behavior. Minecraft server hosts, profilers, APM agents, preload allocators, and crash diagnostics can all be legitimate in real deployments. Grace therefore treats guard policy as operator choice: warn-by-default for native self-integrity and preload presence, abort only when the build configuration asks for abort semantics.
Callsite protection
Encryption alone protects bytecode at rest, but it does not answer who may trigger materialization. If an attacker can run arbitrary Java code in the same process, knows or obtains the passphrase, and can call the bootstrap directly, the bundle can be legitimately decrypted. Grace's callsite protection narrows that startup path.
Callsite protection is optional and enabled by build metadata. The metadata is encrypted in the same bundle as class entries. The native runtime therefore needs to decrypt the bundle before it can inspect callsite policy, but it runs the verification before defining protected classes. On mismatch, it throws SecurityException and returns without class definition.
Runtime verification procedure
The implementation follows this procedure:
- Locate the
__grace_callsite__entry. If absent, verification is skipped. - Parse the metadata into an expected bootstrap name, bootstrap hash, allowed caller names, and caller hashes.
- Obtain
Thread.currentThread().getStackTrace()from JNI. - Skip frames whose class is
java.lang.Thread,java.lang.Throwable, ordev.pixelib.grace.api.GraceNative. - Treat the first remaining frame as the bootstrap frame and compare its class name against the expected bootstrap class name.
- Read the bootstrap class resource through
ClassLoader.getResourceAsStream, hash the bytes with SHA-256, and compare the hash with encrypted metadata. - If configured caller count is greater than zero, find the next non-internal frame, require its class name to be in the allowlist, read its class resource, hash it, and compare with metadata.
- Return true only if every configured check succeeds.
This check is stronger than a Java-level conditional because the decision is made in native code before protected classes are defined. A bytecode patcher cannot simply edit a visible Java if statement in the decryptor, because the decryptor is not Java bytecode. The check also binds the bootstrap and callers to their build-time bytes. Replacing com.example.Main with a class of the same name is detected unless the attacker can also modify the encrypted metadata or bypass the native check.
What callsite protection does not do
Callsite verification does not prove user intent, does not authenticate a human, and does not make an allowed caller safe by itself. It verifies that the call chain contains specific class identities and class bytes at startup. If an allowed class is itself under attacker control, or exposes a public method that triggers Grace with attacker-supplied parameters, callsite verification will not save the application. It is a startup path constraint, not a policy engine.
No post-init runtime cost
Grace's most important engineering property is that it does not remain in the hot path. Many bytecode protection systems add a custom class loader, per-class decryption, method guards, interpreted dispatch, string decryptors, or repeated integrity checks. Those techniques can impose steady-state overhead and create more Java-level logic for an attacker to inspect.
Grace pays its cost during initialization only:
- Read bundle bytes from the JAR resource
- Load the native library once per process
- Derive keys
- Decrypt the full bundle
- Parse entries
- Run configured guard checks
- Verify callsite metadata when present
- Call
DefineClassfor protected classes - Instantiate the configured entrypoint
After the entrypoint object returns, calls into protected code are ordinary JVM calls. JIT compilation, inlining, allocation, synchronization, exceptions, and reflection follow standard JVM behavior. There is no Grace callback on every method invocation and no Grace decryption step on each protected method access.
| Operation | When paid | Repeated after init |
|---|---|---|
| Native library extraction | Startup | No |
| Native library load | Startup | No |
| Bundle read | Startup | No |
| AES decryption | Startup | No |
| Guard checks | Startup | No |
| Callsite verification | Startup | No |
| Class definition | Startup | No |
| Application method calls | Normal execution | No Grace hook |
Security analysis
The distinction between fixed artifact exposure, optional abort policy, warn-by-default checks, best-effort hardening, and residual risk is important. Grace fixes plaintext class exposure in the delivered JAR. It can also refuse selected startup environments when configured to do so. It does not and cannot protect bytecode after the JVM has accepted it as executable code.
| Vector | Status | Mechanism | Residual risk |
|---|---|---|---|
| Offline decompile from JAR | Fixed | Protected .class entries removed, stored only in encrypted bundle | Public API, bootstrap, and passthrough classes remain visible |
| Static ZIP extraction | Fixed | JAR contains assets.bundle, not protected class entries | Bundle bytes can be copied for offline attack |
| Java-level decryptor patching | Mitigated | Decryption and verification live in C/JNI | Native code can still be patched or hooked |
| Direct bootstrap invocation | Mitigated | Stack inspection checks bootstrap and caller identity | An allowed caller can still intentionally invoke startup |
| Bootstrap or caller replacement | Mitigated | Runtime SHA-256 hashes compared with encrypted metadata | Resource lookup can be subverted by a malicious class loader |
| Wrong passphrase | Fixed | AES padding, key digest, and GRACEOK checks fail | Weak passphrases remain brute-forceable offline |
| Debugger at startup | Optional abort | abortOnDebugger can fail startup | Later attachment can still expose process state |
| Startup JVM agent args | Optional abort | Scans for -javaagent, -agentpath, -agentlib | Dynamic attach remains a residual risk |
| Preload injection | Warn by default | Checks LD_PRELOAD / DYLD_INSERT_LIBRARIES | Legitimate allocators may require warning-only mode |
| Core dump exposure | Optional | Requests OS suppression at startup | Platform support differs |
| Native segment patching | Warn by default (Linux) | Hashes executable PT_LOAD segment | macOS and Windows skip this check |
| Memory scraping | Best effort | Key buffer locked when available, entries wiped before free | Plaintext exists transiently in native heap |
| Modified JVM | Residual | A hostile runtime can record DefineClass inputs | Requires stronger isolation outside Grace |
Confidentiality at rest
For protected classes, Grace removes the most direct reverse-engineering path: open the JAR, copy com/example/Foo.class, and run a decompiler. The attacker instead receives encrypted bytes. Since entry names and class bytes are inside the inner encrypted plaintext, the bundle does not reveal the protected class list without decryption. This is materially different from obfuscation, where the protected bytecode remains present and executable in the artifact.
Startup exposure
Grace must eventually produce plaintext class bytes. The native loader holds decrypted entries in native heap allocations, then passes each class to DefineClass. Once this happens, the JVM owns executable class metadata. This is the unavoidable transition from confidentiality-at-rest to execution. The design minimizes Java-level exposure but does not make execution opaque.
Key handling
The passphrase is supplied to GraceNative.init as a Java String. The native layer derives K_outer, releases the UTF-8 string, releases the bundle byte array after decryption, and wipes native key buffers. The current entry point tries to place K_outer in a locked native buffer before key derivation, but this is best effort because operating systems can deny locked memory. Decrypted bundle plaintext is copied into parsed entry allocations, and those entry data buffers are securely zeroed before free. This prevents native key and entry buffers from remaining intentionally live after startup, but it does not erase all copies of the Java passphrase and does not prevent plaintext from crossing into JVM internals through DefineClass.
Integrity
Grace currently detects many corrupt or wrong-key inputs through AES padding, the key digest, the GRACEOK trailer, parser bounds checks, and JVM class verification. This is operational integrity, not cryptographic authentication. An attacker with ciphertext modification capability is not given a useful plaintext oracle in normal execution, but the construction should not be described as AEAD. A future format version should authenticate the full header and ciphertext.
Implementation details
Java API
The public API is deliberately small:
public final class GraceNative { public static native Object init( String key, byte[] bundleData, String entrypointClass); }
The generated bootstrap is responsible for loading the native library before the native method is called. EncryptedClassLoader is only a holder for the entrypoint object:
public class EncryptedClassLoader { public <T> T getEntrypoint() { ... } }
This small API surface is part of the protection strategy. The less Java code that participates in decryption and definition, the less visible bytecode an attacker can patch in the delivered artifact.
Native modules
The native source is split by responsibility:
| Module | Responsibility |
|---|---|
grace.c | JNI entry point and top-level startup orchestration |
bundle.c/h | Bundle decryption, parsing, entry ownership, metadata parsing |
guard.c/h | Startup guard policy, memory locking, debugger, preload, agent, core dump, and self-integrity checks |
crypto.c/h | SHA-256, AES-256-CBC encrypt and decrypt wrappers, secure zeroing |
verify.c/h | Stack inspection and bootstrap or caller hash verification |
loader.c/h | JNI DefineClass loop and entrypoint instantiation |
Cross-platform build
The native library is built through CMake and cross-compiled in Docker using Zig as the C compiler. OpenSSL libcrypto is built for each target with apps, tests, modules, and shared libraries disabled for the bundled build. The project currently ships native resources for:
linux-x86_64/libgrace.solinux-aarch64/libgrace.soosx-x86_64/libgrace.dylibosx-aarch64/libgrace.dylibwindows-x86_64/grace.dll
The source enum also names windows-aarch64, but the current Docker build path does not produce that artifact. The bootstrap will request it on a Windows ARM64 runtime, so production packaging should either add that target or fail early with a clear unsupported-platform message.
Evaluation
The existing microbenchmarks measure build-time bundle encryption in GracePerformanceTest. Tests were collected on an AMD Ryzen 9 7950X with OpenJDK 21. The benchmark constructs synthetic class-sized byte arrays and calls BundleWriter.write.
| Classes | 1 KB | 16 KB | 64 KB |
|---|---|---|---|
| 1 | 3.7 | 69.1 | 118.0 |
| 10 | 53.1 | 108.7 | 112.2 |
| 100 | 112.7 | 119.3 | 158.1 |
Build-time encryption throughput in MB/s.
The small single-class case is dominated by fixed overhead such as digest setup, random IV generation, cipher initialization, and object allocation. Larger bundles amortize this fixed cost and reach more than 150 MB/s in the benchmark.
For a bundle containing ten 16 KB entries, the measured raw class data was 163,840 bytes and the output bundle was 164,312 bytes. This is roughly 0.3 percent overhead, or 47 bytes per entry in that benchmark shape. The exact overhead depends on class name length, AES padding, and metadata presence.
| Artifact | Size range |
|---|---|
| Static native libraries (all targets) | 192 KB to 228 KB |
Runtime startup performance should be measured separately from build-time encryption throughput. The startup path includes JAR resource reading, native library extraction, AES decryption, stack inspection, resource hashing for callsite verification, class definition, and class initialization side effects. The important steady-state result follows from design rather than benchmark numbers: after initialization, there are no Grace checks in application call paths.
Comparison with alternatives
Obfuscation
Traditional bytecode obfuscators rename identifiers, alter control flow, encode strings, remove metadata, or insert opaque predicates. These transformations are useful and can be combined with Grace, but they leave bytecode present in the artifact. Grace solves a different layer: protected bytecode is not stored as plaintext in the JAR at all.
Java-level encrypted class loaders
A common approach is to write a custom Java class loader that decrypts class bytes on demand. This protects class files from simple ZIP extraction, but the decryption routine, key derivation, and policy checks are themselves Java bytecode. They can be decompiled and patched using the same tooling that the protector is meant to resist. Grace moves these operations into native code and materializes all protected classes during startup.
Native rewrites
Another approach is to move sensitive logic into native code. That can improve resistance against Java decompilers, but it changes the programming model and forces application logic through JNI or a full native rewrite. Grace keeps application logic in Java. Native code is used only as the startup materializer and verifier.
Remote licensing or server-side logic
Remote services can keep secrets off the client entirely, which is stronger for some licensing and anti-fraud cases. That approach requires online availability and changes product architecture. Grace is an offline artifact hardening layer. It can complement remote licensing, but it is not a replacement for server-side trust boundaries.
Limitations and future work
Grace's main limitations are direct consequences of executing on an untrusted client machine.
- Once classes are defined, JVM-level tools can observe or dump them if the process permits agents or instrumentation, or if an agent attaches after the startup checks have completed
- The passphrase must exist somewhere before native key derivation. If the application exposes it, Grace cannot recover secrecy
- The native library can still be debugged, patched outside the currently checked region, replaced, or hooked by an attacker with sufficient local control
- The current CBC-based bundle format is not AEAD and should be upgraded to authenticate ciphertext and headers
- Callsite verification depends on stack shape and class resource lookup. It is effective against ordinary unauthorized Java callers, but not against a hostile JVM or malicious class loader environment
- Guard policy is configured for startup. It does not continuously monitor the process after protected classes are defined
- Native self-integrity is currently implemented for Linux executable ELF segments. macOS and Windows report the check as unavailable
- The loader currently continues past individual class definition failures. That is useful for some dependency-order cases, but a stricter mode could fail closed when any protected class cannot be defined
Future work should prioritize authenticated encryption, stronger native artifact authenticity, macOS and Windows self-integrity support, dynamic-attach hardening, clearer Windows ARM64 packaging behavior, passphrase integration with external secret providers, and a strict class definition mode for deployments that prefer fail-closed semantics.
Conclusion
Grace protects Java bytecode at rest by removing selected plaintext classes from the shipped JAR and replacing them with an encrypted bundle. At startup, a small generated bootstrap loads a native library and calls one JNI entry point. Native code decrypts the bundle, runs configured startup guard policy, optionally verifies the bootstrap and caller class chain, defines protected classes through DefineClass, constructs the entrypoint, and then leaves normal JVM execution alone.
The core tradeoff is explicit. Grace does not make client-side Java code impossible to recover from a running process. It does make static extraction and ordinary decompilation of protected classes from the artifact fail by design, and it does so without imposing any post-init runtime cost on protected application logic.