✍️ API Signer

KNETApiSigner umożliwia generowanie podpisów kryptograficznych dla requestów API. Obsługuje popularne schematy używane przez AWS, Stripe, i inne serwisy.

💡 Kiedy używać?
  • Integracja z API wymagającymi podpisów (AWS, Stripe webhooks)
  • Tworzenie własnych zabezpieczonych API
  • Weryfikacja webhooków
  • Podpisywanie requestów dla bezpieczeństwa

📦 Import

import rip.nerd.kitsunenet.security.KNETApiSigner

🚀 Szybki start

HMAC-SHA256 (najpopularniejszy)

// Utwórz signer z secret key
val signer = KNETApiSigner.hmacSha256("your-secret-key-here")

// Podpisz request
val request = KNETRequest.post(
    url = "https://api.example.com/orders",
    data = mapOf("product_id" to 123, "quantity" to 2)
)

val signedRequest = signer.sign(request)

// signedRequest zawiera teraz nagłówki:
// X-Signature: a1b2c3d4e5f6...
// X-Timestamp: 1609459200
// X-Nonce: abc123def456

// Wykonaj podpisany request
val response = client.request(signedRequest)

🔐 Obsługiwane algorytmy

Algorytm Metoda Użycie
HMAC-SHA256 hmacSha256() Najpopularniejszy, Stripe, większość API
HMAC-SHA512 hmacSha512() Wyższe bezpieczeństwo
AWS Signature V4 awsStyle() Amazon Web Services
Stripe Webhook stripeStyle() Weryfikacja webhooków Stripe
Simple Hash simpleHash() Proste API key + timestamp
Custom custom() Dowolna logika

🔧 API Reference

hmacSha256()

Tworzy signer HMAC-SHA256 - najpopularniejszy standard.

fun hmacSha256(
    secret: String,
    headerName: String = "X-Signature"
): KNETApiSigner
Przykład
val signer = KNETApiSigner.hmacSha256("my-api-secret")

val request = KNETRequest.post(
    "https://api.example.com/payments",
    mapOf("amount" to 1000, "currency" to "PLN")
)

val signedRequest = signer.sign(request)

// Nagłówki po podpisaniu:
println(signedRequest.headers)
// {
//   "X-Signature": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
//   "X-Timestamp": "1609459200",
//   "X-Nonce": "a1b2c3d4e5f6g7h8"
// }

hmacSha512()

Tworzy signer HMAC-SHA512 - dłuższy hash, wyższe bezpieczeństwo.

fun hmacSha512(
    secret: String,
    headerName: String = "X-Signature"
): KNETApiSigner
Przykład
val signer = KNETApiSigner.hmacSha512("my-secure-secret")

val signedRequest = signer.sign(request)
// Generuje 128-znakowy hex signature

awsStyle()

Uproszczona wersja AWS Signature Version 4.

fun awsStyle(
    accessKey: String,
    secretKey: String,
    region: String = "us-east-1",
    service: String = "execute-api"
): KNETApiSigner
Przykład - AWS API Gateway
val signer = KNETApiSigner.awsStyle(
    accessKey = "AKIAIOSFODNN7EXAMPLE",
    secretKey = "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY",
    region = "eu-central-1",
    service = "execute-api"
)

val request = KNETRequest.get("https://api.aws-example.com/items")
val signedRequest = signer.sign(request)

// Nagłówki:
// Authorization: AWS4-HMAC-SHA256 Credential=AKIAIOSFODNN7EXAMPLE/20210101/eu-central-1/execute-api/aws4_request, Signature=...
// X-Amz-Date: 20210101T120000Z
⚠️ Uwaga

To jest uproszczona implementacja AWS Sig V4. Dla pełnej kompatybilności z AWS SDK użyj oficjalnego AWS SDK dla Androida.

stripeStyle()

Generowanie i weryfikacja podpisów w stylu Stripe webhook.

fun stripeStyle(webhookSecret: String): KNETApiSigner
Przykład - weryfikacja webhook Stripe
// W Twojej obsłudze webhook:
val signer = KNETApiSigner.stripeStyle("whsec_your_webhook_secret")

// Payload z body requestu webhook
val payload = """{"type":"payment_intent.succeeded","data":{"amount":1000}}"""

// Signature z nagłówka Stripe-Signature
val signatureHeader = "t=1609459200,v1=abc123..."

// Timestamp z nagłówka (sekundy)
val timestamp = 1609459200L

// Weryfikacja
val isValid = signer.verify(payload, signatureHeader, timestamp)

if (isValid) {
    // Webhook jest autentyczny
    processWebhook(payload)
} else {
    // Odrzuć - możliwy atak
    Log.w("Webhook", "Invalid signature!")
    return HttpResponse(401, "Invalid signature")
}

simpleHash()

Prosty hash: apiKey + timestamp + secret.

fun simpleHash(apiKey: String, secret: String): KNETApiSigner
Przykład
val signer = KNETApiSigner.simpleHash(
    apiKey = "my-api-key",
    secret = "my-secret"
)

val signedRequest = signer.sign(request)
// Generuje: SHA256(apiKey + timestamp + secret)

custom()

Całkowicie niestandardowa logika podpisywania.

fun custom(
    builder: (SignatureData) -> String
): KNETApiSigner
Przykład - własny algorytm
val signer = KNETApiSigner.custom { data ->
    // data zawiera: method, url, headers, body, timestamp, nonce

    // Twoja własna logika
    val payload = buildString {
        append(data.method)
        append("|")
        append(data.url)
        append("|")
        append(data.timestamp)
        append("|")
        append(data.body ?: "")
    }

    // Użyj dowolnego algorytmu
    MessageDigest.getInstance("SHA-256")
        .digest(payload.toByteArray())
        .joinToString("") { "%02x".format(it) }
}

val signedRequest = signer.sign(request)

sign()

Podpisuje request dodając nagłówki.

fun sign(request: KNETRequest): KNETRequest

Dodaje następujące nagłówki:

generateSignature()

Generuje tylko podpis (bez modyfikacji requestu).

fun generateSignature(request: KNETRequest): String
Przykład
val signature = signer.generateSignature(request)
println("Signature: $signature")
// Możesz użyć tej wartości ręcznie

verify()

Weryfikuje podpis (do webhooków).

fun verify(
    payload: String,
    signature: String,
    timestamp: Long? = null
): Boolean
Przykład
val isValid = signer.verify(
    payload = requestBody,
    signature = receivedSignature,
    timestamp = receivedTimestamp
)

if (!isValid) {
    throw SecurityException("Invalid signature")
}

⚙️ Builder API

Pełna kontrola nad konfiguracją signera:

val signer = KNETApiSigner.builder()
    .algorithm("HmacSHA256")           // Algorytm HMAC
    .secret("my-secret-key")           // Secret key
    .headerName("X-API-Signature")     // Nazwa nagłówka z podpisem
    .includeTimestamp(true)            // Czy dodać timestamp
    .timestampHeaderName("X-Request-Time")  // Nazwa nagłówka timestamp
    .customBuilder { data ->           // Opcjonalnie: custom logika
        // własna implementacja
    }
    .build()
Metoda Typ Domyślnie Opis
algorithm() String "HmacSHA256" Algorytm HMAC
secret() String "" Secret key
headerName() String "X-Signature" Nagłówek z podpisem
includeTimestamp() Boolean true Czy dodać timestamp
timestampHeaderName() String "X-Timestamp" Nagłówek timestamp

📊 SignatureData

Dane dostępne w custom builder:

data class SignatureData(
    val method: String,              // GET, POST, etc.
    val url: String,                 // Pełny URL
    val headers: Map<String, String>, // Nagłówki requestu
    val body: String?,               // Body jako string
    val timestamp: Long,             // Unix timestamp (sekundy)
    val nonce: String?               // Unikalny identyfikator
)

💡 Praktyczne przykłady

Klient API z podpisywaniem

class SecureApiClient(
    private val apiKey: String,
    private val apiSecret: String
) {
    private val client = KNETClient()
    private val signer = KNETApiSigner.hmacSha256(apiSecret)

    suspend fun createOrder(order: Order): OrderResult {
        val request = KNETRequest(
            url = "https://api.secure.com/orders",
            method = "POST",
            headers = mapOf(
                "X-API-Key" to apiKey,
                "Content-Type" to "application/json"
            ),
            data = order.toMap()
        )

        // Podpisz request
        val signedRequest = signer.sign(request)

        // Wykonaj
        val response = client.request(signedRequest)
        return response.json<OrderResult>()
    }
}

Interceptor do automatycznego podpisywania

class SigningInterceptor(
    private val signer: KNETApiSigner
) : KNETInterceptor {

    override suspend fun intercept(
        request: KNETRequest,
        chain: suspend (KNETRequest) -> KNETResponse
    ): KNETResponse {
        // Podpisz każdy request automatycznie
        val signedRequest = signer.sign(request)
        return chain(signedRequest)
    }
}

// Użycie
val client = KNETClient.builder()
    .addInterceptor(SigningInterceptor(
        KNETApiSigner.hmacSha256("secret")
    ))
    .build()

// Teraz każdy request będzie automatycznie podpisany
val response = client.get("https://api.example.com/data")

Obsługa webhooków w serwerze

// W endpoint obsługującym webhook (np. z Ktor/Spring)
@PostMapping("/webhook/stripe")
fun handleStripeWebhook(
    @RequestBody payload: String,
    @RequestHeader("Stripe-Signature") signature: String
): ResponseEntity<*> {

    val signer = KNETApiSigner.stripeStyle(
        System.getenv("STRIPE_WEBHOOK_SECRET")
    )

    // Parsuj timestamp z nagłówka
    val timestamp = signature
        .split(",")
        .find { it.startsWith("t=") }
        ?.substringAfter("t=")
        ?.toLongOrNull()

    // Weryfikuj podpis
    if (!signer.verify(payload, signature, timestamp)) {
        return ResponseEntity.status(401).body("Invalid signature")
    }

    // Sprawdź czy nie za stary (5 min tolerance)
    val now = System.currentTimeMillis() / 1000
    if (timestamp != null && (now - timestamp) > 300) {
        return ResponseEntity.status(401).body("Timestamp too old")
    }

    // Przetwórz webhook
    val event = parseStripeEvent(payload)
    processEvent(event)

    return ResponseEntity.ok().build()
}

Podpisywanie z różnymi nagłówkami

// Niektóre API wymagają określonych nagłówków
val signer = KNETApiSigner.builder()
    .algorithm("HmacSHA256")
    .secret("my-secret")
    .headerName("Authorization")  // Zamiast X-Signature
    .timestampHeaderName("Date")  // Standard HTTP
    .customBuilder { data ->
        // Formatuj jako Bearer token z signature
        val signature = hmacSha256(
            "${data.method}|${data.url}|${data.timestamp}",
            "my-secret"
        )
        "HMAC-SHA256 $signature"
    }
    .build()

🔒 Bezpieczeństwo

⚠️ Najlepsze praktyki
  • Nigdy nie hardcoduj secretów - używaj zmiennych środowiskowych lub bezpiecznego storage
  • Rotuj klucze regularnie - zmieniaj secret keys co jakiś czas
  • Używaj HTTPS - podpisy chronią integralność, nie poufność
  • Weryfikuj timestamp - odrzucaj stare requesty (replay attack)
  • Używaj nonce - zapobiega duplikacji requestów
// ❌ ŹLE - hardcoded secret
val signer = KNETApiSigner.hmacSha256("my-secret-key")

// ✅ DOBRZE - z BuildConfig
val signer = KNETApiSigner.hmacSha256(BuildConfig.API_SECRET)

// ✅ DOBRZE - z EncryptedSharedPreferences
val secret = encryptedPrefs.getString("api_secret", null)
    ?: throw SecurityException("API secret not configured")
val signer = KNETApiSigner.hmacSha256(secret)

// ✅ DOBRZE - z Android Keystore
val secret = keystore.getSecretKey("api_secret")
val signer = KNETApiSigner.hmacSha256(secret)

📚 Zobacz też