Skip to main content

HMAC Auth SDK for Kotlin

Kotlin SDK for authenticating requests to the SafeSky API using HMAC-SHA256 signatures.

Click here to download the SDK

Installation

Gradle (Kotlin DSL)

Add to your build.gradle.kts:

dependencies {
    implementation("com.safesky:hmac-auth:1.0.0")
    implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.2")
}
Maven

Add to your pom.xml:

<dependency>
    <groupId>com.safesky</groupId>
    <artifactId>hmac-auth</artifactId>
    <version>1.0.0</version>
</dependency>
<dependency>
    <groupId>org.jetbrains.kotlinx</groupId>
    <artifactId>kotlinx-serialization-json-jvm</artifactId>
    <version>1.6.2</version>
</dependency>

Quick Start

import com.safesky.authentication.SafeSkyHmacAuth
import java.net.URI
import java.net.http.HttpClient
import java.net.http.HttpRequest
import java.net.http.HttpResponse

fun main() {
    // Your SafeSky API key
    val apiKey = "YOUR_SAFESKY_API_KEY_HEREHere"
    
    // Generate authentication headers for a GET request
    val headers = SafeSkyHmacAuth.generateAuthHeaders(
        apiKey,
        "GET",
        "https://api.safesky.app/v1/uav?lat=50.6970&lng=4.3908"
    )
    
    // Make authenticated request
    val client = HttpClient.newHttpClient()
    val requestBuilder = HttpRequest.newBuilder()
        .uri(URI("https://api.safesky.app/v1/uav?lat=50.6970&lng=4.3908"))
        .GET()
    
    headers.toMap().forEach { (key, value) ->
        requestBuilder.header(key, value)
    }
    
    val response = client.send(requestBuilder.build(), HttpResponse.BodyHandlers.ofString())
    println(response.body())
}

Usage Examples

The SDK includes 4 complete examples in Example.kt:

  1. GET nearby aircraft - Retrieve traffic in a specific area
  2. POST UAV position - Submit UAV telemetry data
  3. POST advisory (GeoJSON) - Publish GeoJSON FeatureCollection with polygon and point
  4. Manual step-by-step - Detailed authentication flow
Example 1: GET Request
import com.safesky.authentication.SafeSkyHmacAuth
import java.net.URI
import java.net.http.HttpClient
import java.net.http.HttpRequest
import java.net.http.HttpResponse

fun getNearbyAircraft() {
    val apiKey = "YOUR_SAFESKY_API_KEY_HERE"
    val url = "https://api.safesky.app/v1/uav?lat=50.6970&lng=4.3908&rad=20000"
    
    // Generate auth headers
    val authHeaders = SafeSkyHmacAuth.generateAuthHeaders(apiKey, "GET", url)
    
    // Make request
    val client = HttpClient.newHttpClient()
    val requestBuilder = HttpRequest.newBuilder()
        .uri(URI(url))
        .GET()
        .header("Content-Type", "application/json")
    
    authHeaders.toMap().forEach { (key, value) ->
        requestBuilder.header(key, value)
    }
    
    val response = client.send(requestBuilder.build(), HttpResponse.BodyHandlers.ofString())
    println("Status: ${response.statusCode()}")
    println(response.body())
}
Example 2: POST Request
import com.safesky.authentication.SafeSkyHmacAuth
import kotlinx.serialization.json.*
import java.net.URI
import java.net.http.HttpClient
import java.net.http.HttpRequest
import java.net.http.HttpResponse

fun postUavPosition() {
    val apiKey = "YOUR_SAFESKY_API_KEY_HERE"
    val url = "https://api.safesky.app/v1/uav"
    
    // Build request body
    val bodyData = buildJsonArray {
        addJsonObject {
            put("id", "my_uav_001")
            put("altitude", 110)
            put("latitude", 50.69378)
            put("longitude", 4.39201)
            put("last_update", System.currentTimeMillis() / 1000)
            put("status", "AIRBORNE")
            put("call_sign", "Test UAV")
            put("ground_speed", 10)
            put("course", 250)
            put("vertical_rate", 5)
        }
    }
    
    val body = bodyData.toString()
    
    // Generate auth headers (pass body for POST)
    val authHeaders = SafeSkyHmacAuth.generateAuthHeaders(apiKey, "POST", url, body)
    
    // Make request
    val client = HttpClient.newHttpClient()
    val requestBuilder = HttpRequest.newBuilder()
        .uri(URI(url))
        .POST(HttpRequest.BodyPublishers.ofString(body))
        .header("Content-Type", "application/json")
    
    authHeaders.toMap().forEach { (key, value) ->
        requestBuilder.header(key, value)
    }
    
    val response = client.send(requestBuilder.build(), HttpResponse.BodyHandlers.ofString())
    println("Status: ${response.statusCode()}")
}
Example 3: POST Advisory (GeoJSON)
import com.safesky.authentication.SafeSkyHmacAuth
import kotlinx.serialization.json.*

fun postAdvisory() {
    val apiKey = "YOUR_SAFESKY_API_KEY_HERE"
    val url = "https://api.safesky.app/v1/advisory"
    
    val timestamp = System.currentTimeMillis() / 1000
    
    val bodyData = buildJsonObject {
        put("type", "FeatureCollection")
        putJsonArray("features") {
            addJsonObject {
                put("type", "Feature")
                putJsonObject("properties") {
                    put("id", "my_advisory_id1")
                    put("max_altitude", 111)
                    put("last_update", timestamp)
                    put("call_sign", "Advisory test with polygon")
                    put("remarks", "Inspection powerlines")
                }
                putJsonObject("geometry") {
                    put("type", "Polygon")
                    putJsonArray("coordinates") {
                        addJsonArray {
                            add(buildJsonArray { add(4.3948); add(50.6831) })
                            // ... more coordinates ...
                        }
                    }
                }
            }
            addJsonObject {
                put("type", "Feature")
                putJsonObject("properties") {
                    put("id", "my_advisory_id2")
                    put("max_altitude", 150)
                    put("max_distance", 500)
                    put("last_update", timestamp)
                    put("call_sign", "Advisory test with a point")
                    put("remarks", "Inspection rails")
                }
                putJsonObject("geometry") {
                    put("type", "Point")
                    putJsonArray("coordinates") {
                        add(4.4)
                        add(50.7)
                    }
                }
            }
        }
    }
    
    val body = bodyData.toString()
    val authHeaders = SafeSkyHmacAuth.generateAuthHeaders(apiKey, "POST", url, body)
    
    // Make request (similar to Example 2)
}
Example 4: Manual Step-by-Step
import com.safesky.authentication.SafeSkyHmacAuth
import java.net.URI

fun manualAuthentication() {
    val apiKey = "YOUR_SAFESKY_API_KEY_HERE"
    
    // Step 1: Derive KID
    val kid = SafeSkyHmacAuth.deriveKid(apiKey)
    println("KID: $kid")
    
    // Step 2: Derive HMAC key
    val hmacKey = SafeSkyHmacAuth.deriveHmacKey(apiKey)
    
    // Step 3: Generate timestamp and nonce
    val timestamp = SafeSkyHmacAuth.generateTimestamp()
    val nonce = SafeSkyHmacAuth.generateNonce()
    
    // Step 4: Build canonical request
    val canonicalRequest = SafeSkyHmacAuth.buildCanonicalRequest(
        "GET",
        "/v1/uav",
        "lat=50.6970&lng=4.3908",
        "api.safesky.app",
        timestamp,
        nonce,
        ""
    )
    
    // Step 5: Generate signature
    val signature = SafeSkyHmacAuth.generateSignature(canonicalRequest, hmacKey)
    println("Signature: $signature")
}

API Reference

Main Object: SafeSkyHmacAuth

Singleton object providing HMAC authentication functions.

######## generateAuthHeaders(apiKey, method, url, body = "")

Generates all HMAC authentication headers for a SafeSky API request.

Parameters:

  • apiKey (String): Your SafeSky API key (e.g., ssk_live_...)
  • method (String): HTTP method (GET, POST, etc.)
  • url (String): Full request URL including protocol, host, path, and query string
  • body (String, optional): Request body for POST/PUT requests (default: empty string)

Returns: SafeSkyAuthHeaders data class containing all required headers

Example:

val headers = SafeSkyHmacAuth.generateAuthHeaders(
    "YOUR_SAFESKY_API_KEY_HERE",
    "POST",
    "https://api.safesky.app/v1/uav",
    Json.encodeToString(data)
)
Data Class: SafeSkyAuthHeaders

Container for authentication headers.

Properties:

  • authorization (String): Authorization header value
  • xSsDate (String): X-SS-Date header value
  • xSsNonce (String): X-SS-Nonce header value
  • xSsAlg (String): X-SS-Alg header value

Methods:

  • toMap()Map<String, String>: Convert headers to map for HTTP requests
Helper Functions

######## deriveKid(apiKey)String

Derives the KID (Key Identifier) from an API key.

Formula: KID = base64url(SHA256("kid:" + api_key)[0:16])

######## deriveHmacKey(apiKey)ByteArray

Derives the HMAC signing key from an API key using HKDF-SHA256.

######## generateNonce()String

Generates a cryptographically secure nonce (UUID format).

######## generateTimestamp()String

Generates the current timestamp in ISO8601 format with milliseconds.

Format: YYYY-MM-DDTHH:MM:SS.sssZ

######## buildCanonicalRequest(...)String

Builds the canonical request string for HMAC signature.

Parameters:

  • method (String): HTTP method
  • path (String): Request path
  • queryString (String): Query string without leading ?
  • host (String): Host header value
  • timestamp (String): ISO8601 timestamp
  • nonce (String): Unique nonce
  • body (String): Request body

######## generateSignature(canonicalRequest, hmacKey)String

Generates HMAC-SHA256 signature for the canonical request.

Returns: Base64-encoded signature

Authentication Flow

  1. Derive KID: KID = base64url(SHA256("kid:" + api_key)[0:16])
  2. Derive HMAC Key: HKDF-SHA256 with salt and info strings
  3. Generate Nonce: UUID v4 for replay protection
  4. Generate Timestamp: ISO8601 format with milliseconds
  5. Build Canonical Request:
    METHOD
    /path
    query_string
    host:hostname:port
    x-ss-date:timestamp
    x-ss-nonce:nonce
    
    body_hash_sha256_hex
    
  6. Sign: HMAC-SHA256(hmac_key, canonical_request) → base64
  7. Build Headers: Authorization header with KID, signed headers, and signature

Running Examples

## Using Gradle
./gradlew run

## Or with Gradle wrapper
gradle run

## Compile only
./gradlew build

Kotlin/JVM Support

  • Kotlin 1.9+ (recommended)
  • JVM 17+ (required)
  • Kotlin 1.8+ (compatible)

Android Integration

This SDK works seamlessly with Android:

import com.safesky.authentication.SafeSkyHmacAuth
import okhttp3.OkHttpClient
import okhttp3.Request

class SafeSkyRepository(private val apiKey: String) {
    private val client = OkHttpClient()
    
    suspend fun getNearbyAircraft(lat: Double, lng: Double): List<Aircraft> {
        val url = "https://api.safesky.app/v1/uav?lat=$lat&lng=$lng&rad=20000"
        val headers = SafeSkyHmacAuth.generateAuthHeaders(apiKey, "GET", url)
        
        val requestBuilder = Request.Builder().url(url)
        headers.toMap().forEach { (key, value) ->
            requestBuilder.addHeader(key, value)
        }
        
        val response = client.newCall(requestBuilder.build()).execute()
        return if (response.isSuccessful) {
            Json.decodeFromString(response.body?.string() ?: "[]")
        } else {
            emptyList()
        }
    }
}

Ktor Integration

import com.safesky.authentication.SafeSkyHmacAuth
import io.ktor.client.*
import io.ktor.client.request.*

suspend fun getNearbyAircraft(client: HttpClient, apiKey: String) {
    val url = "https://api.safesky.app/v1/uav?lat=50.6970&lng=4.3908"
    val headers = SafeSkyHmacAuth.generateAuthHeaders(apiKey, "GET", url)
    
    val response = client.get(url) {
        headers.toMap().forEach { (key, value) ->
            header(key, value)
        }
    }
}

Security Notes

  • Keep API keys secret: Never commit them to version control
  • Use BuildConfig or resources: Store keys securely in Android
  • HTTPS only: Always use HTTPS in production (api.safesky.app)
  • Replay protection: Server validates nonces within 15-minute window
  • Clock skew: Server allows ±5 minutes timestamp tolerance

Troubleshooting

Invalid Signature
  • Check body is identical (no extra whitespace)
  • Verify URL includes all query parameters
  • Ensure host header matches URL (include port for non-standard ports)
Replay Attack Detected
  • Nonce was already used within 15-minute window
  • Generate fresh nonce for each request
Timestamp Out of Range
  • Server time differs by more than ±5 minutes
  • Synchronize system clock with NTP

Dependencies

  • Kotlin Standard Library - Built-in
  • kotlinx-serialization-json - JSON serialization (for examples)
  • java.security - Built-in (SHA-256, HMAC)
  • java.net.http - Built-in JDK 11+ (HTTP client)

Support

For questions or issues:

  • Email: support@safesky.app
  • Documentation: https://docs.safesky.app