czwartek, 4 września 2025

OSDP - Security Channel

W tym poście chciałbym opisać sposób ustawienia Secure Channel do komunikacji z głowicą czytającą karty RFID. 



Jedną z najważniejszych funkcjonalności protokołu OSDP jest usługa Security Channel. Jest to szyfrowany kanał komunikacyjny pomiędzy kontrolerem a urządzeniem peryferyjnym np. czytnikiem kart. 

Security Channel oparty jest na szyfrowaniu AES-128 w trybie CBC wraz z mechanizmem wzajemnego uwierzytelniania. Realizowane to jest na bazie wymiany wyzwań kryptograficznych. Po ustawieniu bezpiecznej sesji, każda wiadomość przesyłana pomiędzy urządzeniami jest szyfrowana oraz zawiera sumę kontrolną MAC. 

Proces zestawienia Security Channel przebiega według specyfikacji OSDP v2.1.7 i obejmuje wymianę kluczy sesyjnych oraz synchronizację ramek. Kanał SC można inicjować ręcznie komendą OSDP_CHLNG, a zakończyć OSDP_SCRYPT.

Warto dodać, że implementacja SC jest kluczowa z punktu widzenia zgodności z normami SIA OSDP Verified oraz wymaganiami niektórych certyfikacji bezpieczeństwa fizycznego. Systemy wykorzystujące OSDP bez SC są wciąż podatne na ataki typu replay, sniffing czy spoofing, a więc nie zapewniają pełnej ochrony danych.

Jeśli integrujesz OSDP w systemie KD, warto od razu zaprojektować obsługę SC – nawet jeśli początkowo będzie wyłączona. Ułatwia to późniejszą migrację do środowiska o podwyższonych wymaganiach bezpieczeństwa.

OSDP należy stosować wraz z Security Channel, stosowanie OSDP bez tej funkcjonalności zapewni jedynie łatwiejsze (wymagające mniej przewodów) sterowanie diodami czy brzęczykiem na głowicy czytającej. Natomiast nie zapewni większego bezpieczeństwa niż Wiegand. 

Standard OSDP można zakupić tutaj. Cena to 200 dolarów, lub 50 dolarów jak jest się członkiem SIA. 
Można też pobrać wcześniejsze wersje online

Obliczenie CRC:


Zacznę od obliczeń CRC. 

  1. #include <stdio.h>
  2. #include <stdint.h>
  3.  
  4. #define PRE_CALCULATE_CRC_CCIT 0x1D0F
  5.  
  6. uint16_t calculateCRC(uint8_t *data, uint8_t size)
  7. {
  8.     int i, j;
  9.     uint16_t crc = PRE_CALCULATE_CRC_CCIT;
  10.     for (i = 0; i < size; i++)
  11.     {
  12.         uint16_t xr = data[i] << 8;
  13.         crc = crc ^ xr;
  14.  
  15.         for (j = 0; j < 8; j++)
  16.         {
  17.             if (crc & 0x8000)
  18.             {
  19.                 crc = (crc << 1) ^ 0x1021;
  20.             }
  21.             else
  22.             {
  23.                 crc = crc << 1;
  24.             }
  25.         }
  26.     }
  27.     return crc & 0xFFFF;
  28. }
  29.  
  30. int main(void)
  31. {
  32.     // Przykładowe dane do testu
  33.     uint8_t testData[] = { 0x53, 0x7F, 0x0D, 0x00, 0x04, 0x6E, 0x00,
  34.                             0x80, 0x25, 0x00, 0x00 };
  35.  
  36.     uint16_t crc = calculateCRC(testData, sizeof(testData));
  37.  
  38.     // Wyświetlenie wyniku
  39.     printf("CRC : 0x%04X\n", crc);
  40.     printf("CRC converted to OSDP: %x,%x", (crc & 0x00FF), (crc >> 8 & 0x00FF));
  41.  
  42.     return 0;
  43. }

Funkcje można przetestować na testowej ramce z wcześniej wspomnianego dokumentu, lub jednej z ramek opisanych poniżej. 

Powyższy przykład daje wynik: 

  1. CRC : 0x386E
  2. CRC converted to OSDP: 6e,38

Co zgadza się z przykład umieszczonym w powyższym dokumencie:

  1. Example 1:
  2. 537F0D00046E0080250000(6E38)
  3. 6E38 is the CRC here (in Little endian format)

Jak można zaobserwować na powyższym przykładzie CRC liczone jest z całej ramki danych. Bez 0xFF, które pojawia się jako prefix przed całą komendą.

Przebieg komunikacji:


Komunikacja przebiega w następujący sposób:

Kierunek Kod komendy Nazwa Opis
TX 0x76 Początek ramki 0xFF
RX 0x76 Początek nowej ramki 0x53
TX 0x77 Adres urządzenia 0x00 - 0x7E
RX 0x78 Niższy bajt długości danych Od SOM do CRC
TX 0x60 Wyższy bajt długości danych Od SOM do CRC
RX 0x48 Flagi kontrolne
TX 0x60 Kod komendy
RX 0x40 Dane dla komendy
TX 0x60 Dane dla komendy
RX 0x40 Dane dla komendy

I tak dalej. W poniższym opisie skupie się na wyżej przedstawionej komunikacji. 

Teraz przejdę przez poszczególne komendy i generowanie na jej podstawie potrzebnych kluczy. 
Opiszę to na podstawie ramek komunikacyjnych odczytanych programem Wireshark. 

Głowica zabezpieczona standardowym hasłem SCBD:

  1. 30 31 32 33 34 35 36 37 38 39 3A 3B 3D 3E 3F

Pierwsza komenda CHLNG:


Na początku wysłamy ramkę CHLNG (0x76). W niej zawieramy RndA, które generujemy w programie. np w taki sposób:

  1. static void generate_random_bytes(uint8_t *buffer) {
  2.     uint32_t timeData[7];
  3.     RTC_ReadTime(timeData);  // Zapełnia: [year, month, day, dow, hour, min, sec]
  4.  
  5.     // Łączenie i mieszanie danych w 8 bajtów
  6.     buffer[0] = (uint8_t)(timeData[0] & 0xFF);               // year low byte
  7.     buffer[1] = (uint8_t)(((timeData[0] >> 8) ^ timeData[1]) & 0xFF); // year high ^ month
  8.     buffer[2] = (uint8_t)((timeData[2] ^ timeData[3]) & 0xFF);        // day ^ dow
  9.     buffer[3] = (uint8_t)((timeData[4] * 13) & 0xFF);         // hour * 13
  10.     buffer[4] = (uint8_t)((timeData[5] * 7 + timeData[1]) & 0xFF); // min * 7 + month
  11.     buffer[5] = (uint8_t)((timeData[6] ^ timeData[5]) & 0xFF);      // sec ^ min
  12.     buffer[6] = (uint8_t)((timeData[2] * timeData[6]) & 0xFF);      // day * sec
  13.     buffer[7] = (uint8_t)(((timeData[4] << 3) ^ timeData[6]) & 0xFF); // hour shifted ^ sec
  14. }

Ramka będzie wyglądała tak:

  1. ff 53 01 13 00 0c 03 11 00 76 e5 e9 0b 12 d5 3b 05 9b 60 f2

Nie jest ona jeszcze szyfrowana. Ponieważ nie został ustawiony Secure Channel. 

Lp Wartosc Nazwa Opis
1 0xFF Początek ramki
2 0x53 Początek nowej ramki
30x01 Adres urządzenia
4 0x13 MSG Control Information
5 0x00 Wyższy bajt długości danych
6 0x0C Flagi kontrolne
7 0x03 SEC_BLK_LEN
8 0x11 SEC_BLK_TYPE 0x11 rozpoczęcie nowego połączenia SC
9 0x00 SEC_BLK_DATA
10 0x76 Kod komendy
11 0xE5 RND_A[0]
12 0xE9 RND_A[1]
13 0x0B RND_A[2]
14 0x12 RND_A[3]
15 0xD5 RND_A[4]
16 0x3B RND_A[5]
17 0x05 RND_A[6]
18 0x9B RND_A[7]
19 0x60 CRC
20 0xF2 CRC


0x0C - Msg Control information - 0000 1100 BIN - co przekłada się na:
  • 0x04 - CKSUM/CRC - Ustawione CRC 16 bitów. Czyli dwa ostatnie bajty wiadomości składa się na CRC 
  • 0x08 - SCB - ustawiony tzw Security Block
W odpowiedzi od czytnika przychodzi następująca ramka danych:

  1. ff 53 81 2b 00 0c 03 12 00 76 20 1d 03 03 00 7c 05 3f 63 be 54 f6 cb 80 24 7e 36 8a 49 ae 3e 25 a8 63 00 7f 24 e0 f5 3e 75 b5 06 bf

Lp Wartosc Nazwa Opis
1 0xFF Początek ramki
2 0x53 Początek nowej ramki
30x81 Adres urządzenia Zwiększony o 0x80
4 0x2B MSG Control Information
5 0x00 Wyższy bajt długości danych
6 0x0C Flagi kontrolne
7 0x03 SEC_BLK_LEN
8 0x12 SEC_BLK_TYPE SC krok 2 
9 0x00 SEC_BLK_DATA
10 0x76 Kod komendy
11 0x20 Klient ID[0]
12 0x1D Klient ID[1]
13 0x03 Klient ID[2]
14 0x03 Klient ID[3]
15 0x00 Klient ID[4]
16 0x7C Klient ID[5]
17 0x05 Klient ID[6]
18 0x3F Klient ID[7]
19 0x63 RND_B[0]
20 0xBE RND_B[1]
21 0x54 RND_B[2]
22 0xF6 RND_B[3]
230xCB RND_B[4]
24 0x80 RND_B[5]
25 0x24 RND_B[6]
26 0x7E RND_B[7]
27 0x36 PD_Cryptogram[0]
28 0x8A PD_Cryptogram[1]
29 0x49 PD_Cryptogram[2]
30 0xAE PD_Cryptogram[3]
31 0x3E PD_Cryptogram[4]
32 0x25 PD_Cryptogram[5]
33 0xA8 PD_Cryptogram[6]
34 0x63 PD_Cryptogram[7]
35 0x00 PD_Cryptogram[8]
36 0x7F PD_Cryptogram[9]
37 0x24 PD_Cryptogram[10]
38 0xE0 PD_Cryptogram[11]
39 0xF5 PD_Cryptogram[12]
40 0x3E PD_Cryptogram[13]
41 0x75 PD_Cryptogram[14]
42 0xB5 PD_Cryptogram[15]
43 0x06 CRC
44 0xBF CRC


Komenda od głowicy przesyła istotne dane które należy zapisać czyli RND_B (PDChallenge) czyli losowe wyzwanie generowane przez czytnik oraz kryptogram. 

Otrzymane dane należy zapisać. 

  1. void osdp_secure_set_rnbb(const uint8_t *val) {
  2.     for(uint8_t i = 0; i<8; i++){
  3.         osdp_SecureChannelData.PDChallenge[i] = *(val + i);
  4.     }
  5. }

  1. void osdp_secure_set_(const uint8_t *val) {
  2.     for(uint8_t i = 0; i<16; i++){
  3.         osdp_SecureChannelData.PDCryptogram[i] = *(val + i);
  4.     }  
  5. }

Po odebraniu i zapisaniu należy wygenerować kilka danych oraz zweryfikować otrzymane wartości. 
Zaczynamy od SMAC1. Jest to pierwszy kod uwierzytelniający. Wyliczający na podstawienie wartości CPChallenge (RNDA). 

  1. void osdp_secure_generate_SMAC1(void) {
  2.     struct AES_ctx ctx;
  3.     uint8_t plaintext[16] = {0x00};
  4.     uint8_t encrypted[16] = {0x00};
  5.  
  6.     AES_init_ctx(&ctx, osdp_SecureChannelData.SCBK_D);
  7.  
  8.     plaintext[0] = 0x01;
  9.     plaintext[1] = 0x01;
  10.     plaintext[2] = osdp_SecureChannelData.CPChallenge[0];
  11.     plaintext[3] = osdp_SecureChannelData.CPChallenge[1];
  12.     plaintext[4] = osdp_SecureChannelData.CPChallenge[2];
  13.     plaintext[5] = osdp_SecureChannelData.CPChallenge[3];
  14.     plaintext[6] = osdp_SecureChannelData.CPChallenge[4];
  15.     plaintext[7] = osdp_SecureChannelData.CPChallenge[5];
  16.     plaintext[8] = 0x00;
  17.     plaintext[9] = 0x00;
  18.     plaintext[10] = 0x00;
  19.     plaintext[11] = 0x00;
  20.     plaintext[12] = 0x00;
  21.     plaintext[13] = 0x00;
  22.     plaintext[14] = 0x00;
  23.     plaintext[15] = 0x00;
  24.     memcpy(encrypted, plaintext, 16);
  25.     AES_ECB_encrypt(&ctx, encrypted);
  26.  
  27.     for(uint8_t i = 0; i<16; i++) {
  28.         osdp_SecureChannelData.smac1[i] = encrypted[i];
  29.     }
  30. }

Teraz pora na SMAC2. Wyliczany na podstawie tych samych danych CPChallenge. Różni się prefiksem. Wyliczam je dopiero po odebraniu odpowiedzi, aby mieć pewność, że secure channel zostanie ustawiony. 

void osdp_secure_generate_SMAC2(void) {

struct AES_ctx ctx;

uint8_t plaintext[16] = {0x00};

uint8_t encrypted[16] = {0x00};


AES_init_ctx(&ctx, osdp_SecureChannelData.SCBK_D);


plaintext[0] = 0x01;

plaintext[1] = 0x02;

plaintext[2] = osdp_SecureChannelData.CPChallenge[0];

plaintext[3] = osdp_SecureChannelData.CPChallenge[1];

plaintext[4] = osdp_SecureChannelData.CPChallenge[2];

plaintext[5] = osdp_SecureChannelData.CPChallenge[3];

plaintext[6] = osdp_SecureChannelData.CPChallenge[4];

plaintext[7] = osdp_SecureChannelData.CPChallenge[5];

plaintext[8] = 0x00;

plaintext[9] = 0x00;

plaintext[10] = 0x00;

plaintext[11] = 0x00;

plaintext[12] = 0x00;

plaintext[13] = 0x00;

plaintext[14] = 0x00;

plaintext[15] = 0x00;


memcpy(encrypted, plaintext, 16);

AES_ECB_encrypt(&ctx, encrypted);


for(uint8_t i = 0; i<16; i++) {

osdp_SecureChannelData.smac2[i] = encrypted[i];

}

}


Klucz sesyjny stosowany do szyfrowania danych ENC:

  1. void osdp_secure_generate_enc(void) {
  2.     struct AES_ctx ctx;
  3.     uint8_t plaintext[16] = {0x00};
  4.     uint8_t encrypted[16] = {0x00};
  5.  
  6.     AES_init_ctx(&ctx, osdp_SecureChannelData.SCBK_D);
  7.  
  8.     plaintext[0] = 0x01;
  9.     plaintext[1] = 0x82;
  10.     plaintext[2] = osdp_SecureChannelData.CPChallenge[0];
  11.     plaintext[3] = osdp_SecureChannelData.CPChallenge[1];
  12.     plaintext[4] = osdp_SecureChannelData.CPChallenge[2];
  13.     plaintext[5] = osdp_SecureChannelData.CPChallenge[3];
  14.     plaintext[6] = osdp_SecureChannelData.CPChallenge[4];
  15.     plaintext[7] = osdp_SecureChannelData.CPChallenge[5];
  16.     plaintext[8] = 0x00;
  17.     plaintext[9] = 0x00;
  18.     plaintext[10] = 0x00;
  19.     plaintext[11] = 0x00;
  20.     plaintext[12] = 0x00;
  21.     plaintext[13] = 0x00;
  22.     plaintext[14] = 0x00;
  23.     plaintext[15] = 0x00;
  24.     memcpy(encrypted, plaintext, 16);
  25.     AES_ECB_encrypt(&ctx, encrypted);
  26.  
  27.     for(uint8_t i = 0; i<16; i++) {
  28.         osdp_SecureChannelData.senc[i] = encrypted[i];
  29.     }
  30. }

Zostało jeszcze obliczenie PDCryptogram oraz CPCryptogram, ze sprawdzeniem czy dane zgadzają się z tymi otrzymanymi od głowicy:

  1. void osdp_secure_generate_cryptograms(void) {
  2.     osdp_generate_cryptogram(osdp_SecureChannelData.CPChallenge, osdp_SecureChannelData.PDChallenge,
  3.             osdp_SecureChannelData.senc, osdp_SecureChannelData.PDCryptogramToVerify);
  4.     osdp_generate_cryptogram(osdp_SecureChannelData.PDChallenge, osdp_SecureChannelData.CPChallenge,
  5.             osdp_SecureChannelData.senc, osdp_SecureChannelData.CPCryptogram);
  6. }
  7.  
  8. static void osdp_generate_cryptogram(const uint8_t *clientRandom,
  9.         const uint8_t *serverRandomNumber, const uint8_t * _enc, uint8_t *output) {
  10.     uint8_t buffer[16];
  11.  
  12.     memcpy(buffer, clientRandom, 8);
  13.     memcpy(buffer + 8, serverRandomNumber, 8);
  14.  
  15.     struct AES_ctx ctx;
  16.     AES_init_ctx(&ctx, _enc);
  17.  
  18.     memcpy(output, buffer, 16);
  19.     AES_ECB_encrypt(&ctx, output);
  20. }
  21.  
  22. uint8_t osdp_secure_verify_PDCryptogram(void) {
  23.     for(uint8_t i=0; i<16;i++) {
  24.         if(osdp_SecureChannelData.PDCryptogram[i] != osdp_SecureChannelData.PDCryptogramToVerify[i]) {
  25.             return 1;
  26.         }
  27.     }
  28.     return 0;
  29. }

Jeśli wszystko się zgadza to przechodzimy do przesłania kolejnej ramki OSDP SC o numerze 0x13. 

Druga komenda CCRYPT:

Ramka wygląda następująco:

  1. ff 53 01 1b 00 0d 03 13 00 77
  2. bd 28 45 19 90 50 f0 4f 47 b8 c7 1c 81 0a e8 d7
  3. 55 b7

Gdzie: 

LpWartoscNazwaOpis
10xFFPoczątek ramki
20x53Początek nowej ramki
30x01Adres urządzenia
40x1BMSG Control Information
50x00Wyższy bajt długości danych
60x0DFlagi kontrolne
70x03SEC_BLK_LEN
80x13SEC_BLK_TYPESC krok 2 
90x00SEC_BLK_DATA
100x77Kod komendyCCRYPT
110xBDCPCryptogram[0]
120x28CPCryptogram[1]
130x45CPCryptogram[2]
140x19CPCryptogram[3]
150x90CPCryptogram[4]
160x50CPCryptogram[5]
170xF0CPCryptogram[6]
180x4FCPCryptogram[7]
190x47CPCryptogram[8]
200xB8CPCryptogram[9]
210xC7CPCryptogram[10]
220x1CCPCryptogram[11]
230x81CPCryptogram[12]
240x0ACPCryptogram[13]
250xE8CPCryptogram[14]
260xD7CPCryptogram[15]
270x55CRC
280xB7CRC

W odpowiedzi dostaniemy komendę RMAC_I, zawierającą dane R-MAC. 

  1. ff 53 81 1b 00 0d 03 14
  2. 01 // wartość 01 oznacza że kryptogram serwera został zaakceptowany 16 bit RMAC
  3. 78
  4. 0d 25 d8 95 0b 04 d4 ec 3c 49 c3 85 24 63 95 70 //16 bit RMAC
  5. 9d 61

Po jej odebraniu przechodzimy do obliczeń RMAC:

  1. void osdp_secure_encrypt_rmac(void) {
  2.     struct AES_ctx ctx;
  3.     struct AES_ctx ctx2;
  4.     uint8_t encrypted[16] = {0x00};
  5.     AES_init_ctx(&ctx, osdp_SecureChannelData.smac1);
  6.     AES_init_ctx(&ctx2, osdp_SecureChannelData.smac2);
  7.  
  8.     memcpy(encrypted, osdp_SecureChannelData.CPCryptogram, 16);
  9.     AES_ECB_encrypt(&ctx, encrypted);
  10.     AES_ECB_encrypt(&ctx2, encrypted);
  11.  
  12.     for(uint8_t i=0; i<16; i++) {
  13.         osdp_SecureChannelData.rmac[i] = encrypted[i];
  14.     }
  15. }

Powyższa funkcja wykonuje podwójne szyfrowanie AES w trybie ECB. Najpierw tworzone są dwa dwa konteksty AES. Jeden jest inicjalizowany kluczem SMAC1, drugi kluczem SMAC2. Następnie pobierany jest CPCryptogram. Dalej następuje jego szyfrowanie za pomocą klucza SMAC1. Wynik tego szyfrowania jest kolejny raz szyfrowany. Tym razem kluczem SMAC2. Tak zaszyfrowane dane są zapisywane jako RMAC. 

Następnie weryfikujemy poprawność danych:

  1. uint8_t osdp_secure_veryfi_rmac (const uint8_t *ptr) {
  2.     for(uint8_t i=0; i<16;i++) {
  3.         if(osdp_SecureChannelData.rmac[i] != ptr[i]) {
  4.             return 1;
  5.         }
  6.     }
  7.     return 0;
  8. }

Jeśli wszystko jest ok to zaczynamy przesyłanie komendy POLL. Secure channel został ustawiony.

Komenda POLL


Jest to też pierwsza komenda do której dodawana jest MAC. 

  1. ff 53 01 0e 00 0e 02
  2. 15 //Z MAC bez szyfrowania
  3. 60
  4. 2b 3b 10 b0 - 4 pierwsze bajty MAC
  5. 5e a1

MAC generowany jest z bajtów ramki, bez 0xFF. Czyli generujemy MAC z tej części ramki:

  1. 53 01 0e 00 0e 02 15 60

Funkcja generująca MAC jest następująca: 

  1. void generate_mac(const uint8_t* message, size_t message_len,
  2.                   const uint8_t smac1[16], const uint8_t smac2[16],
  3.                   const uint8_t cmac[16], const uint8_t rmac[16],
  4.                   int is_command, uint8_t* out_mac)
  5. {
  6.     struct AES_ctx ctx;
  7.     uint8_t iv[16];
  8.     //jak 1 to RMAC jak 0 to cmac
  9.     memcpy(iv, is_command ? rmac : cmac, 16);
  10.  
  11.     size_t offset = 0;
  12.     uint8_t block[16];
  13.     uint8_t prev_block[16];
  14.     memcpy(prev_block, iv, 16);
  15.  
  16.     while (offset < message_len)
  17.     {
  18.         size_t block_len = ((message_len - offset) >= BLOCK_SIZE) ? BLOCK_SIZE : (message_len - offset);
  19.         memset(block, 0, 16);
  20.         memcpy(block, message + offset, block_len);
  21.  
  22.         offset += block_len;
  23.  
  24.         // Last block: apply padding and switch key if necessary
  25.         if (offset >= message_len)
  26.         {
  27.             if (block_len < BLOCK_SIZE)
  28.             {
  29.                 block[block_len] = PADDING_BYTE;
  30.             }
  31.  
  32.             AES_init_ctx_iv(&ctx, smac2, prev_block);
  33.         }
  34.         else
  35.         {
  36.             AES_init_ctx_iv(&ctx, smac1, prev_block);
  37.         }
  38.  
  39.         AES_CBC_encrypt_buffer(&ctx, block, BLOCK_SIZE);
  40.  
  41.         memcpy(prev_block, block, BLOCK_SIZE);  // Next IV
  42.     }
  43.  
  44.     memcpy(out_mac, block, 16);
  45. }

MAC do testów można oczywiście obliczyć też online:


Do ramki dodajemy 2B3B10B0 potem obliczamy CRC i przesyłamy dane. Po obliczeniu MAC należy jeszcze zaktualizować CMAC, jeśli jest to wiadomość wychodząca, RMAC, jeśli jest to wiadomość odbierana od czytnika. 

Jako pierwsza odpowiedź od czytnika może być ACK, bądź LSTATR. Po pierwszym uruchomieniu często wysyłany jest LSTATR (w przypadku mojej głowicy tak to działa).

  1. ff 53 81
  2. 1e 00
  3. 0e 02 18 //Z MAC, dane zaszyfrowane, trzeba odszyfrować przed sprawdzeniem
  4. 48 //LSTATR
  5. cb 12 16 67 97 7c 30 4c bb bb fc 56 ec 33 2c 0d //Dane zaszyfrowane
  6. 2d 3b f4 34 //MAC 0-4
  7. b9 34 //CRC

Funkcja weryfikująca MAC i zastępująca dane:

  1. uint8_t osdp_secure_calculate_check_mac(const uint8_t *rec_msg, const uint8_t rec_msg_len)
  2. {
  3.     if (rec_msg_len < 29) {
  4.         return 2;
  5.     }
  6.  
  7.     uint8_t message_raw[24];
  8.  
  9.     for (size_t i = 0; i < sizeof(message_raw); i++) {
  10.         message_raw[i] = rec_msg[1 + i];
  11.     }
  12.  
  13.     const size_t message_len_raw = sizeof(message_raw);
  14.  
  15.     uint8_t inout = 0;
  16.     uint8_t mac[16] = {0x00};
  17.  
  18.     uint8_t smac1[16] = {0x00};
  19.     uint8_t smac2[16] = {0x00};
  20.     uint8_t cmac[16] = {0x00};
  21.     uint8_t rmac[16] = {0x00};
  22.  
  23.     osdp_secure_get_smac1(&smac1[0]);
  24.     osdp_secure_get_smac2(&smac2[0]);
  25.     osdp_secure_get_rmac(&rmac[0]);
  26.     osdp_secure_get_cmac(&cmac[0]);
  27.  
  28.     generate_mac(message_raw,
  29.             message_len_raw,
  30.             smac1,
  31.             smac2,
  32.             cmac,
  33.             rmac,
  34.             inout,
  35.             mac);
  36.  
  37.     if (inout) {
  38.         osdp_secure_set_cmac(&mac[0]);
  39.     } else {
  40.         osdp_secure_set_rmac(&mac[0]);
  41.     }
  42.  
  43.     if((rec_msg[25] == mac[0]) && (rec_msg[26] == mac[1]) &&
  44.        (rec_msg[27] == mac[2]) && (rec_msg[28] == mac[3])){
  45.         return 0;
  46.     } else {
  47.         return 1;
  48.     }
  49.     return 0xFF;
  50. }

W celu odszyfrowania danych należy się lekko natrudzić. Funkcja wykonywująca te operacje jest następująca:

  1. size_t osdp_secure_decrypt_rec_msg(const uint8_t *cmac, size_t cmac_len, const uint8_t *rec_msg,
  2.                                     size_t rec_msg_len, uint8_t *decryptedFrame, size_t decryptedFrame_len)
  3. {
  4.     if (!cmac || !rec_msg || !decryptedFrame) {
  5.         return 0;
  6.     }
  7.     if (cmac_len < 16) {
  8.         return 0;
  9.     }
  10.     if (rec_msg_len < 25) {
  11.         return 0;
  12.     }
  13.     if (decryptedFrame_len < 16) {
  14.         return 0;
  15.     }
  16.  
  17.     uint8_t decryptiv__CMAC_raw[16] = {
  18.             cmac[0], cmac[1], cmac[2], cmac[3], cmac[4], cmac[5],
  19.             cmac[6], cmac[7], cmac[8], cmac[9], cmac[10], cmac[11],
  20.             cmac[12], cmac[13], cmac[14], cmac[15],
  21.     };
  22.  
  23.     for(uint8_t i=0; i<16; i++) {
  24.         decryptiv__CMAC_raw[i] = ~decryptiv__CMAC_raw[i];
  25.     }
  26.  
  27.     uint8_t encrypted_raw[16] = {
  28.             rec_msg[9], rec_msg[10], rec_msg[11], rec_msg[12],
  29.             rec_msg[13], rec_msg[14], rec_msg[15], rec_msg[16],
  30.             rec_msg[17], rec_msg[18], rec_msg[19], rec_msg[20],
  31.             rec_msg[21], rec_msg[22], rec_msg[23], rec_msg[24]
  32.     };
  33.  
  34.     uint8_t decrypted_raw[sizeof(encrypted_raw)] = {0};
  35.     size_t decrypted_len_raw = decrypt_data(encrypted_raw, sizeof(encrypted_raw), osdp_SecureChannelData.senc, decryptiv__CMAC_raw, decrypted_raw);
  36.  
  37.     for(uint8_t i = 0; i<16; i++) {
  38.         *(decryptedFrame + i) = decrypted_raw[i];
  39.     }
  40.  
  41.     return decrypted_len_raw;
  42. }

Czyli jak widać dane odebrane odszyfrowujemy z pomocą CMAC, na którym należy wykonać operację not. 

Weryfikacja online:


Dalej przesyłamy kolejny raz komendę POLL:

  1. ff 53 01 0e 00 0f 02 15 60
  2. 0b d2 52 a1
  3. dd 86

Na którą dostajemy odpowiedź ACK:

  1. ff 53 81 0e 00 0f 02 16 40
  2. 59 d6 84 5b
  3. 64 7c

W celu szyfrowania danych wychodzących do czytnika np. LED czy Buzzer. Wykonujemy następującą operacje:

  1. size_t osdp_secure_encrypt_send_msg(const uint8_t *rmac, size_t rmac_len, const uint8_t *msgToEncrypt,
  2.                                     size_t msgToEncryptLen, uint8_t *encryptedFrame, size_t encryptedFrame_len) {
  3.     if (!rmac || !msgToEncryptLen || !encryptedFrame) {
  4.         return 0;
  5.     }
  6.     if (rmac_len < 16) {
  7.         return 0;
  8.     }
  9.     if (encryptedFrame_len < 16) {
  10.         return 0;
  11.     }
  12.  
  13.     uint8_t encryptiv__RMAC_LED[16] = {
  14.             rmac[0], rmac[1], rmac[2], rmac[3], rmac[4], rmac[5],
  15.             rmac[6], rmac[7], rmac[8], rmac[9], rmac[10], rmac[11],
  16.             rmac[12], rmac[13], rmac[14], rmac[15],
  17.     };
  18.  
  19.     for(uint8_t i=0; i<16; i++) {
  20.         encryptiv__RMAC_LED[i] = ~encryptiv__RMAC_LED[i];
  21.     }
  22.  
  23.     uint8_t encrypted_raw[16] = {
  24.         0x00
  25.     };
  26.  
  27.     for(uint8_t i=0; i<msgToEncryptLen; i++) {
  28.         encrypted_raw[i] = msgToEncrypt[i];
  29.     }
  30.  
  31.     uint8_t encryptedFrameTmp[16] = { 0x00 };
  32.     size_t encryptedLen = encrypt_data(&encrypted_raw[0],
  33.             (sizeof(encrypted_raw) - 2),
  34.              osdp_SecureChannelData.senc,
  35.              encryptiv__RMAC_LED,
  36.              encryptedFrameTmp);
  37.    
  38.     #if 0  //Weryfikacja
  39.     volatile uint8_t decrypt_frame[16] = {0x00};
  40.     size_t decryptlen = decrypt_data(&encryptedFrameTmp[0],
  41.         sizeof(encryptedFrameTmp),
  42.              osdp_SecureChannelData.senc,
  43.              encryptiv__RMAC_LED,
  44.              decrypt_frame);
  45.     #endif
  46.    
  47.     for(uint8_t i = 0; i<16; i++) {
  48.         *(encryptedFrame + i) = encryptedFrameTmp[i];
  49.     }
  50.  
  51.     return encryptedLen;
  52. }

I tak dalej. Po każdej komendzie należy aktualizować odpowiednio CMAC i RMAC.