Grace: Encrypting Java Bytecode at Rest Without Runtime Cost

Mats Moolhuizen
The Hague, Netherlands
mats.coffee

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.

Grace build and startup pipeline
Grace build and startup pipeline

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 .class files 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 libgrace with 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 skipPackages entry, or starts with that entry plus a dot, it remains as a plaintext passthrough class
  • If it exactly matches a configured protectedPaths entry, 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, currently 1
  • 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:

OffsetFieldMeaning
0lock_sensitive_memrequest key-buffer memory locking
1abort_on_debuggerabort if a debugger is detected
2abort_on_jvmtiabort on startup JVM agent arguments
3abort_on_ld_preloadabort on preload environment variables
4disable_core_dumpsrequest process core dump suppression
5integrity_check0 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.

Nested bundle format
Nested bundle format

The key schedule is intentionally simple and identical in Java and C:

Kouter=SHA256(user_passphrase)K_{\mathrm{outer}} = \mathrm{SHA256}(\mathit{user\_passphrase})
Kinner=SHA256(Koutergrace-inner)K_{\mathrm{inner}} = \mathrm{SHA256}(K_{\mathrm{outer}} \parallel \texttt{grace-inner})
key_digest=SHA256(SHA256(Kouter))\mathit{key\_digest} = \mathrm{SHA256}(\mathrm{SHA256}(K_{\mathrm{outer}}))

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:

OffsetSizeField
04magic 0x47524345, string GRCE
42version, currently 1
62flags, currently 0
816outer AES-CBC IV
24nouter 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:

FieldSizeMeaning
name length4 BUTF-8 class name byte length
namevariabledotted class name, not null-terminated
data length8 Bclass or metadata payload length
datavariableraw .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:

  1. 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.
  2. Convert the Java passphrase to UTF-8 and derive K_outer with SHA-256, using the locked buffer when available.
  3. Access the Java byte array containing the encrypted bundle.
  4. Call bundle_load, which decrypts both AES layers and parses all entries into native heap allocations.
  5. Release the Java bundle byte array and wipe K_outer.
  6. Parse __grace_guards__ when present and call grace_guard_check. This runs before protected classes are defined.
  7. Obtain the class loader associated with GraceNative for resource lookups and class definition.
  8. Call grace_verify_callsite. This runs after bundle decryption because metadata is encrypted, but before any protected class is defined.
  9. Filter metadata entries out of the parsed entry array.
  10. Call loader_define_classes to define every protected class via JNI DefineClass.
  11. Call loader_create_entrypoint, which resolves the configured entrypoint class and invokes its no-argument constructor.
  12. Free decrypted bundle entries from native memory and return the entrypoint object to Java.

Startup sequence inside the native runtime
Startup sequence inside the native runtime

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 TracerPid from /proc/self/status; macOS uses sysctl and P_TRACED; Windows uses IsDebuggerPresent and CheckRemoteDebuggerPresent. If abortOnDebugger is enabled, startup fails with SecurityException. 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 checks DYLD_INSERT_LIBRARIES. If abortOnLdPreload is enabled, startup fails. Otherwise the current implementation still warns when either environment variable is present.
  • Startup JVM agent detection. When abortOnJvmtiAgent is enabled, the runtime reads RuntimeMXBean.getInputArguments() and scans for -javaagent:, -agentpath:, and -agentlib:. This does not cover every dynamic attach path.
  • Core dump suppression. When disableCoreDumps is enabled, Linux uses prctl(PR_SET_DUMPABLE, 0) and macOS uses ptrace(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 integrityCheck is 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 executable PT_LOAD segment, 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 attempts mmap with MAP_LOCKED, then falls back to posix_memalign plus mlock; macOS uses posix_memalign plus mlock; Windows uses VirtualAlloc plus VirtualLock. 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.

Stack frame verification
Stack frame verification

Runtime verification procedure

The implementation follows this procedure:

  1. Locate the __grace_callsite__ entry. If absent, verification is skipped.
  2. Parse the metadata into an expected bootstrap name, bootstrap hash, allowed caller names, and caller hashes.
  3. Obtain Thread.currentThread().getStackTrace() from JNI.
  4. Skip frames whose class is java.lang.Thread, java.lang.Throwable, or dev.pixelib.grace.api.GraceNative.
  5. Treat the first remaining frame as the bootstrap frame and compare its class name against the expected bootstrap class name.
  6. Read the bootstrap class resource through ClassLoader.getResourceAsStream, hash the bytes with SHA-256, and compare the hash with encrypted metadata.
  7. 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.
  8. 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 DefineClass for 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.

OperationWhen paidRepeated after init
Native library extractionStartupNo
Native library loadStartupNo
Bundle readStartupNo
AES decryptionStartupNo
Guard checksStartupNo
Callsite verificationStartupNo
Class definitionStartupNo
Application method callsNormal executionNo 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.

VectorStatusMechanismResidual risk
Offline decompile from JARFixedProtected .class entries removed, stored only in encrypted bundlePublic API, bootstrap, and passthrough classes remain visible
Static ZIP extractionFixedJAR contains assets.bundle, not protected class entriesBundle bytes can be copied for offline attack
Java-level decryptor patchingMitigatedDecryption and verification live in C/JNINative code can still be patched or hooked
Direct bootstrap invocationMitigatedStack inspection checks bootstrap and caller identityAn allowed caller can still intentionally invoke startup
Bootstrap or caller replacementMitigatedRuntime SHA-256 hashes compared with encrypted metadataResource lookup can be subverted by a malicious class loader
Wrong passphraseFixedAES padding, key digest, and GRACEOK checks failWeak passphrases remain brute-forceable offline
Debugger at startupOptional abortabortOnDebugger can fail startupLater attachment can still expose process state
Startup JVM agent argsOptional abortScans for -javaagent, -agentpath, -agentlibDynamic attach remains a residual risk
Preload injectionWarn by defaultChecks LD_PRELOAD / DYLD_INSERT_LIBRARIESLegitimate allocators may require warning-only mode
Core dump exposureOptionalRequests OS suppression at startupPlatform support differs
Native segment patchingWarn by default (Linux)Hashes executable PT_LOAD segmentmacOS and Windows skip this check
Memory scrapingBest effortKey buffer locked when available, entries wiped before freePlaintext exists transiently in native heap
Modified JVMResidualA hostile runtime can record DefineClass inputsRequires 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:

ModuleResponsibility
grace.cJNI entry point and top-level startup orchestration
bundle.c/hBundle decryption, parsing, entry ownership, metadata parsing
guard.c/hStartup guard policy, memory locking, debugger, preload, agent, core dump, and self-integrity checks
crypto.c/hSHA-256, AES-256-CBC encrypt and decrypt wrappers, secure zeroing
verify.c/hStack inspection and bootstrap or caller hash verification
loader.c/hJNI 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.so
  • linux-aarch64/libgrace.so
  • osx-x86_64/libgrace.dylib
  • osx-aarch64/libgrace.dylib
  • windows-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.

Classes1 KB16 KB64 KB
13.769.1118.0
1053.1108.7112.2
100112.7119.3158.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.

ArtifactSize 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.