OZero Security
Documentation

Get Started

This guide walks you through installing OZero Security and setting up your first security protections. No prior security experience is needed — you will be up and running in about 3 minutes.

~3 minutes · Unity 2020.3 LTS+ · iOS 12.0+ / Android API 21+

Overview

OZero Security protects Unity games by running security logic in the Native C++ layer, where most hacking tools cannot reach. The SDK provides ready-to-use modules that you enable through a simple dashboard inside the Unity Editor. No manual scene setup or boilerplate code is required to get the core protection running.

What you get out of the box:
  • Build integrity check (app tampering detection)
  • Speed hack and time hack detection
  • Memory injection monitoring
  • Encrypted in-game variable types (Secure Types)
  • Encrypted save files and PlayerPrefs
Zero-wiring Auto-bootstrap & Encrypted Config
  • Enabled detectors are prepared automatically when the SDK starts — no scene setup and no boilerplate calls required.
  • Security settings are protected during the build so plaintext configuration is not exposed in normal player deployments.
  • A Native C++ runtime guard adds an additional validation layer beyond managed Unity code.
  • Threat response is centralized through SDK policy and Pro telemetry, making field investigation easier without exposing implementation details.

1 Import the Package

Open the Unity Editor and import the OZero Security package. You can import it via the Unity Package Manager or by double-clicking the .unitypackage file.

In the Import Unity Package dialog that appears, leave all items checked and click Import. All required scripts, native plugins, and editor tools will be added to your project automatically.

Import Unity Package dialog — replace with actual screenshot
After importing, Unity may need a moment to compile scripts. Wait until the progress bar in the bottom-right disappears before continuing.

2 Open the Dashboard

Once the import is complete, open the OZero Security Dashboard from the Unity menu bar:

OZero Security → Security → Config & Dashboard
Unity menu bar showing OZero Security → Security → Config & Dashboard — replace with actual screenshot

A custom Editor window will open. This is the central control panel for all OZero security modules. You do not need to touch anything in the Project hierarchy — everything is managed here.

3 Enable Modules

Inside the Dashboard, you will see a list of available security modules with toggle switches. Enable the modules you want to use. The recommended starting set is shown below.

OZero Security Dashboard showing module toggles — replace with actual screenshot

Security Presets

Choose a preset first, then adjust individual modules only when your project needs a custom policy. For most live games, start with Standard because it balances protection, performance, and false-positive risk.

Preset Recommended use Policy summary
Low Prototype, development builds, or early QA Keeps only lightweight checks active. Platform-native checks and force quit are relaxed so test environments do not get blocked too early.
Standard Recommended default for most released games Enables the core protection set, startup validation, runtime re-validation, emulator checks, and recommended IL2CPP file coverage while keeping compatibility practical.
Strict High-risk live service, PvP, or competitive builds Turns on maximum coverage and treats more failures as fatal. Use after testing your platform, signing, and store distribution flow.
Module What it does Recommended
Build Integrity Validator Detects if the app binary was modified Yes
Speed Hack Detector Catches time-manipulation cheats Yes
Injection Detector Monitors for memory hooking tools Yes
Install Source Validator Blocks pirated APKs (Android only) Optional

You do not need to press any "Auto Setup" button. Toggle the modules you want enabled in the Dashboard and save the OZeroSecurityConfig asset. At player launch, the SDK prepares enabled detectors automatically before gameplay starts.

There is no Auto Setup button — module activation is fully automatic. Toggle the modules you want, save the OZeroSecurityConfig asset, and every enabled module will be auto-created at the next launch. No scene placement or button clicks required.

License Model — Standard / Plus / Pro

OZero Security ships in three tiers. Standard is fully serverless with the shared native module. Plus stays serverless but adds app-specific Native Variant packages bound by manifest and Bundle ID. Pro includes Plus and unlocks server-side capabilities such as Cloud Telemetry, Signed Time, remote policy, attestation, and per-device limits.

The matrix below is the developer-facing summary of what the SDK actually does at runtime per tier. For the full feature comparison, see the License Modes comparison on the homepage.
Aspect Standard Plus Pro
License key — (none) OZ-PLS-XXXX ×6 OZ-PRO-XXXX ×6
Network at boot Never — fully offline No runtime server required; portal download only One POST /v1/activate per device, then cached
9 detector modules All 9 — Build / Speed / Inject / Device / Source / Physics / Memory / File / PlayerPrefs All 9 (identical local modules) All 9 (identical to Standard)
Native Variant Shared native module App-specific Variant + manifest binding Included
Cloud Telemetry Off (silent no-op) Off (serverless) On — abort events posted to /v1/telemetry
Signed Time (anti-clock-tamper) Off — WebTime uses HTTPS HEAD only Off — same as Standard On — digitally-signed /v1/time
Per-device cap Unlimited (no key, no enforcement) Project-bound license; no runtime device cap Default 5 / adjustable
Source code access Managed C# only Managed C# only Managed C# only
If you ship Standard today and later upgrade to Plus or Pro, keep the same gameplay code. Plus adds a Variant manifest/native package, while Pro additionally enables server capabilities through OZeroLicenseConfig.

Registering a Plus / Pro License Key

Standard tier needs no setup. For Plus or Pro, add one ScriptableObject asset and paste the key you received after purchase. Plus uses the key for project-bound Variant validation and portal downloads; Pro also uses it for runtime server activation.

1. Create the config asset

In the Project window, Assets → Create → OZero → License Config. Place the asset under any folder named Resources/ and rename it to exactly OZeroLicenseConfig (case sensitive — the runtime calls Resources.Load<OZeroLicenseConfig>("OZeroLicenseConfig")).

A common location is Assets/OZeroSDK/Resources/OZeroLicenseConfig.asset, but any Resources/ folder anywhere in the project works.

2. Fill in the Inspector fields

Field Required for Description
tier All Pick Standard, Plus, or Pro from the dropdown. Standard uses the shared native module. Plus requires a project-bound Variant manifest. Pro includes Plus and enables server features.
licenseKey Plus / Pro The key you received by email. Plus keys use OZ-PLS-...; Pro keys use OZ-PRO-.... Leaving this empty makes the SDK behave as Standard/serverless even if the tier field says Plus or Pro.
appIdentifier Auto The SDK sends Unity's Application.identifier during activation. The server compares this value with the bundle / package id registered for the license, so make sure the Player Settings identifier matches your issued key.
serverBaseUrl Pro Default https://api.ozero.security — leave it unless OZero Support has given you a private endpoint. Must be HTTPS in release builds. Trailing slash is trimmed automatically.
serverPublicKeyHex Pro The 32-byte digital signature public key (64 hex characters) of the OZero License Server. Used by the SDK to verify the activation token's signature (Phase 1.5 / N-1 hardening). If left empty, signature verification is silently skipped — only do that for transitional builds; production should always set this value. Get the canonical hex string from your purchase email.
tokenTtlSeconds Optional How long (in seconds) a cached activation result is trusted while offline. Default 604800 (7 days). After expiry the SDK keeps running but server-only features (telemetry / signed time) turn off until the next successful activation.
offlineProPolicyMode Pro Controls how signed Pro portal block policies are applied while the device is offline. Recommended: ApplyCachedBlockPolicies. Use RequireFreshPolicy only for online-only games that must reject stale policy. IgnoreCachedBlockPolicies is a legacy compatibility mode and is not recommended for live builds.
activationTimeoutSeconds Optional Maximum time (seconds) the SDK waits for /v1/activate before giving up and falling back to the cached entitlement. Default 6.0. The activation call never blocks scene load — this only bounds how long the background Task waits before logging a timeout.
enableLog Optional When true, license-flow events are written through OZeroSecLog (cache hit / activation success / timeout / digital signature mismatch). Default true; turn off for shipping if you want a quiet log.

3. Build & verify

No code change is required. OZeroLicenseBootstrap wires itself via [RuntimeInitializeOnLoadMethod(AfterAssembliesLoaded)] and reads the asset at app start. Watch the Player log on first launch for a line like [OZeroLicense] activated; tier=pro caps=5. If you see [OZeroLicense] Standard / serverless mode. with a Pro config in place, double-check that the asset is under a Resources/ folder and named OZeroLicenseConfig (no extension, no rename suffix).

The license key is not a secret — losing it just lets someone else burn through your per-device cap, which the server enforces. So the asset travels with the build (it is not encrypted, by design). Treat the key like a product code, not a password.

Server Activation Flow

The activation flow runs in the background at app start. The SDK never blocks scene load on it — even on a 6-second timeout the cached entitlement (or Standard fallback) is used so your players never sit on a black screen waiting for HTTP.

Boot sequence

  1. The SDK initializes the license runtime automatically at startup.
  2. Standard and Plus continue without a runtime server call.
  3. Pro sends a lightweight activation request in the background.
  4. If activation succeeds, Pro server features such as telemetry, signed time, and attestation become available.
  5. If activation fails, expires, or times out, gameplay continues in Standard mode without showing an error to players.

What goes over the wire

The activation request is a small JSON POST. Required fields are licenseKey, deviceId, sdkVersion, and platform. appIdentifier, companyName, productName, and webglOrigin are added only when Unity provides non-empty values. You can override the device id source via OZeroLicenseRuntime.DeviceIdProvider.

POST https://api.ozero.security/v1/activate
Content-Type: application/json

{
  "licenseKey":    "OZ-PRO-XXXX-XXXX-XXXX-XXXX-XXXX-XXXX",
  "deviceId":      "<DeviceIdProvider result; default SystemInfo.deviceUniqueIdentifier>",
  "sdkVersion":    "<OZeroSdkVersion.ManagedVersion>",
  "platform":      "<android | ios | windows | macos | linux | webgl | ...>",
  "appIdentifier": "<Application.identifier>",
  "companyName":   "<Application.companyName>",
  "productName":   "<Application.productName>",
  "webglOrigin":   "<WebGL origin only>"
}

The server replies with a JSON body containing a JWT-shaped signedToken (header.payload.signature, base64url, no padding), the resolved tier, a capabilities array, a serverFeaturesEnabled flag, and (optionally) an expiresAt unix-ms. The SDK calls OZerodigital signature.VerifySignature on the signedToken using your serverPublicKeyHex and only then accepts the entitlement — anything else is fail-closed.

Graceful degradation

Server maintenance, network timeouts, license expiry, suspension, revocation, and bundle mismatch are not treated as tamper. They downgrade Pro-only features to Standard/serverless mode so the player session stays alive. Confirmed tamper states such as build mismatch, blocked build, injection, and debugger detection follow the configured threat-response policy.

If you want to know whether the live activation succeeded (rather than just trusting cache), await OZeroLicenseRuntime.Initialize(); from your own bootstrap and inspect OZeroLicenseRuntime.Entitlement?.cachedAtMillis. A value within the last few seconds means the server answered fresh; an older value means you are running on cache.

Offline Behaviour

OZero is designed so your players can always launch your game, regardless of network state. The exact behaviour depends on the tier:

Standard — always offline

No server is contacted, ever. All nine detector modules run with no degradation. The license runtime sets IsServerless = true and HasEntitlement = false. OZeroTelemetryReporter attaches but no-ops on every event.

Pro — offline within TTL

After the first successful /v1/activate on a given device, the entitlement is cached in PlayerPrefs (encrypted with a device-bound key — see Cache Security below). On subsequent launches the cache is read synchronously before the network call even starts, so if the player is offline the SDK still knows what tier they are on and which capabilities are unlocked.

The cached entitlement is trusted for tokenTtlSeconds from the cachedAtMillis stamp written at activation time. Default is 7 days (604800). After TTL expiry on a device that has been offline the whole time, the cache is dropped and the SDK degrades to Standard capabilities until the next successful activation. Detectors keep working — only telemetry posting and signed-time validation turn off.

Pro — signed offline block policy

Graceful degradation is for license, network, and server availability failures. It does not mean that an explicitly blocked build becomes allowed offline. When Pro activation succeeds, the server signs the current portal block policy into the activation token and the SDK caches that policy separately from the entitlement.

With the recommended ApplyCachedBlockPolicies mode, blocked manifest hashes, SDK versions, and app versions are still rejected by Build Integrity while the signed policy is valid, even if the player turns on airplane mode. The policy follows the signed token TTL, so run one successful online activation after changing portal block rules.

A first launch that has never received a Pro activation cannot know portal block rules while fully offline. For high-value live games, keep server-side session/OZA validation on important gameplay or economy flows as the final gate.
State Detectors Telemetry Signed Time
Online, fresh activation All 9 on On On
Offline, cache < TTL All 9 on Queued (delivered next online) Falls back to WebTime HTTPS HEAD
Offline, cache > TTL All 9 on Off (degraded) Off (degraded)
No cache (first launch + offline) All 9 on Off until first online launch Off until first online launch
Telemetry events fired while offline are not buffered to disk — they are dropped with a single warn-level log. This is intentional: a buffered queue would itself be a tampering surface. The next successful detection while online posts normally.

Troubleshooting

Every license event is logged through OZeroSecLog, which routes to Unity's Debug.Log by default (configurable in OZeroSecurityConfig). Filter the Player log by the [OZeroLicense] prefix to see the boot-time decisions; [OZeroTelemetry] for the reporter. Common symptoms and fixes:

Symptom (log line) Likely cause What to check
[OZeroLicense] Standard / serverless mode. Asset missing or tier=Standard or empty key Confirm OZeroLicenseConfig.asset is under a Resources/ folder, named exactly OZeroLicenseConfig, with tier=Pro and a non-empty licenseKey.
/v1/activate failed: 401: invalid_key Key typo or wrong tier prefix Re-paste the key from the original purchase email. The format is case-sensitive, dashes are required, the prefix OZ-PRO- must match the asset's tier dropdown.
/v1/activate failed: 403: bundle_mismatch appIdentifier mismatch The server-side license record binds the key to a specific bundle / package id. Ensure Application.identifier in Player Settings matches the id registered for the license.
activation token signature verification failed Wrong serverPublicKeyHex or MITM Compare the hex string in the asset against the canonical pubkey from your purchase email — character for character, all 64 hex chars. If correct and still failing, suspect a corporate proxy/MITM appliance and try a direct mobile-data connection.
/v1/activate timed out Network slow / firewall SDK falls back to cache automatically — only an issue on first-launch / no-cache. Confirm the device can reach https://api.ozero.security/health; raise activationTimeoutSeconds if you operate in high-latency regions.
cached entitlement past TTL; clearing. Player offline > TTL Expected. Detectors keep working; telemetry / signed-time turn off until next online launch. Raise tokenTtlSeconds if your audience routinely runs offline for > 7 days.
cache hit; tier=pro on a different machine than expected Device fingerprint changed If SystemInfo.deviceUniqueIdentifier rotates (some Android ROMs / OS reset), the v2 cache becomes unreadable and the SDK simply re-activates. No user action required — the next online launch heals it.
Never ship a build with enableLog = true if you are concerned about leaking which detectors fired in your shipping log files. The license-flow logs reveal tier / capability state, which is fine to expose, but pair-disable the global OZeroSecurityConfig.enableLog as well so detector internals stay quiet.

Project Settings

Before configuring individual security modules, check the Unity Player Settings that affect native plugins, store builds, and platform validation.

Minimum Build Targets

Platform Minimum target Notes
iOS 12.0+ For iOS builds, set Project Settings > Player > iOS > Target minimum iOS Version to 12.0 or later. OZero does not overwrite this PlayerSettings value; keep it aligned with your app support policy.
Android API 21+ For Android builds, set Project Settings > Player > Android > Minimum API Level to Android 5.0 'Lollipop' (API level 21) or later. Use IL2CPP and ARM64 for store release builds.

Android ProGuard / R8 Settings

OZero Security does not require a separate Java SDK package, but Android release builds that enable Minify, ProGuard, or R8 must preserve Unity's Java bridge and any custom Android bridge classes used by your project. This keeps JNI calls for package information, installer source, APK signing certificate checks, and boot asset loading stable after obfuscation.

In Unity, open Project Settings > Player > Android > Publishing Settings. If Minify Release is enabled, also enable a custom ProGuard file and add the rules below to proguard-user.txt. If Minify is disabled, no extra ProGuard setup is required.
# OZero Security - Unity Android ProGuard/R8 keep rules
-keep class com.unity3d.player.UnityPlayer { *; }
-keep class com.unity3d.player.UnityPlayerActivity { *; }
-keep class com.unity3d.player.UnityPlayerGameActivity { *; }
-keep class com.unity3d.player.UnityPlayerForActivityOrService { *; }
-keepattributes *Annotation*,InnerClasses,EnclosingMethod,Signature

# If your game adds custom Java/Kotlin bridge classes that OZero or your code
# calls through AndroidJavaClass / AndroidJavaObject, keep those classes too.
# Replace the package below with your own bridge package.
# -keep class com.yourcompany.yourgame.bridge.** { *; }

5 Configure OZeroSecurityConfig

The OZeroSecurityConfig ScriptableObject asset is included when you import the package. Select it in the Project window to inspect and adjust all security module settings. The config is a singleton — access it at runtime via OZeroSecurityConfig.Instance.

OZeroSecurityConfig Inspector panel — replace with actual screenshot

General Settings

These top-level fields apply globally across all modules.

Field Type Default Description
developerSecret string A unique passphrase used as the key-derivation function input for OZeroSV_File and OZeroSafePlayerPrefs encryption. Set a unique value before release and never change it after launch — doing so will make existing encrypted data unreadable.
enableLog bool false Enables internal debug logging from OZero Security. Disable in production builds to avoid leaking detection logic details.
enableFailureDiagnostics bool false Writes local security failure diagnostic files to Application.persistentDataPath. Enable only for QA or customer support sessions because files can include module names, hashes, device state, installer package names, and runtime configuration.
Important: Set a unique developerSecret before your first release. Do not use the default value and do not change it after launch.

Response Settings

Controls what happens when any security module fires a detection event.

Field Type Default Description
forceQuitOnDetection bool true Automatically force-quits the application when a confirmed threat is detected. In Standard and Strict presets this is enabled so the internal fallback receiver can enforce the response even if a managed callback is patched or missing. Disable only for controlled QA flows where you intentionally want to observe detections without terminating the app.

Build Integrity Settings

Configures the build integrity validator that detects modified game files, debugger attachment, and suspicious runtime environments.

Field Type Default Description
Activate Build Integrity checkbox On The Dashboard activation checkbox. Internally this maps to useIntegrity, but users should enable or disable the module with the Activate checkbox.
validateOnStartupbooltrueRuns the integrity check immediately when the game launches.
validateInEditorboolfalseAlso validates in the Unity Editor for testing. Keep this off during ordinary development unless you are intentionally testing integrity behavior.
enablePeriodicValidationbooltrueRepeats integrity checks while the game is running. Disable only when startup-only validation is enough for your build.
periodicCheckIntervalfloat300 sBase interval in seconds for periodic validation. Set to 0 or less to disable periodic checks.
periodicCheckJitterPercentfloat35%Randomizes the periodic interval so attackers cannot predict exact validation timing. For example, 300 seconds with 35% jitter runs roughly between 195 and 405 seconds.
timingAnomalyConsecutiveRequiredint7How many timing-only anomalies must accumulate before debugger timing drift is treated as a violation. Strong debugger signals can still fail immediately.
timingAnomalyWindowSecondsfloat900 sTime window used to accumulate timing-only anomalies. A longer window is more tolerant; Strict uses a shorter window.
timingAnomalyFrameHitchSuppressionSecondsfloat20 sTemporarily suppresses timing-only debugger checks after a large frame hitch, such as scene loading, shader compilation, GC, or OS scheduling stalls.
checkAssemblyHashbooltrueVerifies managed assembly hashes against the manifest generated at build time.
checkDebuggerbooltrueDetects debugger attachment and debugger-like runtime timing behavior using native platform checks.
failOnDebugBuildboolfalseTreats a Unity Debug build as a violation. Enable this for release-only validation flows after QA is complete.
checkPlatformNativebooltrueRuns native platform checks such as emulator, debugger, virtualization, signing, proxy, or tool-process signals depending on platform.
failIfManifestMissingbooltrueTriggers a violation if OZeroIntegrityManifest.ozero is missing.
failIfAssemblyHashBlobMissingbooltrueTriggers a violation if the assembly hash blob inside the manifest is missing.
requireCodeSignature (Windows)boolfalseRequires the Windows main executable to be code-signed.
blockVirtualMachine (Windows)boolfalseBlocks common VM environments such as VirtualBox, VMware, or QEMU. Hyper-V has a separate switch.
blockHyperV (Windows)boolfalseBlocks Hyper-V VMBus signals. Use carefully because it can also block WSL2, Docker Desktop, and Windows Sandbox users.
blockNetworkProxies (Windows)boolfalseDetects network proxy and packet inspection tools such as Wireshark, Fiddler, Charles, and HTTPDebugger. Enable for controlled competitive builds.
blockReverseEngineeringTools (Windows)boolfalseDetects reverse-engineering tools such as ILSpy, dotPeek, Ghidra, WinDbg, and IDA Pro.
blockSystemMonitorTools (Windows)boolfalseDetects system monitoring tools such as Process Explorer, Process Monitor, and Process Hacker. Consider false-positive risk for power users.
il2cppHashGameAssemblybooltrueHashes GameAssembly.dll on Windows IL2CPP builds. This is the minimum recommended IL2CPP file guard.
il2cppHashGlobalGameManagersbooltrueAdds globalgamemanagers to IL2CPP manifest coverage.
il2cppHashSharedAssetsbooltrueAdds sharedassets* files to IL2CPP coverage.
il2cppHashSceneFilesbooltrueAdds Unity scene files such as level* to IL2CPP coverage.
il2cppHashResourcesAssetsboolfalseAdds resources.assets to IL2CPP coverage. Useful in Strict mode, but test patch workflows first.
il2cppAdditionalWatchedFilesList<string>Additional Windows IL2CPP output files to include in manifest hashing when your project has custom native payloads.
blockEmulator (Android)booltrueAndroid only. Treats emulator or unsupported runtime signals as integrity violations.
blockSystemRwMount (Android)booltrueAndroid only. Treats writable system partitions or root-like mount state as an integrity violation.
androidShaKeysList<string>Allowed SHA-256 fingerprints for APK/AAB signing certificates. Leave empty to skip this check; include both debug and release keys if development builds must pass.
expectedBundleIds (iOS)List<string>Allowed iOS bundle identifiers. Leave empty to skip the bundle ID check.
excludedAssembliesList<string>Assembly names without .dll to exclude from manifest generation and runtime hash verification. Unity/Mono framework assemblies are excluded automatically.
checkIntegrityWithServer (Pro)boolfalseEnables Pro server attestation. The client requests a nonce, submits integrity evidence, and receives a signed OZA token that your game server can validate.
enableManagedVerification (Pro)boolfalseLets OZero validate the issued OZA token and return an allow/warn/block verdict plus a short managed session for teams without their own backend. The SDK refreshes before that managed session expires. Requires checkIntegrityWithServer.
requireManagedVerification (Pro)boolfalseWhen enabled, a managed-verification network failure is treated as a validation failure. Keep this off unless your launch flow can tolerate strict online gating.
attestRefreshLeadSeconds (Pro)float300 sHow early before OZA token expiry the client should attempt refresh. Set to 0 to disable pre-expiry refresh.
attestRefreshCooldownSeconds (Pro)float30 sMinimum cooldown between attestation refresh attempts.
manifestSigningPublicKeystringBase64 RSA-2048 public key used to verify the signed integrity manifest. Generate it from the OZero key-pair tool.
requireManifestSignatureboolfalseTreats a missing or invalid manifest signature as an integrity violation. Enable for release builds after generating a key pair.
manifestSigningPrivateKeyPathstringOZeroSigningKeysEditor-only path to the private key PEM used at build time. This path is not included in player builds.
Pro Strict Attestation can be enabled from the OZero admin/customer policy side. In that mode the nonce is bound to the submitted manifest hash, platform, SDK version, and app identity, and the server requires a registered build version plus strong integrity evidence before issuing an OZA token.

Events

Event Description
OnValidationPassed Fires when local integrity checks complete successfully. Pro server attestation may still be pending.
OnAttestationPassed Fires only after the Pro OZA attestation token is issued. Use this, or AttestationToken.IsValid(nowMillis), before game-server login, PvP, ranking, or currency flows.
OnValidationFailed Fires when the integrity check detects a violation. Also triggers the global onHackDetected event with ModulationType.BuildIntegrity.

Generate RSA Signing Keys (Build Integrity)

If you have enabled Build Integrity with Require Manifest Signature turned on, OZero uses public-key signature asymmetric signing to guarantee that the integrity manifest has not been replaced or tampered with. Before making any release build you must generate a key pair from inside the Unity Editor.

How manifest signing works

At build time, OZero signs the assembly manifest with your private key and embeds the resulting signature in the build. At runtime, the public key stored in OZeroSecurityConfig verifies that signature. If the signature does not match — meaning the manifest or the binary was altered — the game treats it as tampering and responds according to your Response Settings.

Build Integrity Manifest Signing UI

Generating the key pair

  1. In the Unity Editor, select OZeroSecurityConfig in the Project window.
  2. In the Inspector, expand the Build Integrity section.
  3. Enable the Require Manifest Signature checkbox.
  4. Click the Generate Key Pair button.
  5. OZero generates an public-key signature key pair. The public key is written directly into OZeroSecurityConfig (stored in your Assets). The private key is saved to:
    [ProjectRoot]/OZeroSigningKeys/manifest_private_key.pem
  6. A confirmation dialog shows the private key location. Click OK to dismiss it.
⚠ Critical — private key safety
  • The private key file is stored outside the Assets/ folder so Unity does not include it in builds. Never move it inside Assets/.
  • Add OZeroSigningKeys/ to your .gitignore immediately. Committing the private key to version control is a critical security risk.
  • Back up the private key to a secure, offline location (encrypted USB drive, password manager, etc.). Anyone with this file can forge a valid manifest for your game.
  • If you lose the private key, you must generate a new key pair. The new public key will be embedded in the next build. All previously shipped builds that carry the old signature will fail manifest verification and must be replaced.

Custom private key path (CI/CD & teams)

The Manifest Signing Private Key Path field (visible in the Inspector under Manifest Signing — Editor Only) lets you specify an alternative location for the private key. This is useful in two scenarios:

  • CI/CD pipelines — Store the private key as a CI secret and inject its path at build time so the build machine never needs the key on disk permanently.
  • Team environments — Keep the key on a shared secure server or secrets manager and point the field at the mounted path. Only the team member running the release build needs access.

Validating an existing key pair

If you are unsure whether the private key on disk still matches the public key stored in OZeroSecurityConfig, click the Validate Key Pair button. OZero will sign a small test payload with the private key and verify it with the public key. If they match, a success notice appears. If not, you must generate a new pair.

When to re-generate the key pair
  • Private key is lost or compromised.
  • Validate Key Pair reports a mismatch (keys are out of sync).
  • Deliberately rotating keys as part of a scheduled security policy.

After re-generating, the new public key is embedded in the next build automatically. Any existing shipped builds will fail manifest verification with the new public key — you will need to push an update.

Dual-fingerprint key rotation (zero-downtime)

The Generate Key Pair button stores the public key in two places at once: inside OZeroSecurityConfig (a ScriptableObject in your Assets), AND as a SHA-256 fingerprint constant inside Assets/OZeroSDK/Scripts/Security/BuildIntegirity/OZeroManifestTrustAnchor.cs. At runtime the SDK first checks that the public key in the asset matches the pinned fingerprint in the assembly — if they don't match, the manifest is rejected even before the RSA signature is checked. This closes the repacker attack where someone swaps both the asset and the .sig file with attacker-owned keys.

Two fingerprint slots

Inside OZeroManifestTrustAnchor.cs there are two const slots: ExpectedPublicKeyFingerprintHex (the current production fingerprint) and PreviousPublicKeyFingerprintHex (an optional second fingerprint, empty in steady state). The Matches() verifier returns true if the live SPKI hash equals either value, using a constant-time XOR compare. This dual-slot model is the foundation of the "zero-downtime key rotation" feature on the homepage: a freshly built SDK can trust BOTH the new key (Expected) AND the old key (Previous) for a bounded grace window, so old shipped builds keep verifying their old manifests while you push the new release.

Rotation procedure (manual, but takes 5 minutes)

  1. Note the current fingerprint. Open Assets/OZeroSDK/Scripts/Security/BuildIntegirity/OZeroManifestTrustAnchor.cs and copy the value of ExpectedPublicKeyFingerprintHex into your clipboard (or a scratch file).
  2. Move it into the previous slot. In the same file, paste the value into PreviousPublicKeyFingerprintHex (which is normally ""). Save.
  3. Generate the new key pair. In the Inspector for OZeroSecurityConfig, click Generate Key Pair. The dialog will warn that this invalidates previously baked manifests — click Regenerate. The new private key is written to the PEM file, the new public key replaces the asset value, and the new SHA-256 fingerprint is auto-injected into ExpectedPublicKeyFingerprintHex. Your PreviousPublicKeyFingerprintHex from step 2 is left untouched.
  4. Build and ship the new SDK release through your normal patch / store channel. New manifests in this build are signed with the new private key.
  5. Wait for fleet rollover. During the grace window the new build trusts both kids; the old build (still pinning only the old fingerprint as Expected) keeps verifying its own old manifest. Neither path breaks. Typical window: 1–4 weeks for a live game.
  6. Clear the previous slot. Once telemetry confirms the old build is at 0%, set PreviousPublicKeyFingerprintHex = "" and ship one more SDK release. Rotation complete.
Why "no forced update" works

The old shipped binary already contains an old manifest signed by the old key, AND an embedded copy of the old fingerprint as its Expected slot — it does not need to know anything about the new key to keep working. The new build needs the Previous slot only so that, if a player force-reinstalls or rolls back to an old version mid-window, the cached old manifest still verifies. Once the old build is gone from the field, you can clear Previous in the next release.

Troubleshooting

All five symptoms below come from the same defence stack — the build-time PreBuild validator (OZeroBuildPreflightValidator), the boot-time anchor guard (OZeroManifestTrustAnchor.ValidateConfigurationOrFail), and the runtime verifier (OZeroBuildIntegrityValidator.VerifyManifestSignature). The fix in each case is to bring the three artefacts (private key PEM, public key in OZeroSecurityConfig, fingerprint in OZeroManifestTrustAnchor) back into sync.

Symptom 1 — release build aborts immediately on launch with reason code 0x0E (TRUST_ANCHOR_MISSING)

ExpectedPublicKeyFingerprintHex is an empty string. Editor and Development builds tolerate this with a warning, but release builds fail-fast at boot before any untrusted manifest can be accepted. Fix: run Generate Key Pair once in the Editor — it auto-injects the fingerprint into the const. If the const is already populated but the build still aborts, the assembly hash blob (oz_ahash.bin) may be out of date — rebuild from a clean state.

Symptom 2 — build halts with "no manifest signing public key is configured" before compilation starts

The PreBuild validator (callbackOrder 51) caught that OZeroSecurityConfig.Integrity.ManifestSigningPublicKey is empty. Fix: open OZeroSecurityConfig in the Inspector → expand Build Integrity → click Generate Key Pair, then rebuild. Note: this check is skipped on Android / iOS (OS-level signing) and on IL2CPP Standalone (no per-DLL files to sign), so a missing key only blocks Mono Standalone targets.

Symptom 3 — runtime log: "Manifest signing public key does NOT match the pinned trust anchor fingerprint — APK appears to have been repacked with attacker-controlled keys"

The public key string in OZeroSecurityConfig hashes to a value that matches neither ExpectedPublicKeyFingerprintHex nor PreviousPublicKeyFingerprintHex. In a legitimately built binary this happens after a manual edit got the two artefacts out of sync. Fix: in the Editor, run Tools → OZero → Validate Manifest Signing Keys — if the key pair is consistent the tool will auto-rewrite the const for you. If it reports a mismatch, regenerate the pair via Generate Key Pair. In a tampered binary, this message is the intended fail-closed signal — investigate whether the APK / .exe was repacked.

Symptom 4 — after rotating, players on the OLD shipped build start failing manifest verification

You forgot step 2 of the rotation procedure. The new SDK release shipped with PreviousPublicKeyFingerprintHex = "", so a player who already had the old build cached and force-restarts hits a manifest signed under the new key (which their old build doesn't trust) — or, worse, a force-reinstall pulls down a freshly cached old manifest that the trust anchor in their old build can't verify because the PEM has rotated. Fix: ship a hotfix SDK release with PreviousPublicKeyFingerprintHex set to the old fingerprint; the next manifest fetch will verify under either kid.

Symptom 5 — local builds work, but CI builds fail with "private key not found" or sign with the wrong key

CI machines do not have OZeroSigningKeys/manifest_private_key.pem on disk because it is correctly excluded by .gitignore. Fix: store the PEM as a CI secret (GitHub Actions Secret, GitLab Variable, Jenkins Credentials, etc.) and have the pipeline write it to OZeroSigningKeys/manifest_private_key.pem in the workspace before the Unity build step. Alternatively, set Manifest Signing Private Key Path in OZeroSecurityConfig to a path the CI runner can mount (e.g. a secrets-manager mount). Make sure every CI runner uses the SAME PEM — different PEMs produce different signatures even when the public key matches.

Install Source Settings

Restricts the game to only run when installed from authorized stores or sources. Useful for preventing sideloaded or repackaged APKs.

Field Type Default Description
Activate Install Source bool true The Dashboard activation checkbox for the Install Source module. Internally this maps to useInstallSource.
allowGooglePlayStore bool true Permits installs from the Google Play Store.
allowSamsungGalaxyStore bool false Permits installs from the Samsung Galaxy Store.
allowAmazonAppstore bool false Permits installs from the Amazon Appstore.
allowHuaweiAppGallery bool false Permits installs from the Huawei AppGallery.
allowOneStore bool false Permits installs from ONE Store (Korea).
allowXiaomiGetApps bool false Permits installs from Xiaomi GetApps.
allowOppoAppMarket bool false Permits installs from the OPPO App Market.
allowVivoAppStore bool false Permits installs from the Vivo App Store.
allowADB bool false Permits installs via ADB (Android Debug Bridge). Enable only for internal testing.
allowDetectionFailedboolfalseAllows the game to continue when Android installer-package detection fails because the OS or JNI query was unavailable. Keep disabled in release builds.
allowUnknownSourcesboolfalseAllows installer packages that are not in the built-in or custom allowlist. Use only for regional stores after testing.
enableServerSync (Pro)boolfalseSends the detected installer package to the OZero server for allowlist validation and audit. Pro only.
customAuthorizedPackages List<string> Additional authorized installer package names (e.g. com.yourcompany.launcher).
reportViolationToCallback bool true Routes unauthorized install-source detections to the SDK response callback/security manager path.
logRawInstallerPackage bool true Logs the raw installer package name to the console. Useful during development to identify the correct package name for a custom store.

Steam Anti-Piracy Settings

Checks Steam launch path, App ID, entitlement state, and release hygiene for PC builds distributed through Steam. Standard performs local validation; stronger ownership verification through Steam server evidence is provided by Pro server Steam Attestation.

Field Type Default Description
Activate Steam Anti-PiracycheckboxOffActivation checkbox for Steam Anti-Piracy. It is off by default because Steam App ID and distribution flow are project-specific. First verify the status in observe/QA mode, then tighten the release-build policy.
expectedSteamAppIdint0Your project’s Steam App ID. If you used development AppID 480 for local checks, replace it with the real App ID before release.
requireSteamLaunchbooltrueChecks whether the game was started through Steam rather than direct executable launch. Keep development direct-run tests separate from release policy.
requireSteamApiInitbooltrueChecks whether Steam API initialization succeeds. Local development launches can fail depending on Steamworks setup, so final judgement should be verified through the actual Steam distribution path.
requireSubscribedCurrentAppbooltrueChecks whether the current Steam account owns or is entitled to the app. This is local validation; server-side ownership verification is handled by Pro.
requiredDlcAppIdsList<int>Optional DLC App IDs that must be owned for protected DLC flows.
blockSteamAppIdTxtInReleasebooltrueFails release builds that still contain a development steam_appid.txt file. This file is only for local development checks and should not be distributed.
validateSteamApiDllHashboolfalseOptionally validates the Steam API DLL hash against known-good SHA-256 values.
allowFamilySharing / allowFreeWeekend / allowTimedTrialbooltrueControls whether Steam family sharing, free weekend, and timed trial entitlement states are accepted.
enableServerSteamAttestation (Pro)boolfalsePro only. Submits Steam entitlement evidence to OZero server for stronger server-side ownership verification than local client results.

Device Binding Settings

Binds a game account to the original device using a hardware fingerprint. Detects account transfers to different hardware.

Commercial operations purpose

Device Binding is most useful as a Pro operations layer: it connects a license to server-registered hardware fingerprints so your team can isolate risky devices, reduce casual license sharing, and support legitimate device changes without turning the entire license off.

Block repeat-risk devices

Use after reviewing Security Events such as SpeedHack, Injection, Install Source, or Build Integrity evidence. A blocked device receives DEVICE_BLOCKED on the next activation, registration, verify, or policy check.

Reset legitimate users

Use Reset Token for phone replacement, OS reinstall, hardware replacement, or a normal fingerprint mismatch. The token is 5-minute, single-use, and bound to the exact license and device.

Control device slots

For Pro licenses, maxDevices limits how many active server fingerprints can register. Blocked fingerprints do not count as reusable trust; use reset or deletion only after support verification.

Reset Token support flow

  1. Confirm the player account and the reason for reset through your normal support process.
  2. Open Customer Portal → Device Binding, find the target device, and issue a Reset Token.
  3. Send the token only through an authenticated support channel. Do not store it in long-lived public chat or screenshots.
  4. Your game or support UI should pass the token to OZeroDeviceBindingDetector.Instance?.ClearStoredFingerprint(token.Trim()).
  5. After the SDK consumes the token, restart or re-enter the protected startup flow so the current hardware fingerprint is registered again.
Do not market Device Binding as a perfect hardware identity. Platform identifiers can change after OS resets, privacy changes, or hardware replacement. Treat it as a cost-raising and operations-control layer, and combine it with Build Integrity, Injection, SpeedHack, Install Source, and server validation for high-value live games.
Field Type Default Description
Activate Device Binding bool true The Dashboard activation checkbox for the Device Binding module. Internally this maps to useDeviceBinding.
hardwareChangeTolerance int (0–3) 1 How many hardware component changes are tolerated before triggering a violation. 0 = strict (no changes), 3 = lenient (major hardware swap). A value of 1 accommodates minor OS or firmware updates.
storageKey string "ozero_dfp" The PlayerPrefs key under which the encrypted device fingerprint is stored. Change only if this key conflicts with an existing key in your game.
enableServerSync bool false Syncs the device fingerprint with your backend server for server-side validation.
maxDevices (Pro)int0Documents the intended server-side device limit. The authoritative limit is enforced by the server license record, so changing this local value alone does not raise the production limit.

Speed & Time Hack Settings

Detects time-scale manipulation (speed hacks) by comparing Unity Time.realtimeSinceStartup against native platform timers and optionally a trusted web time source.

Field Type Default Description
Activate Speed & Time Hack bool true The Dashboard activation checkbox for Speed & Time Hack detection. Internally this maps to useSpeedHack.
autoStart bool true Starts detection automatically on game launch. Disable to start manually via OZeroSpeedHackDetector.StartDetection().
checkInterval float 1.0 s How often (in seconds) the detector samples and compares timers.
requiredDetections int 3 Number of consecutive anomalous samples required before a violation is triggered. Increase to reduce false positives on unstable devices.
ratioTolerance float 0.15 Allowed deviation (±15%) between Unity time and native timer before a sample is considered anomalous.
maxAllowedRatio float 4.0 Maximum allowed time ratio. A ratio above this value is immediately flagged as a speed hack, regardless of requiredDetections.
detectSlowHack bool true Also detects tools that slow down time (ratio below 1.0 minus tolerance), not just speed-ups.
enableTimeScaleDetection bool true Detects abnormal Time.timeScale values set programmatically (e.g. by memory editors).
hackDetectMultiplier float 1.3 A sample exceeding maxAllowedRatio × hackDetectMultiplier is counted as a detection even before requiredDetections is reached.
enableThreadTimerCheck bool true Uses a background thread timer as an additional reference clock, making it harder for hacks that only affect the main thread.
useWebTimeValidation bool false Periodically fetches the current time from webTimeUrl and compares it to the device clock to detect device-level time manipulation.
webTimeUrl string URL of a trusted time API endpoint (must return a Unix timestamp or RFC 2616 Date header). Required when useWebTimeValidation is enabled.
webSyncInterval float 15 s How often (in seconds) to sync with the web time server.
webRatioTolerance float 0.15 Allowed deviation (±15%) between device time and web server time.
timeOffsetTolerance float 60 s Maximum absolute clock offset (in seconds) allowed between device and web server before triggering a violation.
focusIgnoreTime float 4 s Seconds to ignore after the app regains focus (e.g. returning from multitasking). Prevents false positives when the OS freezes the app.
loadingGraceTime float 6 s Grace period (in seconds) at startup during which detection results are ignored to avoid false positives during scene loading.
lagSpikeIgnore float 0.5 s Samples where the frame delta exceeds this value are discarded (considered a genuine lag spike, not a time manipulation).
buildFailIfTimeScaleTamperedbooltrueBuild-time guard that fails validation when protected code appears to alter Time.timeScale outside the configured policy.
timeScaleTamperExemptionsList<string>Class or method patterns that are allowed to change Time.timeScale intentionally, such as pause or bullet-time systems.
webTimeUrlsstring[]Optional multiple trusted time endpoints. The detector can require more than one successful endpoint before trusting web time.
minSuccessfulEndpointsint2Minimum number of web-time endpoints that must respond successfully when multiple endpoints are configured.
maxConsecutiveFailuresint6Number of consecutive web-time failures tolerated before applying the unavailable policy.
onWebTimeUnavailableenumWarnOnlyPolicy used when trusted web time cannot be reached. Choose a stricter mode only after testing network conditions.
detectTimeHackbooltrueEnables device-clock and web-time manipulation checks in addition to speed ratio checks.
webSyncJitterPercentfloat20%Randomizes web-time sync intervals to avoid predictable polling.
sustainedLagThresholdfloat0.15Threshold for treating repeated slow frames as sustained lag rather than cheat evidence.
sustainedLagGraceDurationfloat3 sGrace duration applied while sustained lag is being classified.
overloadStrictMultiplierint3Multiplier used to make overload handling stricter after repeated abnormal timing conditions.
enableRemoteSpeedHackConfig (Pro)boolfalseAllows Pro server policy to override selected Speed & Time Hack thresholds without changing client code.
remoteSpeedHackConfigIntervalfloat300 sHow often remote Speed & Time Hack configuration is refreshed.
enableSignedServerTime (Pro)boolfalseUses signed server time when available so time validation cannot be spoofed with an unsigned response.

Physics Hack Detector

Component-based — not in OZeroSecurityConfig.
OZeroPhysicsHackDetector is designed for multiplayer/MMORPG games where each player object needs independent detection thresholds. Attach the component directly to your player prefab and configure the settings per object in the Inspector. Call Initialize(playerId) in your spawn logic to activate detection.
Field Type Default Description
maxAllowedSpeed float 15 u/s Maximum allowed movement speed (Unity units per second). Tune to match your game's maximum legitimate player speed.
distanceTolerance float 2.0 u Extra distance allowance to compensate for network latency or physics inaccuracies.
obstacleLayer LayerMask Layer mask for walls and obstacles. Used to detect wall-clipping via linecast.
checkInterval float 0.05 s How often (in seconds) the physics validator samples player position and velocity.
maxDeltaTimeCap float 0.1 s Caps delta time for distance calculations to prevent lag spikes from masking teleport detection.

Injection & Hooking Settings

Detects unauthorized code injection such as assembly injection, DLL hijacking, or IL2CPP patching.

Field Type Default Description
Activate Injection & HookingcheckboxOnThe Dashboard activation checkbox for Injection & Hooking detection. Internally this maps to useInjection.
enableServerWhitelist (Pro)boolfalseDownloads trusted native module policy from the server, so approved overlays or partner modules can be allowed centrally.
serverWhitelistRefreshIntervalfloat0 sRefresh interval for server-managed whitelist policy. 0 means refresh only at startup or policy bootstrap.
requireSignerForNativeWhitelistboolfalseRequires native whitelist entries to include signer identity, not only file hash. Stronger, but needs clean module signer data.
enableRemoteInjectionConfig (Pro)booltrueAllows Pro server policy to tune Injection thresholds and probes without client code changes.
remoteInjectionConfigIntervalfloat300 sHow often remote Injection policy is refreshed.
enableWindowsModuleIdentityScanbooltrueScans loaded Windows native modules and compares hash/signer identity against trusted policy.
scanIntervalSecondsfloat1 sBase runtime scan interval for injection probes.
windowsModuleIdentityScanIntervalSecondsfloat5 sSeparate interval for the heavier Windows module identity scan.
scanJitterPercentfloat20%Adds random jitter to scan timing so probes are harder to predict.
enableExecutablePrivateMemoryScanbooltrueDetects executable private memory regions that are commonly used by injected shellcode or runtime patchers.
enableInlineHookScanboolfalseDetects suspicious inline patches at known code entry points. Strict mode enables this after compatibility testing.
enableThreadStartAddressScanbooltrueChecks thread start addresses for suspicious locations outside trusted modules.
enableExternalProcessHandleScanbooltrueDetects external processes that hold suspicious handles to the game process.
detectionConfidenceThresholdint70Minimum confidence score required before the detector treats combined injection signals as a violation.
enableWebRuntimeTamperScan (WebGL)booltrueWebGL-only lightweight browser tamper checks. This is not native C++ protection; treat it as limited client-side signal collection.
WebGL probesboolstrueControls DevTools, clock hook, network hook, WASM hook, storage hook, and crypto hook checks for WebGL builds.

Encrypt PlayerPrefs

To protect data stored in Unity's PlayerPrefs (settings, user preferences, etc.), replace PlayerPrefs with OZeroSafePlayerPrefs. The API is identical.

using OZeroSDK.Security;

// Save a value
OZeroSafePlayerPrefs.SetInt("score", 4200);
OZeroSafePlayerPrefs.SetFloat("volume", 0.8f);
OZeroSafePlayerPrefs.SetString("username", "Hero");

// Read a value
int    score    = OZeroSafePlayerPrefs.GetInt("score", 0);
float  volume   = OZeroSafePlayerPrefs.GetFloat("volume", 1.0f);
string username = OZeroSafePlayerPrefs.GetString("username", "");

Encrypt Save Files

To encrypt save files, use OZeroSV_File instead of File.ReadAllText / File.WriteAllText. The file is automatically encrypted on write and decrypted on read. Tampering is detected on load.

using OZeroSDK.Security;

string path = Application.persistentDataPath + "/save.json";

// Write encrypted file
OZeroSV_File.WriteAllText(path, jsonString);

// Read and decrypt file
string json = OZeroSV_File.ReadAllText(path);
Important: do not change developerSecret after launch. It is one of the values used to protect OZeroSafePlayerPrefs and OZeroSV_File data. If you change it after release, protected data saved by older versions may no longer open.

4 Protect In-Game Variables (Secure Types)

Secure Types replace ordinary C# variable types with encrypted equivalents. Values are stored in the Native C++ heap so tools like Cheat Engine cannot find them by scanning memory. The replacement is a simple name swap — all operators and implicit conversions work the same way.

Unity Inspector showing OZeroSV_Int and OZeroSV_Float fields — replace with actual screenshot

Supported types

Before After (OZero)
intOZeroSV_Int
longOZeroSV_Int64
uintOZeroSV_UInt
ulongOZeroSV_UInt64
shortOZeroSV_Short
ushortOZeroSV_UShort
byteOZeroSV_Byte
floatOZeroSV_Float
doubleOZeroSV_Double
decimalOZeroSV_Decimal
boolOZeroSV_Bool
stringOZeroSV_String
Vector2OZeroSV_Vector2
Vector3OZeroSV_Vector3
byte[]OZeroSV_Buffer

Example

// ── Before: plain C# variable ──────────────────────────────
using UnityEngine;

public class PlayerStats : MonoBehaviour
{
    public int   gold  = 0;
    public float speed = 5.0f;
    public int   hp    = 100;
}

// ── After: OZero Secure Types (only the type name changes) ──
using UnityEngine;
using OZeroSDK.Security;

public class PlayerStats : MonoBehaviour
{
    public OZeroSV_Int   gold  = 0;
    public OZeroSV_Float speed = 5.0f;
    public OZeroSV_Int   hp    = 100;

    // All arithmetic works exactly as before — no code changes needed
    void TakeDamage(int dmg) => hp -= dmg;
    void AddGold(int amount) => gold += amount;
}
Secure Types use the OZeroSDK.Security namespace. Add the using directive to any script that declares them.

7 Handle Security Events (Optional)

By default, OZero force-quits the application when it detects a threat (if Force Quit On Detection is enabled). If you prefer to handle threats yourself — for example, to show a warning screen, log the event to your server, or apply in-game penalties — you can register a callback.

using UnityEngine;
using OZeroSDK.Security;

public class SecurityHandler : MonoBehaviour
{
    void OnEnable()
    {
        // RegisterUserCallback runs on the user chain only.
        // The built-in default handler runs independently and cannot be silenced.
        OZeroSecurityManager.Instance.RegisterUserCallback(OnThreatDetected);
    }

    void OnDisable()
    {
        OZeroSecurityManager.Instance.UnregisterUserCallback(OnThreatDetected);
    }

    void OnThreatDetected(OZeroSecurityEvent evt)
    {
        // evt contains Type, AbortCode, AbortCodeHex, MessageKey, Message, and WillAbort.
        Debug.LogWarning(
            $"OZero threat: {evt.Type} {evt.AbortCodeHex} {evt.Message}");

        switch (evt.Type)
        {
            case ModulationType.SpeedHack:
                // e.g. kick the player, show warning, report to server
                break;

            case ModulationType.BuildIntegrity:
                // evt.WillAbort is usually true for fatal build integrity violations.
                break;

            case ModulationType.Injection:
                break;
        }

        if (evt.WillAbort)
        {
            // Last chance to flush your own analytics or save state.
        }
    }
}

The full list of ModulationType values is documented in the API Reference.

If you register a callback but still want force-quit as a fallback, keep Force Quit On Detection enabled in OZeroSecurityConfig. Your callback fires first, then the application exits.
Four independent defence paths

A violation fans out to: (1) OZeroSecurityManager.BuiltinResponse — the default handler that always runs; (2) your OZeroSecurityReceiver.HandleModulation override and Inspector onHackDetected event — optional scene-level hook; (3) OZeroInternalFallbackReceiver — reads Response.ForceQuitOnDetection and enforces it even with no receiver in the scene; (4) OZ_ReportViolation on the native side — optional C++ export that bypasses the managed patch surface entirely. Patching or stubbing any single path no longer silences the SDK.

Next Steps

You now have the core OZero protection running. Explore the full API Reference to learn about every class, method, and configuration option in detail.