Abou Chleih

{the magic lies between the brackets}

Menü Schließen

Schlagwort: .NET

ASP.NET Core App via Apache bereitstellen #2 – Konfiguration Apache

Um eine weitere Apache Instanz zu erstellen, gibt es bereits ein Skript, welches einem einige Arbeit abnimmt.
Die grundsätzliche Idee dahinter ist jedoch einfach: Man kopiert die originale Instanz (apache2) , also Konfigurationsdateien, Binaries, etc. in einen zweiten Ordner (apache2-xxx).

Je nach System findet sich das Skript in
/usr/share/doc/apache2.2-common/examples/setup-instance xxx
oder
/usr/share/doc/apache2/examples/setup-instance xxx

xxx ist hierbei der Name der Instanz, die erstellt werden soll: /usr/share/doc/apache2/examples/setup-instance netcore

Dies erstellt also die entsprechenden Verzeichnis und Dateien /etc/apache2-netcore, /etc/logrotate.d/apache2-netcore, /var/log/apache2-netcore sowie das Init-Skript für den Boot /etc/init.d/apache2-netcore.

Um die entsprechenden Links zu aktivieren, also apache2-netcore als Service verfügbar zu machen. Führen wir nun ein update-rc.d apache2-netcore defaults aus.
Dies verlinkt die executables in /etc/rcX.d/, also dem entsprechenden Runlevel für den Boot Prozess.

Nun können wir den Service mit init.d oder service starten: /etc/init.d/apache2-netcore start oder service apache2-netcore start

Nun können wir uns an die Konfiguration de Apache2 Instanz NetCore machen. Dazu navigieren wir ins Verzeichnis /etc/apache2-netcore/sites-available/ und bearbeiten das file 000-default.conf (HTTP) oder default-ssl.conf (HTTPS).

Diese Dateien haben folgenden Aufbau (in diesem Beispiel hört die ASP.NET Core Application auf localhost:5000):

<VirtualHost *:8080>
     ServerName sub.mydomain.com 
     ErrorLog ${APACHE_LOG_DIR}/errorNetCore.log 
     CustomLog ${APACHE_LOG_DIR}/accessNetCore.log combined 
     ProxyPass / http://localhost:5000/ 
     ProxyPassReverse / http://localhost:5000/ 
     ProxyPreserveHost On
</VirtualHost>

ServerName: Hört auf Aufruf von sub.mydomain.com
ErrorLog: Schreibt Error Log in entsprechendes File, (Apache bspw. PHP Fehler kommen hier rein)
CustomLog: Schreibt alle Aufrufe in File (=AccessLog Config)
ProxyPass: Forward Proxy Config, leitet alle Anfragen von http://sub.mydomain.com:8080/ an http://localhost:5000/ weiter
/ steht hier für das Top Directory, ProxyPass /sub/ http://localhost:5000/ würde bspw. http://sub.mydomain.com:8080/sub/ an http://localhost:5000/ weiterleiten
ProxyPassReverse: Reverse Proxy, leitet Response um. Schreibt alle Urls in der Rückantwort um, bspw. sollte http://localhost:5000 zu http://sub.mydomain.com:8080 umgeschrieben werden.
ProxyPreserveHost: Header bleibt erhalten

Nun speichern wir die Seite und aktivieren sie mit a2ensite sub.mydomain.com und starten die Instanz neu service apache2-netcore restart.

Anschließend starten wir die ASP.NET Core Applikation: dotnet PFADZURDLL

ASP.NET Core App via Apache bereitstellen #1 – Compiling & Vorbereitung

Mit .NET Core ist es seit einiger Zeit problemlos möglich cross-plattform Applikationen zu erstellen.
Darunter fallen auch ASP.NET Core Web-Anwendungen.
Um diese Anwendungen allerdings allgemein bereitzustellen, müssen wir diese zuerst für den allgemeinen Zugriff aus dem Internet freigeben.
Dies geschieht auf Grund der Plattformunabhängigkeit natürlich ohne Microsoft IIS. In diesem Beispiel wird ein Apache2 Webserver in der Standardkonfiguration genutzt.
Voraussetzung sind daher ein installierter Server mit funktionaler Apache2 Installation und natürlich eine ASP.NET Core Anwendung.

Was werden wir hier nun genau machen?

  • Compilen der ASP.NET Core Anwendung
  • Konfigurieren einer eigenen Apache Instanz, die als Reverse Proxy fungiert
  • Bereitstellen der Applikation

Um die ASP.NET Anwendung bereitzustellen, müssen wir diese zuerst compilen, dies mache ich bevorzugt auf meiner Entwicklungsmaschine (hier: Windows).
Wir gehen als in das Projektverzeichnis ({Solutionpath}\src\{Projectpath}) und öffnen dort eine Kommandozeile oder PowerShell.

Mit dem Befehl dotnet publish -c Release (-c = –configuration) starten wir nun den Build Prozess (im Release Modus).
Sollten nun Fehler kommen, dass „bower“ nicht gefunden werden kann, so kann dies an zwei Dingen liegen.

  • Bower nicht installiert
  • Path-Variable nicht korrekt

Um Bower zu installieren, geben wir in der PowerShell einfach npm install bower ein, das entsprechende Node.js Projekt wird dann geladen und installiert.
Anschließend installieren wir noch Gulp via npm install gulp, eine weitere Abhängigkeit.

Sollte der Fehler immer noch erscheinen, so müssen wir die Path-Variable auf das Web Verzeichnis unserer Visual Studio Installation verweisen lassen:
PowerShell: $env:path = $env:path + ";C:\Program Files (x86)\Microsoft Visual Studio 14.0\Web\External"
Die VS Version entsprechend anpassen (hier: VS 2015).

Nun sollte das Projekt compilen und die lauffähige Version unter {Solutionpath}\src\{Projectpath}\bin\Release zu finden sein.

Diesen Ordner jetzt auf den Linux Server laden.

Um die Applikation jetzt ausführen zu können, benötigen wir die .NET Core Runtime auf dem Server (hier: Debian).
Um diese zu installieren, laden wir uns das Archiv (tar.gz) herunter und entpacken es:
Die Befehle dazu finden sich auf der entsprechenden Microsoft-Website:

Für v1.0.3:
sudo apt-get install curl libunwind8 gettext
curl -sSL -o dotnet.tar.gz https://go.microsoft.com/fwlink/?linkid=847105
sudo mkdir -p /opt/dotnet && sudo tar zxf dotnet.tar.gz -C /opt/dotnet
sudo ln -s /opt/dotnet/dotnet /usr/local/bin

Für v2.0.0:
deb [arch=amd64] https://packages.microsoft.com/repos/microsoft-ubuntu-trusty-prod trusty main" > /etc/apt/sources.list.d/dotnetdev.list
apt-get update
apt-get install dotnet-sdk-2.0.0

Anschließend sollte man das Projekt starten können, zum Testen reicht hier: dotnet {Projektname}.dll im Pfad {App}\publish\.

[C#/.NET] Eigenen OpenFileDialog erstellen

Zwar hat das .NET-Framework mit den von der CommonDialog-Klassen ebernden Dialog-Klassen (dazu gehören OpenFileDialog, FolderBrowserDialog) schon Dialoge, welche die Funktion bieten Daten zu öffnen, zu speichern oder das Verzeichnis zu wählen. Diese sind aber leider nicht erweiterbar und daher nicht für sämtliche Zwecke zu gebrauchen.

In diesem Beitrag werde ich deshalb eine Möglichkeit aufzeigen, einen eigenen Dialog zu erstellen für verschiedene Zwecke zu erstellen.

Einfacher Dialog zum Öffnen von Dateien

Zum Erstellen eines einfachen OpenFileDialogs benötigen wir erstmal eine Form. In der Form platzieren wir ein TreeView, sowie zwei Buttons. Zusätzlich benötigen wir eine ImageList, welche die eigentlichen Icons der Dateien und Verzeichnisse zwischenspeichert und aus welcher wir die benötigten Icons beziehen werden.

Nachdem wir die benötigten Controls hinzugefügt haben, benötigen wir eine Methode, die uns alle Laufwerke in die TreeView lädt. Diese bilden nämlich jeweils den Stammknotenpunkt (RootTreeNode).

Die Methode sieht wir folgt aus und soll bei dem Aufruf der Form durchlaufen werden:

private void GetAllDrives()
{
	DriveInfo[] drives = DriveInfo.GetDrives();
	foreach (var drive in drives)
	{
		TreeNode rootTreeNode = new TreeNode();
		rootTreeNode.Text = drive.Name;
		rootTreeNode.Tag = drive.Name;
		rootTreeNode.ImageIndex = GetIconOfFile_Folder(drive.Name);
		rootTreeNode.SelectedImageIndex = rootTreeNode.ImageIndex;
		rootTreeNode.Nodes.Add(" "); //Placeholder to enable expanding (+)
		FolderAndFiles_treeView.Nodes.Add(rootTreeNode);
	}
}

Die Methode GetIconOfFile_Folder bezieht das Icon des Elements, wie es der Windows Explorer auch anzeigt.
Zum Beziehen wird der Namespace System.Runtime.InteropServices benötigt. Diesen binden wir per

using System.Runtime.InteropServices;

ein.
Folgender Code wird zum Beziehen der Icons benötigt:

[StructLayout(LayoutKind.Sequential)]
public struct SHFILEINFO
{
	public IntPtr hIcon;
	public IntPtr iIcon;
	public uint dwAttributes;
	[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 260)]
	public string szDisplayName;
	[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 80)]
	public string szTypeName;
};

class Win32
{
	public const uint SHGFI_ICON = 0x100;
	public const uint SHGFI_LARGEICON = 0x0;    // 'Large icon
	public const uint SHGFI_SMALLICON = 0x1;    // 'Small icon

	[DllImport("shell32.dll")]
	public static extern IntPtr SHGetFileInfo(string pszPath,
								uint dwFileAttributes,
								ref SHFILEINFO psfi,
								uint cbSizeFileInfo,
								uint uFlags);
}

private int GetIconOfFile_Folder(string Path)
{
	IntPtr hImgSmall;    //the handle to the system image list
	SHFILEINFO shinfo = new SHFILEINFO();

	hImgSmall = Win32.SHGetFileInfo(Path, 0, ref shinfo,
								   (uint)Marshal.SizeOf(shinfo),
									Win32.SHGFI_ICON |
									Win32.SHGFI_SMALLICON);

	System.Drawing.Icon myIcon =
		   System.Drawing.Icon.FromHandle(shinfo.hIcon);

	FolderAndFiles_imageList.Images.Add(myIcon);

	return FolderAndFiles_imageList.Images.Count - 1; //Ab 0 (zero) wird angefangen, somit ist die Gesamtzahl n+1
}

Bisher werden nur die aufklappbaren Laufwerke angezeigt. Sie besitzen jedoch noch keine weiteren Knoten. Diese füllen wir indem wir einen EventHandler für das BeforeExpand-Event mittels

FolderAndFiles_treeView.BeforeExpand += FolderAndFiles_treeView_BeforeExpand;

erstellen.

Die dazugehörige Methode FolderAndFiles_treeView_BeforeExpand sieht wie folgt aus:

private void FolderAndFiles_treeView_BeforeExpand(object sender, TreeViewCancelEventArgs e)
{
	e.Node.Nodes.Clear();
	GetFilesAndFolder(e.Node, (string)e.Node.Tag);
}

Das Event ruft also die Methode GetFilesAndFolder auf, welche zuerst die unter dem erweiterten Knoten liegenden Knoten löscht und danach die Verzeichnisse und Dateien bezieht. Diese Methode benötigt zum einen den erweiterten Knoten, sowie das Verzeichnis aus welchem die Ordner und Dateien bezogen werden sollen (dies wurde im Tag gespeichert) als Parameter.
Die Methode selbst sieht wie folgt aus:

private void GetFilesAndFolder(TreeNode tn, string Path)
{
	try
	{
		string[] Directories = Directory.GetDirectories(Path);
		string[] Files = Directory.GetFiles(Path);

		foreach (string dir in Directories)
		{
			TreeNode dirTreeNode = new TreeNode();
			dirTreeNode.Tag = dir;
			dirTreeNode.Text = new DirectoryInfo(dir).Name;
			dirTreeNode.ImageIndex = GetIconOfFile_Folder(dir);
			dirTreeNode.SelectedImageIndex = dirTreeNode.ImageIndex;
			dirTreeNode.Nodes.Add(" ");
			tn.Nodes.Add(dirTreeNode);
		}

		foreach (string file in Files)
		{
			TreeNode fileTreeNode = new TreeNode();
			fileTreeNode.Tag = file;
			fileTreeNode.Text = new FileInfo(file).Name;
			fileTreeNode.ImageIndex = GetIconOfFile_Folder(file);
			fileTreeNode.SelectedImageIndex = fileTreeNode.ImageIndex;
			tn.Nodes.Add(fileTreeNode);
		}
	}
	catch (Exception ex)
	{
		MessageBox.Show(ex.Message, ex.Source, MessageBoxButtons.OK, MessageBoxIcon.Exclamation);
	}
}

Den schwierigsten Teil haben wir jetzt hinter uns, die Dateien und Verzeichnisse werden bezogen und in der TreeView angezeigt und die jeweiligen Icons bezogen.
Jetzt müssen wir lediglich noch die zwei Buttons belegen.
Ich habe dies so gelöst:
Abbrechen

private void Cancel_button_Click(object sender, EventArgs e)
{
	this.DialogResult = System.Windows.Forms.DialogResult.Cancel;
	this.Close();
}

Annehmen

private void Accept_button_Click(object sender, EventArgs e)
{
	string filePath = (string)FolderAndFiles_treeView.SelectedNode.Tag;
	if (CheckIfPathIsFile(filePath) == true) // Sollte es sich um eine Datei handeln
	{
		this.FilePath = filePath; //Bei FilePath handelt es sich um ein public Property
		this.DialogResult = System.Windows.Forms.DialogResult.OK;
		this.Close();
	}
	else //Sollte es sich um ein Verzeichnis handeln
	{
		FolderAndFiles_treeView.SelectedNode.Expand(); //erweitere den aktuell gewählten Knoten
	}
}

In der Methode des AcceptButton-ClickEvents wird auch geprüft, ob es sich bei dem gewählten Element um eine Datei oder ein Verzeichnis handelt. Dies geschieht mit folgender Methode:

private bool CheckIfPathIsFile(string Path)
{
	FileAttributes attr = File.GetAttributes(Path);
	if ((attr & FileAttributes.Directory) == FileAttributes.Directory)
		return false;
	else
		return true;
}

Es wird also Wahr zurückgegeben, wenn es sich um eine Datei handelt.

Zu guter Letzt müssen wir nur noch den FileDialog aufrufen, dies geschieht mit folgender Methode:

private void OpenSimpleFileDialog()
{
	SimpleOpenFileDialog simpleOpenFileDia = new SimpleOpenFileDialog();
	if(simpleOpenFileDia.ShowDialog() == System.Windows.Forms.DialogResult.OK)
		MessageBox.Show(simpleOpenFileDia.FilePath);
}

Das Ergebnis sieht wie folgt aus:

2013-12-02 14_43_34-SimpleOpenFileDialog

[C#/.NET] Eigene Erweiterungen für den Windows Explorer mit SharpShell erstellen

In meinem letzten Beitrag habe ich beschrieben, wie man mit Hilfe einer Microsoft Library eigene Dateiattribute hinzufügen kann.
Auf Grund der Tatsache, dass der Windows Explorer nur die Standardattribute anzeigt (siehe Grafik), müssen wir zur Anzeige der eigenen Attribute entweder

  • ein eigenes Programm entwickeln (Handhabe umständlich)
    oder
  • eine Erweiterung für den Windows Explorer erstellen.

Standardattribute:
Standardattribute_Details

Und um letzteren Punkt handelt dieser Beitrag.

Achtung: Die Library verwendet .NET Framework 4, d.h. eine Erstellung ist nur mit dieser .NET Version möglich und einer IDE, welche diese unterstüzt (VS2010 und höher)

Um zu beginnen, benötigen wir die Library, mit welcher eine Extension erstellt werden kann. Diese findet ihr entweder auf unserer Downloadseite (SharpShell Project) oder auf der Projektpage.

Wir erstellen nun ein neues Projekt vom Typ „Windows Forms-Steuerelementenbibliothek“ (man kann auch einfach eine Klassenbibliothek erstellen) und fügen eine Referenz auf die DLL ein.

Rechtsklick auf das Projekt -> Verweis hinzufügen… ->Durchsuchen -> Durchsuchen -> Datei auswählen.

Nun können wir mit dem eigentlichen Coden beginnen.
SharpShell bietet einige Extensions an, unter Anderem sogenannte Shell Property Sheets/Pages (Erweiterung der Eigenschaftsseite einer Datei/eines Ordners), auf die wir hier näher eingehen.

Zuerst beginnen wir mit dem Sheet, welches alle Pages hält.
Wir erstellen also eine Klasse (ich nenne sie hier einfach mal Sheet) und lassen sie von der abstrakten SharpPropertySheet-Klasse erben. Diese Klasse hält alle nötigen Funktion, welche noch ausgecodet werden müssen.
Die Methoden sind die folgenden:

protected abstract bool CanShowSheet();
protected abstract IEnumerable CreatePages();

Erstere Methode gibt an, ob die Seite angezeigt werden soll.
Letztere gibt die Pages zurück, welche angezeigt werden sollen.

protected override bool CanShowSheet()
{
            return SelectedItemPaths.Count() == 1; //Sobald mindestens eine Datei angewählt, soll die Seite angezeigt werden
}

protected override IEnumerable CreatePages()
{
            CustomPage page = new CustomPage(); //Erstelle eine Page oder mehrere
            return new[] { page }; //und schreibe sie in das Array, welches zurückgegeben wird.
} 

Nun existiert die Klasse CustomPage noch nicht, wir müssen sie erst erstellen.
Da wir ein „Windows Forms-Steuerelementenbibliothek“-Projekt erstellt haben, haben wir schon eine von der IDE vordefinierte Klasse.
Diese nutzen wir nun und benennen wir in „CustomPage“.
Anschließend lassen wir sie von der SharpPropertyPage-Klasse im Namespace „SharpShell.SharpPropertySheet erben.
In den Konstruktor der Klasse schreiben wir einfach mal den Page-Titel:

public CustomPage()
{
     InitializeComponent();
     PageTitle = "Custom Properties";      //Definiert den Page-Titel
}

Diese Klasse hat einige virtual Methoden, wovon wir eine zwingend benötigen:

 public virtual void OnPageInitialised(SharpPropertySheet parent);

Diese Funktion wird aufgerufen, sobald die Seite initialisiert wurde, also ähnlich der Funktion

Form.Load();

Hier bauen wir unsere Logik ein, bspw. können wir hier eine MessageBox aufrufen, welche den Pfad der markierten Datei zurückgibt, bzw. den Pfad der ersten markierten Datei.

    MessageBox.Show(parent.SelectedItemPaths.First().ToString()); 

Oder wir können die CustomProperties, welche wir gesetzt haben auslesen.
Dazu einfach die DSO-Library einbinden und die CustomProperties auslesen.
Ich habe dazu eine ListView eingebettet und fülle diese mit dem Key Bezeichner und dem Wert des Properties:

 public override void OnPageInitialised(SharpPropertySheet parent)
        {
            filePath = parent.SelectedItemPaths.First(); //Pfad der Datei speichern
            OleDocumentProperties myFile = new DSOFile.OleDocumentProperties();
            myFile.Open(@filePath, false, DSOFile.dsoFileOpenOptions.dsoOptionDefault);
            int ctr = 1;
            foreach (DSOFile.CustomProperty property in myFile.CustomProperties)
            {
                ListViewItem key = new ListViewItem(property.Name);
                key.SubItems.Add(property.get_Value());
                lstVw_keys.Items.Add(key);
                ctr++;
            }
            myFile.Close(true);
        } 

Voila, schon ist man eigentlich fertig.
Nun müssen wir allerdings noch ein paar Parameter definieren.
Wir gehen zurück in die Sheet-Klasse und definieren einmal, wie die Clients mit dem verwalteten Code umgehen:

     [ComVisible(true)] 

Nun müssen wir noch definieren, welche Dateien überhaupt betroffen sind:

 [COMServerAssociation(AssociationType.ClassOfExtension,".txt",".css",".js")] 

In diesem Fall also Dateien mit den Endungen .txt, .css und .js.

Jetzt können wir die DLL compilen und schließlich per regAsm einbinden (sehr kompliziert und nervenaufreibend).
Alternativ kann man auch den ServerManager, welcher Teil der SharpShell-Tools ist, benutzen.
Ich zitiere hier jetzt einfach mal meinen Stackoverflow-Beitrag:

I had the same problem while using regasm.exe.
Furthermore there are many things to mention when registering an assembly through regasm.
For example you have to use the x64/x86 version of the regasm.exe, depending on your system.

x64: C:\Windows\Microsoft.NET\Framework64\v4.0.30319\regAsm.exe
x86: C:\Windows\Microsoft.NET\Framework\v4.0.30319\regAsm.exe
After having so many problems, I switched to the ServerManager.exe, which is part of the SharpShell Tools. It can be downloaded on the project page.
The usage is quite easy:

  • Load the DLL with „Load server…“
  • Click on „Install Server (xYZ)“
  • And after that on „Register Server (xYZ)“

Restart the Windows Explorer and you should be done (not necessarily needed).

Das Ergebnis sieht in etwa so aus:

CustomPropertiesTab_Pic

Nun seid ihr fertig und könnt eure CustomProperties beliebig hinzufügen, löschen und anzeigen lassen.

Ein Beispielprojekt findet ihr hier: CustomPropertyTab

[C#] XML in Applikation einlesen

Um schnell mal eine XML-Datei in die Applikation einzulesen, bedarf es dank des .NET-Frameworks nicht viel Arbeit.

Ich nutze dazu die XDocument-Klasse mit welcher es möglich ist XML zu parsen und zu schreiben.

Mein XML hatte folgende Form:

<?xml version="1.0" encoding="utf-8" ?>
<Phonenumbers>
  <phonenumber>
    <Name>Test</Name>
    <Number>12345678</Number>
  </phonenumber>
  <phonenumber>
    <Name>Test2</Name>
    <Number>11111111</Number>
  </phonenumber>
  <phonenumber>
    <Name>Test3</Name>
    <Number>222222222</Number>
  </phonenumber>
  <phonenumber>
    <Name>Test4</Name>
    <Number>333333333</Number>
  </phonenumber>
  <phonenumber>
    <Name>Test5</Name>
    <Number>44444444444</Number>
  </phonenumber>
  <phonenumber>
    <Name>Test6</Name>
    <Number>55555555555</Number>
  </phonenumber>
  <phonenumber>
    <Name>Test7</Name>
    <Number>66666666666</Number>
  </phonenumber>
  <phonenumber>
    <Name>Test8</Name>
    <Number>77777777777</Number>
  </phonenumber>
</Phonenumbers>

In die Applikation wird das XML mit folgendem Code geladen:

string xml = System.IO.File.ReadAllText(PfadZurXMLDatei);
XDocument xmlReader =  XDocument.Parse(xml);
var List = from element in xmlReader.Descendants("phonenumber")
		select new
		{
			name = (string)element.Element("Name"),
			number = (string)element.Element("Number"),
			//Weitere Elemente lesen
		};
foreach (var item in List)
{
	listBox1.Items.Add(item.name);
	listBox2.Items.Add(item.number);
}

[C#/.NET] Verschiedene Authentifizierungsmöglichkeiten #4 – ActiveDirectory

[C#/.NET] Verschiedene Authentifizierungsmöglichkeiten #1 – Plain
[C#/.NET] Verschiedene Authentifizierungsmöglichkeiten #2 – Verschlüsselung und SecureString
[C#/.NET] Verschiedene Authentifizierungsmöglichkeiten #3 – MySQL und Hash

ActiveDir_auth
Bei Active Directory (ab Windows Server 2008 Active Directory Domain Services) handelt es sich um einen Verzeichnisdienst (englisch directory service) von Microsoft. Sie speichert Benutzerinformationen(Benutzernamen, Kennwort sowie Gruppen) zentral auf einer Datenbank und stellt diese den Clients im Netzwerk zur Verfügung. Dadurch ist es, im Gegensatz zu einer Arbeitsgruppe, möglich die Benutzerdaten auf jeden Rechner in der Domäne zu verteilen. Somit ist es nicht mehr nötig jedes Profil auf jedem Rechner einzurichten, was eine enorme Zeitersparnis ergibt.

In vielen Firmen wird Active Directory genutzt und eine Authentifizierung ist  mit C# in Verbindung mit dem .NET-Framework schnell geschrieben.
Ich werden hier beschreiben, wie sich ein Benutzer durch Eingabe seines Benutzernamens und Kennworts authentifizieren kann, sowie die Überprüfung, ob sich der Benutzer in einer Gruppe befindet.

Authentifizierung über Benutzernamen und Kennwort:

Zuerst widmen wir uns der Authentifizierung an einer Domäne mittels Benutzername und Passwort.
Mit folgendem Code beziehen wir die aktuell verwendete Domäne aus dem System.

System.Net.NetworkInformation.IPGlobalProperties.GetIPGlobalProperties().DomainName;

Die einfachste Variante ergibt sich mit der Verwendung von PrincipalContext-Klasse, welche sich im Namespace System.DirectoryServices.AccountManagement befindet

public static bool? AuthenticateUserWithAD(string Domain, string Username, string Password)
{
	try
	{
		using (PrincipalContext pc = new PrincipalContext(ContextType.Domain, Domain))
		{
			//Überprüft die übergebenen Anmeldedaten mit dem angegeben ContextType (ApplicationDirectory, Domain oder Machine)
			return pc.ValidateCredentials(Username, Password); //Gibt true oder false zurück
		}
	}
	catch (Exception ex)
	{
		MessageBox.Show(ex.Message, ex.Source);
		return null;
	}
}

Wenn man nicht auf PrincipalContext zurückgreifen möchte, so kann man den Benutzer auch mittels DirectoryEntry aus dem Namespace System.DirectoryServices authentifizieren.

Dazu verwendet man folgenden Methode:

private static readonly int ERROR_DS_NO_SUCH_OBJECT = -2147016656; //Fehlercode für LDAP_NO_SUCH_OBJECT 0x80072030 | http://msdn.microsoft.com/en-us/library/aa746528(v=vs.85).aspx
public static DirectoryEntry GetDirectoryEntry(string Domain, string username, string password)
{
	DirectoryEntry dirEntry = new DirectoryEntry();
	dirEntry.Path = "LDAP://" + Domain;
	dirEntry.Username = username;
	dirEntry.Password = password;

	try
	{
		if (dirEntry.NativeObject == null)
		{
			// Kein ActiveDir-Objekt gefunden
			dirEntry = null;
		}
	}
	catch (COMException ex)
	{
		if (ex.ErrorCode == ERROR_DS_NO_SUCH_OBJECT)
		{
			MessageBox.Show("Es wurde kein Objekt, welches mit den Eingaben übereinstimmt gefunden", "Fehler");
		}
		else
		{
			MessageBox.Show(ex.ErrorCode.ToString() + "\r\n" + ex.Message);
		}
		dirEntry.Close();
		dirEntry = null;
	}
	return dirEntry;
}

Um nun einen Wahrheitswert (Boolean) zurückzugeben, muss man prüfen, ob das DirectoryEntry-Objekt gefüllt oder null ist.
Dazu verwendet man folgende Methode, welche wir später dann auch über unsere Klasse aufrufen.

public static bool AuthenticateUser(string Domain, string username, string password)
{
	return GetDirectoryEntry(Domain, username, password) != null;
}

Falls man nur wissen möchte, ob der Benutzer existiert, so kann man ebenfalls PrincipalContext verwenden, jedoch diesmal in Verbindung mit der statischen Methode FindByIdentity der UserPrincipal-Klasse

public static bool CheckIfUserExists(string Domain, string Username)
{
	using (var pc = new PrincipalContext(ContextType.Domain, Domain))
	{
		using (var User = UserPrincipal.FindByIdentity(pc, IdentityType.SamAccountName, Username))
		{
			return User != null;
		}
	}
}

Überprüfung, ob sich Benutzer in Gruppe befindet:

Nun möchte man natürlich nicht immer den Benutzer authentifizieren oder prüfen, ob dieser existiert, sondern spezielle Funktionen einem gewissen Kreis von Personen bereitstellen.

Dazu bietet ActiveDirectory s.g. Gruppen

Eine Gruppe besteht aus Benutzer- und Computerkonten, Kontakten und anderen Gruppen, die als eine Einheit verwaltet werden können. Benutzer und Computer, die zu einer bestimmten Gruppe gehören, werden als Gruppenmitglieder bezeichnet.
Die Verwendung von Gruppen vereinfacht die Verwaltung, indem vielen Konten in einem Schritt einheitliche Berechtigungen und Rechte zugewiesen werden können, anstatt jedem Konto einzeln Berechtigungen und Rechte zuweisen zu müssen.

Für die Prüfung, ob der Benutzer Mitglied der Gruppe ist, gibt es zwei Möglichkeiten.

  1. Unter Verwendung von DirectoryEntry
  2. Benutzung WindowsIdentity

Zuerst unter Verwendung von DirectoryEntry

public static bool? CheckIfUserIsInGroupWithDirectoryEntry(string Domain, string Username, string Password, string Group)
{
	if (Username == "" || Password == "")
	{
		return false;
	}

	try
	{
		DirectoryEntry entry = new DirectoryEntry("LDAP://" + Domain, Username, Password);
		DirectorySearcher mySearcher = new DirectorySearcher(entry);
		mySearcher.Filter = "(&(objectClass=user)(|(cn=" + Username + ")(sAMAccountName=" + Username + ")))";
		SearchResult result = mySearcher.FindOne();

		foreach (string GroupPath in result.Properties["memberOf"])
		{
			if (GroupPath.Contains(Group))
			{
				return true;
			}
		}
	}
	catch (DirectoryServicesCOMException DirSvrComEx)
	{
		MessageBox.Show(DirSvrComEx.Message);
		return null;
	}
	return false;
}

Und die Alternative mit WindowsIdentity:

public static bool? CheckIfUserIsInGroup(string Username, string Groupname)
{
	try
	{
		using (var identity = new WindowsIdentity(Username))
		{
			var principal = new WindowsPrincipal(identity);
			return principal.IsInRole(Groupname);
		}
	}
	catch (Exception ex)
	{
		MessageBox.Show(ex.Message);
		return null;
	}
}

Möchte man den aktuellen Benutzer verwenden (und somit keine Eingabe der Login-Informationen erfordern), so kann man die Methode wie folgt anpassen

public static bool? CheckIfCurrentUserIsInGroup(string Groupname)
{
	try
	{
		IntPtr accountToken = WindowsIdentity.GetCurrent().Token;
		using (var identity = new WindowsIdentity(accountToken))
		{
			var principal = new WindowsPrincipal(identity);
			return principal.IsInRole(Groupname);
		}
	}
	catch (Exception ex)
	{
		MessageBox.Show(ex.Message);
		return null;
	}
}

Zu guter Letzt das Demo-Projekt:

20130811_Verweis-Manager - OleDBTest_000034

ActiveDirectory_Auth.zip

[C#/.NET] Verschiedene Authentifizierungsmöglichkeiten #3 – MySQL und Hash

[C#/.NET] Verschiedene Authentifizierungsmöglichkeiten #1 – Plain
[C#/.NET] Verschiedene Authentifizierungsmöglichkeiten #2 – Verschlüsselung und SecureString

Eine Authentifizierung mittels MySQL ist ziemlich einfach und schnell geschrieben.

Wir müssen lediglich den .NET Connector für MySQL herunterladen, den Verweis auf die DLL hinzufügen und folgenden Namespace mittels

using MySql.Data.MySqlClient;

einbinden.

Nun benötigen wir ein Objekt der MySQLConnection, welches den ConnectionString beinhaltete und eine Verbindung zum MySQL-Server bereitstellt.

Ich erstelle ein Field vom Typ MySQLConnection (also eine Variable, auf welche alle Methoden innerhalb der Klasse zugreifen können), welches jedoch noch nicht initialisiert wurde.

MySqlConnection connection = null;

In der öffentlichen Methode setupConnection() erstelle ich den ConnectionString (mit den Parametern) und initialisiere den MySQLConnector mit dem ConnectionString.

public void setupConnection(string server, string database, string user, string password)
{
     string myConnectionString = "SERVER=" + server + ";" +
     "DATABASE=" + database + ";" +
     "UID=" + user + ";" +
     "PASSWORD=" + password + ";";
     connection = new MySqlConnection(myConnectionString);
}

Nun erstelle ich eine weitere öffentliche Methode, welche zur Authentifizierung dient.

public bool? Authenticate(string username, string password, string table) //Das Fragezeichen (?) nach einem Typ gibt an, dass der <a href="http://msdn.microsoft.com/de-de/library/1t3y8s4s(v=vs.90).aspx" target="_blank">Typ NULL-Werte zulässt (nullable)</a>
{
	try
	{
		MySqlCommand command = connection.CreateCommand();
		command.CommandText = "SELECT * FROM "+ table +" WHERE Username=?Username"; //Erstellt die parametrisierte Abfrage (Tabellen können nicht parameterisiert werden)
		command.Parameters.AddWithValue("?Username", username); //Weißt dem Parameter einen Wert zu
		MySqlDataReader Reader;
		connection.Open(); //Öffnet die Verbindung zum Server
		Reader = command.ExecuteReader(); Führt die Abfrage auf dem Server aus
		while (Reader.Read()) //Solange noch Daten verfügbar sind
		{
			if (Reader[1].ToString() == username) //Prüfe ob der Wert der ZWEITEN(!) Spalte mit dem übergebenen Usernamen übereinstimmt
			{ //Falls dies der Fall ist,
				if (Reader[2].ToString() == password)//prüfe ob der Wert der dritten Spalte mit dem Passwort übereinstimmt
				{//Sollte dem so sein,
					connection.Close(); //so schließe die Verbindung
					return true; //und geben den Wahrheitswert "wahr" zurück
				}
			}
		}
                //Sollten die Parameter nicht mit den Daten aus der DB übereingestimmt haben
		connection.Close(); //So schließe die Verbindung
		return false; //Und geben "false" zurück
	}
	catch (Exception ex)
	{
		MessageBox.Show(ex.Message);
		connection.Close();
		return null;
	}
}

Hashing

Nun ist es natürlich äußerst fahrlässig Passwörter klar in der Datenbank zu hinterlegen. Im besten Fall soll es unmöglich sein, das Passwort wieder im Klartext aus der Datenbank zu beziehen und genau hier wird Hashing genutzt. Im Gegensatz zur Verschlüsselung ist es dabei nicht mehr möglich das gehashte Passwort in das ursprüngliche zurückzuwandeln.
Es gibt ziemlich viele Hashfunktionen, zwei der Bekanntesten sind MD5 und SHA.

MD5
SHA-2
Vorteile
Nachteile
Vorteile
Nachteile
Schnell
Nicht kollisionsresistent
Bisher nicht geknackt
Langsamer als MD5
Viele Regenbogentabellen
Gilt als kollisionsresistent

Die Schnelligkeit beider Hashfunktionen ist aber auch eine der größten Schwächen in Bezug auf die Sicherheit des Passworts. So ist heutzutage möglich ein 8-stelliges Passwort, bestehend aus Zahlen und Buchstaben, gehasht mit SHA-2 GPU-gestützt innerhalb eines Tages zu knacken.

MD5

Nun aber zur Implementierung einer MD5-Hashfunktion mit C# und dem .NET-Framework. Dazu verwenden wir die MD5CryptoSerivceProvider-Klasse

public string CreateMD5Hash(string unhashedString)
{
	if (String.IsNullOrEmpty(unhashedString))
		return string.Empty;

	MD5 md5 = new MD5CryptoServiceProvider(); //Alternativ kann man auch MD5 md5 = MD5.Create(); verwenden
        //Create() initalisiert MD5CryptoServiceProvider, da MD5CryptoServiceProvider MD5 implementiert. MD5 ist eine abstrakte Klasse
	byte[] unhashedByteArray = Encoding.Default.GetBytes(unhashedString);
	byte[] result = md5.ComputeHash(unhashedByteArray);

	return System.BitConverter.ToString(result).ToLower().Replace("-", "");
}

SHA-2

Beinahe den selben Code kann man für SHA-2 verwenden. Statt dem MD5CryptoServiceProvider verwenden wir die SHA256CryptoServiceProvider-Klasse

public string CreateSHA2Hash(string unhashedString)
{
	if (String.IsNullOrEmpty(unhashedString))
		return string.Empty;

	SHA256 sha2 = new SHA256CryptoServiceProvider();
	byte[] unhashedByteArray = Encoding.Default.GetBytes(unhashedString);
	byte[] result = sha2.ComputeHash(unhashedByteArray);

	return System.BitConverter.ToString(result).ToLower().Replace("-", "");
}

Und jetzt haben wir eine Todsünde begangen. Das Hashes sind nicht gesalzen und können somit einfach per Rainbow Tables geknackt werden.

Salted MD5

Genau für diesen Zweck wurde HMAC entwickelt. Ein Implementierung in das .NET-Framework erfolgte mit der HMACMD5-Klasse

public string CreateSaltedMD5Hash(string unhashedString, byte[] key)
{
	HMACMD5 saltedMD5 = new HMACMD5();

	saltedMD5.Key = key;

	byte[] unhashedByteArray = Encoding.Default.GetBytes(unhashedString);
	byte[] result = saltedMD5.ComputeHash(unhashedByteArray);

	return System.BitConverter.ToString(result).ToLower().Replace("-", "");
}

Salted SHA-2

Für SHA2 gibt es die HMACSHA256-Klasse

public string CreateSaltedSHA2Hash(string unhashedString, byte[] key)
{
	HMACSHA256 saltedSHA2 = new HMACSHA256();

	saltedSHA2.Key = key;
	byte[] unhashedByteArray = Encoding.Default.GetBytes(unhashedString);
	byte[] result = saltedSHA2.ComputeHash(unhashedByteArray);

	return System.BitConverter.ToString(result).ToLower().Replace("-", "");
}

Wie ich oben bereits erwähnt habe, ist die Geschwindigkeit die größte Schwäche dieser Algorithmen. Darum sollte man MD5 und SHA auch mit Salt nicht zum Hashen von Passwörtern verwenden (MD5 und SHA dienen zur Berechnung einer eindeutigen Prüfsumme, nicht zum Hashen von Passwörtern). Für diesen Zweck wurden langsamere Algorithmen entwickelt, welche generell für das Verschlüsseln von Passwörtern verwendet werden sollten.  Die Bekanntesten sind PBKDF2 und bcrypt.

Zu guter Letzt das Demo-Projekt:

20130811_Verweis-Manager - OleDBTest_000034

MySQL_Hash.zip

[C#/.NET] AES-Ver- und Entschlüsselung mit Rijndael

Bei AES (Advanced Encryption Standard) bzw Rijndael (ausgesprochen: Rain-Dahl [rɛindaːl]) handelt es sich um einen Verschlüsselungsalgorithmus, welcher im Oktober 2000 als Standard festgelegt wurde und den Vorgänger DES (Data Encryption Standard) ablöst.
AES setzt auf eine Blockverschlüsselung, d.h. der Schlüssel wird in mehrer Rundenschlüssel eingeteilt, mit welchem pro Runde in jedem Block die Bytes ersetzt (SubBytes), verschoben (ShiftRows) und vermischt (MixColumns) werden. Es gibt je nach Schlüssellänge 10 (128 bit), 12 (192 bit) oder 14 (256 bit) Runden (R) und eine zusätzliche Endrunde, so werden R+1 Rundenschlüssel benötigt.

Für weitere Informationen empfehle ich:

Nun zur Verschlüsselung eines Strings mit AES in C# zum einen nach AES-Spezifikationen (FIPS-197), zum anderen nach Rijndael (welcher ja zum AES Algorithmus gewählt wurde).
Zwischen AES und Rijndael gibt es, trotz der Wahl von Rijndael zum Algorithmus, Unterschiede, welche eine Kompatibilität untereinander unmöglich machen.

  1. Rijndael erlaubt variable Block- und Schlüsselgrößen (128, 160, 192, 224,  256), wohingegen bei AES eine Blockgröße von 128 bit und eine Schlüsselgröße von 128, 192 oder 256 bit vorgeschrieben sind.
  2. Rijndael erlaubt zusätzlich zu Cipher Block Chaining Mode (CBC) die Verwendung von Cipher Feedback Mode (CFB) als Betriebsmodus, wohingegen AES nur Ersteres unterstützt (bzw. CFB nur mit 128 bit Feedback)

Nun aber zum Code:

AES (FIPS-197)Rijndael
Ich habe folgenden statische Klasse erstellt, mit der wir sämtliche Vorgänge (also Ver- und Entschlüsseln) durchführen können.
Statische AES Klasse

Und so nutzt man die oben erstellte Klasse, in vollem Umfang:

Aufruf/Nutzung der AES-Klasse
Folgende Klasse (Form) wird zur Erstellung des Schlüssels verwendet und über die Methode AES_CreateOwnSaltedKey aufgerufen.
Form zum Erstellen des Schlüssels

Folgende Klasse (Form) bietet die Möglichkeit der Eingabe des Schlüssels, welcher zu Entschlüsselung benötigt wird. Ein Objekt der Klasse wird in der Methode AES_DecryptWithOwnKey erstellt und angezeigt.

Klasse zur Schlüssel-Eingabe zur Entschlüsselung
Inhalt folgt noch. Der Code gleicht jenem der Aes-Klasse jedoch zu 90%

Zu guter Letzt das Demo-Projekt:

20130811_Verweis-Manager - OleDBTest_000034

AES_Rijndael.zip

[C#/.NET] Verschiedene Authentifizierungsmöglichkeiten #2 – Verschlüsselung und SecureString

[C#/.NET] Verschiedene Authentifizierungsmöglichkeiten #1 – Plain

Im ersten Teil der Serie beschriebe ich, wie man eine einfach Authentifizierung mit hart-codierten oder im Klartext (Plain) gespeicherten Passwörtern erstellt. Nun möchte man das Passwort natürlich eher selten so offen und unsicher speichern. Im Folgenden geht es um das Verschlüsseln und Entschlüsseln, sowie das Sichern von Texten (Strings)

Verschlüsseln:

Als Paradebeispielt für eine unsichere Verschlüsselung gilt die Caesar-Verschlüsselung (hier ROT13), dabei wird ein Buchstabe einfach durch den 13 Stellen vor- oder hinter liegenden Buchstaben ersetzt.

    static class Rot13
    {
        ///
/// Verschiebe Buchstaben um 13 Stellen
        ///
        public static string Enrypt(string pString)
        {
            string rot13String = "";
            foreach (char c in pOrgString)
            {
                if (c >= 'a' && c <= 'm' || c >= 'A' && c <= 'M')//falls der Buchstabe zwischen a/A und m/M liegt (m/M da es sich um den 13. Buchstaben handelt)
                {
                    rot13String += ((char)((int)c + 13)); //dann füge dem verschlüsselten String den Buchstaben, welcher 13 Stellen hinter dem   originalen Buchstaben steht hinzu
                }
                else if (c >= 'n' && c <= 'z' || c >= 'N' && c <= 'Z') //falls der Buchstabe zwischen n/N und z/Z liegt (n/N da es sich um den 14. Buchstaben also den ersten rotierten handelt)
                {
                    rot13String += ((char)((int)c - 13)); // dann ziehe 13 Stellen ab und füge den neune Char hinzu
                }
                else //Sollte es sich um keinen Buchstaben im lateinischen Alphabet handeln,
                {
                    rot13String += c; //So füge ihn unverändert hinzu
                }
            }
            return rot13String;
        }
    }

Entschlüsselt wird es indem man selben Code nochmals mit dem ROT13-verschlüsselten String als Parameter ausführt

Für die Verschlüsselung eines Strings mit AES (Rijndael-Algorithmus) verweise ich auf: [C#] AES-Ver und Entschlüsselung mit Rijndael

SecureString

Die Idee hinter dem SecureString ist es, die Zeit in der sich das Passwort (bzw. der zu sichernde Text) als Typ String im System befindet auf ein Minimum zu reduzieren. Dazu wird der String in ein Binärobjekt gewandelt und im unmanaged Memory (nicht verwalteter Speicher) gespeichert. Das führt dazu, dass das Objekt nicht durch den Garbage Collector berührt wird. Der SecureString wird fest im RAM verankert, sodass er nicht Teil der virtuellen Speichers ist und so „geswappt“ wird (Swap kann Daten auf der Festplatte hinterlassen, welche auch nach Beenden der Applikation noch vorhanden sind).
Früher oder später wird es nötig sein die Daten als String zwischenzuspeichern (beispielsweise, wenn der User das Passwort eingeben muss oder es wieder angezeigt werden soll).

Nun aber zum Code:
1. Erstellen eines SecureStrings:
Das Erstellen eines SecureString geschieht in der Regel mit der Methode AppendChar(char c). Zusätzlich gibt es noch die Methoden InsertAt(int index, char c), RemoveAt(int index) und SetAt(int index, char c).

        public void ConvertCharArrayToSecureString(char[] CharArray)
        {
            foreach (char c in CharArray) //IEnumerable
            {
                secureStr.AppendChar(c); //Für jeden Buchstaben (char) in dem String(Text in der Textbox), fügen wir den aktuellen Buchstaben dem SecureString an
            }
        }

Sobald das Passwort in den SecureString gewandelt wurde, solltet ihr den String/die TextBox leeren und den SecureString mittels der Methode MakeReadOnly() als schreibgeschützt festlegen.

Rufen wir nun die ToString() des SecureStrings auf und geben den Rückgabewert per MessageBox aus, so wird uns lediglich der Typ des Objekts angezeigt
2013-09-16 13_39_42-

2. Wandeln eines SecureStrings in einen String

Nun möchte man möglicherweise den SecureString doch auslesen. Dies lässt sich mit folgender Methode bewerkstelligen:

        public string ConvertSecureStringToString()
        {
            IntPtr unmanagedString = IntPtr.Zero; //Erstellt einen Zeiger (zeigt bisher auf nichts)
            try
            {
                unmanagedString = Marshal.SecureStringToGlobalAllocUnicode(secureStr); //kopiert den SecureString in den nicht verwalteten Speicher (dient als Puffer)
                return Marshal.PtrToStringUni(unmanagedString); //Reserviert einen verwalteten String (unseren Rückgabetyp) und kopiert eine nicht verwaltete Zeichenfolge (unser SecureString) hinein.
            }
            finally //Der im Finally-Block befindliche Code wirk auf jeden Fall ausgeführt (auch bei einer Exception)!
            {
                Marshal.ZeroFreeGlobalAllocUnicode(unmanagedString); //Gibt den Zeiger, welcher mit SecureStringToGlobalAllocUnicode reserviert wurde, frei (Puffer wird befreit)
            }
        }

Bitte vergesst den Code im finally-Block nicht, denn ohne diesen entsteht eine Möglichkeit das Passwort auszulesen.

In unserem oberen Beispiel muss ich einen kompletten String bzw. ein komplettes CharArray übergeben, sodass es hier noch die Möglichkeit gibt, das komplette Passwort an einen Stück zu auszulesen, um dies noch weiter zu erschweren, kann man wie folgt vorgehen.

Man erstelle eine normale TextBox (in meinem Beispiel SecureString_SecurerTextBox_textBox genannt) und registriere einen Eventhandler für das KeyPress-Event. In der Methode des Events verwendet man dann folgenden Code:

private void SecureString_SecurerTextBox_textBox_KeyPress(object sender, KeyPressEventArgs e)
        {
            if (e.KeyChar == '\b') //Falls die Rücktaste gedrückt wurde,
            {
                DeleteLastCharFromSecureString(); //soll der letzte Buchstabe des SecureStrings gelöscht werden
                if(SecureString_SecurerTextBox_textBox.Text.Length > 0) //Sowie der letzte Buchstabe des Textes in der TextBox, wenn dort noch mindestens ein Buchstabe vorhanden ist
                    SecureString_SecurerTextBox_textBox.Text = SecureString_SecurerTextBox_textBox.Text.Remove(SecureString_SecurerTextBox_textBox.Text.Length - 1, 1);
            }
            else //Sollte die Taste eines anderen Buchstaben gedrückt worden sein,
            {
                AddCharToSecureString(e.KeyChar); //rufe die Methode zum Anfügen des Buchstabens auf
                SecureString_SecurerTextBox_textBox.Text += "*"; //und füge dem Text der TextBox ein Sternchen an
            }
            e.Handled = true; //Bricht das KeyPress-Ereignis ab (es wird kein Buchstabe hinzugefügt)
        }

Folgende Methoden sind zusätzlich erforderlich:

        private void AddCharToSecureString(char c)
        {
            secureStr.AppendChar(c); //Fügt einen Buchstaben an letzter Stelle des SecureStrings an
        }

        private void DeleteCharFromSecureString()
        {
            if(secureStr.Length > 0)
                secureStr.RemoveAt(secureStr.Length - 1); //Löscht den letzten Buchstaben im SecureString
        }

Durch die Vorgehensweise wird der Buchstabe nur als Parameter übergeben und in der TextBox lediglich ein Sternchen (*) hinterlegt. Die Zeit in der sich das Passwort offen/komplett im Speicher liegt, ist stark reduziert. Leider leidet die Benutzerfreundlichkeit und die Übersichtlichkeit des Codes darunter.

Zu guter Letzt das Demo Projekt:

20130811_Verweis-Manager - OleDBTest_000034

ROT13 und SecureString

© 2018 Abou Chleih. Alle Rechte vorbehalten.

Thema von Anders Norén.