# Weryfikacja sygnatury

Weryfikacja sygnatury kryptograficznej to **absolutny wymóg** i najważniejszy element integracji powiadomień IPN. To
jedyny mechanizm, który daje Ci 100% gwarancji, że dane przesyłane w powiadomieniu (np. kwota i status opłacenia
zamówienia) nie zostały zmanipulowane w drodze (tzw. atak Man-in-the-Middle) i faktycznie pochodzą od systemu SimPay.

Brak weryfikacji sygnatury oznacza, że każdy, kto zna adres Twojego endpointu, może wysłać fałszywe powiadomienie i
wymusić na Twoim sklepie darmowe wydanie towaru.

## Jak wyliczana jest sygnatura?

Do każdego powiadomienia IPN dołączamy pole `signature`. Zawiera ono unikalny ciąg znaków (hash w formacie lowercase
hex) wygenerowany za pomocą algorytmu **SHA-256**.

Sygnaturę generuje się poprzez zestawienie ze sobą kluczowych wartości z powiadomienia, oddzielenie ich separatorem `|`
i dodanie na samym końcu Twojego prywatnego klucza IPN (dostępnego w Panelu Klienta SimPay).

Bazowy ciąg znaków (przed zahashowaniem) wygląda następująco:


```text
type|notification_id|date|data_value1|data_value2|...|data_valueN|TWÓJ_KLUCZ_IPN
```

### Zasady wyliczania

1. **Kolejność ma znaczenie:** Wartości z pola `data` muszą być dołączane do ciągu dokładnie w takiej kolejności, w
jakiej występują w oryginalnym payloadzie JSON.
2. **Ignorowanie brakujących pól:** Jeśli dokumentacja wskazuje, że jakieś pole w obiekcie `data` może być
opcjonalne i faktycznie nie występuje w otrzymanym powiadomieniu – po prostu całkowicie je pomijasz. Nie wstawiasz
pustych separatorów `||`.
3. **Wstawienie pustego stringa zamiast wartości null** Niektóre języki programowania (np. Python) mogą interpretować
null jako `None`, w takim przypadku należy wstawić pusty string zamiast wartości np. `None`, aby zachować spójność ciągu znaków.
4. **Pole `signature`:** Naturalnie, wartość otrzymana w polu `signature` nie bierze udziału w generowaniu Twojego
własnego hasha (służy tylko do końcowego porównania).


## Elastyczność struktury (Bardzo ważne!)

Projektując algorytm walidacji w swoim systemie, musisz wziąć pod uwagę, w jaki sposób rozwijamy nasze API:

* **Główny root powiadomienia** (pola: `type`, `notification_id`, `date`, `data`, `signature`) jest stały i **nigdy się
nie zmieni**.
* **Obiekt `data` jest elastyczny.** Zastrzegamy sobie prawo do dodawania nowych parametrów wewnątrz obiektu `data` w
przyszłości (nigdy nie usuniemy istniejącego, ale możemy dodać nowy).


Dlatego **nie należy "na sztywno" (hardcodować)** konkretnych nazw pól z obiektu `data` w funkcji walidującej. Zamiast
tego powinieneś użyć funkcji, która dynamicznie spłaszcza (tzw. *flatten*) cały otrzymany obiekt JSON i wyciąga z niego
wszystkie wartości. Przygotowaliśmy dla Ciebie gotowe rozwiązania tego problemu w poniższych przykładach.

## Przykładowy ciąg znaków

Załóżmy, że otrzymujesz następujące dane:

* `type`: `example:type`
* `notification_id`: `0196bf93-aca4-7253-8a2d-0941a037f25b`
* `date`: `2025-05-20T16:36:07Z`
* Obiekt `data` zawiera:
  * `status`: `transaction_paid`
  * `amount.currency`: `PLN`
  * `amount.value`: `5.25`
  * `transaction_id`: `123123123123`
  * `meta`: `test`
* Twój klucz to: `keyFromPanel`


Prawidłowo połączony ciąg elementów będzie wyglądał tak:


```text
example:type|0196bf93-aca4-7253-8a2d-0941a037f25b|2025-05-20T16:36:07Z|transaction_paid|PLN|5.25|123123123123|test|keyFromPanel
```

*(Gdyby pole `meta` nie wystąpiło wcale, ciąg kończyłby się na `...|123123123123|keyFromPanel`).*

Tak przygotowany ciąg przekazujesz do funkcji hashującej `sha256` i porównujesz z polem `signature` z powiadomienia.

## Przykłady implementacji (Gotowe SDK)

Aby ułatwić Ci bezpieczną integrację, przygotowaliśmy gotowe klasy walidatorów w najpopularniejszych językach
programowania. Automatycznie radzą one sobie z elastycznością obiektu `data`.

PHP

```php
<?php

class SimPaySignatureValidator
{
    public function isValid(): bool
    {
        $payload = json_decode(@file_get_contents('php://input'), true);
        if (empty($payload)) {
            return false;
        }

        $data = $this->flattenArray($payload);
        $data[] = 'KLUCZ_IPN_USŁUGI';

        $signature = hash('sha256', implode('|', $data));

        return hash_equals($signature, $payload['signature']);
    }

    private function flattenArray(array $array): array
    {
        unset($array['signature']);

        $return = [];

        array_walk_recursive($array, function ($a) use (&$return) {
            $return[] = $a;
        });

        return $return;
    }
}

$validator = new SimPaySignatureValidator();
if(!$validator->isValid()) {
  http_response_code(403);
  echo 'INVALID_SIGNATURE';
  die();
}

// reszta ipn
```

Node

```js
const crypto = require('crypto');

class SimPaySignatureValidator {
    constructor(payload) {
        this.payload = payload;
    }

    isValidHex(str) {
        // Sprawdź czy string ma odpowiednią długość dla SHA-256 (64 znaki)
        if (typeof str !== 'string' || str.length !== 64) {
            return false;
        }

        // Sprawdź czy zawiera tylko znaki hex (0-9, a-f, A-F)
        return /^[0-9a-fA-F]+$/.test(str);
    }

    isValid() {
        if (!this.payload || typeof this.payload !== 'object') {
            return false;
        }

        const data = this.flattenArray(this.payload);
        data.push('SERVICE_IPN_KEY');

        const signature = crypto
            .createHash('sha256')
            .update(data.join('|'))
            .digest('hex');

        const payloadSignature = this.payload.signature || '';

        // Sprawdź czy sygnatura z payload jest prawidłowym hexem
        if (!this.isValidHex(payloadSignature)) {
            return false;
        }

        // Bezpieczne porównanie używając timingSafeEqual
        return crypto.timingSafeEqual(
            Buffer.from(signature, 'hex'),
            Buffer.from(payloadSignature, 'hex')
        );
    }

    flattenArray(array) {
        const arrayClone = { ...array };
        delete arrayClone.signature;

        const result = [];

        const flatten = (obj) => {
            if (Array.isArray(obj)) {
                obj.forEach(item => flatten(item));
            } else if (typeof obj === 'object' && obj !== null) {
                Object.values(obj).forEach(value => flatten(value));
            } else {
                result.push(obj);
            }
        };

        flatten(arrayClone);
        return result;
    }
}

// Przykład użycia z Express.js
const express = require('express');
const app = express();

app.use(express.json());

app.post('/webhook', (req, res) => {
    const validator = new SimPaySignatureValidator(req.body);

    if (!validator.isValid()) {
        return res.status(403).send('INVALID_SIGNATURE');
    }

    res.send('OK');
});

const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
    console.log(`Server running on port ${PORT}`);
});

// Eksport klasy do użycia w innych modułach
module.exports = SimPaySignatureValidator;
```

Python (Flask)

```python
import hashlib
import hmac
import json
from flask import Flask, request, abort

app = Flask(__name__)

class SimPaySignatureValidator:
    SECRET_KEY = 'SERVICE_IPN_KEY'

    def is_valid(self, payload: dict) -> bool:
        if not payload:
            return False

        signature_from_payload = payload.get('signature')
        if not signature_from_payload:
            return False

        data = self.flatten_array(payload)
        data.append(self.SECRET_KEY)
        signature = hashlib.sha256('|'.join(data).encode()).hexdigest()

        return hmac.compare_digest(signature, signature_from_payload)

    def flatten_array(self, obj, exclude_key='signature'):
        result = []

        def _recurse(o):
            if isinstance(o, dict):
                for k, v in o.items():
                    if k == exclude_key:
                        continue
                    _recurse(v)
            elif isinstance(o, list):
                for item in o:
                    _recurse(item)
            else:
                # Zamiana None na pusty string
                result.append(str(o) if o is not None else '')

        _recurse(obj)
        return result

@app.route('/', methods=['POST'])
def validate_signature():
    try:
        payload = request.get_json(force=True)
    except Exception:
        abort(403, 'INVALID_SIGNATURE')

    validator = SimPaySignatureValidator()
    if not validator.is_valid(payload):
        abort(403, 'INVALID_SIGNATURE')

    return Response("OK", status=200, mimetype="text/plain")

if __name__ == '__main__':
    app.run(port=5000)
```

Laravel SDK

```php
// for more details visit https://github.com/SimPaypl/simpay-laravel
<?php

use SimPay\Laravel\Facades\SimPay;

if(!SimPay::payment()->ipnSignatureValid(request())) {
  abort(403, 'invalid signature');
}
```

TypeScript SDK

```ts
// for more details visit https://github.com/SimPaypl/simpay-typescript
import express from "express";
import {
    SimPayClient,
    SimPayIpnError,
    type PaymentIpnNotification,
} from "@simpay/typescript";

const app = express();
app.use(express.json());

const simpay = new SimPayClient({
    api: {
        password: process.env.SIMPAY_API_PASSWORD!,
    },
    service: {
        id: process.env.SIMPAY_SERVICE_ID!,
    },
    ipn: {
        signatureKey: process.env.SIMPAY_IPN_SIGNATURE_KEY!,
        validateSourceIp: true,
    },
});

app.post("/webhooks/simpay/payment", async (req, res) => {
    try {
        await simpay.notifications.payment.verify({
            payload: req.body,
            sourceIp: req.ip,
        });

        const payload = req.body as PaymentIpnNotification;

        // handle rest of your ipn logic here, e.g. update order status in database

        res.status(200).send("OK");
    } catch (error) {
        if (error instanceof SimPayIpnError) {
            res.status(400).send(error.code);
            return;
        }

        res.status(500).send("ERROR");
    }
});
```