cybercinch-shield-releases

CyberCinch Shield — Android SDK Developer Manual

The onboarding guide for integrating the CyberCinch Shield Android SDK into your own app. For third-party developers with no prior familiarity with this codebase.

This document covers how to integrate the SDK. For a field-by-field reference of every ShieldConfig tuning option, see the Configuration Reference — this manual links to it rather than duplicating it.

Everything below is verified against the real, current SDK source as of this writing (SDK version 1.0.0) — no aspirational API, nothing described as working that isn't. Where the SDK has a real, known gap, it's called out explicitly rather than glossed over — see §7, Known Limitations in particular.


1. Quick Start

// build.gradle.kts (:app) — see §2 for the full repository setup this requires
dependencies {
    implementation("com.cybercinch:cybercinch-shield-android:1.0.0")
}
// Application.onCreate()
class MyApplication : Application() {
    override fun onCreate() {
        super.onCreate()
        CyberCinchShield.initialize(
            context = this,
            apiKey  = "cc_live_your_key_here"
        )
    }
}
// Anywhere with a coroutine scope
lifecycleScope.launch {
    val result = CyberCinchShield.scan()
    if (result.riskLevel == RiskLevel.CRITICAL) {
        // handle a compromised device — see §5, §10
    }
}

That's the shape of a minimal integration. The rest of this manual explains what each step actually does, what it doesn't do yet, and how to handle the result correctly.


2. Installation

2.1 Public, no account or token required

The SDK is distributed via a small, dedicated public Maven repository — cybercinchtech/cybercinch-shield-releases on GitHub, served as a static Maven repo through GitHub Pages (you're looking at it right now). This is a genuine "no install step"-style public flow: no GitHub account, no collaborator invitation, no personal access token. Add the repository URL below and the dependency line in §2.5 — that's the whole setup.

This repository contains only build artifacts (the AAR, POM, sources jar, checksums) — never source code. The SDK's actual source stays in the private cybercinchtech/cybercinch-mobile-shield repository; publishing a new version means copying build output into the releases repo, not exposing anything new.

2.2 Coordinate

com.cybercinch:cybercinch-shield-android:1.0.0

2.3 Repository configuration

Add the releases repository. If your app uses Gradle's centralized repository management (dependencyResolutionManagement in settings.gradle.kts — the default for new Android Studio projects), add it there:

// settings.gradle.kts
dependencyResolutionManagement {
    repositories {
        google()
        mavenCentral()
        maven {
            name = "CyberCinchShieldReleases"
            url = uri("https://cybercinchtech.github.io/cybercinch-shield-releases/")
        }
    }
}

If your project instead declares repositories per-module (no centralized dependencyResolutionManagement, or it's set to allow project-level repos), the same maven { ... } block goes in your :app module's build.gradle.kts repositories {} block instead.

google() and mavenCentral() need to already be present (almost certainly are, in any real Android project) — the SDK's AAR declares transitive dependencies on the Play Integrity API, Play Services App Set, OkHttp, Kotlin coroutines, and AndroidX lifecycle-process, which resolve from those two repositories, not from the releases repository.

2.4 Internal / early-access track (invited collaborators only)

The public path above is all you need for a normal integration. If you're doing early internal testing with direct CyberCinch involvement, the SDK is also available via GitHub Packages on the private cybercinchtech/cybercinch-mobile-shield repository — this requires being invited as a collaborator and a classic PAT scoped to read:packages (not a fine-grained PAT — fine-grained tokens are rejected by the GitHub Packages/Maven endpoint even with Contents: Read-only access that works fine against the repo's real Contents API). This track exists for CyberCinch-coordinated testing prior to a public release, not for normal integration — if you don't already know you're on it, you're not; use §2.1–§2.3 instead.

2.5 Add the dependency

// app/build.gradle.kts
dependencies {
    implementation("com.cybercinch:cybercinch-shield-android:1.0.0")
}

minSdk must be 29 or higher in your app's defaultConfig — this is the SDK's own minimum.


3. Initialization

fun initialize(
    context: Context,
    apiKey: String,
    config: ShieldConfig = ShieldConfig()
)

Call this once, in Application.onCreate(), before anything else that depends on the SDK:

class MyApplication : Application() {
    override fun onCreate() {
        super.onCreate()
        CyberCinchShield.initialize(
            context = this,
            apiKey  = "cc_live_your_key_here"
        )
    }
}

3.1 initialize() is fire-and-forget — it never throws

initialize() returns immediately and does its real work (license validation, attestation, config fetch) on an internal background coroutine. It cannot throw back into your calling code, under any circumstance — not for a malformed API key, not for a network failure, not even if you configure strict license enforcement (see licenseValidationFailOpen in the Configuration Reference §5.1). Any failure during initialization is handled internally; nothing propagates to the call site. Don't wrap this call in try/catch expecting to catch a real failure — there's nothing for it to catch.

3.2 Calling it twice is safe, and silently ignored

A second initialize() call while the SDK is already initializing or initialized does nothing but log a warning — the SDK keeps using whatever ShieldConfig your first call passed. This is safe to rely on if your app has multiple entry points that might both attempt initialization.

3.3 There is no way to ask "is it ready yet?"

The SDK exposes no isReady(), no ready-state callback, and no suspend function that waits for initialization to finish. If you need to know when the SDK has become usable, the only real signal is calling scan() and handling the exception it throws if the SDK isn't ready yet (see §6) — there is no polling or observability API to reach for instead. Don't assume one exists.


4. Running a Scan

suspend fun scan(): ScanResult
lifecycleScope.launch {
    val result = CyberCinchShield.scan()
    // handle result — see §5
}

4.1 The SDK runs one scan automatically, before you ever call scan() yourself

This is not optional detail to skip — the SDK auto-triggers exactly one scan() call on its own, immediately after initialize() successfully completes, via an internal auto-init mechanism (a ContentProvider that arms this behavior at process start, independent of when you call initialize()). If you have telemetry enabled, you will see events dispatched to the backend before your app ever explicitly calls scan(). This is expected, not a bug in your integration — just be aware of it so early telemetry activity doesn't confuse you during testing.

Every scan after that first automatic one is entirely up to you — call scan() however often makes sense for your app (once per session, once per sensitive screen, on a timer, etc.).

4.2 scan() throws if the SDK isn't ready

try {
    val result = CyberCinchShield.scan()
    // ...
} catch (e: IllegalStateException) {
    // SDK not ready yet — see below
}

scan() throws IllegalStateException if called before initialize() has completed (or if initialization failed internally — see §3.1 and §6). The exception message is the same either way ("not ready — call initialize() first and wait for it to complete") — you cannot distinguish "still starting up, try again shortly" from "permanently failed" from the exception alone. See §6 for the recommended handling pattern.


5. Handling Results

data class ScanResult(
    val rooted: Boolean,
    val jailbroken: Boolean,
    val emulator: Boolean,
    val runtimeCompromised: Boolean,
    val appTampered: Boolean,
    val riskScore: Int,
    val riskLevel: RiskLevel,           // LOW, MEDIUM, HIGH, CRITICAL
    val events: List<SecurityEvent>,
    val serverVerified: Boolean,
    val scanTimestamp: Long
)
FieldMeaning
rootedtrue if root-detection signals fired this scan.
jailbrokenAlways false on Android. This field exists for shape-parity with the iOS SDK; the Android module never sets it true. Don't wait for it to fire.
emulatortrue if emulator-detection signals fired.
runtimeCompromisedtrue if Frida/debugger/hook-framework signals fired.
appTamperedtrue if integrity verification (cert/DEX/lib hash) failed.
riskScoreAggregate numeric score across all fired signals.
riskLevelriskScore bucketed into LOW/MEDIUM/HIGH/CRITICAL.
eventsThe individual SecurityEvents that contributed to this scan — see below.
serverVerifiedSee §5.2 — not what its name might suggest.
scanTimestampSystem.currentTimeMillis() at scan completion (wall-clock, not monotonic).

5.1 A false boolean can mean "checked, clean" or "never checked"

Each detector only runs if it's enabled in your ShieldConfig and your license plan grants the relevant feature (root/runtime detection require the runtime_protection license feature). If a detector doesn't run — disabled locally, or not licensed — its corresponding boolean simply stays false, identical to a real clean result. You cannot tell "we checked for root and found none" apart from "we never checked for root" by looking at rooted alone. If that distinction matters for your app, inspect events (each fired signal has a metadata map identifying which detector and check produced it) rather than trusting the top-level booleans in isolation.

5.2 serverVerified reflects license state at startup, not per-scan attestation

serverVerified is true if your license was successfully validated against the license server when initialize() ran — it is not recomputed per scan, and it is not a live, per-scan attestation verdict. A device that was server-verified at app launch keeps reporting serverVerified = true on every scan for the rest of that process's lifetime, even if connectivity is later lost. See §7 for why there is currently no separate, real per-scan server verdict signal.

5.3 SecurityEvent

data class SecurityEvent(
    val eventId: String,
    val eventType: SecurityEventType,
    val riskContribution: Int,
    val detectedAt: Long,
    val metadata: Map<String, String>
)

One entry per individual signal that fired (e.g. a specific root-detection check, a specific Frida signal). metadata carries check-specific detail.


6. Error Handling

The real error surface, summarized from §3 and §4:

CallCan it throw?
initialize()Never. Fire-and-forget; all internal failures are swallowed (§3.1).
scan()IllegalStateException if the SDK isn't READY yet — covers both "still initializing" and "failed" indistinguishably.
setTelemetryConsent(), flushTelemetry(), sdkVersion()Never.
reportError()Never — guaranteed safe to call from a catch block or an uncaught-exception handler.

Recommended pattern

Since initialize() can't signal failure and there's no readiness API to poll (§3.3), the practical approach is: call initialize() fire-and-forget in Application.onCreate(), then wrap every scan() call site in try/catch for IllegalStateException with your own retry/backoff:

suspend fun scanWithRetry(maxAttempts: Int = 3, delayMs: Long = 1000): ScanResult? {
    repeat(maxAttempts) { attempt ->
        try {
            return CyberCinchShield.scan()
        } catch (e: IllegalStateException) {
            if (attempt == maxAttempts - 1) return null
            delay(delayMs)
        }
    }
    return null
}

Since the exception doesn't distinguish "not ready yet" from "permanently failed", cap your retries — there's no way to know from the SDK alone whether waiting longer will help.

Reporting your own errors through the SDK

fun reportError(throwable: Throwable, fatal: Boolean = false)

Call this from your own catch blocks or uncaught-exception handler to have the SDK emit a CLIENT_ERROR_REPORTED telemetry event (not a security signal — riskScore is always 0 for these). Safe to call unconditionally; it never throws itself.


7. Known Limitations

Two public API methods are present and callable but do not currently do anything. Both are tracked, known gaps — not something wrong with your integration if you notice they have no effect.

onServerVerdict(callback: (ScanResult) -> Unit) — dead code

Registers a callback that is intended to deliver an asynchronous server attestation verdict. It is never invoked — confirmed by tracing every reference to the internal field it writes to. A callback registered here will simply never fire, regardless of attestation success or failure. Use ScanResult.serverVerified instead (§5.2) for the real, working signal about server verification state — noting the caveat there that it reflects init-time license state, not a live per-scan verdict.

enableSslPinning(vararg certificateHashes: String) / ShieldConfig.enableSslPinning — dead code

Neither the method nor the corresponding ShieldConfig flag do anything. The hashes you pass are stored internally and never read again; the config flag is never read at all. No certificate pinning is applied to your app's network calls as a result of calling this, regardless of what you pass or whether the flag is set. Do not depend on this for certificate pinning today.


8. ProGuard / R8 & Permissions

8.1 ProGuard / R8

If your app enables R8/minification for release builds (isMinifyEnabled = true), no action is required on your part. The SDK ships a consumer-rules.pro bundled into the AAR, applied automatically to your build via Gradle's consumerProguardFiles mechanism. It keeps the SDK's public data-contract classes (CyberCinchShield, ShieldConfig, ScanResult, SecurityEvent, and their nested enums) and every native-JNI-bound internal class from being renamed or stripped. You don't need to add any -keep rules yourself for this SDK to keep working correctly under minification.

8.2 Permissions

The SDK's own manifest declares no permissions of its own. It relies entirely on your app already holding android.permission.INTERNET — true of virtually every real app, but worth confirming explicitly if your app is unusually permission-minimal.

No manifest edits are required on your part for the SDK's internal auto-init mechanism (a ContentProvider merged automatically via manifest merger) — nothing to add, nothing to configure.


9. Further Configuration

Everything above covers integration mechanics. For a complete field-by-field reference of ShieldConfig — detection module toggles, telemetry dispatch modes and batching, license fail-open/fail-closed behavior, backend URL overrides, logging levels, and two complete example configurations (a high-security/fintech profile and a low-overhead profile) — see the Configuration Reference.


10. Common Integration Patterns

Gating a sensitive screen on scan result

A common pattern: block access to a sensitive screen (payment, account recovery, etc.) until a scan comes back clean.

class SensitiveActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_sensitive)

        lifecycleScope.launch {
            val result = try {
                CyberCinchShield.scan()
            } catch (e: IllegalStateException) {
                // SDK not ready yet — decide your own fallback; this example
                // fails closed rather than assuming a clean device.
                showBlockedState("Security check unavailable — try again shortly")
                return@launch
            }

            when {
                result.riskLevel == RiskLevel.CRITICAL -> {
                    showBlockedState("This device does not meet our security requirements")
                }
                result.rooted || result.runtimeCompromised -> {
                    showBlockedState("This device does not meet our security requirements")
                }
                else -> {
                    showSensitiveContent()
                }
            }
        }
    }

    private fun showBlockedState(message: String) { /* ... */ }
    private fun showSensitiveContent() { /* ... */ }
}

Note this example uses only scan()'s direct, synchronous return value — deliberately not onServerVerdict (§7). Whether to fail open or closed when scan() throws (SDK not ready) is a judgment call for your specific app's risk tolerance; the example above fails closed.