{"id":55000487,"date":"2025-10-01T00:00:00","date_gmt":"2025-12-28T17:10:17","guid":{"rendered":"http:\/\/access-im-unternehmen.aix-dev.de\/aiu\/?p=487"},"modified":"-0001-11-30T00:00:00","modified_gmt":"-0001-11-30T00:00:00","slug":"SQL_ServerZugangsdaten_sicher_speichern","status":"publish","type":"post","link":"https:\/\/vbentwickler.de\/SQL_ServerZugangsdaten_sicher_speichern\/","title":{"rendered":"SQL Server-Zugangsdaten sicher speichern"},"content":{"rendered":"<p><img loading=\"lazy\" decoding=\"async\" src=\"http:\/\/vg08.met.vgwort.de\/na\/16867d8b8c8e42aca54b55001d68b1bf\" width=\"1\" height=\"1\" alt=\"\"><b>Im Artikel &#8220;SQL Server: Tabellen verkn&uuml;pfen&#8221; (www.vbentwickler.de\/485) haben wir gezeigt, wie wir die Tabellen eines SQL Servers per ODBC als Tabellenverkn&uuml;pfung in Access-Datenbanken verf&uuml;gbar machen k&ouml;nnen. Dabei gibt es zwei Varianten, um &uuml;ber entsprechende Verbindungszeichenfolgen auf die Tabellen des SQL Servers zuzugreifen: Windows-Authentifizierung und SQL Server-Authentifizierung. Bei der ersten werden die Zugangsdaten &uuml;ber den aktuellen Windows-Benutzer ermittelt. Dieses Kennwort kennt in der Regel nur der Benutzer und die Verkn&uuml;pfung der Tabellen kann nach seiner Anmeldung am Rechner ohne weitere Interaktion erfolgen. Bei der SQL Server-Authentifizierung jedoch m&uuml;ssen zus&auml;tzlich die Zugangsdaten des Benutzers zum SQL Server angegeben werden. Damit der Benutzer diese nicht immer manuell eingeben muss, m&ouml;chten wir diese sicher speichern &#8211; so, dass niemand diese auslesen kann, auch wenn er Zugriff zum Rechner des Benutzers hat. Wie das gelingt, zeigen wir in diesem Artikel.<\/b><\/p>\n<p>Wenn ein Benutzer sich an seinen Rechner angemeldet hat und seine Access-Anwendung ge&ouml;ffnet hat, muss er, um auf per ODBC verkn&uuml;pfte SQL Server-Tabellen zuzugreifen, auf irgendeine Weise seine Zugangsdaten f&uuml;r den SQL Server und die entsprechende Datenbank angeben, damit diese zum Aktualisieren oder Neuanlegen der Tabellenverkn&uuml;pfungen genutzt werden k&ouml;nnen. Wie das Aktualisieren oder Neuverkn&uuml;pfen gelingt, haben wir in aller Ausf&uuml;hrlichkeit im Artikel <b>SQL Server: Tabellen verkn&uuml;pfen <\/b>(<b>www.vbentwickler.de\/485<\/b>) beschrieben. Dazu m&uuml;ssen der Benutzername und das Kennwort einmalig angegeben und in die Verbindungszeichenfolge zum Herstellen der Tabellenverkn&uuml;pfungen eingetragen werden. Nach dem initialen Aktualisieren oder Neuanlegen der Tabellenverkn&uuml;pfungen ist w&auml;hrend der aktiven Access-Session keine weitere Eingabe dieser Daten erforderlich, da diese intern gespeichert werden. Erst wenn der Benutzer diese Access-Session durch Schlie&szlig;en der Anwendung beendet, werden die tempor&auml;r gespeicherten Zugangsdaten aus dem Speicher gel&ouml;scht.<\/p>\n<p>Da jede Eingabe Zeit kostet, wollen wir hier eine L&ouml;sung vorstellen, mit der wir die Zugangsdaten an irgendeiner Stelle im System speichern k&ouml;nnen, damit der Benutzer diese nicht bei jedem Start der Anwendung manuell eingeben muss. Wir gehen an dieser Stelle davon aus, dass der Benutzer sich nach Feierabend von seinem Rechner abmeldet und diesen auch in kurzen Pausen sperrt. Ansonsten k&ouml;nnten andere Benutzer auf den Rechner zugreifen und die Daten der verkn&uuml;pften Tabellen m&uuml;helos auslesen und in eine andere Datenbank kopieren.<\/p>\n<p>Dennoch w&uuml;rde er dazu eine gewisse Zeit ben&ouml;tigen. Sicherstellen wollen wir auf jeden Fall, dass ein Dritter sich keinen Zugang zu den von uns im System des Benutzers gespeicherten Zugangsdaten zum SQL Server zu verschaffen. Dies w&auml;re kritisch, weil ein Dritter diese dann nutzen k&ouml;nnte, um von einem anderen Rechner in aller Ruhe auf die Tabellen der entsprechenden SQL Server-Datenbank zuzugreifen und diese zu kopieren, zu &auml;ndern oder zu l&ouml;schen oder auch neue Daten hinzuzuf&uuml;gen.<\/p>\n<p>Die vorgestellte L&ouml;sung soll also nicht nur dazu dienen, dem Benutzer mehr Komfort beim Starten der Access-Anwendung zu bieten, indem er nicht jedes Mal seine SQL Server-Zugangsdaten eingeben muss. Sie soll auch daf&uuml;r sorgen, dass diese Zugangsdaten so gespeichert werden, dass kein Dritter diese auslesen kann, auch wenn er uneingeschr&auml;nkten Zugriff auf den Rechner hat.<\/p>\n<h2>Geeignete Speicherorte<\/h2>\n<p>Es gibt verschiedene geeignete Speicherorte f&uuml;r solche Zugangsdaten. Der naheliegendste ist eine Optionentabelle in der Access-Anwendung selbst. Der Benutzer m&uuml;sste einmal seine SQL Server-Zugangsdaten eingeben. Diese w&uuml;rden dann direkt nach der Eingabe in der entsprechenden Tabelle gespeichert werden.<\/p>\n<p>Eine weitere Option ist eine externe Datei &#8211; dabei kann es sich um eine simple Textdatei handeln oder auch eine XML- oder eine JSON-Datei, in die wir diese Informationen in strukturierter Form eintragen k&ouml;nnen.<\/p>\n<p>Schlie&szlig;lich gibt es noch die Registry. Diese bietet im Pfad <b>HKEY_CURRENT_USER\\Software\\VB and VBA Program Settings <\/b>einen Bereich, der speziell per VBA oder VB leicht beschrieben oder ausgelesen werden kann, ohne dass wir Funktionen der Windows-API bem&uuml;hen m&uuml;ssen.<\/p>\n<p>All diese Speicherorte haben Vor- und Nachteile gemeinsam. Der Vorteil ist, dass man diese nur auslesen kann, wenn man unter dem entsprechenden Benutzerkonto angemeldet ist. Voraussetzung daf&uuml;r ist, dass sich die Dateien im jeweiligen Benutzerordner befinden. Bei dem angesprochenen Registry-Bereich ist das automatisch sichergestellt, da sich dieser unter dem Element <b>HKEY_CURRENT_USER <\/b>befindet, der ohnehin nur im Kontext der entsprechenden Benutzeranmeldung auslesbar ist.<\/p>\n<p>Aber wenn der Benutzer einmal seinen Arbeitsplatz verl&auml;sst, zum Beispiel um sich einen Kaffee zu holen, ohne sich abzumelden oder den Bildschirm zu sperren, k&ouml;nnte ein Dritter diese Daten ebenfalls m&uuml;helos auslesen und anschlie&szlig;end von einem anderen Rechner &uuml;ber diesen Zugang auf den SQL Server zugreifen.<\/p>\n<p>Es gilt also, noch eine weitere Sicherheitsstufe zu integrieren.<\/p>\n<h2>Sicheres Speichern der Zugangsdaten<\/h2>\n<p>Dies geschieht wiederum in zwei Stufen. Die erste Stufe ist, dass wir die Zugangsdaten nach der Eingabe und vor dem Speichern durch einen entsprechenden Algorithmus verschl&uuml;sseln. Dieser Algorithmus wird als Funktion in der Datenbankanwendung hinterlegt.<\/p>\n<p>Nun kann ein Dritter ohne weitere Ma&szlig;nahmen immer noch das VBA-Projekt der Access-Anwendung &ouml;ffnen und Einblick in diesen Algorithmus erhalten, um diesen dann auf einem anderen Rechner auszuf&uuml;hren. Hier folgt die zweite Stufe, indem wir sicherstellen, dass Benutzer keine ungesicherte <b>.accdb<\/b>-Version der Access-Anwendung nutzen, sondern nur eine <b>.accde<\/b>-Version, die keinen Einblick in den VBA-Code mehr gew&auml;hrt. So ist sichergestellt, dass niemand den Verschl&uuml;sselungsalgorithmus ermitteln kann.<\/p>\n<p>Schlie&szlig;lich sollten wir nicht nur einfach den Benutzernamen und das Kennwort verschl&uuml;sseln, zum Beispiel mit SHA1. Das Problem ist: Wenn jemand Zugriff auf die Registry oder die Datei hat, in der die Zugangsdaten gespeichert sind, kann er einfach alle g&auml;ngigen Algorithmen zur Entschl&uuml;sselung einmal durchgehen und wird so fr&uuml;her oder sp&auml;ter auf die richtige Methode sto&szlig;en und kann die Zugangsdaten entschl&uuml;sseln.<\/p>\n<p>Das Zauberwort an dieser Stelle lautet Pepper. So nennt man eine Zeichenkette, die als zus&auml;tzlicher Faktor f&uuml;r die Verschl&uuml;sselung verwendet werden kann. Diesen Faktor speichern wir wiederum im VBA-Projekt, das wir durch Umwandeln der Datenbankdatei in eine <b>.accde <\/b>weitgehend unlesbar machen.<\/p>\n<p>Weitgehend deshalb, weil es durchaus Unternehmen gibt, die <b>.accde<\/b>-Dateien entschl&uuml;sseln k&ouml;nnen, so dass auch unsere Pepper-Zeichenkette ausgelesen werden kann. Solche Unternehmen lassen sich aber, wenn sie im legalen Bereich operieren, vom Auftraggeber best&auml;tigen, dass sie berechtigt sind, das VBA-Projekt dieser Anwendung wieder lesbar zu machen.<\/p>\n<h2>Verwenden von DPAPI zum Ver- und Entschl&uuml;sseln<\/h2>\n<p>In diesem Artikel verwenden wir keine der &uuml;blichen Verschl&uuml;sselungsmethoden wie SHA1 et cetera, sondern DPAPI. DPAPI steht f&uuml;r Data Protection API und ist ein integrierter Windows-Mechanismus, mit dem Anwendungen Daten einfach und sicher verschl&uuml;sseln k&ouml;nnen, ohne selbst Kryptographie implementieren zu m&uuml;ssen.<\/p>\n<p>Die Besonderheit von DPAPI ist, dass es einen Schl&uuml;ssel verwendet, der an das Windows-Benutzerkonto oder an den Computer gebunden ist.<\/p>\n<p>Es gibt zwei Modi:<\/p>\n<ul>\n<li><b>USER-MODE<\/b>: Hier ist der Schl&uuml;ssel ist an den aktuell angemeldeten Windows-Benutzer gebunden. Nur dieser Benutzer kann die Daten wieder entschl&uuml;sseln. Dazu kann er die API-Funktionen <b>CryptProtectData <\/b>und <b>CryptUnprotectData <\/b>nutzen.<\/li>\n<li><b>MACHINE-MODE<\/b>: Hier ist der Schl&uuml;ssel ist an den Rechner gebunden. Damit kann jedes Programm unter jedem Benutzer kann entschl&uuml;sseln, was zum Beispiel f&uuml;r Serverdienste verwendet werden kann.<\/li>\n<\/ul>\n<p>F&uuml;r Access-Frontend-Anwendungen ist USER-MODE perfekt. Wenn wir die Funktion <b>CryptProtectData <\/b>nutzen, wird die zu verschl&uuml;sselnde Zeichenkette in einen Datenblock gepackt. Windows verschl&uuml;sselt diesen Datenblock mit einem Schl&uuml;ssel, der aus dem Windows-Benutzerpasswort und geheimen Systemschl&uuml;sselnabgeleitet wird. Dies liefert ein Byte-Array zur&uuml;ck, den wir dann zum Beispiel in der Registry speichern k&ouml;nnen.<\/p>\n<p>Wenn wir sp&auml;ter mit der Funktion <b>CrypUnprotectData <\/b>entschl&uuml;sseln, &uuml;bergeben wir das verschl&uuml;sselte Byte-Array. Windows pr&uuml;ft dann, ob der aktuelle Benutzer der gleiche Benutzer ist, der verschl&uuml;sselt hat. Wenn ja, wird der verschl&uuml;sselte Text unverschl&uuml;sselt wieder ausgegeben.  Anderenfalls schl&auml;gt die Verschl&uuml;sselung fehl.<\/p>\n<h2>Code zum Ver- und Entschl&uuml;sseln<\/h2>\n<p>Um eine Verschl&uuml;sselung mit einer Pepper-Zeichenkette zu nutzen, ben&ouml;tigen wir als Erstes eine Konstante, in der wir diese Zeichenkette speichern:<\/p>\n<pre><span style=\"color:blue;\">Private <\/span>Const APP_PEPPER<span style=\"color:blue;\"> As String<\/span> = \"DEIN_GEHEIMER_PEPPER\"<\/pre>\n<p>Hier musst Du Deine eigene Pepper-Zeichenfolge eintragen.<\/p>\n<p>Au&szlig;erdem ben&ouml;tigen wir die Deklaration zweier API-Funktionen namens <b>CryptProtectData <\/b>und <b>CryptUnprotectData <\/b>und zwei weitere API-Funktionen namens <b>RtlMoveMemory <\/b>und <b>LocalFree<\/b>. Diese finden wir, neben dem Typ <b>DATA_BLOB<\/b>, in Listing 1.<\/p>\n<pre><span style=\"color:blue;\">Private <\/span>Type DATA_BLOB\r\n     cbData<span style=\"color:blue;\"> As Long<\/span>\r\n     pbData<span style=\"color:blue;\"> As Long<\/span>Ptr\r\nEnd Type\r\n#If VBA7 Then\r\n     <span style=\"color:blue;\">Private <\/span>Declare PtrSafe Function CryptProtectData Lib \"crypt32.dll\" ( _\r\n         ByRef pDataIn<span style=\"color:blue;\"> As <\/span>DATA_BLOB, _\r\n         ByVal szDataDescr<span style=\"color:blue;\"> As Long<\/span>Ptr, _\r\n         ByVal pOptionalEntropy<span style=\"color:blue;\"> As Long<\/span>Ptr, _\r\n         ByVal pvReserved<span style=\"color:blue;\"> As Long<\/span>Ptr, _\r\n         ByVal pPromptStruct<span style=\"color:blue;\"> As Long<\/span>Ptr, _\r\n         ByVal dwFlags<span style=\"color:blue;\"> As Long<\/span>, _\r\n         ByRef pDataOut<span style=\"color:blue;\"> As <\/span>DATA_BLOB)<span style=\"color:blue;\"> As Long<\/span>\r\n     <span style=\"color:blue;\">Private <\/span>Declare PtrSafe Function CryptUnprotectData Lib \"crypt32.dll\" ( _\r\n         ByRef pDataIn<span style=\"color:blue;\"> As <\/span>DATA_BLOB, _\r\n         ByVal ppszDataDescr<span style=\"color:blue;\"> As Long<\/span>Ptr, _\r\n         ByVal pOptionalEntropy<span style=\"color:blue;\"> As Long<\/span>Ptr, _\r\n         ByVal pvReserved<span style=\"color:blue;\"> As Long<\/span>Ptr, _\r\n         ByVal pPromptStruct<span style=\"color:blue;\"> As Long<\/span>Ptr, _\r\n         ByVal dwFlags<span style=\"color:blue;\"> As Long<\/span>, _\r\n         ByRef pDataOut<span style=\"color:blue;\"> As <\/span>DATA_BLOB)<span style=\"color:blue;\"> As Long<\/span>\r\n     <span style=\"color:blue;\">Private <\/span>Declare PtrSafe Sub RtlMoveMemory Lib \"kernel32\" ( _\r\n         ByRef Destination<span style=\"color:blue;\"> As <\/span>Any, _\r\n         ByVal Source<span style=\"color:blue;\"> As Long<\/span>Ptr, _\r\n         ByVal Length<span style=\"color:blue;\"> As Long<\/span>Ptr)\r\n     <span style=\"color:blue;\">Private <\/span>Declare PtrSafe Function LocalFree Lib \"kernel32\" ( _\r\n         ByVal hMem<span style=\"color:blue;\"> As Long<\/span>Ptr)<span style=\"color:blue;\"> As Long<\/span>Ptr\r\n#End If<\/pre>\n<p><b><span style=\"color:darkgrey;\">Listing 1: API-Deklarationen f&uuml;r das Ver- und Entschl&uuml;sseln<\/span><\/b><\/p>\n<h2>Die Funktion ProtectString<\/h2>\n<p>Die Funktion <b>ProtectString<\/b> &uuml;bernimmt die Aufgabe, unsere Zeichenkette, also zum Beispiel den Benutzernamen oder das Kennwort sicher zu verschl&uuml;sseln, bevor sie beispielsweise in der Registry gespeichert wird. Dabei kombiniert die Funktion den in <b>APP_PEPPER<\/b> hinterlegten Pepper mit der Windows-internen DPAPI-Verschl&uuml;sselung (siehe Listing 2).<\/p>\n<pre><span style=\"color:blue;\">Public Function <\/span>ProtectString(ByVal s<span style=\"color:blue;\"> As String<\/span>)<span style=\"color:blue;\"> As String<\/span>\r\n     <span style=\"color:blue;\">Dim <\/span>sWithPepper<span style=\"color:blue;\"> As String<\/span>\r\n     <span style=\"color:blue;\">Dim <\/span>inBlob<span style=\"color:blue;\"> As <\/span>DATA_BLOB\r\n     <span style=\"color:blue;\">Dim <\/span>outBlob<span style=\"color:blue;\"> As <\/span>DATA_BLOB\r\n     <span style=\"color:blue;\">Dim <\/span>bytes()<span style=\"color:blue;\"> As Byte<\/span>\r\n     <span style=\"color:blue;\">Dim <\/span>lRes<span style=\"color:blue;\"> As Long<\/span>\r\n     <span style=\"color:blue;\">If <\/span><span style=\"color:blue;\">Len<\/span>(s) = 0<span style=\"color:blue;\"> Then<\/span> <span style=\"color:blue;\">Exit Function<\/span>\r\n     sWithPepper = APP_PEPPER & s\r\n     bytes = StrConv(sWithPepper, vbFromUnicode)\r\n     inBlob.cbData = <span style=\"color:blue;\">UBound<\/span>(bytes) - <span style=\"color:blue;\">LBound<\/span>(bytes) + 1\r\n     inBlob.pbData = VarPtr(bytes(<span style=\"color:blue;\">LBound<\/span>(bytes)))\r\n     lRes = CryptProtectData(inBlob, 0, 0, 0, 0, 0, outBlob)\r\n     <span style=\"color:blue;\">If <\/span>lRes &lt;&gt; 0<span style=\"color:blue;\"> Then<\/span>\r\n         ReDim bytes(0 To outBlob.cbData - 1)\r\n         RtlMoveMemory bytes(0), outBlob.pbData, outBlob.cbData\r\n         <span style=\"color:blue;\">Call<\/span> LocalFree(outBlob.pbData)\r\n         ProtectString = BytesToHex(bytes)\r\n     <span style=\"color:blue;\">Else<\/span>\r\n         <span style=\"color:blue;\">Debug.Print<\/span> \"CryptProtectData failed. LastDllError=\" & Err.LastDllError\r\n         ProtectString = \"\"\r\n     <span style=\"color:blue;\">End If<\/span>\r\n<span style=\"color:blue;\">End Function<\/span><\/pre>\n<p><b><span style=\"color:darkgrey;\">Listing 2: Funktion zum Verschl&uuml;sseln von Zeichenketten<\/span><\/b><\/p>\n<p>Zun&auml;chst pr&uuml;ft die Funktion, ob der &uuml;bergebene String &uuml;berhaupt einen Wert enth&auml;lt. Ist das nicht der Fall, bricht sie sofort ab und liefert einen leeren R&uuml;ckgabewert.<\/p>\n<p>Danach wird der Pepper aus <b>APP_PEPPER<\/b> vor das eigentliche Klartextpasswort gesetzt. Dieser Pepper wird nicht mitgespeichert und dient als zus&auml;tzliche Schutzschicht, da ein Angreifer ohne Kenntnis dieses Werts den entschl&uuml;sselten Text nicht korrekt validieren k&ouml;nnte.<\/p>\n<p>Anschlie&szlig;end wird die zusammengesetzte Zeichenkette (<b>APP_PEPPER <\/b>plus Benutzername\/Kennwort) durch die Funktion <b>StrConv <\/b>in ein ANSI-Byte-Array umgewandelt. DPAPI arbeitet intern mit Bin&auml;rdaten, daher muss der String zun&auml;chst in ein Array von Bytes transformiert werden. Dieses Bytearray wird danach in eine <b>DATA_BLOB<\/b>-Struktur &uuml;bergeben, die lediglich aus der Anzahl der Bytes und einem Zeiger auf das erste Byte besteht. Wichtig ist hierbei, dass die Struktur nicht selbst Daten enth&auml;lt, sondern direkt auf das vorhandene Array zeigt.<\/p>\n<p>Nun kommt der zentrale Schritt: Die Funktion ruft <b>CryptProtectData <\/b>auf, eine Windows-API-Funktion, die die Daten benutzergebunden verschl&uuml;sselt. Das bedeutet, dass nur derselbe Windows-Benutzer die Daten sp&auml;ter wieder entschl&uuml;sseln kann &#8211; ideal f&uuml;r sichere lokale Speicherung.<\/p>\n<p>Der R&uuml;ckgabewert zeigt an, ob der Vorgang erfolgreich war. Bei Erfolg werden die verschl&uuml;sselten Bytes aus dem zur&uuml;ckgegebenen BLOB in ein eigenes Bytearray kopiert. Anschlie&szlig;end wird der durch die API belegte Speicher wieder freigegeben, um Speicherlecks zu vermeiden.<\/p>\n<p>Zum Abschluss wandelt die Funktion das verschl&uuml;sselte Bytearray mit einer weiteren Funktion namens <b>BytesToHex <\/b>in einen Hex-String um. Diese Darstellung eignet sich besonders gut f&uuml;r Registry-Eintr&auml;ge oder andere textbasierte Speicherorte, da sie keinerlei Sonderzeichen enth&auml;lt und verlustfrei wieder zur&uuml;ckkonvertiert werden kann.<\/p>\n<p>Falls die Verschl&uuml;sselung fehlschl&auml;gt, gibt die Funktion eine Debug-Meldung aus und liefert einen leeren String zur&uuml;ck.<\/p>\n<p>Die Funktion <b>BytesToHex <\/b>sieht wie folgt aus:<\/p>\n<pre><span style=\"color:blue;\">Private Function <\/span>BytesToHex(ByRef b()<span style=\"color:blue;\"> As Byte<\/span>)<span style=\"color:blue;\"> As String<\/span>\r\n     <span style=\"color:blue;\">Dim <\/span>i<span style=\"color:blue;\"> As Long<\/span>\r\n     <span style=\"color:blue;\">Dim <\/span>s<span style=\"color:blue;\"> As String<\/span>\r\n     For i = <span style=\"color:blue;\">LBound<\/span>(b) To <span style=\"color:blue;\">UBound<\/span>(b)\r\n         s = s & Right$(\"0\" & Hex$(b(i)), 2)\r\n     <span style=\"color:blue;\">Next<\/span> i\r\n     BytesToHex = s\r\n<span style=\"color:blue;\">End Function<\/span><\/pre>\n<h2>Entschl&uuml;sseln der Zugangsdaten<\/h2>\n<p>Zum Entschl&uuml;sseln der Daten verwenden wir die Funktion <b>UnprotectString<\/b> aus Listing 3.<\/p>\n<pre><span style=\"color:blue;\">Public Function <\/span>UnprotectString(ByVal s<span style=\"color:blue;\"> As String<\/span>)<span style=\"color:blue;\"> As String<\/span>\r\n     <span style=\"color:blue;\">Dim <\/span>enc()<span style=\"color:blue;\"> As Byte<\/span>\r\n     <span style=\"color:blue;\">Dim <\/span>dec()<span style=\"color:blue;\"> As Byte<\/span>\r\n     <span style=\"color:blue;\">Dim <\/span>inBlob<span style=\"color:blue;\"> As <\/span>DATA_BLOB\r\n     <span style=\"color:blue;\">Dim <\/span>outBlob<span style=\"color:blue;\"> As <\/span>DATA_BLOB\r\n     <span style=\"color:blue;\">Dim <\/span>lRes<span style=\"color:blue;\"> As Long<\/span>\r\n     <span style=\"color:blue;\">Dim <\/span>tmp<span style=\"color:blue;\"> As String<\/span>\r\n     <span style=\"color:blue;\">If <\/span><span style=\"color:blue;\">Len<\/span>(s) = 0<span style=\"color:blue;\"> Then<\/span> <span style=\"color:blue;\">Exit Function<\/span>\r\n     enc = HexToBytes(s)\r\n     <span style=\"color:blue;\">If <\/span>(<span style=\"color:blue;\">UBound<\/span>(enc) &lt; <span style=\"color:blue;\">LBound<\/span>(enc))<span style=\"color:blue;\"> Then<\/span> <span style=\"color:blue;\">Exit Function<\/span>   '' leeres Array\r\n     inBlob.cbData = <span style=\"color:blue;\">UBound<\/span>(enc) - <span style=\"color:blue;\">LBound<\/span>(enc) + 1\r\n     inBlob.pbData = VarPtr(enc(<span style=\"color:blue;\">LBound<\/span>(enc)))\r\n     lRes = CryptUnprotectData(inBlob, 0, 0, 0, 0, 0, outBlob)\r\n     <span style=\"color:blue;\">If <\/span>lRes &lt;&gt; 0<span style=\"color:blue;\"> Then<\/span>\r\n         ReDim dec(0 To outBlob.cbData - 1)\r\n         RtlMoveMemory dec(0), outBlob.pbData, outBlob.cbData\r\n         <span style=\"color:blue;\">Call<\/span> LocalFree(outBlob.pbData)\r\n         tmp = StrConv(dec, vbUnicode)\r\n         <span style=\"color:blue;\">If <\/span>Left$(tmp, <span style=\"color:blue;\">Len<\/span>(APP_PEPPER)) = APP_PEPPER<span style=\"color:blue;\"> Then<\/span>\r\n             UnprotectString = Mid$(tmp, <span style=\"color:blue;\">Len<\/span>(APP_PEPPER) + 1)\r\n         <span style=\"color:blue;\">Else<\/span>\r\n            UnprotectString = \"\"\r\n         <span style=\"color:blue;\">End If<\/span>\r\n     <span style=\"color:blue;\">Else<\/span>\r\n         <span style=\"color:blue;\">Debug.Print<\/span> \"CryptUnprotectData failed. LastDllError=\" & Err.LastDllError\r\n         UnprotectString = \"\"\r\n     <span style=\"color:blue;\">End If<\/span>\r\n<span style=\"color:blue;\">End Function<\/span><\/pre>\n<p><b><span style=\"color:darkgrey;\">Listing 3: Funktion zum Entschl&uuml;sseln von Zeichenketten<\/span><\/b><\/p>\n<p>Sie erledigt die umgekehrte Aufgabe wie <b>ProtectString<\/b>:Sie nimmt den verschl&uuml;sselten Text als Parameter als Hex-String entgegen, entschl&uuml;sselt ihn mit der Windows-DPAPI und entfernt anschlie&szlig;end den vorangestellten Pepper, sodass am Ende wieder das urspr&uuml;ngliche Klartextkennwort &uuml;brigbleibt.<\/p>\n<p>Zuerst werden einige Variablen deklariert:<\/p>\n<ul>\n<li><b>enc()<\/b> und <b>dec()<\/b>: Bytearrays f&uuml;r die verschl&uuml;sselten beziehungsweise entschl&uuml;sselten Daten<\/li>\n<li><b>inBlob <\/b>und <b>outBlob<\/b>: Strukturen, die die Windows-API-Funktionen <b>CryptUnprotectData <\/b>verwenden<\/li>\n<li><b>lRes<\/b>: nimmt den R&uuml;ckgabewert dieser Funktion auf<\/li>\n<li><b>tmp<\/b>: tempor&auml;rer String f&uuml;r den entschl&uuml;sselten Text inklusive Pepper.<\/li>\n<\/ul>\n<p>Ganz am Anfang pr&uuml;ft die Funktion, ob der Eingabestring <b>s <\/b>&uuml;berhaupt Zeichen enth&auml;lt &#8211; ist er leer, steigt <b>UnprotectString <\/b>direkt aus, weil es dann schlicht nichts zu entschl&uuml;sseln gibt.<\/p>\n<p>Als N&auml;chstes wird der Hex-String aus der Registry mit der Funktion <b>HexToBytes <\/b>wieder in ein Bytearray zur&uuml;ckverwandelt. Diese Funktion definieren wir wie folgt:<\/p>\n<pre><span style=\"color:blue;\">Private Function <\/span>HexToBytes(ByVal s<span style=\"color:blue;\"> As String<\/span>)<span style=\"color:blue;\"> As Byte<\/span>()\r\n     <span style=\"color:blue;\">Dim <\/span>b()<span style=\"color:blue;\"> As Byte<\/span>\r\n     <span style=\"color:blue;\">Dim <\/span>i<span style=\"color:blue;\"> As Long<\/span>, n<span style=\"color:blue;\"> As Long<\/span>\r\n     n = <span style=\"color:blue;\">Len<\/span>(s) \\ 2\r\n     <span style=\"color:blue;\">If <\/span>n = 0<span style=\"color:blue;\"> Then<\/span>\r\n         ReDim b(0 To -1) '' leeres Array\r\n         HexToBytes = b\r\n         <span style=\"color:blue;\">Exit Function<\/span>\r\n     <span style=\"color:blue;\">End If<\/span>\r\n     ReDim b(0 To n - 1)\r\n     For i = 0 To n - 1\r\n         b(i) = CByte(\"&H\" & Mid$(s, 2 * i + 1, 2))\r\n     <span style=\"color:blue;\">Next<\/span> i\r\n     HexToBytes = b\r\n<span style=\"color:blue;\">End Function<\/span><\/pre>\n<p><b>HexToBytes <\/b>macht aus zwei Hex-Zeichen jeweils ein Byte. Danach folgt eine Sicherheitspr&uuml;fung: Wenn <b>enc <\/b>ein leeres Array ist (Obergrenze < Untergrenze), wird ebenfalls sofort beendet - dann ist irgendetwas mit den Eingabedaten schiefgelaufen. Im Normalfall enth&auml;lt <b>enc<\/b> jetzt also genau die Bytes, die <b>ProtectString<\/b> zuvor erzeugt und als Hex kodiert hatte.<\/p>\n<p>Diese Bytes werden nun in die <b>DATA_BLOB<\/b>-Struktur f&uuml;r die API &uuml;berf&uuml;hrt.<\/p>\n<p><b>cbData <\/b>bekommt die Anzahl der Bytes, <b>pbData <\/b>die Speicheradresse des ersten Array-Eelements. Wichtig ist hier: Der Blob zeigt direkt auf das aktuelle Array im Speicher, es wird nichts kopiert. Jetzt kann <b>CryptUnprotectData <\/b>die Daten entschl&uuml;sseln.<\/p>\n<p>Wenn <b>lRes <\/b>ungleich <b>0 <\/b>ist, war der Aufruf erfolgreich und <b>outBlob <\/b>enth&auml;lt einen Zeiger auf die entschl&uuml;sselten Daten sowie deren L&auml;nge. Dann werden diese Bytes in ein eigenes Bytearray <b>dec() <\/b>kopiert.<\/p>\n<p>So geh&ouml;ren die entschl&uuml;sselten Bytes wirklich der VBA-Welt und liegen nicht mehr im von Windows verwalteten Speicher. Anschlie&szlig;end wird der von DPAPI angelegte Speicher mit der API-Funktion <b>LocalFree <\/b>wieder freigegeben, damit es keine Leaks gibt.<\/p>\n<p>Nun liegen die entschl&uuml;sselten Daten als ANSI-Bytefolge in <b>dec()<\/b>. Um daraus wieder einen normalen VBA-String zu machen, werden die Bytes mit der Funktion <b>StrConv <\/b>zur&uuml;ck in Unicode konvertiert.<\/p>\n<p>In <b>tmp <\/b>steht nun genau das, was ProtectString urspr&uuml;nglich in Bytes gepackt hat, n&auml;mlich eine Zeichenkette aus <b>APP_PEPPER <\/b>und der zu verschl&uuml;sselnden Zeichenkette. Der letzte Schritt besteht darin, diesen Pepper wieder zu entfernen. Dazu wird gepr&uuml;ft, ob der String tats&auml;chlich mit dem erwarteten Pepper beginnt. <\/p>\n<p>Stimmt das Pr&auml;fix, schneidet <b>Mid$ <\/b>den Pepper ab und gibt nur den Teil nach <b>APP_PEPPER <\/b>zur&uuml;ck &#8211; das ist die urspr&uuml;nglich verschl&uuml;sselte Zeichenkette. Passt das Pr&auml;fix nicht, geht die Funktion auf Nummer sicher und liefert eine leere Zeichenkette zur&uuml;ck; dann sind vermutlich Daten manipuliert oder mit einem anderen Pepper verschl&uuml;sselt worden.<\/p>\n<p>Falls bereits der API-Aufruf <b>CryptUnprotectData <\/b>scheitert (<b>lRes = 0<\/b>), wird im <b>Else<\/b>-Zweig eine Meldung mit dem Fehlercode im Direktbereich ausgegeben und ebenfalls ein leerer String zur&uuml;ckgeliefert. Auf diese Weise bekommen wir entweder ein korrekt entschl&uuml;sseltes Passwort oder eine leere Zeichenkette, wenn irgendetwas mit den Daten oder der Umgebung nicht stimmt.<\/p>\n<p>Damit haben wir nun das Werkzeug an der Hand, um die Verschl&uuml;sselung und Entschl&uuml;sselung durchzuf&uuml;hren.<\/p>\n<h2>Wahl des Speicherortes f&uuml;r die verschl&uuml;sselten Daten<\/h2>\n<p>Nun m&uuml;ssen wir noch einen geeigneten Speicherort finden. Hier bevorzugen wir die Registry, und zwar den bereits erw&auml;hnten Bereich f&uuml;r VBA\/VB-Anwendungen.<\/p>\n<p>Den Bereich finden wir in der Registry wie unter Bild 1 vor. Um hier einen Eintrag unterhalb von <b>VB and VBA Program Settings <\/b>unter <b>SQL Server|Mitarbeiterverwaltung <\/b>anzulegen, deklarieren wir zun&auml;chst zwei Konstanten:<\/p>\n<p class=\"image\"><img decoding=\"async\" src=\"..\/fileadmin\/_temp_\/2025_05\/pic_487_002.png\" alt=\"Speichern der verschl&uuml;sselten Benutzerdaten in der Registry\" width=\"599,6265\" height=\"205,4277\" \/><\/p>\n<p><b><span style=\"color:darkgrey;\">Bild 1: Speichern der verschl&uuml;sselten Benutzerdaten in der Registry<\/span><\/b><\/p>\n<pre><span style=\"color:blue;\">Public <\/span>Const cStrAppName<span style=\"color:blue;\"> As String<\/span> = \"SQL Server\"\r\n<span style=\"color:blue;\">Public <\/span>Const cStrSection<span style=\"color:blue;\"> As String<\/span> = \"Mitarbeiterverwaltung\"<\/pre>\n<p>Dann erstellen wir eine Funktion namens <b>SaveAppSetting<\/b>, die genau diese Konstanten verwendet, um darunter Name-Wert-Paare zu hinterlegen. Diese Funktion nutzt die eingebaute Funktion <b>SaveSetting<\/b>, der wir als ersten und zweiten Parameter die Werte der jeweiligen Konstanten &uuml;bergeben. Als dritten und vierten Wert &uuml;bergeben wir den Namen und den Wert der Einstellung aus den beiden Parametern <b>strKey <\/b>und <b>strSetting<\/b>:<\/p>\n<pre><span style=\"color:blue;\">Public Sub <\/span>SaveAppSetting(strKey<span style=\"color:blue;\"> As String<\/span>, _\r\n         strSetting<span style=\"color:blue;\"> As String<\/span>)\r\n     SaveSetting cStrAppName, cStrSection, strKey, strSetting\r\n<span style=\"color:blue;\">End Sub<\/span><\/pre>\n<p>Zum Auslesen nutzen wir die Funktion aus Listing 4. Diese nimmt den Namen des auszulesenden Schl&uuml;ssels entgegen sowie einen Standardwert, den wir hier allerdings nicht nutzen. Die Funktion springt in unserem Fall direkt in den <b>Else<\/b>-Teil der <b>If&#8230;Then<\/b>-Bedingung und liest den Wert mit der eingebauten Funktion <b>GetSetting<\/b>, die wiederum die Werte der beiden Konstanten f&uuml;r die &uuml;bergeordneten Elemente erwartet sowie den auszulesenden Schl&uuml;ssel. Das Ergebnis schreiben wir in die Variable <b>strTemp<\/b>, der wiederum als R&uuml;ckgabewert der Funktion dient.<\/p>\n<pre>Bild 1Public Function GetAppSetting(strKey<span style=\"color:blue;\"> As String<\/span>, <span style=\"color:blue;\">Optional<\/span> strDefault<span style=\"color:blue;\"> As String<\/span> = \"\", _\r\n         <span style=\"color:blue;\">Optional<\/span> bolSaveIfNotExists<span style=\"color:blue;\"> As Boolean<\/span>)<span style=\"color:blue;\"> As String<\/span>\r\n     <span style=\"color:blue;\">Dim <\/span>strTemp<span style=\"color:blue;\"> As String<\/span>\r\n     <span style=\"color:blue;\">If <\/span>bolSaveIfNotExists<span style=\"color:blue;\"> Then<\/span>\r\n         strTemp = GetSetting(cStrAppName, cStrSection, strKey)\r\n         <span style=\"color:blue;\">If <\/span><span style=\"color:blue;\">Len<\/span>(strTemp) = 0<span style=\"color:blue;\"> Then<\/span>\r\n             SaveSetting cStrAppName, cStrSection, strKey, strDefault\r\n             strTemp = strDefault\r\n         <span style=\"color:blue;\">End If<\/span>\r\n     <span style=\"color:blue;\">Else<\/span>\r\n         strTemp = GetSetting(cStrAppName, cStrSection, strKey, strDefault)\r\n     <span style=\"color:blue;\">End If<\/span>\r\n     GetAppSetting = strTemp\r\n<span style=\"color:blue;\">End Function<\/span><\/pre>\n<p><b><span style=\"color:darkgrey;\">Listing 4: Funktion zum Auslesen von Werten aus der Registry<\/span><\/b><\/p>\n<p>Damit die Werte f&uuml;r den Benutzernamen und das Kennwort initial in die Registry geschrieben werden, lesen wir diese im Beispiel mit <b>InputBox<\/b>-Funktionen aus und speichern diese in den Variablen <b>strUsername <\/b>und <b>strPassword<\/b>:<\/p>\n<pre><span style=\"color:blue;\">Public Sub <\/span>Test_SaveAndEncodeUserAndPasswordInRegistry()\r\n     <span style=\"color:blue;\">Dim <\/span>strUsername<span style=\"color:blue;\"> As String<\/span>\r\n     <span style=\"color:blue;\">Dim <\/span>strPassword<span style=\"color:blue;\"> As String<\/span>\r\n         \r\n     strUsername = InputBox(\"Benutzername:\", _\r\n         \"SQL Server-Anmeldung\")\r\n     strPassword = InputBox(\"Kennwort:\", _\r\n         \"SQL Server-Anmeldung\")\r\n     \r\n     <span style=\"color:blue;\">Call<\/span> SaveAndEncodeUsernameAndPasswordInRegistry( _\r\n         strUsername, strPassword)\r\n<span style=\"color:blue;\">End Sub<\/span><\/pre>\n<p>Damit rufen wir eine weitere Funktion auf, welche die eingegebenen Daten verschl&uuml;sselt und in die Registry schreibt. Diese nimmt folglich den Benutzernamen und das Kennwort entgegen und deklariert zwei Variablen f&uuml;r die entsprechenden verschl&uuml;sselten Zeichenketten.<\/p>\n<p>Dann rufen wir f&uuml;r <b>strUsername <\/b>und <b>strPassword <\/b>jeweils die Funktion <b>ProtectString <\/b>auf und speichern das Ergebnis in <b>strUsernameProtected <\/b>beziehungsweise <b>strPasswordProtected<\/b>.<\/p>\n<p>Schlie&szlig;lich wird der Inhalt <b>strUsernameProtected <\/b>mit <b>SaveAppSetting <\/b>unter dem Namen <b>Benutzername <\/b>in der Registry gespeichert und <b>strPasswordProtected <\/b>unter dem Namen <b>Kennwort<\/b>:<\/p>\n<pre><span style=\"color:blue;\">Public Sub <\/span>SaveAndEncodeUsernameAndPasswordInRegistry( _\r\n         strUsername<span style=\"color:blue;\"> As String<\/span>, strPassword<span style=\"color:blue;\"> As String<\/span>)\r\n     <span style=\"color:blue;\">Dim <\/span>strUsernameProtected<span style=\"color:blue;\"> As String<\/span>\r\n     <span style=\"color:blue;\">Dim <\/span>strPasswordProtected<span style=\"color:blue;\"> As String<\/span>\r\n     \r\n     strUsernameProtected = ProtectString(strUsername)\r\n     strPasswordProtected = ProtectString(strPassword)\r\n     \r\n     SaveAppSetting \"Benutzername\", strUsernameProtected\r\n     SaveAppSetting \"Kennwort\", strPasswordProtected\r\n<span style=\"color:blue;\">End Sub<\/span><\/pre>\n<p>Um diese Daten auszulesen und beispielsweise beim n&auml;chsten Start der Anwendung in die Variablen <b>strUsername <\/b>und <b>strPassword <\/b>zu schreiben, verwenden wir folgende Prozedur. <\/p>\n<pre><span style=\"color:blue;\">Public Sub <\/span>Test_ReadAndDecodeUsernameAndPasswordFromRegistry()\r\n     <span style=\"color:blue;\">Dim <\/span>strUsername<span style=\"color:blue;\"> As String<\/span>\r\n     <span style=\"color:blue;\">Dim <\/span>strPassword<span style=\"color:blue;\"> As String<\/span>\r\n     If ReadAndDecodeUsernameAndPasswordFromRegistry( _\r\n             strUsername, strPassword) = <span style=\"color:blue;\">True<\/span> Then\r\n         <span style=\"color:blue;\">Debug.Print<\/span> \"Benutzername: \" & strUsername\r\n         <span style=\"color:blue;\">Debug.Print<\/span> \"Kennwort: \" & strPassword\r\n     <span style=\"color:blue;\">Else<\/span>\r\n         <span style=\"color:blue;\">MsgBox<\/span> \"Ermitteln von Benutzername und \" _\r\n             & \"Kennwort nicht erfolgreich.\"\r\n     <span style=\"color:blue;\">End If<\/span>\r\n<span style=\"color:blue;\">End Sub<\/span><\/pre>\n<p>Diese &uuml;bergibt der folgenden Funktion namens <b>ReadAndDecodeUsernameAndPasswordFromRegistry <\/b>leere Parameter namens <b>strUsername <\/b>und <b>strPassword<\/b>. Diese liest zun&auml;chst die Werte f&uuml;r Benutzername und Kennwort in die Variablen <b>strUsernameProtected <\/b>und <b>strPasswordProtected <\/b>ein. Dann nutzt sie die Funktion <b>UnprotectString<\/b>, um Benutzername und Kennwort aus den verschl&uuml;sselten Zeichenketten zu ermitteln:<\/p>\n<pre><span style=\"color:blue;\">Public Function <\/span>ReadAndDecodeUsernameAndPasswordFromRegistry(strUsername<span style=\"color:blue;\"> As String<\/span>, strPassword<span style=\"color:blue;\"> As String<\/span>)<span style=\"color:blue;\"> As Boolean<\/span>\r\n     <span style=\"color:blue;\">Dim <\/span>strUsernameProtected<span style=\"color:blue;\"> As String<\/span>\r\n     <span style=\"color:blue;\">Dim <\/span>strPasswordProtected<span style=\"color:blue;\"> As String<\/span>\r\n     strUsernameProtected = GetAppSetting(\"Benutzername\")\r\n     strPasswordProtected = GetAppSetting(\"Kennwort\")\r\n     strUsername = UnprotectString(strUsernameProtected)\r\n     strPassword = UnprotectString(strPasswordProtected)\r\n     If <span style=\"color:blue;\">Not<\/span> <span style=\"color:blue;\">Len<\/span>(strUsername) = 0 _\r\n             And <span style=\"color:blue;\">Not<\/span> <span style=\"color:blue;\">Len<\/span>(strPassword) = 0 Then\r\n         ReadAndDecodeUsernameAndPasswordFromRegistry _\r\n             = <span style=\"color:blue;\">True<\/span>\r\n     <span style=\"color:blue;\">End If<\/span>\r\n<span style=\"color:blue;\">End Function<\/span><\/pre>\n<p>Wurden sowohl <b>strUsername <\/b>als auch <b>strPassword <\/b>gef&uuml;llt, gibt die Funktion den Wert <b>True <\/b>zur&uuml;ck.<\/p>\n<h2>Zusammenfassung und Ausblick<\/h2>\n<p>Damit haben wir das Handwerkzeug f&uuml;r das Verschl&uuml;sseln und Speichern der Zugangsdaten in der Registry und f&uuml;r das Auslesen und Entschl&uuml;sseln der Benutzerdaten aus der Registry zusammen.<\/p>\n<p>Damit k&ouml;nnen wir nun beim Start einer Anwendung versuchen, die ben&ouml;tigten Zugangsdaten aus der Registry auszulesen. War das nicht erfolgreich, w&uuml;rde man die Zugangsdaten per InputBox abfragen und dann verschl&uuml;sseln und in der Registry speichern. <\/p>\n<p>Wurden die Zugangsdaten in der Registry gefunden, liegen diese in den Variablen <b>strUsername<\/b> und <b>strPassword <\/b>vor, die man gezielt dort nutzen wollte, wo Verbindungszeichenfolgen zusammengestellt werden, die f&uuml;r den Zugriff auf den SQL Server per SQL Server-Authentifizierung n&ouml;tig sind.<\/p>\n<p>Die Verschl&uuml;sselung ist recht sicher, da wir einen in der Datenbank im Code fest eingebundenen Wert als zus&auml;tzlichen Verschl&uuml;sselungsfaktor nutzen, sodass niemand, der diesen nicht kennt, den Text ohne gr&ouml;&szlig;eren Aufwand so schnell wieder entschl&uuml;sseln kann.<\/p>\n<p>Eine Schwachstelle k&ouml;nnte sein, dass der zus&auml;tzliche Faktor &#8220;nur&#8221; im Code verdrahtet und durch das Umwandeln in eine <b>.accde<\/b> gesch&uuml;tzt ist. Es gibt Dienstleister im Internet, die in der Lage sind, dies teilweise r&uuml;ckg&auml;ngig zu machen.<\/p>\n<p>Insgesamt erhalten wir so jedoch eine recht sichere Methode, um Benutzername und Kennwort bei Verwendung der SQL Server-Authentifizierung nicht immer wieder eingeben zu m&uuml;ssen.<\/p>\n<p>Sicherer ist es ohnehin, mit der Windows-Authentifizierung zu arbeiten, aber das ist nicht in allen Umgebunden m&ouml;glich.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Im Artikel &#8220;SQL Server: Tabellen verkn&uuml;pfen&#8221; (www.vbentwickler.de\/485) haben wir gezeigt, wie wir die Tabellen eines SQL Servers per ODBC als Tabellenverkn&uuml;pfung in Access-Datenbanken verf&uuml;gbar machen k&ouml;nnen. Dabei gibt es zwei Varianten, um &uuml;ber entsprechende Verbindungszeichenfolgen auf die Tabellen des SQL Servers zuzugreifen: Windows-Authentifizierung und SQL Server-Authentifizierung. Bei der ersten werden die Zugangsdaten &uuml;ber den aktuellen Windows-Benutzer ermittelt. Dieses Kennwort kennt in der Regel nur der Benutzer und die Verkn&uuml;pfung der Tabellen kann nach seiner Anmeldung am Rechner ohne weitere Interaktion erfolgen. Bei der SQL Server-Authentifizierung jedoch m&uuml;ssen zus&auml;tzlich die Zugangsdaten des Benutzers zum SQL Server angegeben werden. Damit der Benutzer diese nicht immer manuell eingeben muss, m&ouml;chten wir diese sicher speichern &#8211; so, dass niemand diese auslesen kann, auch wenn er Zugriff zum Rechner des Benutzers hat. Wie das gelingt, zeigen wir in diesem Artikel.<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"om_disable_all_campaigns":false,"_monsterinsights_skip_tracking":false,"_monsterinsights_sitenote_active":false,"_monsterinsights_sitenote_note":"","_monsterinsights_sitenote_category":0,"_uf_show_specific_survey":0,"_uf_disable_surveys":false,"footnotes":""},"categories":[662025,66052025,44000006],"tags":[],"yst_prominent_words":[],"class_list":["post-55000487","post","type-post","status-publish","format-standard","hentry","category-662025","category-66052025","category-SQL_Server_und_Co"],"aioseo_notices":[],"_links":{"self":[{"href":"https:\/\/vbentwickler.de\/data\/wp\/v2\/posts\/55000487","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/vbentwickler.de\/data\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/vbentwickler.de\/data\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/vbentwickler.de\/data\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/vbentwickler.de\/data\/wp\/v2\/comments?post=55000487"}],"version-history":[{"count":0,"href":"https:\/\/vbentwickler.de\/data\/wp\/v2\/posts\/55000487\/revisions"}],"wp:attachment":[{"href":"https:\/\/vbentwickler.de\/data\/wp\/v2\/media?parent=55000487"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/vbentwickler.de\/data\/wp\/v2\/categories?post=55000487"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/vbentwickler.de\/data\/wp\/v2\/tags?post=55000487"},{"taxonomy":"yst_prominent_words","embeddable":true,"href":"https:\/\/vbentwickler.de\/data\/wp\/v2\/yst_prominent_words?post=55000487"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}