Kampis Elektroecke

XMega – AES

XMega Blockdiagramm

Dieser Teil des XMega-Tutorials soll sich mit dem AES Verschlüsselungs- und Entschlüsselungssystem der XMega Mikrocontroller beschäftigen. Es wird ein kleiner Einblick auf die Funktionsweise der Verschlüsselung gegeben und gezeigt, wie das Verschlüsselungssystem genutzt werden kann.

AES – ohne viel Theorie:

Beim AES (Adcanced Encryption Standard) handelt es sich um ein symmetrisches Verschlüsselungsverfahren, welches als Blockchiffre arbeitet. Bei einem symmetrisches Verschlüsselungsverfahren verwenden sowohl Sender, als auch Empfänger den selben Schlüssel um eine Nachricht zu Kodieren, bzw. zu Dekodieren und eine Blockchiffre bildet Blöcke mit einer festen Länge (also einer festen Anzahl Zeichen bzw. Bytes) auf Geheimblöcke mit einer fester Länge ab. Eine andere Chiffremethode ist die Stromchiffre, bei der einzelne Zeichen kodiert werden.

AES wurde als direkter Nachfolger zum älteren DES-Verfahren entwickelt und verwendet eine feste Blocklänge von 128 Bit, sprich jede Nachricht wird in n Blöcke zu je 128 Bit aufgeteilt. Als Schlüssel kann eine Länge von 128, 192 und 256 Bit gewählt werden. Für AES gibt es verschiedene Implementierungsmöglichkeiten, wobei ich in diesem Tutorial nur auf zwei Methoden eingehen möchte…

  • ECB-Modus (Electronic Code Book): Bei diesem Modus werden die einzelnen Datenblöcke unabhängig voneinander verschlüsselt, wodurch alle Klartextmuster auch in den verschlüsselten Daten zu finden sind, wodurch dieser Modus sehr unsicher ist. Daher wird dieser Modus in der Regel nicht genutzt (ein prominentes Beispiel für die Verwendung des ECB-Modus ist (leider) dieses…)

  • CBC-Modus (Cipher Block Chaining): Bei diesem Modus wird der verschlüsselte Datenblock mittels XOR-Verknüpfung mit dem nächsten Datenblock verknüpft und dann verschlüsselt. Für den ersten Datenblock wird ein Initialisierungsvektor genutzt. 

Das AES-Modul der XMega-Mikrocontroller unterstützt AES mit einer Schlüssellänge von 128 Bit. Wenn zusätzlich noch der DMA genutzt wird, kann AES sehr effizient im CBC-Modus genutzt werden.

Implementierung für den XMega:

Der ECB-Modus:

Im ersten Schritt soll der einfachere ECB-Modus implementiert werden. Das Ziel ist es hier einen einzelnen Block aus 16 Byte zu verschlüsseln. Zudem verarbeitet das AES-Modul die einzelnen Bytes eines Blocks nacheinander, weshalb die Zahl in ein Array aus 16 Byte aufgeteilt werden muss.

uint8_t Data[16] = {
	0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
};

Gleiches gilt für den verwendeten Schlüssel:

uint8_t Key[16] = {
	0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,	0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
};

Zur Kontrolle kann man sich über diesen Link das Ergebnis der Verschlüsselung anschauen. Es muss sich das nachfolgende Array ergeben:

uint8_t EncryptedData_ECB[16] = {
	0xEB, 0x52, 0x99, 0x60, 0xF9, 0x9E, 0x03, 0xFF, 0xBD, 0xAF, 0xC1, 0x7D, 0x62, 0xC0, 0xF4, 0x7A
};

Es wird empfohlen das Modul vor der ersten Benutzung zu reseten, damit sich das Modul in einem definierten Zustand befindet.

AES.CTRL = AES_RESET_bm;

Nach dem Reset können die Daten und der Schlüssel in das Modul kopiert werden. Dies erfolgt durch das byteweise Beschreiben des STATE-, bzw. des KEY-Registers:

for(uint8_t i = 0; i < 16; i++)
{
	AES.KEY = *(Key++);
}

for(uint8_t i = 0; i < 16; i++)
{
	AES.STATE = *(Data++);
}

Die Richtung, also ob das Modul verschlüsseln oder entschlüsseln soll, sowie der Start des Moduls wird über das CTRL-Register eingestellt. 

AES.CTRL = (AES.CTRL & (~AES_DECRYPT_bm)) | AES_START_bm;

Sobald das Modul die Verschlüsselung beendet hat, wird das SRIF-Bit, bzw. bei einem Fehler das ERROR-Bit im STATUS-Register gesetzt. Diese beiden Bits werden nach dem Start des Moduls abgefragt und sobald das Modul fertig und kein Fehler aufgetreten ist, werden die Daten ausgelesen und in das Zielarray kopiert.

while(!(AES.STATUS & (AES_SRIF_bm | AES_ERROR_bm)));
if(!(AES.STATUS & AES_ERROR_bm))
{
	for(uint8_t i = 0; i < 16; i++)
	{
		*(EncryptedData++) = AES.STATE;
	}
}

Die Entschlüsselung der Daten erfolgt auf ähnliche Weise, nur in umgekehrter Reihenfolge. Auch die Entschlüsselung ist bereits in Hardware implementiert, alles was der Anwender jetzt noch braucht ist der letzte Schlüssel, der bei der Schlüsselexpansion im AES-Algorithmus entsteht. Dieser neue Schlüssel kann ebenfalls mit dem AES-Modul berechnet werden, indem man ein leeres Array verschlüsselt und dann am Ende des Prozesses den Schlüssel aus dem Modul ausliest:

AES.CTRL = AES_RESET_bm;

for(uint8_t i = 0; i < 16; i++)
{
	AES.KEY = *(Key++);
}

for(uint8_t i = 0; i < 16; i++)
{
	AES.STATE = 0x00;
}

AES.CTRL = (AES.CTRL & (~AES_DECRYPT_bm)) | AES_START_bm;
while(!(AES.STATUS & (AES_SRIF_bm | AES_ERROR_bm)));

if(!(AES.STATUS & AES_ERROR_bm))
{
	for(uint8_t i = 0; i < 16; i++)
	{
		*(Subkey++) = AES.KEY;
	}
	AES.STATUS = AES_SRIF_bm;
}

In diesem Fall muss das SRIF-Bit im STATUS-Register von Hand gelöscht werden, da man keine Daten aus dem Modul ausliest und das Bit somit nicht automatisch gelöscht wird.

Mit dem nun erzeugten Schlüssel können die verschlüsselten Daten wieder entschlüsselt werden.

for(uint8_t i = 0; i < 16; i++)
{
	AES.KEY = *(Key++);
}

for(uint8_t i = 0; i < 16; i++)
{
	AES.STATE = *(EncryptedData++);
}

AES.CTRL |= (AES_START_bm | AES_DECRYPT_bm);
while(!(AES.STATUS & (AES_SRIF_bm | AES_ERROR_bm)));

if(!(AES.STATUS & AES_ERROR_bm))
{
	for(uint8_t i = 0; i < 16; i++)
	{
		*(Data++) = AES.STATE;
	}	
}

Das Vorgehen ist komplett identisch zu dem Vorgehen beim Verschlüsseln, außer das jetzt das DECRYPT-Bit im CTRL-Register gesetzt und nicht gelöscht wird. Das Ergebnis ist der unverschlüsselte Datenblock.

Der CBC-Modus:

Da der ECB-Modus nun funktioniert, soll jetzt der CBC-Modus implementiert werden. Der Unterschied zum ECB-Modus ist, dass der verschlüsselte Block mittels XOR-Funktion auf den nächsten unverschlüsselten Block gerechnet wird. Somit ergibt sich für die Verschlüsselung der folgende Ablauf:

Und für die Entschlüsselung:

Die Verknüpfung mittels XOR-Funktion ist als Hardware im Modul integriert und wird über das XOR-Bit im STATUS-Register aktiviert. Werden nun Daten in das STATE-Register geschrieben, werden diese Daten über eine XOR-Funktion mit bereits vorhandenen Daten im Speicher verknüpft.

Für den CBC-Modus muss ein Initialisierungsvektor festgelegt werden, der dann für die Verschlüsselung bzw. die Entschlüsselung des ersten Blocks genutzt wird.

uint8_t InitVector[16] = {
	0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,	0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
};

In diesem Beispiel sollen nun drei Datenblöcke verschlüsselt werden. Der Schlüssel ist der gleiche wie beim ECB-Modus.

uint8_t Key[16] = {
	0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,	0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
};

uint8_t DataBlock_CBC[16 * 3] = {
	0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07,	0x08, 0x09, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
	0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,	0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
	0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,	0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
};

Auch hier kann das Ergebnis der Verschlüsselung wieder online kontrolliert werden.

Aus dem Ablauf des CBC-Modus ergibt sich die folgende Funktion:

uint8_t* KeyTemp;

for(uint8_t i = 0; i < 16; i++)
{
	AES.STATE = *(InitVector++);
}

AES.CTRL = (AES.CTRL & (~AES_DECRYPT_bm)) | AES_XOR_bm | AES_AUTO_bm;

for(uint8_t Block = 0; Block < BlockCount; Block++)
{
	KeyTemp = Key;
	for(uint8_t i = 0; i < 16; i++)
	{
		AES.KEY = *(KeyTemp++);
	}

	for(uint8_t i = 0; i < 16; i++)
	{
		AES.STATE = *(Data++);
	}

	while(AES_IsBusy());

	if(!(AES.STATUS & AES_ERROR_bm))
	{
		for(uint8_t i = 0; i < 16; i++)
		{
			*(EncryptedData++) = AES.STATE;
		}
	}
}

AES.CTRL = (AES.CTRL & ~(AES_XOR_bm | AES_AUTO_bm));

Zuerst wird der Initialisierungsvektor in das STATE-Register des AES-Moduls geladen und anschließend wird das Modul in den Verschlüsselungsmodus gesetzt, das XOR-Bit gesetzt und der Auto-Trigger aktiviert. Über den Auto-Trigger startet das Modul automatisch, sobald das STATE-Register geladen ist.

Anschließend werden die einzelnen Blöcke geladen. Bei jedem Block wird der Schlüssel neu in das KEY-Register des AES-Moduls kopiert. Als nächstes werden dann die einzelnen Datenbytes des jeweiligen Blocks kopiert. Durch das aktive XOR-Bit werden die kopierten Datenbytes vor dem Speichern mit dem Inhalt des STATE-Registers über eine XOR-Funktion verknüpft. Beim ersten Block entspricht der Inhalt des STATE-Registers dem Initialisierungsvektor, beim zweiten Block entspricht der Inhalt des STATE-Registers dem Ergebnis der Verschlüsselung des ersten Blocks, etc. Sobald das STATE-Register mit Daten gefüllt worden ist, beginnt das Modul automatisch mit dem Verschlüsseln (wegen dem aktiven Auto-Trigger). Dem entsprechend muss nur auf den Abschluss der Operation gewartet werden. Wenn kein Fehler aufgetreten ist, werden die Daten ausgelesen und gespeichert. Sobald alle Daten verarbeitet worden sind, wird der Auto-Trigger deaktiviert und das XOR-Bit gelöscht.

Für die Entschlüsselung ergibt sich ein ähnlicher Ablauf:

uint8_t* KeyTemp;

for(uint8_t Block = BlockCount; Block > 0; Block--)
{
	KeyTemp = Key;
	for(uint8_t i = 0; i < 16; i++)
	{
		AES.KEY = *(KeyTemp++);
	}

	for(uint8_t i = 0; i < 16; i++)
	{
		AES.STATE = *(EncryptedData++);
	}

	AES.CTRL |= (AES_DECRYPT_bm | AES_XOR_bm | AES_START_bm);

	while(AES_IsBusy());

	if(!(AES.STATUS & AES_ERROR_bm))
	{
		if(Block == BlockCount)
		{
			for(uint8_t i = 0; i < 16; i++)
			{
				AES.STATE = *(InitVector++);
			}
		}
		else
		{
		  	uint8_t* LastBlock = EncryptedData - (16 << 1);
			for(uint8_t i = 0; i < 16; i++)
			{
				AES.STATE = *(LastBlock++);
			}
		}

		for(uint8_t i = 0; i < 16; i++)
		{
			*(Data++) = AES.STATE;
		}
		
		AES.CTRL = AES.CTRL & (~AES_XOR_bm);
	}
}

Bei der Entschlüsselung werden zuerst der Schlüssel (auch hier muss wieder der letzte Schlüssel aus der Schlüsselexpansion verwendet werden) und der erste Datenblock eingelesen und anschließend das XOR-Bit  aktiviert, sowie das Modul in den Entschlüsselungsmodus gesetzt und gestartet.

Die entschlüsselten Daten werden nun, falls kein Fehler aufgetreten ist, über eine XOR-Funktion mit dem Initialisierungsvektor verknüpft, falls der entschlüsselte Block der erste Block war. Wurde der erste Block bereits entschlüsselt, werden die entschlüsselten Daten über eine XOR-Funktion mit den verschlüsselten Daten des vorherigen Blocks verknüpft:

uint8_t* LastBlock = EncryptedData - (16 << 1);
for(uint8_t i = 0; i < 16; i++)
{
	AES.STATE = *(LastBlock++);
}

Das Resultat sind die entschlüsselten Datenblöcke.

Verwendung von Interrupts:

Das AES-Modul stellt zudem noch einen Interrupt zur Verfügung, der immer dann ausgelöst wird, wenn eine Ver- oder eine Entschlüsselung durchgeführt worden ist und das Modul gültige Daten enthält.

Die Interrupts können genutzt werden, indem das entsprechende Bitfeld im INTCTRL-Register mit der Priorität der Interrupts gefüllt wird:

AES.INTCTRL = AES_INTLVL_LO_gc;

Danach müssen nur noch die Interrupts im PMIC und in der CPU aktiviert

PMIC.CTRL = PMIC_LOLVLEN_bm;
sei();

und die ISR für das AES-Modul programmiert werden:

ISR(AES_INT_vect)
{
	PORTR.OUTTGL = (0x01 << 0x00);
}

Wenn die vom AES-Modul generierten Daten nicht direkt ausgelesen werden (so wie in meinem Beispiel), muss das Interruptbit in der ISR händisch gelöscht werden, da ansonsten ein weiterer Interrupt auftritt. Andernfalls wird das Bit beim Lesen der Daten automatisch gelöscht.

Das komplette Projekt ist in meinem GitHub-Repository zu finden.

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert