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.
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}"
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
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');
}
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')}")
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();
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);
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. |
Č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();
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:
- Prijavite se na admin panel
- Otvorite Bezbednost u bočnom meniju
- Kliknite na "Regeneriši API kredencijale"
- Potvrdite akciju u dijalogu
- Sačuvajte novi API secret - prikazuje se samo jednom!
.env fajl), nikada u izvornom kodu ili klijentskim aplikacijama.