eFiskalizacija.cloud

Autentifikacija (HMAC-SHA256)

Svi API zahtevi moraju biti potpisani HMAC-SHA256 algoritmom. Ova autentifikacija je obavezna od verzije 1.2.0 i štiti od neovlašćenog pristupa, replay napada i manipulacije podataka.

Važno: API secret nikada ne šaljete u zahtevu. Koristite ga isključivo za lokalno potpisivanje na vašem serveru. Ako sumnjate da je secret kompromitovan, odmah ga regenerišite u admin panelu.

Obavezni HTTP zaglavlja

Svaki API zahtev mora sadržati sledeća zaglavlja:

Zaglavlje Opis Primer
X-API-Key Vaš API ključ (javni identifikator) efisk_abc123def456
X-Timestamp Trenutni Unix timestamp (u sekundama) 1706012345
X-Signature Base64-encoded HMAC-SHA256 potpis a1b2c3d4e5f6...
Content-Type Tip sadržaja (obavezno za POST) application/json

Algoritam potpisivanja

Potpis se generiše u tri koraka:

Korak 1: Hash tela zahteva

Izračunajte SHA-256 hash celokupnog tela zahteva (request body). Za GET zahteve koristite prazan string.

body_hash = SHA256(request_body)

Korak 2: Formiranje stringa za potpisivanje

Sastavite string od timestamp-a, HTTP metode (velikim slovima), putanje i hash-a tela:

string_to_sign = "{timestamp}{METHOD}{path}{body_hash}"
Napomena: Putanja (path) je samo path komponenta URL-a, bez domena i query string-a. Na primer: /api/multitenant.php/fiskalizacija

Korak 3: Generisanje potpisa

Potpišite string koristeći HMAC-SHA256 sa vašim API secret-om, a rezultat enkodujte u Base64:

signature = Base64(HMAC-SHA256(api_secret, string_to_sign))

Primeri implementacije

Izaberite programski jezik za kompletan primer HMAC autentifikacije:

PHP cURL
<?php
declare(strict_types=1);

$apiKey    = 'vas-api-kljuc';
$apiSecret = 'vas-api-secret';
$baseUrl   = 'https://efiskalizacija.cloud';
$path      = '/api/multitenant.php/fiskalizacija';

// Podaci za fiskalizaciju
$data = [
    'stavke' => [
        [
            'naziv'          => 'Laptop ASUS',
            'kolicina'       => 1,
            'jedinicna_cena' => 89990,
            'pdv_stopa'      => 20
        ]
    ],
    'nacin_placanja' => 'kartica'
];

$body = json_encode($data, JSON_UNESCAPED_UNICODE);

// Korak 1: Hash tela zahteva
$bodyHash = hash('sha256', $body);

// Korak 2: String za potpisivanje
$timestamp = (string) time();
$stringToSign = "{$timestamp}POST{$path}{$bodyHash}";

// Korak 3: HMAC potpis
$signature = base64_encode(
    hash_hmac('sha256', $stringToSign, $apiSecret, true)
);

// Slanje zahteva
$ch = curl_init($baseUrl . $path);
curl_setopt_array($ch, [
    CURLOPT_POST           => true,
    CURLOPT_POSTFIELDS     => $body,
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER     => [
        'Content-Type: application/json',
        'X-API-Key: ' . $apiKey,
        'X-Timestamp: ' . $timestamp,
        'X-Signature: ' . $signature,
    ],
]);

$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);

$result = json_decode($response, true);

if ($httpCode === 200 && isset($result['success']) && $result['success']) {
    echo "Fiskalizacija uspešna! PFR: " . $result['data']['pfr_broj'];
} else {
    echo "Greška: " . ($result['error'] ?? 'Nepoznata greška');
}
Python requests
import hashlib
import hmac
import base64
import time
import requests
import json

api_key = "vas-api-kljuc"
api_secret = "vas-api-secret"
url = "https://efiskalizacija.cloud/api/multitenant.php/fiskalizacija"
path = "/api/multitenant.php/fiskalizacija"

# Podaci za fiskalizaciju
data = {
    "stavke": [
        {
            "naziv": "Laptop ASUS",
            "kolicina": 1,
            "jedinicna_cena": 89990,
            "pdv_stopa": 20
        }
    ],
    "nacin_placanja": "kartica"
}

body = json.dumps(data, ensure_ascii=False)

# Korak 1: Hash tela zahteva
body_hash = hashlib.sha256(body.encode()).hexdigest()

# Korak 2: String za potpisivanje
timestamp = str(int(time.time()))
string_to_sign = f"{timestamp}POST{path}{body_hash}"

# Korak 3: HMAC potpis
signature = base64.b64encode(
    hmac.new(
        api_secret.encode(),
        string_to_sign.encode(),
        hashlib.sha256
    ).digest()
).decode()

# Slanje zahteva
response = requests.post(
    url,
    headers={
        "X-API-Key": api_key,
        "X-Timestamp": timestamp,
        "X-Signature": signature,
        "Content-Type": "application/json"
    },
    data=body
)

result = response.json()

if response.status_code == 200 and result.get("success"):
    print(f"Fiskalizacija uspešna! PFR: {result['data']['pfr_broj']}")
else:
    print(f"Greška: {result.get('error', 'Nepoznata greška')}")
JavaScript / Node.js crypto
const crypto = require('crypto');
const https = require('https');

const apiKey = 'vas-api-kljuc';
const apiSecret = 'vas-api-secret';
const path = '/api/multitenant.php/fiskalizacija';

// Podaci za fiskalizaciju
const data = {
    stavke: [
        {
            naziv: 'Laptop ASUS',
            kolicina: 1,
            jedinicna_cena: 89990,
            pdv_stopa: 20
        }
    ],
    nacin_placanja: 'kartica'
};

const body = JSON.stringify(data);

// Korak 1: Hash tela zahteva
const bodyHash = crypto.createHash('sha256').update(body).digest('hex');

// Korak 2: String za potpisivanje
const timestamp = Math.floor(Date.now() / 1000).toString();
const stringToSign = `${timestamp}POST${path}${bodyHash}`;

// Korak 3: HMAC potpis
const signature = crypto
    .createHmac('sha256', apiSecret)
    .update(stringToSign)
    .digest('base64');

// Slanje zahteva
const postData = Buffer.from(body, 'utf-8');

const options = {
    hostname: 'efiskalizacija.cloud',
    port: 443,
    path: path,
    method: 'POST',
    headers: {
        'Content-Type': 'application/json',
        'Content-Length': postData.length,
        'X-API-Key': apiKey,
        'X-Timestamp': timestamp,
        'X-Signature': signature
    }
};

const req = https.request(options, (res) => {
    let responseBody = '';
    res.on('data', (chunk) => responseBody += chunk);
    res.on('end', () => {
        const result = JSON.parse(responseBody);
        if (res.statusCode === 200 && result.success) {
            console.log(`Fiskalizacija uspešna! PFR: ${result.data.pfr_broj}`);
        } else {
            console.error(`Greška: ${result.error || 'Nepoznata greška'}`);
        }
    });
});

req.write(postData);
req.end();
C# / .NET HttpClient
using System;
using System.Net.Http;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;

public class EfiskalizacijaClient
{
    private readonly string _apiKey;
    private readonly string _apiSecret;
    private readonly HttpClient _httpClient;
    private const string BaseUrl = "https://efiskalizacija.cloud";

    public EfiskalizacijaClient(string apiKey, string apiSecret)
    {
        _apiKey = apiKey;
        _apiSecret = apiSecret;
        _httpClient = new HttpClient { BaseAddress = new Uri(BaseUrl) };
    }

    public async Task<string> FiskalizacijaAsync()
    {
        var path = "/api/multitenant.php/fiskalizacija";

        // Podaci za fiskalizaciju
        var data = new
        {
            stavke = new[]
            {
                new
                {
                    naziv = "Laptop ASUS",
                    kolicina = 1,
                    jedinicna_cena = 89990,
                    pdv_stopa = 20
                }
            },
            nacin_placanja = "kartica"
        };

        var body = JsonSerializer.Serialize(data);

        // Korak 1: Hash tela zahteva
        var bodyHash = ComputeSha256Hash(body);

        // Korak 2: String za potpisivanje
        var timestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds().ToString();
        var stringToSign = $"{timestamp}POST{path}{bodyHash}";

        // Korak 3: HMAC potpis
        var signature = ComputeHmacSignature(stringToSign);

        // Priprema zahteva
        var request = new HttpRequestMessage(HttpMethod.Post, path)
        {
            Content = new StringContent(body, Encoding.UTF8, "application/json")
        };

        request.Headers.Add("X-API-Key", _apiKey);
        request.Headers.Add("X-Timestamp", timestamp);
        request.Headers.Add("X-Signature", signature);

        // Slanje zahteva
        var response = await _httpClient.SendAsync(request);
        var responseBody = await response.Content.ReadAsStringAsync();

        using var doc = JsonDocument.Parse(responseBody);
        var root = doc.RootElement;

        if (response.IsSuccessStatusCode &&
            root.TryGetProperty("success", out var success) &&
            success.GetBoolean())
        {
            var pfr = root.GetProperty("data").GetProperty("pfr_broj").GetString();
            return $"Fiskalizacija uspešna! PFR: {pfr}";
        }

        var error = root.TryGetProperty("error", out var err)
            ? err.GetString()
            : "Nepoznata greška";
        return $"Greška: {error}";
    }

    private string ComputeSha256Hash(string input)
    {
        using var sha256 = SHA256.Create();
        var bytes = sha256.ComputeHash(Encoding.UTF8.GetBytes(input));
        return BitConverter.ToString(bytes).Replace("-", "").ToLower();
    }

    private string ComputeHmacSignature(string input)
    {
        using var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(_apiSecret));
        var hash = hmac.ComputeHash(Encoding.UTF8.GetBytes(input));
        return Convert.ToBase64String(hash);
    }
}

// Korišćenje:
// var client = new EfiskalizacijaClient("vas-api-kljuc", "vas-api-secret");
// var result = await client.FiskalizacijaAsync();
// Console.WriteLine(result);
Java / Spring Boot RestTemplate
package rs.efiskalizacija.client;

import org.springframework.http.*;
import org.springframework.web.client.RestTemplate;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;

import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.time.Instant;
import java.util.*;

public class EfiskalizacijaClient {

    private final String apiKey;
    private final String apiSecret;
    private final RestTemplate restTemplate;
    private final ObjectMapper objectMapper;
    private static final String BASE_URL = "https://efiskalizacija.cloud";

    public EfiskalizacijaClient(String apiKey, String apiSecret) {
        this.apiKey = apiKey;
        this.apiSecret = apiSecret;
        this.restTemplate = new RestTemplate();
        this.objectMapper = new ObjectMapper();
    }

    public String fiskalizacija() throws Exception {
        String path = "/api/multitenant.php/fiskalizacija";

        // Podaci za fiskalizaciju
        Map<String, Object> stavka = new LinkedHashMap<>();
        stavka.put("naziv", "Laptop ASUS");
        stavka.put("kolicina", 1);
        stavka.put("jedinicna_cena", 89990);
        stavka.put("pdv_stopa", 20);

        Map<String, Object> data = new LinkedHashMap<>();
        data.put("stavke", Collections.singletonList(stavka));
        data.put("nacin_placanja", "kartica");

        String body = objectMapper.writeValueAsString(data);

        // Korak 1: Hash tela zahteva
        String bodyHash = sha256Hash(body);

        // Korak 2: String za potpisivanje
        String timestamp = String.valueOf(Instant.now().getEpochSecond());
        String stringToSign = timestamp + "POST" + path + bodyHash;

        // Korak 3: HMAC potpis
        String signature = hmacSignature(stringToSign);

        // Priprema zahteva
        HttpHeaders headers = new HttpHeaders();
        headers.setContentType(MediaType.APPLICATION_JSON);
        headers.set("X-API-Key", apiKey);
        headers.set("X-Timestamp", timestamp);
        headers.set("X-Signature", signature);

        HttpEntity<String> request = new HttpEntity<>(body, headers);

        // Slanje zahteva
        ResponseEntity<String> response = restTemplate.exchange(
            BASE_URL + path,
            HttpMethod.POST,
            request,
            String.class
        );

        JsonNode root = objectMapper.readTree(response.getBody());

        if (response.getStatusCode() == HttpStatus.OK &&
            root.has("success") &&
            root.get("success").asBoolean()) {
            String pfr = root.get("data").get("pfr_broj").asText();
            return "Fiskalizacija uspešna! PFR: " + pfr;
        }

        String error = root.has("error")
            ? root.get("error").asText()
            : "Nepoznata greška";
        return "Greška: " + error;
    }

    private String sha256Hash(String input) throws Exception {
        MessageDigest digest = MessageDigest.getInstance("SHA-256");
        byte[] hash = digest.digest(input.getBytes(StandardCharsets.UTF_8));
        return bytesToHex(hash);
    }

    private String hmacSignature(String input) throws Exception {
        Mac mac = Mac.getInstance("HmacSHA256");
        SecretKeySpec keySpec = new SecretKeySpec(
            apiSecret.getBytes(StandardCharsets.UTF_8),
            "HmacSHA256"
        );
        mac.init(keySpec);
        byte[] hash = mac.doFinal(input.getBytes(StandardCharsets.UTF_8));
        return Base64.getEncoder().encodeToString(hash);
    }

    private String bytesToHex(byte[] bytes) {
        StringBuilder sb = new StringBuilder();
        for (byte b : bytes) {
            sb.append(String.format("%02x", b));
        }
        return sb.toString();
    }
}

// Korišćenje:
// EfiskalizacijaClient client = new EfiskalizacijaClient(
//     "vas-api-kljuc",
//     "vas-api-secret"
// );
// String result = client.fiskalizacija();
// System.out.println(result);

Mehanizmi zaštite

HMAC autentifikacija pruža višeslojnu zaštitu:

Napad Zaštita Kako funkcioniše
Replay napad Timestamp validacija Zahtev mora stići u roku od ±5 minuta od trenutka potpisivanja. Stariji zahtevi se odbijaju.
Man-in-the-middle HMAC potpis Bez poznavanja secret-a napadač ne može generisati validan potpis, čak ni ako presretne zahtev.
Body tampering Body hash u potpisu Svaka izmena tela zahteva invalidira potpis jer se hash tela uračunava u string za potpisivanje.
Timing napad Timing-safe poređenje Server koristi hash_equals() za poređenje potpisa, sprečavajući timing analizu.
Bezbednosna napomena: Uvek koristite HTTPS za komunikaciju sa API-jem. HMAC štiti integritet zahteva, ali ne šifruje sadržaj - to obezbeđuje TLS/HTTPS.

Česte greške i rešenja

1. Neispravan timestamp

Greška: Request timestamp expired

Uzrok: Sat na vašem serveru nije sinhronizovan ili koristite milisekunde umesto sekundi.

// POGREŠNO - milisekunde
$timestamp = (string) (time() * 1000);

// ISPRAVNO - sekunde (Unix timestamp)
$timestamp = (string) time();
Savet: Koristite NTP sinhronizaciju na vašem serveru. Tolerancija je ±5 minuta, ali što bliži timestamp, to bolja zaštita od replay napada.

2. Pogrešna putanja u potpisu

Greška: Invalid signature

Uzrok: Putanja u potpisu ne odgovara stvarnoj putanji zahteva.

// POGREŠNO - pun URL
$stringToSign = "{$timestamp}POST" .
    "https://efiskalizacija.cloud/api/multitenant.php/fiskalizacija" .
    "{$bodyHash}";

// POGREŠNO - sa query stringom
$stringToSign = "{$timestamp}POST" .
    "/api/multitenant.php/fiskalizacija?debug=1" .
    "{$bodyHash}";

// ISPRAVNO - samo path
$stringToSign = "{$timestamp}POST" .
    "/api/multitenant.php/fiskalizacija" .
    "{$bodyHash}";

3. Neslaganje tela zahteva

Greška: Invalid signature

Uzrok: Telo korišćeno za potpis razlikuje se od poslatog tela (najčešće zbog ponovnog kodiranja JSON-a).

// POGREŠNO - potpis i slanje koriste različita tela
$bodyForSign = json_encode($data);
$bodyForSend = json_encode($data, JSON_PRETTY_PRINT); // različit string!

// ISPRAVNO - isti string za oba
$body = json_encode($data, JSON_UNESCAPED_UNICODE);
$bodyHash = hash('sha256', $body);
// ... koristite isti $body za cURL

4. HTTP metoda malim slovima

Greška: Invalid signature

Uzrok: Metoda u stringu za potpisivanje mora biti VELIKIM slovima.

// POGREŠNO
$stringToSign = "{$timestamp}post{$path}{$bodyHash}";

// ISPRAVNO
$stringToSign = "{$timestamp}POST{$path}{$bodyHash}";

5. Binary vs Hex HMAC izlaz

Greška: Invalid signature

Uzrok: HMAC mora biti izračunat u binarnom formatu pre Base64 enkodiranja.

// POGREŠNO - hex pa Base64 (dvostruko enkodiranje)
$hmacHex = hash_hmac('sha256', $stringToSign, $apiSecret); // hex string
$signature = base64_encode($hmacHex);

// ISPRAVNO - binary pa Base64
$hmacBinary = hash_hmac('sha256', $stringToSign, $apiSecret, true); // binary
$signature = base64_encode($hmacBinary);

Regeneracija kredencijala

API ključ i secret možete regenerisati u admin panelu:

  1. Prijavite se na admin panel
  2. Otvorite Bezbednost u bočnom meniju
  3. Kliknite na "Regeneriši API kredencijale"
  4. Potvrdite akciju u dijalogu
  5. Sačuvajte novi API secret - prikazuje se samo jednom!
Upozorenje: Regeneracija kredencijala odmah invalidira stare. Svi zahtevi sa starim ključem/secret-om biće odbijeni. Ažurirajte kredencijale u vašem sistemu pre nego što zatvorite stranicu.
Savet za produkciju: Čuvajte API secret u environment varijablama (npr. .env fajl), nikada u izvornom kodu ili klijentskim aplikacijama.