MSDN Magazin > Home > Ausgaben > 2006 > November >  Security Briefs: Probleme eingeschränkter ...
Security Briefs
Probleme eingeschränkter Benutzer und geteiltes Wissen
Keith Brown

Codedownload verfügbar unter: SecurityBriefs2006_11.exe (165 KB)
Browse the Code Online
In diesem Monat werde ich mehr Leserfragen beantworten. Ich berichte über Windows Communication Foundation-Webdienste, die unter normalen Benutzerkonten ausgeführt werden, und über die Verwendung von geteiltem Wissen und doppelter Kontrolle von Schlüsseln zum Schutz von Kreditkartendaten.

F Warum startet mein einfacher Windows® Communication Foundation-Dienst nicht, wenn ich ihn als Nicht-Administrator ausführe?
F Warum startet mein einfacher Windows® Communication Foundation-Dienst nicht, wenn ich ihn als Nicht-Administrator ausführe?

A Zunächst möchte ich sagen, dass es gut ist, dass Sie Ihren Code unter einem normalen Benutzerkonto testen! Dies ist ein wichtiger Aspekt beim Testen, den Entwickler nicht übersehen dürfen.
A Zunächst möchte ich sagen, dass es gut ist, dass Sie Ihren Code unter einem normalen Benutzerkonto testen! Dies ist ein wichtiger Aspekt beim Testen, den Entwickler nicht übersehen dürfen.
Der erste Ansatz, den ich zum Herausfinden der Ursache des Problems empfehle, ist, Filemon und Regmon zu starten. Diese beiden Programme sind bei Sysinternals (das vor kurzem von Microsoft aufgekauft wurde) unter www.sysinternals.com erhältlich. Suchen Sie nach fehlgeschlagenen Versuchen, Dateien oder Registrierungsschlüssel zu öffnen. Leider können diese Tools nicht jeden Fehler diagnostizieren, der dieses Problem verursachen könnte. Selbst die einfachsten in Windows Communication Foundation implementierten Webdienste, die einen HTTP-Kanal abhören, werden standardmäßig nicht als normaler Benutzer ausgeführt; es sei denn, sie werden in IIS bereitgestellt. Dies liegt daran, dass der HTTP-Kanal von Windows Communication Foundation mithilfe des Treibers HTTP.SYS seinen Listener einrichtet. HTTP.SYS lässt nicht zu, dass Nicht-Administratoren Listener ohne ausdrückliche Genehmigung eines Administrators registrieren.
Zur Veranschaulichung dieses Problems habe ich einen einfachen Webdienst erstellt, der aus zwei Dateien besteht: der Quelle für den Dienst und einer Anwendungskonfigurationsdatei. In Abbildung 1 wird der Code für den Dienst angezeigt. Wenn Sie diesen Dienst als Administrator starten, läuft alles bestens. Doch wenn Sie versuchen, ihn als normaler Benutzer auszuführen, geht gar nichts mehr. In Abbildung 2 sehen Sie die Ausnahme, die auftritt, wenn Sie Beta 2 von Microsoft® .NET Framework 3.0 verwenden.
System.ServiceModel.Diagnostics.CallbackException: A user callback threw an exception.  Check the exception stack and inner exception to determine the callback that failed. 
---> System.NullReferenceException: Object reference not set to 
     an instance of an object.
   at System.ServiceModel.Channels.DatagramChannelDemuxer`2.
      OnListenerClosed(Object source, EventArgs args)
   at System.ServiceModel.Channels.CommunicationObject.OnClosed()
   --- End of inner exception stack trace ---
   at System.ServiceModel.Channels.CommunicationObject.OnClosed()
   at System.ServiceModel.Channels.CommunicationObject.Abort()
   at System.ServiceModel.Channels.ChannelListenerBase.OnAbort()
   at System.ServiceModel.Channels.SecurityChannelListener`1.OnAbort()
   at System.ServiceModel.Channels.CommunicationObject.Abort()
   at System.ServiceModel.Channels.ChannelListenerBase.OnAbort()
   at System.ServiceModel.Channels.CommunicationObject.Abort()
   at System.ServiceModel.Dispatcher.ChannelDispatcher.OnAbort()
   at System.ServiceModel.Channels.CommunicationObject.Abort()
   at System.ServiceModel.ServiceHostBase.OnAbort()
   at System.ServiceModel.Channels.CommunicationObject.Abort()
   at System.ServiceModel.Channels.CommunicationObject.Close(
      TimeSpan timeout)
   at System.ServiceModel.Channels.CommunicationObject.Close()
   at System.ServiceModel.Channels.CommunicationObject.Dispose()
   at Program.Main(String[] args) in 
      C:\work\HelloService\Program.cs:line 24
    	
//hello.cs - simple WCF Web service that listens on HTTP
using System;
using System.ServiceModel;

[ServiceContract(Namespace = "http://example.org")]
public interface IHello {
    [OperationContract]
    void SayHello();
}

public class Hello : IHello {
    public void SayHello() {
        Console.WriteLine("Hello");
    }
}

class ConsoleHost {
    static void Main(string[] args) {
        try {
            using (ServiceHost host = new ServiceHost(typeof(Hello), 
                    new Uri("http://localhost:8080/MyServices/"))) {
                host.Open();

                Console.WriteLine(
                    "Service is listening, press any key to quit.");
                Console.ReadKey();
            }
        }
        catch (Exception x) {
            Console.WriteLine(x);
        }
    }
}

<!--app.config-->
<configuration>
  <system.serviceModel>
    <services>
      <service name="Hello">
        <endpoint address="Hello"
                  binding="wsHttpBinding"
                  contract="IHello"/>
      </service>
    </services>
  </system.serviceModel>
</configuration>
Sie können den Dienst in IIS bereitstellen. Das wäre eine Möglichkeit, das Problem zu lösen. Doch für manche Personen ist dies keine Option. Wenn Sie z. B. Windows Communication Foundation in einer Windows Forms-basierten Anwendung verwenden, um Benachrichtigungen über einen HTTP-Webdienst zu empfangen, müssen Sie dieses Problem auf andere Weise beheben. Befolgen Sie dazu die Anleitung in diesem Artikel. Wenn Sie in einem Windows NT®-Dienst bereitstellen, haben Sie dasselbe Problem.
Die Lösung des Problems ist einfach. Sie benötigen lediglich einen Administrator, der Ihrer Anwendung in HTTP.SYS eine Namespacereservierung gewährt. Dadurch sagt der Administrator im Wesentlichen "Dieser Benutzer darf über HTTP auf ein URL-Präfix warten, das ich festlege". Ein Administrator kann diese Abhörberechtigungen einem einzelnen Benutzer oder einer Gruppe gewähren.
Ihr Setupprogramm kann mithilfe der HTTP-API, die leider noch keinen öffentlichen .NET-Wrapper hat, programmgesteuert eine Namespacereservierung gewähren. In Abbildung 3 wird Beispielcode angezeigt, mit dem Sie die ersten Schritte durchführen können, wenn Sie in C# arbeiten. In diesem speziellen Beispiel wird von Ihnen erwartet, ein Benutzer- oder Gruppenkonto dem Namen nach weiterzugeben, und das muss dann von einem Mitglied der lokalen Administratorengruppe ausgeführt werden. Dieser Code ruft außerdem mit P/Invoke die HTTP-API auf. Das bedeutet, er muss vollständig vertrauenswürdig ausgeführt werden, was die beweisbasierte Sicherheit in der Common Language Runtime (CLR) anbetrifft. Wenn Sie dies in einen Microsoft Installer verpacken, sollte alles in Ordnung sein. Doch wenn Sie das von einer heruntergeladenen Anwendung aus versuchen, die mit teilweisem Vertrauen unter einem normalen Benutzerkonto ausgeführt wird, funktioniert es nicht.
using System;
using System.Xml;
using System.Security.Principal;
using System.Runtime.InteropServices;

public class ReserveHttpNamespace {
    static void Main(string[] args) {
        if (args.Length != 2) {
            Console.WriteLine(
                "Usage: reserveHttpNamespace " +
                "prefix account");
            return;
        }
        try {
            ModifyReservation(args[0], args[1], false);
            Console.WriteLine("Success!");
        }
        catch (Exception x) {
            Console.WriteLine(x.Message);
        }
    }

    static void ModifyReservation(
            string urlPrefix, string accountName,
            bool removeReservation) {
        string sddl = createSddl(accountName);
        HTTP_SERVICE_CONFIG_URLACL_SET configInfo;
        configInfo.Key.UrlPrefix = urlPrefix;
        configInfo.Param.Sddl = sddl;
        HTTPAPI_VERSION httpApiVersion =
            new HTTPAPI_VERSION(1, 0);
        int errorCode = HttpInitialize(httpApiVersion,
            HTTP_INITIALIZE_CONFIG, IntPtr.Zero);
        if (0 != errorCode) 
            throw getException("HttpInitialize", errorCode);
        try {
            // do our best to delete any existing ACL
            errorCode = HttpDeleteServiceConfigurationAcl(
                IntPtr.Zero, HttpServiceConfigUrlAclInfo,
                ref configInfo, Marshal.SizeOf(
                    typeof(HTTP_SERVICE_CONFIG_URLACL_SET)),
                IntPtr.Zero);
            if (removeReservation) {
                if (0 != errorCode) throw getException(
                    "HttpDeleteServiceConfigurationAcl",
                    errorCode);
                return;
            }
            errorCode = HttpSetServiceConfigurationAcl(
                IntPtr.Zero, HttpServiceConfigUrlAclInfo,
                ref configInfo, Marshal.SizeOf(
                    typeof(HTTP_SERVICE_CONFIG_URLACL_SET)),
                IntPtr.Zero);
            if (0 != errorCode) throw getException(
                "HttpSetServiceConfigurationAcl", errorCode);
        }
        finally {
            errorCode = HttpTerminate(
                HTTP_INITIALIZE_CONFIG, IntPtr.Zero);
            if (0 != errorCode) throw getException(
                "HttpTerminate",errorCode);
        }
    }

    static Exception getException(string fcn, int errorCode) {
        return new Exception(
            string.Format("{0} failed: {1}",
            fcn, getWin32ErrorMessage(errorCode)));
    }

    static string createSddl(string account) {
        string sid = new NTAccount(account).Translate(
            typeof(SecurityIdentifier)).ToString();
        // DACL that allows generic execute for the user
        // specified by account.
        // See help for HTTP_SERVICE_CONFIG_URLACL_PARAM
        // for details on what this means.
        return string.Format("D:(A;;GX;;;{0})", sid);
    }

    static string getWin32ErrorMessage(int errorCode) {
        return Marshal.GetExceptionForHR(HRESULT_FROM_WIN32(errorCode));
    }
    static int HRESULT_FROM_WIN32(int errorCode) {
        if (errorCode <= 0) return errorCode;
        return (int)((0x0000FFFFU & ((uint)errorCode)) | (7U << 16) |
            0x80000000U);
    }

    // P/Invoke stubs from http.h
    const int HttpServiceConfigUrlAclInfo = 2;
    const int HTTP_INITIALIZE_CONFIG = 2;
    [StructLayout(LayoutKind.Sequential)]
    struct HTTPAPI_VERSION {
        public HTTPAPI_VERSION(short maj, short min) {
            Major = maj; Minor = min;
        }
        short Major;
        short Minor;
    }
    [StructLayout(LayoutKind.Sequential)]
    struct HTTP_SERVICE_CONFIG_URLACL_KEY {
        [MarshalAs(UnmanagedType.LPWStr)]
        public string UrlPrefix;
    }
    [StructLayout(LayoutKind.Sequential)]
    struct HTTP_SERVICE_CONFIG_URLACL_PARAM {
        [MarshalAs(UnmanagedType.LPWStr)]
        public string Sddl;
    }
    [StructLayout(LayoutKind.Sequential)]
    struct HTTP_SERVICE_CONFIG_URLACL_SET {
        public HTTP_SERVICE_CONFIG_URLACL_KEY Key;
        public HTTP_SERVICE_CONFIG_URLACL_PARAM Param;
    }
    [DllImport("httpapi.dll", ExactSpelling = true,
            EntryPoint = "HttpSetServiceConfiguration")]
    static extern int HttpSetServiceConfigurationAcl(
        IntPtr mustBeZero, int configID,
        [In] ref HTTP_SERVICE_CONFIG_URLACL_SET configInfo,
        int configInfoLength, IntPtr mustBeZero2);
    [DllImport("httpapi.dll", ExactSpelling = true,
            EntryPoint = "HttpDeleteServiceConfiguration")]
    static extern int HttpDeleteServiceConfigurationAcl(
        IntPtr mustBeZero, int configID,
        [In] ref HTTP_SERVICE_CONFIG_URLACL_SET configInfo,
        int configInfoLength, IntPtr mustBeZero2);
    [DllImport("httpapi.dll")]
    static extern int HttpInitialize(
        HTTPAPI_VERSION version,
        int flags, IntPtr mustBeZero);
    [DllImport("httpapi.dll")]
    static extern int HttpTerminate(int flags,
        IntPtr mustBeZero);
}
Sie können auch ein Tool namens HTTPCFG.EXE verwenden. Es ist Bestandteil der Supporttools, die sich auf dem Installationsdatenträger Ihres Betriebssystems im Unterverzeichnis SUPPORT befinden. Mithilfe dieses Tools können Sie vorhandene Namespacereservierungen auflisten, in etwa so:
httpcfg query urlacl
Und so erstellen Sie mit HTTPCFG eine Namespacereservierung für ein Benutzerkonto:
httpcfg set urlacl –u http://+:8080/MyServices/ -a D:(A;;GX;;;S-1-5-21-1681502023-2202157333-1552196959-1028)
Das Argument -a für HTTPCFG steht für ACL oder Access Control List (Zugriffssteuerungsliste), das Sie in einer ziemlich obskuren Sprache namens Security Description Definition Language (SDDL) angeben müssen. An dieser Stelle ist nicht genug Platz, um SDDL zu erklären. Diese Übung bleibt also Ihnen überlassen. (Weitere Information zu SDDL finden Sie unter „Security Descriptor Definition Language“, möglicherweise in englischer Sprache.) Das GX, das Sie sehen, bezieht sich auf die GENERIC_EXECUTE-Berechtigung. HTTP.SYS geht davon aus, dass Sie diese Berechtigung gewähren, wenn Sie eine Berechtigung zum Warten auf das Präfix gewähren möchten.
Das Argument -u ist das URL-Präfix, das HTTP.SYS die Form der URLs mitteilt, auf die Sie beim Gewähren der Berechtigung verweisen. Die Form der URL sieht wie folgt aus. Das Schema muss http oder https sein (in Kleinbuchstaben). Der Host berücksichtigt die Groß- und Kleinschreibung nicht und kann die Platzhalter + oder * verwenden (dies wird in Kürze näher erläutert). Der Port ist ein Ganzzahlwert und ist erforderlich, auch wenn es um den Standardport für das Schema geht. Darauf folgt ein optionaler, die Groß- und Kleinschreibung nicht berücksichtigender, relativer URI (in dem vorangegangenen Beispiel ist dies /MyServices). Und schließlich müssen Sie die Zeichenfolge mit einem nachstehenden Schrägstrich beenden; dabei spielt es keine Rolle, ob Sie einen relativen URI verwenden.
Im Verborgenen registriert der HTTP-Kanal von Windows Communication Foundation Namespaces wie die weiter oben gezeigte URL. Dies geschieht mithilfe der HTTP-API, die über einige Regeln zum Weiterleiten von Anforderungen an HTTP-Listener verfügt. Durch die Methode, mit der Sie den Host in der URL angeben, wird die Priorität festgelegt, in der Ihr Listener berücksichtigt wird, wenn eine Anforderung eingeht, die mit mehr als einem Präfix des Listeners übereinstimmt. Zum Beispiel registriert eine Anwendung foo.com:8080/, während eine andere Anwendung foo.com:8080/MyServices/ registriert. Im Allgemeinen hat die ausdrücklichere Registrierung Vorrang. Es gibt jedoch auch Platzhalter, mit denen die Funktionsweise dieser Priorisierung gesteuert werden kann. Eine Anwendung registriert unter Umständen http://*:8080/. Da dieser Listener den Platzhalter * verwendet, nimmt er sich des Bodensatzes dessen an, was die anderen Listener nicht annehmen. Anders ausgedrückt hat * eine geringe Priorität bzw. ist ein schwacher Platzhalter. Wenn die Anwendung andererseits http://+:8080/ registriert, erhält sie oberste Priorität. Dann gehen alle HTTP-Anforderungen an Port 8080 an diese Anwendung, ohne dass auf weitere, spezifischere Registrierungen überprüft wird. Unter UrlPrefix Strings (möglicherweise in englischer Sprache) können Sie mehr darüber erfahren, wie diese URL-Präfixe gebildet werden. Wenn Sie erfolgreich ein Namespacepräfix für den HTTP-Kanal von Windows Communication Foundation reservieren möchten, müssen Sie eine Form verwenden, die allgemein genug ist, um das Präfix zu entdecken, das Windows Communication Foundation registrieren wird.
Wenn Sie zu Abbildung 1 zurückgehen, sehen Sie die Basisadresse, die ich beim ServiceHost registriert habe: localhost:8080/MyServices/. Wenn Sie einfach das Offensichtliche tun und versuchen, dieses URL-Präfix ausdrücklich in HTTPCFG zu registrieren, wird dies leider nicht funktionieren. Im Verborgenen sieht es nämlich so aus, als ob Windows Communication Foundation einen starken Platzhalter registriert, der ein breiteres Netz auswirft und nicht von einer ausdrücklichen Registrierung des Hostnamens abgedeckt wird. Stattdessen müssen Sie die Syntax des starken Platzhalters verwenden und ein URL-Präfix von http://+:8080/MyServices/ registrieren. Dies gilt unabhängig davon, ob Sie dies programmgesteuert erledigen oder über das Tool HTTPCFG.
Es gibt eine Menge Personen, die als Administrator Code schreiben. Traurigerweise werden sie dieses Problem erst bemerken, wenn sie Ihren Code in einer Umgebung ohne Administratorberechtigungen bereitstellen. Dies ist einer der Gründe dafür, warum Sie Ihren Code immer als Nicht-Administrator testen sollten. Noch besser wäre es, wenn Sie versuchten, als Nicht-Administrator Code zu schreiben! Ich will hier keine großen Reden schwingen. Wenn Sie mehr über das Entwickeln als Nicht-Administrator erfahren möchten, sehen Sie sich Aaron Margosis' Weblog (möglicherweise in englischer Sprache) an, in dem er diesem Thema viele Einträge widmet.

F Wie lassen sich geteiltes Wissen und doppelte Kontrolle von Schlüsseln am besten implementieren?
F Wie lassen sich geteiltes Wissen und doppelte Kontrolle von Schlüsseln am besten implementieren?

A Viele Websites akzeptieren die Daten von Kreditkarteninhabern, einschließlich Kreditkartennummern, Rechnungsadressen usw. Ende 2004 wurde eine Norm namens Payment Card Industry Data Security Standards (PCI-DSS) veröffentlicht, um Sicherheitsanforderungen für Unternehmen festzulegen, die diese Art von Daten speichern, verarbeiten oder übertragen. Eine der neueren Anforderungen ist Abschnitt 3.6.6, welcher "Geteiltes Wissen und doppelte Kontrolle von Schlüsseln" fordert, sodass zwei oder drei Personen erforderlich sind, die alle nur ihren Teil des Schlüssels kennen, um den ganzen Schlüssel zu rekonstruieren.
A Viele Websites akzeptieren die Daten von Kreditkarteninhabern, einschließlich Kreditkartennummern, Rechnungsadressen usw. Ende 2004 wurde eine Norm namens Payment Card Industry Data Security Standards (PCI-DSS) veröffentlicht, um Sicherheitsanforderungen für Unternehmen festzulegen, die diese Art von Daten speichern, verarbeiten oder übertragen. Eine der neueren Anforderungen ist Abschnitt 3.6.6, welcher "Geteiltes Wissen und doppelte Kontrolle von Schlüsseln" fordert, sodass zwei oder drei Personen erforderlich sind, die alle nur ihren Teil des Schlüssels kennen, um den ganzen Schlüssel zu rekonstruieren.
Bei einem Onlinetransaktionssystem ist es schwer vorstellbar, dass zwei oder drei Personen herumstehen, die bei jeder Bearbeitung einer Kreditkartenanforderung den dazu erforderlichen geheimen Schlüssel eingeben. Doch wenn Sie vertrauliche Daten wie diese über einen langen Zeitraum speichern, wirkt das Konzept auf einmal viel vernünftiger. Wie würden Sie also solch ein System erstellen, wenn Sie diese Anforderung erfüllen möchten?
Die einfachste Möglichkeit, eine Nachricht unter N verschiedenen Parteien aufzuteilen, besteht darin, N-1 zufällige Schlüssel zu generieren, von denen jeder genauso lang sein muss wie die zu schützende Nachricht. Jedes Byte dieser zufälligen Schlüssel muss mithilfe einer Exklusiv-ODER-Verknüpfung mit dem entsprechenden Byte der Nachricht verknüpft werden. Der Datenwust, der am Ende all dieser Vorgänge herauskommt, wird als Nter Schlüssel behandelt. Anschließend können Sie jeden dieser Schlüssel an unterschiedliche Personen ausgeben und die ursprüngliche Nachricht löschen. Wenn eine Person sich ihren eigenen Schlüssel ansieht, wirkt dieser wie Datenmüll – schließlich handelt es sich um zufällige Daten. Die ursprüngliche Nachricht kann nur rekonstruiert werden, wenn alle Personen wieder zusammengebracht werden, ihre Schlüssel vereinen und diese mit der Exklusiv-ODER-Verknüpfung verarbeiten. Aufgrund der Auswechseleigenschaft der Exklusiv-ODER-Verknüpfung spielt die Reihenfolge dabei keine Rolle. Sofern jeder der geteilten Schlüssel wieder eingebracht wird, erhalten Sie letzten Endes wieder die ursprüngliche Nachricht.
Ich verwende hier den Ausdruck "Nachricht", um auf den Satz von Bytes zu verweisen, der geschützt werden muss. Dabei kann es sich um eine Zeichenfolge wie "Angriff im Morgengrauen!" oder um binäre Daten handeln, etwa einen kryptografischen Schlüssel, mit dem die dauerhaften Daten des Karteninhabers geschützt werden.
Die Sicherheit dieser Technik ist perfekt – theoretisch. (Ich sage hier „theoretisch“, da die Implementierungen in der Regel keinesfalls perfekt sind!) Falls eine Person sich weigert, ihren Schlüssel herauszugeben, macht es für die anderen Personen wenig Sinn, ihre Schlüssel zusammenfügen. Sie können dann auch nicht mehr von der Nachricht lesen, als wenn sie allein wären. Jetzt wird der Nachteil deutlich: Falls jemand seinen Schlüssel verliert, kann die ursprüngliche Nachricht nie wieder hergestellt werden. Falls Sie sich deswegen Sorgen machen, können andere, wesentlich kompliziertere Ansätze verwendet werden, die das Lösen von Gleichungen in einem finiten Feld beinhalten. Siehe dazu das Buch Applied Cryptography, Zweite Ausgabe, von Bruce Schneier (in englischer Sprache). Ich hebe mir die Implementierung dieser komplexeren Schemata für einen künftigen Artikel auf. Jetzt möchte ich eine einfache Lösung zu dem Problem der Schlüsselaufteilung präsentieren: System.Security.Cryptography.
Meine Prüfung des Konzepts ist eine Lösung mit Ebenen, die mit zwei Kernfunktionen in einer Klasse namens SecretSplitter beginnt. Diese Funktionen teilen eine Nachricht mit der zuvor beschriebenen Technik auf bzw. fügen sie wieder zusammen. Der Code für diese Funktionen ist in Abbildung 4 dargestellt. Die erste Funktion, SplitSecret, benötigt ein Bytearray, das die aufzuteilende Nachricht darstellt, sowie eine ganze Zahl, mit der angezeigt wird, in wie viele Schlüssel die Nachricht aufgeteilt werden soll. JoinSecret benötigt eine Anordnung aufgeteilter Schlüssel, die zu der ursprünglichen Nachricht zusammengesetzt werden. Wenn Sie nicht genau die Schlüssel bereitstellen, in die die Nachricht ursprünglich aufgeteilt wurde, erhalten Sie etwas anderes.
public static List<byte[]> SplitSecret(byte[] secret, int count) {
    if (null == secret || 0 == secret.Length) 
        throw new ArgumentException("Non-empty value required",
        "secret");
    if (count < 2) 
        throw new ArgumentException("Must be greater than one", "count");

    RNGCryptoServiceProvider random = new RNGCryptoServiceProvider();

    // Get N-1 new secrets.
    List<byte[]> newSecrets = new List<byte[]>(count);
    for (int i = 0; i < count - 1; ++i) {
        byte[] newSecret = new byte[secret.Length];
        random.GetBytes(newSecret);
        newSecrets.Add(newSecret);
    }

    // XOR all secrets into the existing one to get the final secret.
    byte[] finalSecret = (byte[])secret.Clone();
    foreach (byte[] newSecret in newSecrets) {
        for (int i = 0; i < finalSecret.Length; ++i) {
            finalSecret[i] ^= newSecret[i];
        }
    }
    newSecrets.Add(finalSecret);

    return newSecrets;
}

public static byte[] JoinSecret(List<byte[]> splitSecrets) {
    if (null == splitSecrets) throw new ArgumentNullException();

    byte[] secret = null;
    foreach (byte[] splitSecret in splitSecrets) {
        if (null == splitSecret) throw new ArgumentNullException();
        if (null == secret) {
            secret = (byte[])splitSecret.Clone();
            continue;
        }
        if (splitSecret.Length != secret.Length) 
            throw new ArgumentException(
                "All secrets must be of the same length");

        // XOR all the split secrets together to get the original secret.
        for (int i = 0; i < secret.Length; ++i) {
            secret[i] ^= splitSecret[i];
        }
    }
    return secret;
}
Beachten Sie die Verwendung der Klasse RNGCryptoServiceProvider zum Generieren der geheimen Schlüssel. Dies ist eine viel bessere Technik als System.Random. RNGCryptoServiceProvider wurde speziell dafür entwickelt, zufällige Daten für kryptografische Vorgänge zu generieren und ist daher nicht annähernd so vorhersehbar wie der Strom von Daten, den Sie mit System.Random erhalten. Außerdem sät es sich selbst aus vielen verschiedenen Quellen der Entropie auf Ihrem Computer.
In der Ebene oberhalb dieser beiden Funktionen befinden sich Manager, die Ihnen ermöglichen, ausschließlich mit Zeichenfolgen zu arbeiten, indem die Daten mit Base64 codiert werden. Dieser Code ist weniger relevant, weswegen ich ihn nicht hier zeige. Er ist jedoch nützlich, da wir es häufig mit auf Zeichenfolgen basierenden Nachrichten und Textdateien zu tun haben. Alle diese Funktionen sind in einer Bibliotheksassembly verpackt, die ich SecretSplittingLibrary.dll genannt habe.
Auf der Grundlage dieser Bibliothek habe ich eine Konsolenanwendung erstellt, um zu demonstrieren, wie die Schlüsselaufteilung in einer bestimmten Anwendung funktionieren könnte. Diese Anwendung heißt ThumbDriveSecretSplitter und verfügt über zwei Befehle: mit dem einen wird eine Nachricht aufgeteilt und mit dem anderen wieder zusammengefügt.
Wenn Sie eine Nachricht aufteilen möchten, können Sie den folgenden Befehl ausführen:
ThumbDriveSecretSplitter split "Attack at dawn!"
Dadurch sollte die Nachricht "Angriff im Morgengrauen!" auf alle Thumb drives aufgeteilt werden, die aktuell mit dem Computer verbunden sind. Na ja, wenigstens war das ursprünglich das Ziel. Doch beim ersten Versuch stieß ich auf folgendes Problem: Die Anwendung konnte nur mit Mühe erkennen, welche Volumes Wechselmedien darstellten. Ich habe einen Thumb drive, der in der Datenträgerverwaltung als Festplattenlaufwerk angezeigt wird, und ich nehme an, das ist nicht der einzige mit diesem Problem.
Also habe ich geschwindelt und dem Programm einen kleinen Hinweis gegeben. In die Konfigurationsdatei der Anwendung fügte ich eine Liste mit Datenträgernamen ein, nach denen die Anwendung suchen sollte, um zu ermitteln, welche Laufwerke verwendet werden sollen.
<appSettings>
  <add key="volumeNames"
       value="FatDrive;PSThumbDrv"/>
</appSettings>
Wenn der Befehl jetzt ausgeführt wird, sucht das Programm nach den Laufwerken in der konfigurierten Liste und teilt die Nachricht auf diese Laufwerke auf. Dabei wird im Stammverzeichnis jedes Laufwerks in einer Datei namens "mysecret.txt" ein Base64-codierter geheimer Schlüssel abgelegt. Das bedeutet, dass ich die Nachricht jetzt auf alle Thumb drives aufteilen und die Laufwerke dann an die Personen verteilen kann, die für die Bewahrung des Geheimnisses verantwortlich sind.
Wenn Sie die ursprüngliche Nachricht wiederherstellen möchten, müssen diese Personen lediglich ihre Thumb drives an Ihren Computer anschließen, und dann führen Sie den anderen Befehl aus:
ThumbDriveSecretSplitter join
Dadurch werden die Schlüssel miteinander kombiniert, und die geschützte Nachricht wird gedruckt. Im Folgenden sehen Sie eine Ausgabe einer Sitzung mit ThumbDriveSecretSplitter:
C:>ThumbDriveSecretSplitter.exe split "Attack at dawn!"
Writing E:\mysecret.txt
Writing F:\mysecret.txt

C:>ThumbDriveSecretSplitter.exe join
Reading E:\mysecret.txt
Reading F:\mysecret.txt

Joining secrets...

Attack at dawn!
Wenn Sie diese Technik in einem wirklichen System verwenden, möchten Sie wahrscheinlich vermeiden, dass das Geheimnis für alle Beteiligten angezeigt wird. Dann wäre es sinnlos gewesen, die Nachricht überhaupt zu verschlüsseln. Stattdessen würden Sie vom Programm die Schlüssel sammeln lassen, das Geheimnis berechnen und danach handeln, also wahrscheinlich als geheimen Schlüssel zum Entschlüsseln einer vertraulichen Sicherung von Karteninhaberdaten verwenden.

Senden Sie Fragen und Kommentare für Keith in englischer Sprache an  briefs@microsoft.com.


Keith Brown ist Mitbegründer von Pluralsight, einem führenden Anbieter von Microsoft .NET-Schulungen. Keith ist Autor des Kurses über angewandte .NET-Sicherheit von Pluralsight und des .NET-Entwicklerhandbuchs zur Windows-Sicherheit (beide möglicherweise in englischer Sprache), die sowohl gedruckt als auch im Internet verfügbar sind. Weitere Informationen finden Sie unter pluralsight.com/keith.

Page view tracker