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:
- 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 '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 stringbody(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:
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 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
# 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