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:
- GET nearby aircraft - Retrieve traffic in a specific area
- POST UAV position - Submit UAV telemetry data
- POST advisory (GeoJSON) - Publish GeoJSON FeatureCollection with polygon and point
- 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 stringbody(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:
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 methodpath(String): Request pathqueryString(String): Query string without leading?host(String): Host header valuetimestamp(String): ISO8601 timestampnonce(String): Unique noncebody(String): Request body
######## generateSignature(canonicalRequest, hmacKey) → String
Generates HMAC-SHA256 signature for the canonical request.
Returns: Base64-encoded signature
Authentication Flow
- Derive KID:
KID = base64url(SHA256("kid:" + api_key)[0:16]) - Derive HMAC Key: HKDF-SHA256 with salt and info strings
- Generate Nonce: UUID v4 for replay protection
- Generate Timestamp: ISO8601 format with milliseconds
- Build Canonical Request:
METHOD /path query_string host:hostname:port x-ss-date:timestamp x-ss-nonce:nonce body_hash_sha256_hex - Sign:
HMAC-SHA256(hmac_key, canonical_request)→ base64 - 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