All skills
Skillintermediate

Constant-Time Analysis: Kotlin

Analysis guidance for Kotlin targeting Android and JVM platforms. Kotlin compiles to JVM bytecode, sharing the same runtime characteristics as Java.

Claude Code Knowledge Pack7/10/2026

Overview

Constant-Time Analysis: Kotlin

Analysis guidance for Kotlin targeting Android and JVM platforms. Kotlin compiles to JVM bytecode, sharing the same runtime characteristics as Java.

Understanding Kotlin Compilation

Kotlin compiles to JVM bytecode that runs on the same virtual machine as Java:

Source Code (.kt/.kts)
        |
        v
    kotlinc (Kotlin Compiler)
        |
        v
Bytecode (.class files)
        |
        v
    JIT Compiler (HotSpot/ART)
        |
        v
   Native Code (at runtime)

Key implications for Android:

  1. Android Runtime (ART) - Android uses ART instead of HotSpot JVM
  2. AOT compilation - ART compiles bytecode to native code at install time
  3. Same bytecode vulnerabilities - Division/branch timing issues persist regardless of runtime

Running the Analyzer

# Analyze Kotlin source
uv run {baseDir}/ct_analyzer/analyzer.py CryptoUtils.kt

# Include conditional branch warnings
uv run {baseDir}/ct_analyzer/analyzer.py --warnings CryptoUtils.kt

# Filter to specific functions
uv run {baseDir}/ct_analyzer/analyzer.py --func 'sign|verify' CryptoUtils.kt

# CI-friendly JSON output
uv run {baseDir}/ct_analyzer/analyzer.py --json CryptoUtils.kt

Note: The --arch and --opt-level flags do not apply to Kotlin as it compiles to JVM bytecode.

Dangerous Bytecode Instructions

Kotlin compiles to the same JVM bytecode as Java:

CategoryInstructionsRisk
Integer Divisionidiv, ldiv, irem, lremVariable-time based on operand values
Floating Divisionfdiv, ddiv, frem, dremVariable latency
Conditional Branchesifeq, ifne, iflt, ifge, ifgt, ifle, if_icmp*Timing leak if condition depends on secrets
Table Lookups*aload, *astore, tableswitch, lookupswitchCache timing if index depends on secrets

Constant-Time Patterns

Replace Division

// VULNERABLE: Division instruction emitted
val q = secretValue / divisor

// SAFE: Barrett reduction (for fixed divisor)
// Precompute: mu = (1L shl 32) / divisor
val mu = (1L shl 32) / divisor
val q = ((secretValue.toLong() * mu) ushr 32).toInt()

Replace Branches

// VULNERABLE: Branch timing reveals secret
val result = if (secret != 0) a else b

// SAFE: Constant-time selection using bitwise ops
val mask = -(if (secret != 0) 1 else 0)
// Better: compute mask without branch
val mask = (secret or -secret) shr 31  // -1 if secret != 0, else 0
val result = (a and mask) or (b and mask.inv())

Replace Comparisons

// VULNERABLE: contentEquals() may early-terminate
if (computed.contentEquals(expected)) { ... }

// SAFE: Use MessageDigest.isEqual() for constant-time comparison

if (MessageDigest.isEqual(computed, expected)) { ... }

Secure Random

// VULNERABLE: kotlin.random.Random is predictable

val value = Random.nextInt()

// SAFE: Cryptographically secure

val secureRand = SecureRandom()
val value = secureRand.nextInt()

// Or use Kotlin's secure wrapper (requires kotlin-stdlib-jdk8)

val secureKotlinRandom = SecureRandom().asKotlinRandom()

Android-Specific Considerations

Keystore Operations

// Use Android Keystore for cryptographic key storage

val keyGenerator = KeyGenerator.getInstance(
    KeyProperties.KEY_ALGORITHM_AES,
    "AndroidKeyStore"
)
keyGenerator.init(
    KeyGenParameterSpec.Builder(
        "my_key",
        KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT
    )
    .setBlockModes(KeyProperties.BLOCK_MODE_GCM)
    .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
    .build()
)

Constant-Time Comparison on Android

// Android provides MessageDigest.isEqual()

fun constantTimeEquals(a: ByteArray, b: ByteArray): Boolean {
    return MessageDigest.isEqual(a, b)
}

Secure Random on Android

// SecureRandom works the same on Android

fun generateSecureToken(length: Int): ByteArray {
    val random = SecureRandom()
    val token = ByteArray(length)
    random.nextBytes(token)
    return token
}

Kotlin-Specific Pitfalls

Extension Functions on Primitives

// DANGEROUS: Division in extension function
fun Int.divideBy(divisor: Int) = this / divisor  // Emits IDIV

// The inline modifier doesn't change bytecode behavior
inline fun Int.divideByInline(divisor: Int) = this / divisor  // Still IDIV

When Expressions

// VULNERABLE: when compiles to tableswitch/lookupswitch
when (secretValue) {
    0 -> handleZero()
    1 -> handleOne()
    else -> handleOther()
}

// Consider constant-time alternatives for secret-dependent dispatch

Null Safety Checks

// Nullable operations may introduce branches
val result = secretNullable?.process()  // Introduces null check branch

// Be aware of null-check timing when handling secrets

Setup Requirements

Kotlin Compiler

macOS:

brew install kotlin

Ubuntu/Debian:

sudo snap install kotlin --classic

Windows:

scoop install kotlin
# or
choco install kotlinc

Android Development

For Android projects, the Kotlin compiler is typically bundled with Android Studio. Ensure your project's Kotlin version is up to date in build.gradle.kts:

plugins {
    kotlin("jvm") version "1.9.0"
}

Verification

kotlinc -version  # Should show: kotlinc-jvm X.X.X
javap -version    # Required for bytecode disassembly

Common Mistakes

  1. Using kotlin.random.Random - The default Random is not cryptographically secure; use java.security.SecureRandom

  2. Relying on == for byte arrays - == compares references in Kotlin; use contentEquals() for value comparison, but neither is constant-time

  3. Infix functions for crypto - Custom operators don't change timing characteristics of underlying operations

  4. Coroutines timing - Suspending functions add scheduling overhead that may mask or introduce timing variations

  5. Sealed classes for dispatch - Pattern matching on sealed classes compiles to switches that may leak timing

Further Reading