środa, 16 sierpnia 2023

ATxmega - Szyfrowana transmisja AES

W tym poście chciałbym opisać sposób przygotowania szyfrowanej ramki danych, z wykorzystaniem szyfrowania AES.


Poniżej przejdę do opisu poszczególnych funkcji wykonywanych do szyfrowania transmisji. 

Cała transmisja jest szyfrowana z użyciem AESXXX. Oba urządzenia muszą posiadać wgrany klucz, potrzebny do odszyfrowania przesłanej wiadomości.

Plan działania jest następujący:

1 - Przygotowanie ramki danych
2 - Szyfrowanie wiadomości
3 - Przesłanie wiadomości

Drugie urządzenie:

1 - Odebranie przesłanej wiadomości.
2 - Odszyfrowanie wiadomości
3 - Przygotowanie ramki z odpowiedzią.
4 - Szyfrowanie ramki
5 - Przesłanie odpowiedzi.

Należy pamiętać aby oba urządzenia posiadały ten sam klucz. Nie powinien on być przesyłany pomiędzy nimi w sposób jawny.  Może on być zakodowany w urządzeniu w sposób standardowy, czyli przed podłączeniem urządzeń wgrywamy do nich te same klucze. Bądź na podstawie parametrów np. numerów seryjnych, daty czy czegokolwiek innego. Klucze można zapisać w pamięci EEPROM, najlepiej w sposób niebezpośredni. 

Innym rozwiązaniem jest wgranie domyślnych kluczy na wszystkie urządzenia. Początek transmisji rozpoczyna się od przesłania nowych kluczy na urządzenie, które będą zaszyfrowane kluczem stałym. Aby zabezpieczyć się przed ewentualnymi nie zsynchronizowanymi resetami, można dołożyć stałą ramkę danych, która poinformuje drugie urządzenie, że potrzebuje otrzymać nowe klucze. W taki sposób można rozpoczynać transmisję po restarcie zasilania w urządzeniach. Pozwoli to na ominięcie konieczności wgrywania tych samych kluczy do urządzeń. Dodatkowo, klucze nie będą stałe tylko będą zmieniane w czasie działania.

Pierwsza cześć dotyczy biblioteki wykorzystującej tylko operacje programistyczne. Klucze zdefiniowane są w tablicach o różnej długości, w zależności od wyboru długości klucza (128, 192 lub 256 bitów):

  1. uint8_t aesKey128[16] = {0x24, 0x38, 0x4D, 0x5F, 0x11,
  2.                         0x89, 0xA5, 0xB4, 0x2C, 0x6E,
  3.                         0x1F, 0x26, 0x75, 0x48, 0x97,
  4.                         0x24};
  5.  
  6. uint8_t aesKey192[24] = {0x24, 0x38, 0x4D, 0x5F, 0x11,
  7.                         0x89, 0xA5, 0xB4, 0x2C, 0x6E,
  8.                         0x1F, 0x26, 0x75, 0x48, 0x97,
  9.                         0x1F, 0x26, 0x75, 0x48, 0x97,
  10.                         0x1F, 0x26, 0x75, 0x24};
  11.  
  12. uint8_t aesKey256[32] = {0x24, 0x38, 0x4D, 0x5F, 0x11,
  13.                             0x89, 0xA5, 0xB4, 0x2C, 0x6E,
  14.                             0x1F, 0x26, 0x75, 0x48, 0x97,
  15.                             0x24, 0x38, 0x4D, 0x5F, 0x11,
  16.                             0x89, 0xA5, 0xB4, 0x2C, 0x6E,
  17.                             0x1F, 0x26, 0x75, 0x48, 0x97,
  18.                             0x24, 0x56};

Dla każdego typu kluczy rozmiar bloku wynosi 128 bitów. Co oznacza, że algorytm może jednorazowo zaszyfrować 16 bajtów danych. Do szyfrowania i odszyfrowywania należy przekazywać bloki o wspomnianej długości. 

Podczas użytkowania wywoływane bezpośrednio przez programistę są jedynie funkcje szyfrujące i deszyfrujące:

  1. void aes128_encrypt(uint8_t * state, uint8_t * key);
  2. void aes128_decrypt(uint8_t * state, uint8_t * key);
  3.  
  4. void aes192_encrypt(uint8_t * state, uint8_t * key);
  5. void aes192_decrypt(uint8_t * state, uint8_t * key);
  6.  
  7. void aes256_encrypt(uint8_t * state, uint8_t * key);
  8. void aes256_decrypt(uint8_t * state, uint8_t * key);

Wywołanie funkcji jest następujące:

  1. aes128_encrypt(&encryptBuffer[0], aesKey128);
  2. aes192_encrypt(&encryptBuffer[0], aesKey192);
  3. aes256_encrypt(&encryptBuffer[0], aesKey256);

Należy tutaj pamiętać, że bloki szyfrowania wynoszą 16 bajtów. Przykładowo, jeśli chcemy zaszyfrować 20 bajtów danych, to funkcja musi być wywołana dwukrotnie. Wynikiem będzie blok danych wynoszący 32 bajty. Taki blok w całości musi zostać przesłany do drugiego urządzenia, aby mogło ono z powodzeniem odszyfrować przesłaną ramkę danych.

W celu zabezpieczenia transmisji warto dołożyć do bloku z danymi zestaw losowych bajtów danych.  Pozwoli to na uzyskanie różnych ramek, podczas przesyłania tych samych komend.

Wracając do wspomnianego przykładu. Jeśli blok danych do zaszyfrowania wynosi 20 bajtów, to cała ramka danych musi być zapisana w bloku wynoszącym 32 bajty. Funkcja szyfrująca wywoływana dwa razy:

  1. uint8_t encryptBuffer[32] = {0x00};
  2.  
  3. for(uint8_t i = 0; i<20; i++)
  4. {
  5.     encryptBuffer[i] = rand() % (200 - 100 + 1)) + 1;
  6. }
  7.  
  8. aes128_encrypt(&encryptBuffer[0], aesKey128);
  9. aes128_encrypt(&encryptBuffer[16], aesKey128);

Do odszyfrowania bloku danych należy wywołać następujące funkcje:

  1. aes128_decrypt(&encryptedBuffer[0], aesKey128);
  2. aes192_decrypt(&encryptedBuffer[0], aesKey192);
  3. aes256_decrypt(&encryptedBuffer[0], aesKey256);

Przykładowa implementacja biblioteki AES jest do pobrania z dysku Google. Całość można znaleźć też ogólnodostępną na platformie Github. 

Druga opcja polega na wykorzystywaniu wbudowanego modułu szyfrującego AES, w jaki układ ATXmega jest wyposażony. Pozwala on na obsługę szyfrowania z kluczami 128 (AVR1318)

Do testów wykorzystam następujące dane, wynik szyfrowania sprawdziłem na stronie:

  1. AES ECB
  2. Dane: 101112131415161718191A1B1C1D1E1F
  3. Klucz: 11223344556677889912345678919293
  4. Dane zaszyfrowane: 0BA179D5A61A5b53C994A92BE967E850
  5.  
  6. uint8_t testKey[BLOCK_LENGTH] = {
  7.     0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88,
  8.     0x99, 0x12, 0x34, 0x56, 0x78, 0x91, 0x92, 0x93};
  9.  
  10. uint8_t DataDoEncrypt[16] = {
  11.         0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17,
  12.         0x18, 0x19, 0x1A, 0x1B, 0x1C, 0x1D, 0x1E, 0x1F };

Testowy program wygląda następująco:

  1. void testFunct()
  2. {
  3.     if(!AESTest_LastsubkeyGenerate(testKey, testLastsubkey)) {
  4.         success = false;
  5.         return;
  6.     }
  7.    
  8.     success = AES_DecipherTest(&testKey[0],&DataToEncrypt[0],&EncryptedBuffer[0],&DecryptedBuffer[0]);
  9. }
  10.  
  11. bool AES_DecipherTest(uint8_t *klucz_AES, uint8_t *dataToEncryptPtr, uint8_t *encryptedBufferPtr, uint8_t *decrytedBufferPtr)
  12. {
  13.     uint8_t i = 0;
  14.    
  15.     for (i=0; i<16; i++)
  16.     {
  17.         Plain_AES[i]=0; //wyliczane jest z 0wego bufora
  18.     }
  19.     sysclk_enable_module(SYSCLK_PORT_GEN, SYSCLK_AES);
  20.    
  21.     aes_software_reset();
  22.     aes_configure(AES_ENCRYPT, AES_MANUAL, AES_XOR_OFF);
  23.     aes_isr_configure(AES_INTLVL_OFF);
  24.     aes_set_key((uint8_t*)klucz_AES);
  25.     aes_write_inputdata(dataToEncryptPtr);
  26.     aes_start();
  27.    
  28.     do { } while (aes_is_busy());   // Wait until AES is finished or an error occurs
  29.        
  30.     if (!aes_is_error()) {
  31.         aes_read_outputdata(encryptedBufferPtr);    // Store the result if not error
  32.     }
  33.     else
  34.     {
  35.         return false;
  36.     }
  37.    
  38.     aes_software_reset();
  39.     aes_configure(AES_DECRYPT, AES_MANUAL, AES_XOR_OFF);    // Set AES encryption of a single block in manual mode.
  40.     aes_isr_configure(AES_INTLVL_OFF);      // Disable the AES interrupt
  41.     aes_set_key(testLastsubkey);    // Load key into AES key memory. dla decrypt musi być zmodyfikowany
  42.     aes_write_inputdata(encryptedBufferPtr);    // Load data into AES state memory.
  43.     aes_start();        // Start encryption
  44.    
  45.     do { } while (aes_is_busy());   // Wait until AES is finished or an error occurs.
  46.  
  47.     /* Store the result if not error. */
  48.     if (!aes_is_error())
  49.     {
  50.         aes_read_outputdata(decrytedBufferPtr);
  51.     }
  52.     else
  53.     {
  54.         return false;
  55.     }
  56.    
  57.     return true;
  58. }

  1. bool AESTest_LastsubkeyGenerate(t_key key, t_key last_sub_key)
  2. {
  3.     bool keygen_ok;
  4.     uint8_t i;
  5.  
  6.     /* Before using the AES it is recommended to do an AES software reset to
  7.      * put the module in known state, in case other parts of your code has
  8.      * accessed the AES module. */
  9.     aes_software_reset();
  10.  
  11.     /* Set AES encryption of a single block in manual mode. */
  12.     aes_configure(AES_ENCRYPT, AES_MANUAL, AES_XOR_OFF);
  13.  
  14.     /* Load key into AES key memory. */
  15.     aes_set_key(key);
  16.  
  17.     /* Load dummy data into AES state memory. */
  18.     for (i = 0; i < BLOCK_LENGTH; i++) {
  19.         AES.STATE = 0x00;
  20.     }
  21.  
  22.     /* Start encryption. */
  23.     aes_start();
  24.  
  25.     do {
  26.         /* Wait until AES is finished or an error occurs. */
  27.     } while (aes_is_busy());
  28.  
  29.     /* If not error. */
  30.     if (!aes_is_error()) {
  31.         /* Store the last subkey. */
  32.         aes_get_key(last_sub_key);
  33.         aes_clear_interrupt_flag();
  34.         keygen_ok = true;
  35.     } else {
  36.         aes_clear_error_flag();
  37.         keygen_ok = false;
  38.     }
  39.     return keygen_ok;
  40. }

W celu zabezpieczenia układu przed niepowołanym odczytem warto dodatkowo stosować LockBity. Domyślnie ich wartość jest ustawiona na 0xFF, co dopuszcza przeprowadzania wszystkich operacji na układzie. 


Można wybrać dostępne opcje pomiędzy:

  • Read and write not allowed RWLOCK
  • Read not allowed RLOCK
  • Write not allowed WLOCK
  • No locks NOLOCK

Najbezpieczniej, ustawić tutaj wartość 0x00, i zablokować wszystkie operacje na układzie.


Wyjątkiem jest jedynie format danych. Cały czas można sformatować dane. Po formacie układ nie będzie już zawierał istotnych informacji, a ciągle można zaktualizować oprogramowanie.