Back to Documentation
Kotlin Documentation

Kotlin Documentation

Kotlin/JVM integration guide

Official GuideProduction Ready

Kotlin SDK Guide

Dependencies

Add to your build.gradle.kts:

kotlin
dependencies {
    implementation("com.squareup.okhttp3:okhttp:4.12.0")
    implementation("com.google.code.gson:gson:2.10.1")
}

Basic Usage

kotlin
import okhttp3.*
import com.google.gson.Gson
import com.google.gson.JsonObject
import java.security.MessageDigest
import javax.crypto.Mac
import javax.crypto.spec.SecretKeySpec

class KeyClaimClient(
    private val apiKey: String,
    private val secret: String = apiKey,
    private val baseUrl: String = "https://keyclaim.org/api"
) {
    private val client = OkHttpClient()
    private val gson = Gson()

    data class ChallengeResponse(
        val challenge: String,
        val expires_in: Int
    )

    data class ValidationResponse(
        val valid: Boolean,
        val error: String? = null,
        val signature: String? = null
    )

    // Step 1: Create a challenge
    suspend fun createChallenge(ttl: Int = 30): ChallengeResponse {
        val requestBody = JsonObject().apply {
            addProperty("key", apiKey)
            addProperty("ttl", ttl)
        }

        val body = RequestBody.create(
            requestBody.toString().toByteArray(),
            MediaType.parse("application/json")
        )

        val request = Request.Builder()
            .url("$baseUrl/challenge/create")
            .post(body)
            .build()

        client.newCall(request).execute().use { response ->
            if (!response.isSuccessful) {
                throw Exception("Failed to create challenge: ${response.code}")
            }
            val responseBody = response.body?.string() ?: throw Exception("Empty response")
            return gson.fromJson(responseBody, ChallengeResponse::class.java)
        }
    }

    // Step 2: Generate response from challenge
    // Option A: Simple echo (for testing)
    fun generateResponseSimple(challenge: String): String {
        return challenge
    }

    // Option B: HMAC-SHA256 (recommended)
    fun generateResponseHMAC(challenge: String): String {
        val mac = Mac.getInstance("HmacSHA256")
        val secretKeySpec = SecretKeySpec(
            secret.toByteArray(Charsets.UTF_8),
            "HmacSHA256"
        )
        mac.init(secretKeySpec)
        val hash = mac.doFinal(challenge.toByteArray(Charsets.UTF_8))
        return hash.joinToString("") { "%02x".format(it) }
    }

    // Option C: SHA256 Hash
    fun generateResponseHash(challenge: String): String {
        val digest = MessageDigest.getInstance("SHA-256")
        val hash = digest.digest((challenge + secret).toByteArray(Charsets.UTF_8))
        return hash.joinToString("") { "%02x".format(it) }
    }

    // Step 3: Validate the response
    suspend fun validate(challenge: String, response: String): ValidationResponse {
        val requestBody = JsonObject().apply {
            addProperty("key", apiKey)
            addProperty("challenge", challenge)
            addProperty("response", response)
        }

        val body = RequestBody.create(
            requestBody.toString().toByteArray(),
            MediaType.parse("application/json")
        )

        val request = Request.Builder()
            .url("$baseUrl/challenge/validate")
            .post(body)
            .build()

        client.newCall(request).execute().use { httpResponse ->
            val responseBody = httpResponse.body?.string() ?: return ValidationResponse(
                valid = false,
                error = "Empty response"
            )
            
            val result = gson.fromJson(responseBody, ValidationResponse::class.java)
            return if (httpResponse.isSuccessful) {
                result
            } else {
                result.copy(valid = false)
            }
        }
    }

    // Complete example
    suspend fun validateChallenge(challenge: String, method: String = "hmac"): ValidationResponse {
        val response = when (method) {
            "echo" -> generateResponseSimple(challenge)
            "hmac" -> generateResponseHMAC(challenge)
            "hash" -> generateResponseHash(challenge)
            else -> throw IllegalArgumentException("Unknown method: $method")
        }
        return validate(challenge, response)
    }
}

Usage Example

kotlin
import kotlinx.coroutines.runBlocking

fun main() = runBlocking {
    val client = KeyClaimClient(
        apiKey = "kc_your_api_key",
        secret = "your-secret-key"
    )

    try {
        // Create challenge
        val challengeResp = client.createChallenge(ttl = 30)
        val challenge = challengeResp.challenge
        println("Challenge: $challenge")

        // Generate response
        val response = client.generateResponseHMAC(challenge)
        println("Response: $response")

        // Validate
        val result = client.validate(challenge, response)
        println("Valid: ${result.valid}")
        
        if (result.valid) {
            println("✓ Challenge validated successfully!")
            result.signature?.let { println("Signature: $it") }
        } else {
            println("✗ Validation failed: ${result.error}")
        }
    } catch (e: Exception) {
        e.printStackTrace()
    }
}

Response Generation Methods

Method 1: Echo (Testing)

kotlin
fun generateResponseEcho(challenge: String): String = challenge

Method 2: HMAC-SHA256 (Recommended)

kotlin
fun generateResponseHMAC(challenge: String): String {
    val mac = Mac.getInstance("HmacSHA256")
    val secretKeySpec = SecretKeySpec(
        secret.toByteArray(Charsets.UTF_8),
        "HmacSHA256"
    )
    mac.init(secretKeySpec)
    val hash = mac.doFinal(challenge.toByteArray(Charsets.UTF_8))
    return hash.joinToString("") { "%02x".format(it) }
}

Method 3: SHA256 Hash

kotlin
fun generateResponseHash(challenge: String): String {
    val digest = MessageDigest.getInstance("SHA-256")
    val hash = digest.digest((challenge + secret).toByteArray(Charsets.UTF_8))
    return hash.joinToString("") { "%02x".format(it) }
}