Skip to main content

HMAC Auth SDK for Dart

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

Click here to download the SDK

Installation

Add to your pubspec.yaml:

dependencies:
  crypto: ^3.0.3
  uuid: ^4.0.0
  http: ^1.1.0  # For HTTP requests (optional)

Then run:

dart pub get

Quick Start

import 'package:http/http.dart' as http;
import 'safesky_hmac_auth.dart';

void main() async {
  // Your SafeSky API key
  const apiKey = 'YOUR_SAFESKY_API_KEY_HEREHere';
  
  // Generate authentication headers for a GET request
  final headers = generateAuthHeaders(
    apiKey,
    'GET',
    'https://sandbox-public-api.safesky.app/v1/uav?lat=50.6970&lng=4.3908',
    // 'https://public-api.safesky.app/v1/uav?lat=50.6970&lng=4.3908',  // Production
  );
  
  // Make authenticated request
  final response = await http.get(
    Uri.parse('https://sandbox-public-api.safesky.app/v1/uav?lat=50.6970&lng=4.3908'),
    // Uri.parse('https://public-api.safesky.app/v1/uav?lat=50.6970&lng=4.3908'),  // Production
    headers: headers.toMap(),
  );
  
  print(response.body);
}

Usage Examples

The SDK includes 4 complete examples in example.dart:

  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 'package:http/http.dart' as http;
import 'safesky_hmac_auth.dart';

Future<void> getNearbyAircraft() async {
  const apiKey = 'YOUR_SAFESKY_API_KEY_HERE';
  const url = 'https://sandbox-public-api.safesky.app/v1/uav?lat=50.6970&lng=4.3908&rad=20000';
  // const url = 'https://public-api.safesky.app/v1/uav?lat=50.6970&lng=4.3908&rad=20000';  // Production
  
  // Generate auth headers
  final authHeaders = generateAuthHeaders(apiKey, 'GET', url);
  
  // Make request
  final response = await http.get(
    Uri.parse(url),
    headers: {
      ...authHeaders.toMap(),
      'Content-Type': 'application/json',
    },
  );
  
  print('Status: ${response.statusCode}');
  print('Body: ${response.body}');
}

Example 2: POST Request

import 'dart:convert';
import 'package:http/http.dart' as http;
import 'safesky_hmac_auth.dart';

Future<void> postUavPosition() async {
  const apiKey = 'YOUR_SAFESKY_API_KEY_HERE';
  const url = 'https://sandbox-public-api.safesky.app/v1/uav';
  // const url = 'https://public-api.safesky.app/v1/uav';  // Production
  
  final bodyData = [
    {
      'id': 'my_uav_001',
      'altitude': 110,
      'latitude': 50.69378,
      'longitude': 4.39201,
      'last_update': DateTime.now().millisecondsSinceEpoch ~/ 1000,
      'status': 'AIRBORNE',
      'call_sign': 'Test UAV',
      'ground_speed': 10,
      'course': 250,
      'vertical_rate': 5,
    }
  ];
  
  final body = jsonEncode(bodyData);
  
  // Generate auth headers (pass body for POST)
  final authHeaders = generateAuthHeaders(apiKey, 'POST', url, body: body);
  
  // Make request
  final response = await http.post(
    Uri.parse(url),
    headers: {
      ...authHeaders.toMap(),
      'Content-Type': 'application/json',
    },
    body: body,
  );
  
  print('Status: ${response.statusCode}');
}

Example 3: POST Advisory (GeoJSON)

import 'dart:convert';
import 'package:http/http.dart' as http;
import 'safesky_hmac_auth.dart';

Future<void> postAdvisory() async {
  const apiKey = 'YOUR_SAFESKY_API_KEY_HERE';
  const url = 'https://sandbox-public-api.safesky.app/v1/advisory';
  // const url = 'https://public-api.safesky.app/v1/advisory';  // Production
  
  final timestamp = DateTime.now().millisecondsSinceEpoch ~/ 1000;
  
  final bodyData = {
    'type': 'FeatureCollection',
    'features': [
      {
        'type': 'Feature',
        'properties': {
          'id': 'my_advisory_id1',
          'max_altitude': 111,
          'last_update': timestamp,
          'call_sign': 'Advisory test with polygon',
          'remarks': 'Inspection powerlines',
        },
        'geometry': {
          'type': 'Polygon',
          'coordinates': [
            [
              [4.3948, 50.6831], [4.3952, 50.6832], 
              // ... more coordinates ...
              [4.3948, 50.6831],
            ]
          ],
        },
      },
      {
        'type': 'Feature',
        'properties': {
          'id': 'my_advisory_id2',
          'max_altitude': 150,
          'max_distance': 500,
          'last_update': timestamp,
          'call_sign': 'Advisory test with a point',
          'remarks': 'Inspection rails',
        },
        'geometry': {
          'type': 'Point',
          'coordinates': [4.4, 50.7],
        },
      },
    ],
  };
  
  final body = jsonEncode(bodyData);
  final authHeaders = generateAuthHeaders(apiKey, 'POST', url, body: body);
  
  final response = await http.post(
    Uri.parse(url),
    headers: {
      ...authHeaders.toMap(),
      'Content-Type': 'application/json',
    },
    body: body,
  );
  
  print('Status: ${response.statusCode}');
}

Example 4: Manual Step-by-Step

import 'safesky_hmac_auth.dart';

void manualAuthentication() {
  const apiKey = 'YOUR_SAFESKY_API_KEY_HERE';
  
  // Step 1: Derive KID
  final kid = deriveKid(apiKey);
  print('KID: $kid');
  
  // Step 2: Derive HMAC key
  final hmacKey = deriveHmacKey(apiKey);
  
  // Step 3: Generate timestamp and nonce
  final timestamp = generateTimestamp();
  final nonce = generateNonce();
  
  // Step 4: Build canonical request
  final canonicalRequest = buildCanonicalRequest(
    'GET',
    '/v1/uav',
    'lat=50.6970&lng=4.3908',
    'sandbox-public-api.safesky.app',  // Sandbox (default)
    // 'public-api.safesky.app',       // Production
    timestamp,
    nonce,
    '',
  );
  
  // Step 5: Generate signature
  final signature = generateSignature(canonicalRequest, hmacKey);
  print('Signature: $signature');
}

API Reference

Main 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 containing all required headers

Example:

final headers = generateAuthHeaders(
  'YOUR_SAFESKY_API_KEY_HERE',
  'POST',
  'https://sandbox-public-api.safesky.app/v1/uav',
  // 'https://public-api.safesky.app/v1/uav',  // Production
  body: jsonEncode({...}),
);

SafeSkyAuthHeaders Class

Container for authentication headers with convenience methods.

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)Uint8List

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

generateNonce()String

Generates a cryptographically secure nonce (UUID v4 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

# Install dependencies
dart pub get

# Run examples
dart run example.dart

Flutter Integration

This SDK works seamlessly with Flutter:

import 'package:http/http.dart' as http;
import 'package:safesky_hmac_auth/safesky_hmac_auth.dart';

class SafeSkyService {
  final String apiKey;
  
  SafeSkyService(this.apiKey);
  
  Future<List<dynamic>> getNearbyAircraft(double lat, double lng) async {
    final url = 'https://sandbox-public-api.safesky.app/v1/uav?lat=$lat&lng=$lng&rad=20000';
    // final url = 'https://public-api.safesky.app/v1/uav?lat=$lat&lng=$lng&rad=20000';  // Production
    final headers = generateAuthHeaders(apiKey, 'GET', url);
    
    final response = await http.get(
      Uri.parse(url),
      headers: headers.toMap(),
    );
    
    if (response.statusCode == 200) {
      return jsonDecode(response.body) as List;
    }
    
    throw Exception('Failed to load aircraft');
  }
}

Security Notes

  • Keep API keys secret: Never commit them to version control
  • Use environment variables: Store keys securely
  • HTTPS only: Always use HTTPS for production (public-api.safesky.app) and sandbox (sandbox-public-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

  • crypto: ^3.0.3 - Cryptographic operations (SHA-256, HMAC)
  • uuid: ^4.0.0 - UUID v4 generation for nonces
  • http: ^1.1.0 - HTTP client (optional, for examples)

Support

For questions or issues:

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