Skip to main content

HMAC Auth SDK for Kotlin

SafeSky HMAC Authentication SDK for Kotlin

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

Click here to download the SDK

API Endpoint: All requests use https://uav-api.safesky.app. Each API key is associated with either a sandbox or production environment. The gateway automatically routes your request to the correct backend based on the environment configured for your API key.

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://uav-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://uav-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://uav-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://uav-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://uav-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://uav-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://uav-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://uav-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