MSDN Magazin > Home > Ausgaben > 2007 > September >  .NET-Themen: Informationen zu CryptoRandom
.NET-Themen
Informationen zu CryptoRandom
Stephen Toub and Shawn Farkas

Codedownload verfügbar unter: NETMatters2007_09.exe (151 KB)
Browse the Code Online

F: Ich generiere in meiner Anwendung einige Zufallszahlen mithilfe der System.Random-Klasse. Eine Kollegin hat meinen Code überprüft und vorgeschlagen, dass ich stattdessen RNGCryptoServiceProvider verwenden sollte. Ich würde dem Vorschlag meiner Kollegin gern folgen, möchte jedoch nicht sämtlichen Code ändern, in dem Random verwendet wird, und RNGCryptoServiceProvider sieht bezüglich der zur Verfügung gestellten Methoden völlig anders aus als Random. Haben Sie eine Idee, wie dies einfacher zu bewerkstelligen wäre?
F: Ich generiere in meiner Anwendung einige Zufallszahlen mithilfe der System.Random-Klasse. Eine Kollegin hat meinen Code überprüft und vorgeschlagen, dass ich stattdessen RNGCryptoServiceProvider verwenden sollte. Ich würde dem Vorschlag meiner Kollegin gern folgen, möchte jedoch nicht sämtlichen Code ändern, in dem Random verwendet wird, und RNGCryptoServiceProvider sieht bezüglich der zur Verfügung gestellten Methoden völlig anders aus als Random. Haben Sie eine Idee, wie dies einfacher zu bewerkstelligen wäre?

A: Sie benötigen einen so genannten Adapter. In Bezug auf Entwurfsmuster ausgedrückt, übernimmt ein Adapter die Funktionalität einer Klasse und passt sie so an, dass sie zu der von einer anderen Klasse bereitgestellten (bzw. erwarteten) Schnittstelle passt. In Ihrem konkreten Fall möchten Sie die Funktionalität von RNGCryptoServiceProvider an die Schnittstelle von Random anpassen. Wie der Zufall es will, ist Random nicht versiegelt, und die öffentlichen Methoden von Random sind durchgehend virtuell. Dies bedeutet, dass die Entwickler von Microsoft® .NET Framework tatsächlich davon ausgegangen sind, dass abgeleitete Implementierungen erstellt werden, die ihre eigenen Algorithmen für das Generieren von Zufallszahlen zur Verfügung stellen und dabei die gleiche Schnittstelle beibehalten. Folglich können wir einen von Random abgeleiteten Typ implementieren, der alle öffentlichen Methoden von Random überschreibt und dadurch neue Implementierungen zur Verfügung stellt, die auf RNGCryptoServiceProvider beruhen.
A: Sie benötigen einen so genannten Adapter. In Bezug auf Entwurfsmuster ausgedrückt, übernimmt ein Adapter die Funktionalität einer Klasse und passt sie so an, dass sie zu der von einer anderen Klasse bereitgestellten (bzw. erwarteten) Schnittstelle passt. In Ihrem konkreten Fall möchten Sie die Funktionalität von RNGCryptoServiceProvider an die Schnittstelle von Random anpassen. Wie der Zufall es will, ist Random nicht versiegelt, und die öffentlichen Methoden von Random sind durchgehend virtuell. Dies bedeutet, dass die Entwickler von Microsoft® .NET Framework tatsächlich davon ausgegangen sind, dass abgeleitete Implementierungen erstellt werden, die ihre eigenen Algorithmen für das Generieren von Zufallszahlen zur Verfügung stellen und dabei die gleiche Schnittstelle beibehalten. Folglich können wir einen von Random abgeleiteten Typ implementieren, der alle öffentlichen Methoden von Random überschreibt und dadurch neue Implementierungen zur Verfügung stellt, die auf RNGCryptoServiceProvider beruhen.
Die RNGCryptoServiceProvider-Klasse, die von der abstrakten RandomNumberGenerator-Klasse abgeleitet wird, stellt zwei öffentliche Methoden zur Verfügung: GetBytes und GetNonZeroBytes. Die GetBytes-Methode akzeptiert ein Bytearray und füllt es mit einer kryptografisch stabilen Folge von Zufallszahlen auf. GetNonZeroBytes leistet genau das Gleiche, garantiert jedoch außerdem, dass keines der Bytes null ist.
Für unsere Zwecke ist die erste Methode besser geeignet. Bei Verwendung der zweiten Methode sinkt die Anzahl der generierbaren Werte, da bei dieser Methode jedes Byte auf den Bereich [1,255] anstelle von [0,255] beschränkt wird. Eigentlich sollte GetNonZeroBytes fast nie verwendet werden, da ein wichtiges Kriterium für eine echte Zufallszahl darin besteht, dass es für jedes Bit gleich wahrscheinlich sein sollte, dass es 0 oder 1 ist. Das ist jedoch nicht gegeben, wenn niemals eine Folge von 15 oder mehr aufeinander folgenden Nullbits auftreten kann, da ja kein Byte 0 sein wird (von den beiden Bytes 0x8001 können bis zu 14 Bits nacheinander 0 sein, um jedoch 15 aufeinander folgende Nullbits zu erhalten, muss mindestens ein Byte 0 sein). Wenn wir GetNonZeroBytes verwenden und einen Zähler für aufeinander folgende Nullbits laufen lassen, können wir sicher sein, dass nach 14 Nullbits das nächste Bit den Wert 1 haben muss. Da dieses Bit mit einer Wahrscheinlichkeit von 100 % den Wert 1 und mit einer Wahrscheinlichkeit von 0 % den Wert 0 hat, wird die Folge automatisch vorhersehbarer und ist somit keine wirklich zufällige Folge mehr. Einer der seltenen Fälle, in denen Sie tatsächlich GetNonZeroBytes verwenden müssen, ist das Generieren von PKCS #1-Leerstellen für die RSA-Verschlüsselung.
Diesen kryptografisch stabilen Zufallszahlengenerator vorausgesetzt, besteht unsere Aufgabe in jedem Fall im Erstellen einer Klasse, die wie Random aussieht, jedoch die Methoden von Random mithilfe der GetBytes-Methode einer zugrunde liegenden RNGCryptoServiceProvider-Klasse implementiert. Unsere Implementierung ist in Abbildung 1 dargestellt.
public class CryptoRandom : Random
{
    private RNGCryptoServiceProvider _rng =
        new RNGCryptoServiceProvider();
    private byte[] _uint32Buffer = new byte[4];

    public CryptoRandom() { }
    public CryptoRandom(Int32 ignoredSeed) { }

    public override Int32 Next()
    {
        _rng.GetBytes(_uint32Buffer);
        return BitConverter.ToInt32(_uint32Buffer, 0) & 0x7FFFFFFF;
    }

    public override Int32 Next(Int32 maxValue)
    {
        if (maxValue < 0)
            throw new ArgumentOutOfRangeException("maxValue");
        return Next(0, maxValue);
    }

    public override Int32 Next(Int32 minValue, Int32 maxValue)
    {
        if (minValue > maxValue) 
            throw new ArgumentOutOfRangeException("minValue");
        if (minValue == maxValue) return minValue;
        Int64 diff = maxValue - minValue;
        while (true)
        {
            _rng.GetBytes(_uint32Buffer);
            UInt32 rand = BitConverter.ToUInt32(_uint32Buffer, 0);

            Int64 max = (1 + (Int64)UInt32.MaxValue);
            Int64 remainder = max % diff;
            if (rand < max - remainder)
            {
                return (Int32)(minValue + (rand % diff));
            }
        }
    }

    public override double NextDouble()
    {
        _rng.GetBytes(_uint32Buffer);
        UInt32 rand = BitConverter.ToUInt32(_uint32Buffer, 0);
        return rand / (1.0 + UInt32.MaxValue);
    }

    public override void NextBytes(byte[] buffer)
    {
        if (buffer == null) throw new ArgumentNullException("buffer");
        _rng.GetBytes(buffer);
    }
}

Unsere CryptoRandom-Klasse ist von der Random-Klasse abgeleitet, die zwei Konstruktoren bereitstellt: einen parameterlosen Konstruktor und einen Konstruktor, der einen Ausgangswert vom Typ Int32 akzeptiert. Der erste Konstruktor delegiert einfach an den zweiten Konstruktor, wobei Environment.TickCount als Ausgangswert verwendet wird, und der zweite Konstruktor berechnet anhand des Ausgangswerts einen Anfangswert für die Folge von Pseudozufallszahlen. Beachten Sie die Verwendung des Begriffs „Folge“ an dieser Stelle. Wenn an Random immer derselbe Ausgangswert übergeben wird, generiert Random auch immer dieselbe Folge von Zufallszahlen. Das ist ein enormer Vorteil beim Debuggen (da Sie beim Debuggen einen Ausgangswert angeben können, der Ihnen reproduzierbare „zufällige“ Ergebnisse garantiert). Für das Generieren von echten Zufallszahlen ist Random jedoch denkbar schlecht geeignet.
Es gibt lediglich 2^32 mögliche Werte vom Typ Int32, die Sie als Ausgangswert nutzen können. Daraus folgt, dass Random maximal 2^32 eindeutige Folgen erzeugen kann (und in der Realität ist diese Anzahl aufgrund der derzeitigen Implementierung von Random tatsächlich noch bedeutend geringer). Von größerer Bedeutung ist, dass ein Angreifer aufgrund des Mangels an kryptografisch stabilen Folgen den Verlauf der in einer Folge generierten Zahlen analysieren und zukünftig zu erwartende „zufällige“ Werte vorausahnen kann.
RNGCryptoServiceProvider nimmt vom Entwickler keinen Ausgangswert zur Berechnung des Anfangswerts an, intern wird jedoch trotzdem ein solcher Ausgangswert verwendet. (Weitere Informationen über die Erzeugung dieses Ausgangswerts finden Sie in der Dokumentation zur CryptGenRandom-Funktion unter msdn2.microsoft.com/aa379942.) RNGCryptoServiceProvider stellt mehrere Konstruktoren bereit. Dazu gehören ein Konstruktor, der eine Zeichenfolge akzeptiert, und ein Konstruktor, der ein Bytearray annimmt. Jedoch ist der einzige Konstruktor, der den Parameter verarbeitet, derjenige Konstruktor, der CSP-Parameter (Cryptographic Service Provider, Kryptografiedienstanbieter) akzeptiert. Für unsere Zwecke können wir diesen Konstruktor ignorieren. Um genau dieselbe API wie bei Random beizubehalten, macht unsere CryptoRandom-Klasse dieselben beiden Konstruktoren wie Random verfügbar. Der Ausgangswert wird jedoch ignoriert (es werden keine Informationen zum Ausgangswert an die zugrunde liegende RNGCryptoServiceProvider-Klasse übergeben). Durch die Bereitstellung der gleichen API können sehr leicht Such- und Ersetzungsvorgänge in Ihrem bereits vorhandenen Code vorgenommen werden.
Die einzige Aufgabe von CryptoRandom besteht nun darin, die RNGCryptoServiceProvider-Klasse an die von Random verfügbar gemachte Schnittstelle anzupassen. Daher enthält CryptoRandom einen privaten RNGCryptoServiceProvider-Member. Die hier gespeicherte Instanz wird zur Bearbeitung aller Anforderungen von zufälligen Werten verwendet. Darüber hinaus enthält CryptoRandom ein weiteres privates Feld, ein Bytearray, das in Verbindung mit der GetBytes-Methode von RNGCryptoServiceProvider verwendet wird. Unsere Implementierung erfordert lediglich ein Bytearray einer einzigen Größe für alle Aufrufe von GetBytes, und da die Random-Klasse nicht threadsicher ist, muss das CryptoRandom auch nicht sein. Daher ist es sinnvoll, diesen Bytepuffer einmal zu erstellen und bei jedem Aufruf wiederzuverwenden. (Beachten Sie, dass der parameterlose Konstruktor von RNGCryptoServiceProvider, so wie er derzeit in .NET Framework 2.0 implementiert ist, threadsichere Instanzen erzeugt. Daher könnten wir unseren privaten Member stattdessen als privaten statischen Member erstellen und müssten somit nicht für jede einzelne Instanz von CryptoRandom eine neue Instanz von RNGCryptoServiceProvider erzeugen. Diese Threadsicherheit ist derzeit jedoch nicht dokumentiert und auch in keiner Weise in die Schnittstelle oder den Vertrag der Klasse integriert. Unter diesen Umständen haben wir uns bei unserer Implementierung nicht auf diese Sicherheit verlassen.)
Nachdem wir uns um die Struktur gekümmert haben, können wir die öffentlichen Methoden implementieren, fünf an der Zahl. Am einfachsten kann NextBytes implementiert werden. NextBytes akzeptiert ein Bytearray und füllt es mit zufälligen Werten auf. Signatur und Verhalten sind identisch mit der GetBytes-Methode von RNGCryptoServiceProvider, wodurch NextBytes problemlos implementiert werden kann:
_rng.GetBytes(buffer);
Die nächste zu implementierende Methode ist NextDouble, die einen zufälligen Wert vom Typ Real im Bereich [0,0; 1,0) zurückgibt. Eine Standardlösung für das Generieren eines solchen Werts besteht im Erzeugen einer nichtnegativen ganzen Zahl und im anschließenden Dividieren dieser ganzen Zahl durch Eins + maximal möglicher Integralwert. Dies lässt sich direkt in eine Implementierung übersetzen:
_rng.GetBytes(_uint32Buffer);
UInt32 rand = BitConverter.ToUInt32(_uint32Buffer, 0);
return rand / (1.0 + UInt32.MaxValue);
Hierbei handelt es sich jedoch nicht um eine perfekte Lösung. Um eine Zufallszahl vom Typ Double zu generieren, müssen wir zuerst definieren, was wir unter einer Zufallszahl vom Typ Double verstehen. Möchten wir, dass jedes Bitmuster mit gleicher Wahrscheinlichkeit generiert wird, oder wünschen wir, dass beim Generieren vieler Zufallszahlen vom Typ Double eine gleichmäßige Verteilung dieser Zahlen über das vorgegebene Intervall erfolgt? Aufgrund der Natur der Implementierung reeller Zahlen in .NET Framework sind das unterschiedliche Dinge. Sehr wahrscheinlich haben wir die zweite Definition im Hinterkopf, d. h. wir möchten nicht vollständig zufällige Bits generieren (wodurch der Gesamtbereich an erzeugten Zahlen in Richtung des unteren Endes des Bereichs verschoben würde). Stattdessen möchten wir die im vorherigen Codeausschnitt gezeigte Teilungsmethode verwenden, wodurch nicht alle Bitmuster mit gleicher Wahrscheinlichkeit auftreten, jedoch die generierten Zahlen eine gleichmäßige Verteilung über den gewünschten Bereich hinweg aufweisen. Paradoxerweise führt dieser Ansatz dazu, dass Tests auf Zufälligkeit im Sinne der oben gegebenen ersten Definition nicht bestanden werden. Es ist jedoch sehr unwahrscheinlich, dass Consumer der NextDouble-Methode darauf bestehen, dass wirklich jedes gültige Bitmuster mit gleicher Wahrscheinlichkeit auftritt.
Jetzt besteht die einzige Zufälligkeit in der Eingabe an die NextDouble-Gleichung zum Erzeugen von Zufallszahlen im Zähler. Der Nenner führt aufgrund der Divisionsoperation letztendlich in Wirklichkeit zu einem geringen Verlust an Zufälligkeit. Wir möchten jedoch wirklich erreichen, dass die Division vorgenommen wird und anschließend beim Normalisieren des Ergebnisses zufällige Bits statt Nullen eingeschoben werden. Ohne Implementierung unserer eigenen Gleitkommabibliothek ist das jedoch nicht machbar, und das Erstellen einer eigenen Gleitkommaimplementierung geht weit über den Rahmen dieses Artikels (und sehr wahrscheinlich auch über Ihre Bedürfnisse) hinaus. Stattdessen können wir versuchen, zu optimieren, was wir haben, und das bedeutet, die meisten zufälligen Bits in die Gleichung einzubeziehen. Da der Zähler die einzige Stelle ist, an der wir Zufälligkeit einführen können, und da ein Wert vom Typ Double eine Länge von 64 Bit hat, könnten wir versuchen, statt einen UInt32 zu generieren und durch UInt32.MaxValue+1 zu teilen, einen zufälligen UInt64 zu generieren und durch UInt64.Max+1 zu teilen. Dadurch verdoppelt sich die Anzahl der zufälligen Bits im Eingabeparameter.
Dies führt unglücklicherweise zu einem anderen Problem. Zahlen mit doppelter Genauigkeit speichern eine Approximation einer reellen Zahl. System.Double entspricht der Norm IEEE 754 für binäre Gleitkommaarithmetik und liefert bestenfalls eine Genauigkeit von 17 Dezimalziffern. UInt64.MaxValue (18446744073709551615) verlangt jedoch leider eine Genauigkeit von 20 Dezimalziffern. Daher ist in der Gleitkommaarithmetik von .NET UInt64.MaxValue gleich UInt64.MaxValue + 1, und ein Einsetzen von UInt64 statt UInt32 in unserer NextDouble-Gleichung ändert daher unseren Bereich von [0,0; 1,0) in [0,0; 1,0], eine Verletzung des Entwurfs der Implementierung von System.Random. Aus diesem Grund haben wir uns entschieden, die in Abbildung 1 dargestellte Implementierung beizubehalten. Denken Sie jedoch daran, dass es da draußen sicherlich bessere (und wahrscheinlich auch kompliziertere) Alternativen gibt, falls Sie mit unserer Lösung ein Problem haben.
Im nächsten Schritt müssen wir die parameterlose Überladung von Next implementieren, mit der eine nichtnegative Zufallszahl vom Typ Int32 abgerufen wird. Wir können dies realisieren, indem wir vier zufällige Bytes generieren, das höherwertige Bit verwerfen und die verbleibenden Bits mithilfe der System.BitConverter-Klasse in einen Int32-Wert konvertieren:
_rng.GetBytes(_uint32Buffer);
return BitConverter.ToInt32(_uint32Buffer, 0) & 0x7FFFFFFF;
Das Generieren von vier zufälligen Bytes liefert uns zufällige Werte im Bereich [Int32.MinValue, Int32.MaxValue]. Um jedoch die Semantik von Next einzuhalten, benötigen wir eigentlich einen Wert im Bereich [0, Int32.MaxValue]. Indem wir lediglich die unteren 31 Bits der 32 zufälligen Bits verwenden, halten wir diese Zuordnung ein, da alle negativen Werte in das nichtnegative Äquivalent umgewandelt werden. Da es die gleiche Anzahl negativer Zahlen und nichtnegativer Zahlen gibt und da das Verwerfen dieses einen Bits eine 1:1-Zuordnung zwischen diesen Werten ergibt, besteht für jeden Wert die gleiche Wahrscheinlichkeit, dass er zurückgegeben wird.
Die Dinge klären sich, wenn wir zu den Überladungen von Next weitergehen, die einen zufälligen Int32-Wert innerhalb eines bestimmten Intervalls generieren. Die zweite Überladung von Next, die nur einen Maximalwert annimmt, ist einfach ein Sonderfall der dritten Überladung, die sowohl ein Minimum als auch ein Maximum annimmt, da die zweite Überladung implizit ein Minimum von 0 aufweist. Daher kann die zweite Überladung einfach an die dritte delegieren. (Die parameterlose Überladung von Next ist ebenfalls ein Sonderfall der dritten, sowohl mit einem impliziten Minimum als auch mit einem impliziten Maximum, wird jedoch aus Gründen, die uns später noch klar werden, besser gesondert behandelt.)
Es gibt zwei offensichtliche Möglichkeiten, einen zufälligen Int32-Wert innerhalb eines bestimmten Intervalls zu erzeugen, und eine weniger offensichtliche. Die erste Möglichkeit besteht im Multiplizieren mit einem Zufallswert vom Typ Double:
return (int)(minValue + (NextDouble() * (maxValue – minValue)));
Die zweite Möglichkeit beginnt mit dem Abrufen eines zufälligen UInt32 und der folgenden Operation:
result = minValue + (randomUInt32 % (maxValue – minValue))
Beide dieser Ansätze sind problematisch. Der erste Ansatz, bei dem Sie mit einem zufälligen Double-Wert multiplizieren, ist aufgrund der bereits besprochenen Tatsache, dass der generierte Double-Wert nicht wirklich kryptografisch zufällig ist, fehlerbehaftet. Die andere Methode (zufällige ganze Zahl mod Bereich) funktioniert perfekt, wenn der Bereich eine geradzahlige Potenz von Zwei ist (vorausgesetzt, dass Sie über vollkommen zufällige Eingabebits verfügen, aus denen Sie auswählen). Der Grund dafür ist, dass die von GetBytes generierten UInt32-Werte 2^32 mögliche Werte annehmen können. Um diese Werte gleichmäßig unter allen Werten im Zielbereich zu verteilen, muss die Größe dieses Zielbereichs den Eingabebereich geradzahlig und in eine Potenz von Zwei teilen können, wie z. B. 2^32. Dies ist nur dann der Fall, wenn die Größe des Zielbereichs ebenfalls eine Potenz von Zwei ist. Wenn die Größe des Zielbereichs keine Potenz von Zwei ist, beginnen die „zufälligen“ Ergebnisse, sich in Richtung niedrigerer Werte zu verschieben.
Um ein Beispiel für diese Verzerrung zu geben, nehmen wir an, dass wir eine Zahl im Bereich [21, 27] wünschen. (In diesem Beispiel verwenden wir aus Gründen der einfacheren Darstellung 8 Bit statt 32 Bit, das Prinzip kann jedoch für die 32 Bit, mit denen wir eigentlich arbeiten, verallgemeinert werden.) Entsprechend der weiter oben aufgeführten Gleichung nehmen wir nun eine Zufallszahl zwischen 0 und 255 mod 6 + 21, um den gewünschten Wert zu berechnen. Wenn die Zufallszahl im Bereich [0, 251] liegt, sind die Ergebnisse hervorragend, da jede der Zahlen [0,5] genau 42 Mal herauskommt. Wenn wir jedoch über 251 hinausgehen, erhalten wir 0 (252), 1 (253) 2 (245) und 3 (255) als Werte aus unserer mod-Operation. Das bedeutet, dass 0, 1, 2 und 3 mit einer Wahrscheinlichkeit von 43/256 von der mod-Operation ausgewählt werden, 4 und 5 hingegen mit einer Wahrscheinlichkeit von lediglich 42/256. Deshalb besteht eine Wahrscheinlichkeit von ca. 16,80 %, dass 21, 22, 23 oder 24 zurückgegeben wird, jedoch lediglich eine Wahrscheinlichkeit von ca. 16,41 % für 25 und 26. Wenn wir Angreifer wären, die versuchen, die Ergebnisse zu erraten, hätten wir langfristig größere Erfolge, wenn wir immer eine Zahl am unteren Ende des Bereichs wählen.
Angesichts dieser Informationen haben wir uns entschieden, die dritte Option in unserer Implementierung in Abbildung 1 zu verwenden. Das soeben erläuterte Problem bezüglich des mod-Ansatzes tritt auf, weil einige Werte im Zielbereich das Potenzial haben, gegenüber anderen Werten leicht bevorzugt zu werden. Wir können dieses Problem beheben, indem wir erkennen, wenn derartige Bevorzugungen auftreten und in diesem Fall einfach einen neuen Versuch starten. Wie wir im vorherigen Beispiel gesehen haben, tritt diese Bevorzugung auf, wenn der zufällig ausgewählte Wert der folgenden Bedingung genügt:
RandomValue >= RandomRange - (RandomRange % TargetRange)
In unserem vorherigen Beispiel beträgt RandomRange 256 und TargetRange 6. Daher treten Bevorzugungen auf, wenn RandomValue >= 252 ist. Bei kleinen Zielbereichen ist die Wahrscheinlichkeit für eine solche Bevorzugung sehr gering. Bei sehr großen Zielbereichen (z. B. bei einem Zielbereich der Größe 2^31+1) liegt die Wahrscheinlichkeit des Auftretens von Bevorzugungen nicht unter 50 %. Folglich können wir einen zufälligen Wert generieren und überprüfen, ob dieser Wert in diesem favorisierten Bereich liegt. Wenn dies der Fall ist, errechnen wir einen neuen zufälligen Wert und prüfen erneut. Fällt der Wert nicht in diesen Bereich, geben wir einfach das Ergebnis der Formel des weiter oben aufgeführten zweiten Ansatzes zurück. Dieser Algorithmus könnte zwar theoretisch in einer Endlosschleife landen, die Wahrscheinlichkeit dafür ist jedoch unendlich klein. In den meisten Fällen werden nur wenige Iterationen benötigt. Selbst im ungünstigsten Fall mit einer Wahrscheinlichkeit von 50 %, dass ein ungünstiger Zufallswert abgerufen wird, sollten nicht zu viele Iterationen notwendig sein, um einen gültigen Wert zu finden. So liegt z. B. die Wahrscheinlichkeit, dass nach 10 Iterationen kein gültiger Wert gefunden wird, unter 0,1 %, und nach 20 Iterationen fällt die Wahrscheinlichkeit unter 0,00001 %. Wenn die Auswirkungen dieses Ansatzes auf die Leistung aus bestimmten Gründen nicht hinnehmbar sind, können Sie zu jeder Zeit auf einen der zuvor erläuterten Ansätze zurückgreifen.
Wir haben eine letzte Betrachtung anzustellen: Möglicherweise bemerken Sie jetzt, dass wir die parameterlose Überladung von Next hinsichtlich der anderen Überladungen ohne Verlust an Zufälligkeit implementieren können. Der für die parameterlose Überladung von Next erforderliche Zielbereich ist 2^31, eine Potenz von Zwei, und führt folglich zu einer ganzzahligen Teilung des Zufallsbereichs von 2^32. Eine Schleife wäre somit nicht erforderlich. Tatsächlich führt unsere Bitmanipulation in unserer Implementierung von Next (&'ing mit 0x7FFFFFFF) zum selben Ergebnis, als wenn wir die Operation „Zufallswert mod Zielbereich 2^31“ durchgeführt hätten. Die Einfachheit der Implementierung der parameterlosen Überladung von Next bewegte uns jedoch dazu, die Dinge so zu belassen, wie sie sind, obwohl wir den vorhandenen Code für die Methode auch einfach delegieren und entfernen hätten können.
Damit ist CryptoRandom vollständig und kann in Szenarios verwendet werden, in denen derzeit Random genutzt wird. Sie müssen nicht einmal Deklarationen der Random-Klasse in Ihrem Code ersetzen. Da CryptoRandom von Random abgeleitet wird, müssen Sie lediglich Instanziierungen von Random durch CryptoRandom ersetzen, und alles Andere erledigt sich von selbst.
Diese Anweisungen bedürfen jedoch einiger Erläuterungen. Wenn Random erweitert werden sollte, wie wir zu Beginn dieses Artikels behauptet haben, werden Sie sich sicherlich fragen, warum die Entwickler von RNGCryptoServiceProvider nicht einfach das getan haben, was wir hier vorgeführt haben, und eine Ableitung dieser Klasse implementiert haben, statt eine eigene RandomNumberGenerator-Basisklasse zu erstellen. Wie wir gesehen haben, sieht die Schnittstelle von RNGCryptoServiceProvider anders aus, da wir nicht die vollständige System.Random-Schnittstelle implementieren können, um echte kryptografische Zufallszahlen zurückzugeben. Stattdessen müssen wir mit einem Satz kryptografisch zufälliger Bits beginnen und diese Bits so verändern, dass sie zur Schnittstelle passen, wobei jedoch ein gewisser Grad an Zufälligkeit verloren geht. Sie als Entwickler können dann RNGCryptoServiceProvider nutzen und die von dieser Klasse bereitgestellten kryptografisch zufälligen Bits an Ihre Bedürfnisse anpassen. Wenn die von uns bereitgestellte CryptoRandom-Klasse Ihren Anforderungen genügt, ist das wunderbar. Andernfalls sollten Sie jetzt wenigstens ein Verständnis für die Probleme, die bei der Implementierung Ihrer eigenen Klasse auftreten können, und für die dabei einzugehenden Kompromisse haben.
Ein Wort der Warnung: CryptoRandom ist mit Leistungseinbußen verbunden. Die Implementierung von Random erzeugt zwar keine kryptografisch stabilen Zufallszahlen, ist jedoch im Vergleich zu RNGCryptoServiceProvider extrem schnell. Bei unseren wissenschaftlich nicht exakten Tests war Random mehr als 100 Mal schneller als CryptoRandom. Außerdem werden Sie, wenn Sie sich auf die Reproduzierbarkeit der von Random erzeugten Folgen verlassen, mit den von CryptoRandom erzeugten nicht reproduzierbaren Folgen nicht glücklich werden. Aber darum geht es doch letztendlich, oder?

Senden Sie Ihre Fragen und Kommentare (in englischer Sprache) an netqa@microsoft.com.


Stephen Toub ist leitender Programmmanager im Parallel Computing Platform-Team von Microsoft. Er schreibt außerdem redaktionelle Beiträge für das MSDN Magazin.

Shawn Farkasist Softwareentwickler im CLR-Team bei Microsoft und für alle Kryptografieklassen in .NET Framework verantwortlich.

Page view tracker