VisualBasic .NET - WebDAV als Netzlaufwerk verbinden mit Smartcard Authentifizierung

Stumie

Lt. Junior Grade
Registriert
Sep. 2006
Beiträge
500
Ich habe ein kleines Projekt. Eine GUI, die WebDAV Laufwerke verbindet.

Mit Password und Username klappt es. Nun möchte ich aber auch einbauen, sich mit einer Smartcard einzuloggen.

Das normale Einloggen habe ich mit "WNetAddConnection2" bewerkstelligt. Aber diese API unterstützt nach meinen bisherigen Recherchen keine Smartcards.
Ich habe dann gesucht und bin auf "CredUICmdLinePromptForCredentials" gestoßen, die Smartcard-Authentifizierung ermöglicht. Ich tue mich jedoch sehr schwer im Umgang damit, da es auch so gut wie keine Dokumentationen dazu gibt.

Gibt es eine andere, möglicherweise einfachere, Möglichkeit meine Vorstellungen umzusetzen oder eine gute Doku zur "CredUICmdLinePromptForCredentials"?

Ich würde mich sehr über gute Tipps freuen.
Falls noch informationen benötigt werden, liefere ich diese gerne nach.

Mfg Stumie

OS: Windows 7 64bit
IDE: Visual Studio Express 2010
Programmiersprache: Visual Basic .NET

PUSH!
 
Zuletzt bearbeitet: (PUSH!)
Version 4

EDIT: Ich wage noch einen Push.
 
Zuletzt bearbeitet: (Push)
Puh!

Ich hab mir mal alle Dateien dort runtergeladen.
Die Demo funktioniert nicht und die anderen Zips sind sehr undurchsichtig.
Die Doku stellt auch "nur" stammbaumartig den gesamten Code dar und bietet keine Erklärung zum Ansätzen oder Vorgehensweise. Zumindest habe ich beim ersten mal durchblättern nichts derartiges gefunden.
Da ich nichtmal einen Ansatz habe, fällt es mir sehr schwer aus dieser Vielfalt etwas sinnvolles herauszuziehen.

Zusätzlich würde ich mich jetzt noch nicht wirklich als erfahrenen Programmierer darstellen. Dieses Projekt ist das erste seiner Art in meiner Ausbildung (FiSi).

Der Shell-Befehl "net use" bietet auch eine Option "/smartcard".
Diese Variante mag unsere Zugangskarte aber irgendwie nicht ohne weitere Parameter. Gibt es dazu hier irgendwo Erfahrungswerte?
Das wäre ja ein unschöner aber möglicher "Umweg".
"net use d: \\server\share /smartcard" funktioniert also nicht einfach so...
 
Wo ist denn genau dein Problem?
CredUICmdLinePromptForCredentials ist schon die richtige Funktion (sofern es eine Konsolenanwendung ist, für eine GUI-Anwendung ist es CredUIPromptForCredentials). Sie gibt dir einen Benutzernamen (pszUserName) und ein Passwort (pszPassword) zurück, die du an "WNetAddConnection2" übergeben kannst.
Zum Aufruf sollte eigentlich die verlinkte MSDN-Doku reichen. Wenn du irgendwelche konkrete Schwierigkeiten beim Aufruf hast, frag einfach.
 
Code:
_Inout_   PCTSTR pszUserName

Das ist ein String und

Code:
_Inout_   PBOOL pfSave,

das wäre als Boolean definiert, oder?


Code:
 _In_      PCtxtHandle Reserved,

Wie habe ich das zu definieren?
 
Stumie schrieb:
Code:
_Inout_   PCTSTR pszUserName
Das ist ein String und
Im Prinzip ja, da die Funktion dort aber reinschreibt, solltest du einen StringBuilder übergeben.
Stumie schrieb:
Code:
_Inout_   PBOOL pfSave,
das wäre als Boolean definiert, oder?
Ja.
Stumie schrieb:
Code:
 _In_      PCtxtHandle Reserved,
Wie habe ich das zu definieren?
Die Doku sagt "Currently reserved and must be NULL.", also null bzw. Nothing.
 
Bezieht sich das "NULL" nicht nur auf den Wert der Deklaration und nicht um die Deklaration selbst?
Der Wert ist ja nicht "String" sondern das ist ja die Art der Deklaration, oder ist es in diesem Fall "ByVal Reserved As NULL"?
 
Also ich hab die Funktion nun mal versucht zu formulieren:

Code:
Declare Function CredUIPromptForCredentials Lib "Credui.dll" Alias "CredUIPromptForCredentialsA" (ByVal pszTargetName As String, _
ByVal Reserved As IntPtr, ByVal pszUserName As System.Text.StringBuilder, ByVal ulUserNameMaxChars As Long, _
ByVal pszPassword As System.Text.StringBuilder, ByVal ulPasswordMaxChars As Long, ByVal pfSave As Boolean, _
ByVal dwFlags As Integer) As Integer

Ich habe die Optionalen Deklarationen weg gelassen und "Reserved" als "IntPtr" deklariert.

Ich verstehe nicht wie jetzt IntPtr.Zero übergeben werden soll.

Gibt es sonst noch grobe Fehler?
Mich verwirrt der 'Unterschied zwischen ByVal und ByRef immernoch ganz schön.
Es wird doch überall ein Wert übergeben, oder gibt es irgendwo einen Verweis?
Ich leite doch eigentlich alle Variablen neu ein, oder?

Mein Kopf raucht. :D
 
Stumie schrieb:
Ich verstehe nicht wie jetzt IntPtr.Zero übergeben werden soll.
Du übergibst beim Aufruf der Funktion für den Parameter einfach IntPtr.Zero. "Zero" ist ein statisches Feld auf dem "IntPtr"-Struct. Es repräsentiert einfach einen Null-Pointer.

Stumie schrieb:
Mich verwirrt der 'Unterschied zwischen ByVal und ByRef immernoch ganz schön.
Es wird doch überall ein Wert übergeben, oder gibt es irgendwo einen Verweis?
ByRef solltest du alle Parameter, die mit "_Out_" oder "_InOut_" gekennzeichnet sind übergeben, weil die Funktion die über diese Parameter Werte zurückgibt, also das Übergebene ändern kann.
"pszUserName" und "pszPassword" sind ein Sonderfall. Eigentlich sind das Pointer auf einen String. Da man Strings in .NET aber nicht verändern kann, übergibt man einen StringBuilder. Da der StringBuilder ein Klasse also ein Referenztyp ist, wird bei der Übergabe auch mit "ByVal" die Referenz übergeben.

Stumie schrieb:
Gibt es sonst noch grobe Fehler?
Keine wirklich Groben, nur Kleinigkeiten.
Beachten muss man, dass in der WinAPI sowohl "DWORD" als auch "ULONG" ein 32-bin unsigned Integer sind. Ein "Long" in .NET ist allerdings ein 64-bit signed Integer. DWORDs und ULONGs sollten also richtigerweise als UInt32 deklariert werden. (Ich weiß das ist verwirrend, es hilft aber nichts. ;))

Den optionalen "dwAuthError" würde ich sicherheitshalber einfügen. Beim Aufruf kannst du 0 übergeben.

Das "MarshalAs" bei "pfSave" setzt noch die richtige Bool-Darstellung (siehe auch "Regelbeschreibung" bei http://msdn.microsoft.com/de-de/library/ms182206(v=vs.80).aspx).

Für die Flags (dwFlags) und den Return-Code kann man zur besseren Lesbarkeit Enums anlegen:
Code:
<Flags()> _
Public Enum CREDUI_FLAGS
    INCORRECT_PASSWORD = &H1
    DO_NOT_PERSIST = &H2
    REQUEST_ADMINISTRATOR = &H4
    EXCLUDE_CERTIFICATES = &H8
    REQUIRE_CERTIFICATE = &H10
    SHOW_SAVE_CHECK_BOX = &H40
    ALWAYS_SHOW_UI = &H80
    REQUIRE_SMARTCARD = &H100
    PASSWORD_ONLY_OK = &H200
    VALIDATE_USERNAME = &H400
    COMPLETE_USERNAME = &H800
    PERSIST = &H1000
    SERVER_CREDENTIAL = &H4000
    EXPECT_CONFIRMATION = &H20000
    GENERIC_CREDENTIALS = &H40000
    USERNAME_TARGET_CREDENTIALS = &H80000
    KEEP_USERNAME = &H100000
End Enum

Public Enum CredUIReturnCodes As Integer
    NO_ERROR = 0
    ERROR_CANCELLED = 1223
    ERROR_NO_SUCH_LOGON_SESSION = 1312
    ERROR_NOT_FOUND = 1168
    ERROR_INVALID_ACCOUNT_NAME = 1315
    ERROR_INSUFFICIENT_BUFFER = 122
    ERROR_INVALID_PARAMETER = 87
    ERROR_INVALID_FLAGS = 1004
End Enum

Meiner Meinung nach sollte die Deklaration dann so aussehen:
Code:
Declare Function CredUIPromptForCredentials Lib "Credui.dll" Alias "CredUIPromptForCredentialsA" (
    ByVal pszTargetName As String, _
    ByVal Reserved As IntPtr, _
    ByVal dwAuthError As UInt32, _
    ByVal pszUserName As StringBuilder, _
    ByVal ulUserNameMaxChars As UInt32, _
    ByVal pszPassword As StringBuilder, _
    ByVal ulPasswordMaxChars As UInt32, _
    <MarshalAs(UnmanagedType.Bool)> ByRef pfSave As Boolean, _
    ByVal dwFlags As CREDUI_FLAGS) As CredUIReturnCodes

Auf Aufruf kannst du ich mal selbst probieren. Eine gute Quelle zum Abschauen (bei einer ähnlichen Funktion) gibts bei pinvoke.net (überhaupt sehr gute Quelle für solche WinAPI-Aufrufe, von dort stammen auch die Enum-Definitionen): http://pinvoke.net/default.aspx/credui.CredUIPromptForCredentials

Bei Problemen: Fragen. :)
 
Oh, vielen, vielen Dank!

Das ist wirklich eine sehr ausführliche Antwort. Ich werde gleich morgen versuchen Erfolge zu erzielen.

EDIT:

Es tut mir leid, aber es macht sich einfach immernoch bemerkbar, dass ich ein blutiger Anfänger bin.

Die Funktion schreibt ja nun theoretisch den Usernamen und das Passwort in den Stringbuilder (intern).
Wie kann ich das denn nun extern abrufen? Der Stringbuilder-content lässt sich ja nicht ohne Weiteres in einen normalen String konvertieren. Dass erzählt mir Visual Studio auch immer wieder.

Und als zweiter Punkt stellt sich mir nun noch die Frage, wie ich mit "dwAuthError" umzugehen habe.
Ich würde zwar vermuten, dass dort ein einfach Wert verlangt wird (z.B. 0 oder 1), aber bei MSDN ist dazu nichts geschrieben.
Aber nochmal zur Verständnis: "dwAuthError" gibt doch nur an, ob die Funktion die Erlaubnis hat noch weitere Fenster zu öffnen, um z.B. ein Passwort zu ändern. Oder liege ich da falsch?

Mit der letzten Variante des Codes konnte ich die Funktion auch noch zu keiner Ausgabe bewegen.
Mit meiner letzten Konfig hat sie mir immerhin den Fehlercode "87" ("ERROR_INVALID_PARAMETER") ausgegeben.^^
 
Zuletzt bearbeitet: (weitere Fragen)
Stumie schrieb:
Die Funktion schreibt ja nun theoretisch den Usernamen und das Passwort in den Stringbuilder (intern).
Wie kann ich das denn nun extern abrufen? Der Stringbuilder-content lässt sich ja nicht ohne Weiteres in einen normalen String konvertieren. Dass erzählt mir Visual Studio auch immer wieder.
Einfach nach dem Aufruf der Funktion auf dem StringBuilder "ToString()" aufrufen.

Stumie schrieb:
Und als zweiter Punkt stellt sich mir nun noch die Frage, wie ich mit "dwAuthError" umzugehen habe.
Ich würde zwar vermuten, dass dort ein einfach Wert verlangt wird (z.B. 0 oder 1), aber bei MSDN ist dazu nichts geschrieben.
Aber nochmal zur Verständnis: "dwAuthError" gibt doch nur an, ob die Funktion die Erlaubnis hat noch weitere Fenster zu öffnen, um z.B. ein Passwort zu ändern. Oder liege ich da falsch?
"dwAuthError" gibt an warum das Passwort verlangt wird. Den Wert bekommst du z. B. von anderen API-Funktionen, die eine Authentifizierung erfordern. Da du das Passwort aber selbst willst übergibst du einfach 0.

Stumie schrieb:
Mit der letzten Variante des Codes konnte ich die Funktion auch noch zu keiner Ausgabe bewegen.
Mit meiner letzten Konfig hat sie mir immerhin den Fehlercode "87" ("ERROR_INVALID_PARAMETER") ausgegeben.^^
Dann ist ein übergebener Parameter falsch. Poste doch mal deinen Aufruf und die übergebenen Parameter.
 
Code:
    Private Sub Form1_Load(sender As System.Object, e As System.EventArgs) Handles MyBase.Load

        Dim username As System.Text.StringBuilder
        Dim password As System.Text.StringBuilder
        Dim struser As String
        Dim strpassw As String

        Label1.Text = CredUIPromptForCredentials("Hier-stand-ein-WEBDAV-Link", _
                                   IntPtr.Zero, 0, username, 25, password, 25, True, _
                                   CREDUI_FLAGS.REQUIRE_CERTIFICATE)

        username.ToString(struser, 255)
        password.ToString(strpassw, 255)


    End Sub

Das ist irgendwie totaler bull****. Das weiß ich, aber ich habe keinen Plan wie ich aus diesen Parametern etwas raus bekomme. :freak: :(
Er nimmt das gewurste mit "username" und "password" halt nicht.

EDIT:

Habe nochmal pinvoke durchsucht:

Code:
        Dim userID = New StringBuilder
        Dim userPassword = New StringBuilder
        Dim struser As String
        Dim strpassw As String

        Label1.Text = CredUIPromptForCredentials("Hier-stand-ein-WEBDAV-Link", _
                                   IntPtr.Zero, 0, userID, 25, userPassword, 25, True, _
                                   CREDUI_FLAGS.REQUIRE_CERTIFICATE)

        struser = userID.ToString()
        strpassw = userPassword.ToString()

        Label2.Text = struser
        Label3.Text = strpassw

So meckert das IDE zumindest keine groben Code-Fehler mehr an, aber ich erhalte trotzdem keine Ausgabe auf den Labels. Nichtmal als direktes Funktionsresult.
 
Zuletzt bearbeitet:
So sollte das gehen:
Code:
Private Sub Form1_Load(sender As System.Object, e As System.EventArgs) Handles MyBase.Load
        ' Die StringBuilder musst du erst erzeugen '
        Dim username As System.Text.StringBuilder = new System.Text.StringBuilder()         
        Dim password As System.Text.StringBuilder = new System.Text.StringBuilder()
        Dim struser As String
        Dim strpassw As String
 
        Label1.Text = CredUIPromptForCredentials("Hier-stand-ein-WEBDAV-Link", _
                                   IntPtr.Zero, _
                                   0, _
                                   username, _
                                   255, _ ' 25 scheint mir als Länge etwas knapp '
                                   password, _
                                   255, _
                                   True, _
                                   'CREDUI_FLAGS.REQUIRE_CERTIFICATE soll man laut Doku nicht benutzen '
                                   CREDUI_FLAGS.REQUIRE_SMARTCARD) 
 
        struser = username.ToString() ' ToString ohne Parameter ist, was du willt. '
        strpassw = password.ToString()
    End Sub
 
Zuletzt bearbeitet:
So, nach ein bisschen Test ist mir aufgefallen, dass du ja "CredUIPromptForCredentials" und nicht "CredUICmdLinePromptForCredentials" benutzt.
Dafür fehlt deiner Deklaration aber ein Parameter ("pUiInfo"). Außerdem findet die "Declare"-Syntax scheinbar nicht die richtige Funktion in der DLL. So funktioniert es bei mir einwandfrei (ohne Smartcard, weil ich kein habe):
Code:
<DllImport("credui.dll")>
Shared Function CredUIPromptForCredentials(
    ByRef pUiInfo As CREDUI_INFO, _
    ByVal pszTargetName As String, _
    ByVal Reserved As IntPtr, _
    ByVal dwAuthError As UInt32, _
    ByVal pszUserName As StringBuilder, _
    ByVal ulUserNameMaxChars As UInt32, _
    ByVal pszPassword As StringBuilder, _
    ByVal ulPasswordMaxChars As UInt32, _
    <MarshalAs(UnmanagedType.Bool)> ByRef pfSave As Boolean, _
    ByVal dwFlags As CREDUI_FLAGS) As CredUIReturnCodes
End Function
Dazu brauchst du eine Struktur für "pUiInfo"-Paramter:
Code:
<StructLayout(LayoutKind.Sequential)>
Public Structure CREDUI_INFO
    Public cbSize As UInt32
    Public hwndParent As IntPtr
    Public pszMessageText As String
    Public pszCaptionText As String
    Public hbmBanner As IntPtr
End Structure
Der Aufruf sieht bei mir dann so aus:
Code:
Private Sub Form1_Load(sender As Object, e As EventArgs) Handles MyBase.Load
	Dim username As System.Text.StringBuilder = New System.Text.StringBuilder()
	Dim password As System.Text.StringBuilder = New System.Text.StringBuilder()
        Dim save As Boolean = True ' pfSave ist _InOut_ also auch eine Variable übergeben
	Dim struser As String
	Dim strpassw As String

        ' Struktur erzeugen und Größe ermitteln, weil das die Funktion braucht. '
	Dim info As CREDUI_INFO = New CREDUI_INFO()
	info.cbSize = Marshal.SizeOf(info)

	' Konstanten für die Eingabelängen (aus der credui.h ausgelesen)
	Const ulUserNameMaxChars As UInt32 = 256 + 1 + 256
	Const ulPasswordMaxChars As UInt32 = 512 / 2

	Label1.Text = CredUIPromptForCredentials(
					   info, _
					   "Hier-stand-ein-WEBDAV-Link", _
					   IntPtr.Zero, _
					   0, _
					   username, _
					   ulUserNameMaxChars , _
					   password, _
					   ulPasswordMaxChars , _
					   save, _
					   CREDUI_FLAGS.REQUIRE_SMARTCARD)

	struser = username.ToString()
	strpassw = password.ToString()
End Sub
 
Wahnsinn! Vielen Dank! Soweit funktioniert es schonmal.

Aber als ich anschließend versucht habe die Funktion mit der WNetAddConnection2-Furnktion zu verknüpfen, ist mein Tool abgestürzt mit einer Fehlermeldung zur "mpr.dll". Dies ist die DLL, die von der WNetAddConnection2-Funktion verwendet wird.
Meine Vermutung ist, dass "WNetAddConnection2" nicht mit dem Schlüssel zurecht kommt, der von "CredUIPromptForCredentials" übergeben wird. Laut Doku könnte man mit der Funktion "CredMarshalCredential" den Schlüssel in einen Text-String übersetzen, aber das wäre 1. eine Sicherheitslücke und 2. las sich die CredUIPromptForCredentials-Doku eigentlich auch so, dass Funktionen wie "WNetAddConnection2" mit so einem Schlüssel klar kämen.

EDIT:

Hier nochmal der Ausschnitt aus meinem Quellcode:
Code:
strDavPathfull_secure = "*HIER-STAND-EIN-WEBDAV-LINK"
udtNetzResource.lpRemoteName = strDavPathfull_secure

Dim userID_secure As System.Text.StringBuilder = New System.Text.StringBuilder()
Dim userPassword_secure As System.Text.StringBuilder = New System.Text.StringBuilder()
Dim struser_secure As String
Dim strpassw_secure As String
Dim pfSave As Boolean = True

Dim pUiInfo As CREDUI_INFO = New CREDUI_INFO()
 pUiInfo.cbSize = Marshal.SizeOf(pUiInfo)


CredUIPromptForCredentials(pUiInfo, strDavPathfull, _
                IntPtr.Zero, 0, userID_secure, ulUserNameMaxChars, _
                userPassword_secure, ulPasswordMaxChars, pfSave, _
                CREDUI_FLAGS.REQUIRE_SMARTCARD)

struser_secure = userID_secure.ToString()
strpassw_secure = userPassword_secure.ToString()

If persistent.Checked = True Then
                lngResult = WNetAddConnection2(udtNetzResource, strpassw_secure, struser_secure, CONNECT_UPDATE_PROFILE)
Else
                lngResult = WNetAddConnection2(udtNetzResource, strpassw_secure, struser_secure, 0)
End If

Wenn wir das auch noch gemeinsam hinbekommen, bin ich gezwungen dir ein Bier auszugeben!

EDIT2:

Und ganz neu: initiiert er jetzt beim lngResult gleich eine "1" -> Fehler

EDIT3:

Ich beschäftige mich mal weiter damit, dass er irgendwie nicht einmal mehr die Funktion aufruft.

Wenn ich aber die Funktion einfach mal in einer eigenes Programm schmeiße, ohne den ganzen anderen Kram, kommt diese Fehlermeldung:

FatalExecutionEngineError wurde erkannt.
Message: Die Laufzeit hat einen schwerwiegenden Fehler entdeckt. Fehleradresse: "0x6a5cbb51" in Thread "0xe70". Fehlercode: 0xc0000005. Bei diesem Fehler könnte es sich um ein Problem in der CLR oder in den unsicheren oder nicht verifizierbaren Teilen des Benutzercodes handeln. Übliche Ursachen dieses Bugs sind Marshallerfehler für COM-Interop oder PInvoke, die den Stapel beschädigen können.

Das hört sich nach meiner Vermutung am, bringt mich einer Lösung aber nur bedingt näher.
 
Zuletzt bearbeitet:
Was steht denn in "struser_secure" und "strpassw_secure" drin? Ein "echter" Username und Passwort?
Bei meinen Tests ohne Smartcard hat "CredUIPromptForCredentials" mir immer den Benutzer und das Passwort im Klartext geliefert.
 
Zurück
Oben