Hashingalgorithmen im .NET Framework

Häsch?

Ich möchte nicht erst lang erläutern worum es sich bei Hashing handelt. Lapidar ausgedrückt wird beim Hashing eine mehr oder minder große Eingabemenge in einen Wert umgewandelt der nach Möglichkeit die Eingabemenge eindeutig repräsentiert. Weißt dieser Hashwert Mehrdeutigkeiten auf, besitzt er Kollisionen. Je weniger Kollisionen auftreten desto höher die Qualität einer Hashfunktion, weshalb kollisionsfreie Algorithmen angestrebt werden.


Nonkeyed vs. Keyed

Bei der Verwendung eines Hashs zur Überprüfung der Datenrichtigkeit ist es unabdingbar Angreifer von einer Änderung des gelieferten Hashwerts abzuhalten. Kann er erst einmal seinen eigenen Wert eintragen war der gesamte Aufwand umsonst. Daher ist es sinnvoll einen Schlüssel zu verwenden der den Hash maßgeblich beeinflusst. Auf der anderen Seite sind Schlüssel verwendende Algorithmen aufwändigen und haben bestimmte Anforderungen an die Infrastruktur – der Schlüssel muss ja irgend wo sicher gespeichert oder übertragen werden – die man nicht immer erfüllen will/kann.

Beide Arten haben im .NET Framework grundsätzlich die gleiche Elternklasse welche sich dann in die entsprechenden Implementierungen auffächern. Für die Schlüssel nutzenden Verfahren werden zudem weitere Funktionen über die abstrakte Elternklasses KeyedHashAlgorithm definiert.

Ausschnitt aus der Klassenhirarchie der Hashing Algorithmen

Interessant an dieser Stelle ist vor allem, dass es sich sowohl bei den Klassen in der Ebene unter HashAlgorithm als auch HMAC um abstrakte Klassen mit statischen Factory Methoden handelt, über die eine Standardimplementierung des Algorithmus abgerufen werden kann. Führt man beispielsweise Create für HMAC aus, erhält man ein Objekt vom Typ HMACSHA1.

Berechnung eines Hash

Das vorgehen zum Errechnen eines nonkeyed Hash ist sehr einfach. Wie so oft erstellt man sich ein Objekt und übergibt der entsprechenden Methode die zu verarbeitenden Daten.

MD5 algorithm = MD5.Create();
byte[] hash = algorithm.ComputeHash(data);

Ebenso verhält es sich bei Schlüssel nutzenden Verfahren. Mit dem Unterschied, dass hier dem Konstruktor der tatsächlichen Implementierung oder, bei Nutztung von Create, der Property Key ein entsprechende Schlüssel übergeben werden muss.

Hintergrundrauschen

Viel aufregender als die schnöden Zweizeiler zum generieren der Hashs ist die Implementierung. Denn die notwendigen ComputeHash Methoden werden nicht in jedem Fall von jeder Klasse neu implementiert. Viel mehr überschreiben abgeleitete Klassen die Methoden HashCore, zur Prozessierung während Daten eingelesen werden und HashFinal, zum abschließen der Berechnung nachdem alle Daten eingelesen wurden.

Dies kann dann, wie so oft, zu völlig anderen Umsetzungen führen. Beispielsweise realisieren sowohl SHA1CryptoServiceProvider als auch SHA1Managed den SHA1 Algorithmus, nur eben einmal mit .NET Bordmitteln, eben managed, und einmal unmanged durch einen Aufruf der CryptoAPI.

Des Rätsels Lösung

Damit lichtet sich nicht nur der Schleier über die unterschiedliche Namensgebung, ebenso wie der über der Verwendung der abstrakten Basisklassen für jeden Algorithmus. Es wäre damit einfach möglich zwischen managed und unmanaged umzuschalten, selbst wenn in Zukunft die native Implementierung raus fliegen sollte bzw. es ist Platz für Eigenentwicklungen die dann wie Framework eigene wirken. So geschehen bei der Integration der Next Generation Cryptography API (CNG) ab .NET 3.5. Alle klassen die diese nutzen haben das Postfix Cng wie zum Beispiel SHA1Cng und leuten von der entsprechenden abstrakten Oberklasse, in diesem Fall SHA1, ab.

Extrawürste

Eine Ausnahme an dieser Stelle ist MACTripleDES. Diese Klasse besitzt keine Create Methode. Im Grunde genommen ist sie aber auch „nur“ ein Wrapper um die DES Verschlüsselung als solche. Intern hält es ein Objekt der Klasse TripleDES welches die Eingangsdaten Verschlüsselt sobald ein Hash angefordert wird. Daher erklärt sich auch warum es ein Hashingalgorithmus mit Schlüssel ist.


protected override void HashCore(byte[] rgbData, int ibStart, int cbSize)
{
  if (this.m_encryptor == null)
  {
     this.des.Key = this.Key;
     this.m_encryptor = this.des.CreateEncryptor();
     this._ts = new TailStream(this.des.BlockSize / 8);
     this._cs = new CryptoStream(this._ts, this.m_encryptor, CryptoStreamMode.Write);
  }

  this._cs.Write(rgbData, ibStart, cbSize);
}

Alle Klassen die von HMAC abgeleitet sind verwenden wiederum nonkeyed Hashingalgorithmen und setzen im Konstruktor nur die beiden protected Membervariablen m_hash1 und m_hash2, welche dann von HMAC in den komplett ausprogrammierten Methoden HashFinal und HashCore verwendet werden.

protected override byte[] HashFinal()
{
 if (!this.m_hashing)
 {
    this.m_hash1.TransformBlock(this.m_inner, 0, this.m_inner.Length, this.m_inner, 0);
    this.m_hashing = true;
 }

 this.m_hash1.TransformFinalBlock(new byte[0], 0, 0);
 byte[] hashValue = this.m_hash1.HashValue;
 this.m_hash2.TransformBlock(this.m_outer, 0, this.m_outer.Length, this.m_outer, 0);
 this.m_hash2.TransformBlock(hashValue, 0, hashValue.Length, hashValue, 0);
 this.m_hashing = false;
 this.m_hash2.TransformFinalBlock(new byte[0], 0, 0);

 return this.m_hash2.HashValue;
}