{"id":22916,"date":"2025-10-24T02:42:41","date_gmt":"2025-10-24T00:42:41","guid":{"rendered":"http:\/\/blog.wenzlaff.de\/?p=22916"},"modified":"2025-10-24T09:04:26","modified_gmt":"2025-10-24T07:04:26","slug":"hashi-corp-vault-demo-anwendung-mit-java-swing-gui-teil-2","status":"publish","type":"post","link":"http:\/\/blog.wenzlaff.de\/?p=22916","title":{"rendered":"Hashi Corp Vault Demo Anwendung mit Java Swing GUI (Teil 2)"},"content":{"rendered":"<p>Heute mal eine Java Anwendung, die auf ein <a href=\"http:\/\/blog.wenzlaff.de\/?p=22867\" target=\"_blank\">Hashi Corp Vault<\/a> zugreift. Was ein HashiCorpVault macht, wird auf <a href=\"http:\/\/kleinhirn.eu\/2025\/10\/03\/hashi-corp-vault\/\" target=\"_blank\">Kleinhirn.eu<\/a> zusammengefasst. Im <a href=\"http:\/\/blog.wenzlaff.de\/?p=22867\" target=\"_blank\"> Teil 1<\/a> hatte ich ja die Installation auf einem Raspberry Pi mit dem Workflow beschrieben.<\/p>\n<p>Nach dem Start der Anwendung, \u00f6ffnet sich diese Swing GUI:<\/p>\n<p><img loading=\"lazy\" decoding=\"async\" src=\"http:\/\/blog.wenzlaff.de\/wp-content\/uploads\/2025\/10\/hashicorpvault-demo-app.jpeg\" alt=\"\" width=\"2024\" height=\"1824\" class=\"aligncenter size-full wp-image-22917\" srcset=\"http:\/\/blog.wenzlaff.de\/wp-content\/uploads\/2025\/10\/hashicorpvault-demo-app.jpeg 2024w, http:\/\/blog.wenzlaff.de\/wp-content\/uploads\/2025\/10\/hashicorpvault-demo-app-300x270.jpeg 300w, http:\/\/blog.wenzlaff.de\/wp-content\/uploads\/2025\/10\/hashicorpvault-demo-app-1024x923.jpeg 1024w, http:\/\/blog.wenzlaff.de\/wp-content\/uploads\/2025\/10\/hashicorpvault-demo-app-768x692.jpeg 768w, http:\/\/blog.wenzlaff.de\/wp-content\/uploads\/2025\/10\/hashicorpvault-demo-app-1536x1384.jpeg 1536w\" sizes=\"auto, (max-width: 767px) 89vw, (max-width: 1000px) 54vw, (max-width: 1071px) 543px, 580px\" \/><\/p>\n<p>Hier der Java-Code, auch neueste Version im <a href=\"https:\/\/gitlab.com\/IT-Berater\/twmathe\/-\/tree\/main\/src\/main\/java\/de\/wenzlaff\/vault?ref_type=heads\" target=\"_blank\">GitLab-Repo<\/a>: <!--more--><\/p>\n<pre class=\"nums:false nums-toggle:false minimize:true lang:java decode:true \" >\r\n\r\npackage de.wenzlaff.vault;\r\n\r\nimport java.awt.BorderLayout;\r\nimport java.awt.Dimension;\r\nimport java.awt.GridBagConstraints;\r\nimport java.awt.GridBagLayout;\r\nimport java.awt.Insets;\r\nimport java.net.URI;\r\nimport java.net.http.HttpClient;\r\nimport java.net.http.HttpRequest;\r\nimport java.net.http.HttpResponse;\r\nimport java.nio.charset.StandardCharsets;\r\nimport java.nio.file.Files;\r\nimport java.nio.file.Path;\r\nimport java.security.KeyStore;\r\nimport java.security.cert.Certificate;\r\nimport java.security.cert.CertificateFactory;\r\nimport java.time.Duration;\r\nimport java.util.Collection;\r\nimport java.util.regex.Matcher;\r\nimport java.util.regex.Pattern;\r\n\r\nimport javax.net.ssl.KeyManager;\r\nimport javax.net.ssl.KeyManagerFactory;\r\nimport javax.net.ssl.SSLContext;\r\nimport javax.net.ssl.TrustManagerFactory;\r\nimport javax.swing.BorderFactory;\r\nimport javax.swing.JButton;\r\nimport javax.swing.JFrame;\r\nimport javax.swing.JLabel;\r\nimport javax.swing.JOptionPane;\r\nimport javax.swing.JPanel;\r\nimport javax.swing.JPasswordField;\r\nimport javax.swing.JScrollPane;\r\nimport javax.swing.JTextArea;\r\nimport javax.swing.JTextField;\r\nimport javax.swing.SwingUtilities;\r\nimport javax.swing.WindowConstants;\r\nimport javax.swing.event.DocumentEvent;\r\nimport javax.swing.event.DocumentListener;\r\n\r\n\/**\r\n * HashiCorp Vault Demo Anwendung mit Java Swing GUI.\r\n * \r\n * Diese Anwendung demonstriert die Interaktion mit einem HashiCorp Vault Server\r\n * \u00fcber HTTPS mit Unterst\u00fctzung f\u00fcr:\r\n * \r\n * - Custom TLS\/SSL Zertifikate (VAULT_CACERT, VAULT_CAPATH)\r\n * \r\n * Optional: Mutual TLS (mTLS) mit Client-Zertifikaten (PKCS12)\r\n * \r\n * - AppRole-Authentifizierung als Alternative zur direkten\r\n * Token-Authentifizierung\r\n * \r\n * - KV Secrets Engine v2 (Key-Value Store)\r\n * \r\n * - Secrets schreiben und lesen \u00fcber die Vault HTTP API - Passwort-Sichtbarkeit\r\n * Toggle f\u00fcr sensitive Felder - Umfassende Fehlerbehandlung mit Swing-Dialogen\r\n * \r\n * - Dynamische Konfigurations\u00e4nderungen werden sofort \u00fcbernommen\r\n * \r\n * Die GUI bietet Eingabefelder f\u00fcr alle relevanten Parameter und eine\r\n * Logging-Ausgabe zur \u00dcberwachung der Vault-Operationen.\r\n * \r\n * \r\n * &lt;pre&gt;\r\nVAULT_ADDR=https:\/\/&lt;IP-ADRESSE&gt;:8200\r\nVAULT_CACERT=\/Users\/&lt;PFAD_ZUM_ZERTIFIKAT&gt;\/vault.crt\r\nVAULT_KV_MOUNT=kv\r\nVAULT_ROLE_ID=admin\r\nVAULT_SECRET_ID=&lt;ID&gt;\r\nVAULT_SECRET_PATH=secrets\/twmath\r\nVAULT_TOKEN=&lt;TOKEN&gt;\r\n * &lt;\/pre&gt;\r\n * \r\n * @author Thomas Wenzlaff\r\n * @version 1.0\r\n *\/\r\npublic class VaultDemoSwingGUI extends JFrame {\r\n\r\n\tprivate static final long serialVersionUID = 1L;\r\n\r\n\t\/\/ ========== GUI Komponenten ==========\r\n\r\n\t\/** Eingabefeld f\u00fcr die Vault Server Adresse (HTTPS URL) *\/\r\n\tprivate JTextField txtVaultAddr;\r\n\r\n\t\/** Eingabefeld f\u00fcr den KV Secrets Engine Mount-Punkt (Standard: \"secret\") *\/\r\n\tprivate JTextField txtKvMount;\r\n\r\n\t\/***\r\n\t * Eingabefeld f\u00fcr den Secret-Pfad innerhalb des KV Mount (z.B.\r\n\t * \"secrets\/twmath\")\r\n\t *\/\r\n\tprivate JTextField txtSecretPath;\r\n\r\n\t\/** Eingabefeld f\u00fcr den Vault Token zur direkten Authentifizierung *\/\r\n\tprivate JPasswordField txtToken;\r\n\r\n\t\/** Eingabefeld f\u00fcr die AppRole Role ID (alternative Authentifizierung) *\/\r\n\tprivate JTextField txtRoleId;\r\n\r\n\t\/** Eingabefeld f\u00fcr die AppRole Secret ID (alternative Authentifizierung) *\/\r\n\tprivate JPasswordField txtSecretId;\r\n\r\n\t\/** Eingabefeld f\u00fcr den Pfad zur CA-Zertifikat-Datei (PEM Format) *\/\r\n\tprivate JTextField txtCaCert;\r\n\r\n\t\/** Eingabefeld f\u00fcr das Verzeichnis mit CA-Zertifikaten (PEM Format) *\/\r\n\tprivate JTextField txtCaPath;\r\n\r\n\t\/** Eingabefeld f\u00fcr den Pfad zum Client PKCS12 Keystore (f\u00fcr mTLS) *\/\r\n\tprivate JTextField txtClientP12;\r\n\r\n\t\/** Eingabefeld f\u00fcr das Passwort des Client PKCS12 Keystores *\/\r\n\tprivate JPasswordField txtClientP12Password;\r\n\r\n\t\/** Eingabefeld f\u00fcr die zu schreibenden Secret-Daten im JSON Format *\/\r\n\tprivate JTextArea txtSecretData;\r\n\r\n\t\/** Textbereich f\u00fcr Log-Ausgaben und Vault-Antworten *\/\r\n\tprivate JTextArea txtLog;\r\n\r\n\t\/\/ ========== HTTP Client ==========\r\n\r\n\t\/**\r\n\t * HttpClient Instanz f\u00fcr alle Vault API Aufrufe. Wird mit custom SSLContext\r\n\t * konfiguriert f\u00fcr TLS-Zertifikatvalidierung. Wird bei \u00c4nderungen der\r\n\t * TLS-Konfiguration automatisch invalidiert.\r\n\t *\/\r\n\tprivate HttpClient httpClient;\r\n\r\n\t\/**\r\n\t * Flag, das anzeigt, ob sich TLS-relevante Konfigurationen ge\u00e4ndert haben. Bei\r\n\t * true muss der HttpClient neu initialisiert werden.\r\n\t *\/\r\n\tprivate volatile boolean configChanged = true;\r\n\r\n\t\/**\r\n\t * Konstruktor: Initialisiert die GUI-Komponenten und das Hauptfenster. Setzt\r\n\t * Standard-Werte aus Umgebungsvariablen oder verwendet Fallback-Defaults.\r\n\t *\/\r\n\tpublic VaultDemoSwingGUI() {\r\n\t\tsuper(\"HashiCorp Vault Demo - TWMathe\");\r\n\t\tinitComponents();\r\n\t\tsetupConfigChangeListeners();\r\n\t\tsetDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);\r\n\t\tsetSize(900, 800);\r\n\t\tsetLocationRelativeTo(null); \/\/ Fenster zentrieren\r\n\t}\r\n\r\n\t\/**\r\n\t * Initialisiert alle GUI-Komponenten und Layout-Manager. Verwendet\r\n\t * GridBagLayout f\u00fcr flexible Positionierung der Eingabefelder. Erstellt drei\r\n\t * Hauptbereiche: Konfiguration, Aktionen und Log-Ausgabe.\r\n\t *\/\r\n\tprivate void initComponents() {\r\n\t\t\/\/ Hauptpanel mit BorderLayout\r\n\t\tJPanel mainPanel = new JPanel(new BorderLayout(10, 10));\r\n\t\tmainPanel.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10));\r\n\r\n\t\t\/\/ ========== Konfigurationspanel (oben) ==========\r\n\t\tJPanel configPanel = new JPanel(new GridBagLayout());\r\n\t\tconfigPanel.setBorder(BorderFactory.createTitledBorder(\"Konfiguration\"));\r\n\t\tGridBagConstraints gbc = new GridBagConstraints();\r\n\t\tgbc.insets = new Insets(3, 3, 3, 3);\r\n\t\tgbc.anchor = GridBagConstraints.WEST;\r\n\t\tgbc.fill = GridBagConstraints.HORIZONTAL;\r\n\r\n\t\t\/\/ Initialisierung der Eingabefelder mit Standard-Werten aus Umgebungsvariablen\r\n\t\ttxtVaultAddr = new JTextField(System.getenv().getOrDefault(\"VAULT_ADDR\", \"https:\/\/127.0.0.1:8200\"), 30);\r\n\t\ttxtKvMount = new JTextField(System.getenv().getOrDefault(\"VAULT_KV_MOUNT\", \"secret\"), 30);\r\n\t\ttxtSecretPath = new JTextField(System.getenv().getOrDefault(\"VAULT_SECRET_PATH\", \"secrets\/twmath\"), 30);\r\n\t\ttxtToken = new JPasswordField(System.getenv(\"VAULT_TOKEN\"), 30);\r\n\t\ttxtRoleId = new JTextField(System.getenv(\"VAULT_ROLE_ID\"), 30);\r\n\t\ttxtSecretId = new JPasswordField(System.getenv(\"VAULT_SECRET_ID\"), 30);\r\n\t\ttxtCaCert = new JTextField(System.getenv(\"VAULT_CACERT\"), 30);\r\n\t\ttxtCaPath = new JTextField(System.getenv(\"VAULT_CAPATH\"), 30);\r\n\t\ttxtClientP12 = new JTextField(System.getenv(\"VAULT_CLIENT_P12\"), 30);\r\n\t\ttxtClientP12Password = new JPasswordField(System.getenv().getOrDefault(\"VAULT_CLIENT_P12_PASSWORD\", \"\"), 30);\r\n\r\n\t\t\/\/ Zeile f\u00fcr Zeile: Label + Textfeld hinzuf\u00fcgen\r\n\t\tint row = 0;\r\n\r\n\t\t\/\/ Vault Server Adresse (HTTPS URL)\r\n\t\taddLabelAndField(configPanel, gbc, row++, \"Vault Adresse:\", txtVaultAddr);\r\n\r\n\t\t\/\/ KV Secrets Engine Mount-Punkt\r\n\t\taddLabelAndField(configPanel, gbc, row++, \"KV Mount:\", txtKvMount);\r\n\r\n\t\t\/\/ Secret-Pfad (Speicherort des Secrets)\r\n\t\taddLabelAndField(configPanel, gbc, row++, \"Secret Pfad:\", txtSecretPath);\r\n\r\n\t\t\/\/ Vault Token (direkte Authentifizierung) mit Toggle-Button\r\n\t\taddLabelAndPasswordField(configPanel, gbc, row++, \"Vault Token:\", txtToken);\r\n\r\n\t\t\/\/ AppRole Role ID (alternative Authentifizierung)\r\n\t\taddLabelAndField(configPanel, gbc, row++, \"Role ID:\", txtRoleId);\r\n\r\n\t\t\/\/ AppRole Secret ID (alternative Authentifizierung) mit Toggle-Button\r\n\t\taddLabelAndPasswordField(configPanel, gbc, row++, \"Secret ID:\", txtSecretId);\r\n\r\n\t\t\/\/ Pfad zur CA-Zertifikat-Datei\r\n\t\taddLabelAndField(configPanel, gbc, row++, \"CA Cert:\", txtCaCert);\r\n\r\n\t\t\/\/ Verzeichnis mit CA-Zertifikaten\r\n\t\taddLabelAndField(configPanel, gbc, row++, \"CA Path:\", txtCaPath);\r\n\r\n\t\t\/\/ Client PKCS12 Keystore f\u00fcr mTLS\r\n\t\taddLabelAndField(configPanel, gbc, row++, \"Client P12:\", txtClientP12);\r\n\r\n\t\t\/\/ Passwort f\u00fcr Client PKCS12 Keystore mit Toggle-Button\r\n\t\taddLabelAndPasswordField(configPanel, gbc, row++, \"Client P12 PW:\", txtClientP12Password);\r\n\r\n\t\tmainPanel.add(configPanel, BorderLayout.NORTH);\r\n\r\n\t\t\/\/ ========== Aktionspanel (Mitte links) ==========\r\n\t\tJPanel actionPanel = new JPanel(new BorderLayout(5, 5));\r\n\t\tactionPanel.setBorder(BorderFactory.createTitledBorder(\"Aktionen\"));\r\n\r\n\t\t\/\/ Textbereich f\u00fcr Secret-Daten im JSON Format\r\n\t\ttxtSecretData = new JTextArea(\"{\\\"username\\\":\\\"demo\\\",\\\"password\\\":\\\"s3cr3\\\"}\", 4, 40);\r\n\t\ttxtSecretData.setBorder(BorderFactory.createTitledBorder(\"Secret Daten (JSON)\"));\r\n\t\tactionPanel.add(new JScrollPane(txtSecretData), BorderLayout.CENTER);\r\n\r\n\t\t\/\/ Button-Panel f\u00fcr Vault-Operationen\r\n\t\tJPanel buttonPanel = new JPanel();\r\n\r\n\t\t\/**\r\n\t\t * Button: AppRole Login Authentifiziert sich beim Vault Server mit Role ID und\r\n\t\t * Secret ID. Gibt bei Erfolg ein Token zur\u00fcck, das f\u00fcr weitere Operationen\r\n\t\t * verwendet wird.\r\n\t\t *\/\r\n\t\tJButton btnLogin = new JButton(\"AppRole Login\");\r\n\t\tbtnLogin.addActionListener(e -&gt; performAppRoleLogin());\r\n\r\n\t\t\/**\r\n\t\t * Button: Secret Schreiben Schreibt die eingegebenen Secret-Daten (JSON) in den\r\n\t\t * konfigurierten Vault-Pfad. Ben\u00f6tigt einen g\u00fcltigen Token (entweder direkt\r\n\t\t * oder \u00fcber AppRole Login).\r\n\t\t *\/\r\n\t\tJButton btnWrite = new JButton(\"Secret Schreiben\");\r\n\t\tbtnWrite.addActionListener(e -&gt; performWriteSecret());\r\n\r\n\t\t\/**\r\n\t\t * Button: Secret Lesen Liest das Secret vom konfigurierten Vault-Pfad und zeigt\r\n\t\t * es im Log an. Ben\u00f6tigt einen g\u00fcltigen Token (entweder direkt oder \u00fcber\r\n\t\t * AppRole Login).\r\n\t\t *\/\r\n\t\tJButton btnRead = new JButton(\"Secret Lesen\");\r\n\t\tbtnRead.addActionListener(e -&gt; performReadSecret());\r\n\r\n\t\t\/**\r\n\t\t * Button: Log L\u00f6schen L\u00f6scht die gesamte Log-Ausgabe f\u00fcr bessere\r\n\t\t * \u00dcbersichtlichkeit.\r\n\t\t *\/\r\n\t\tJButton btnClear = new JButton(\"Log L\u00f6schen\");\r\n\t\tbtnClear.addActionListener(e -&gt; txtLog.setText(\"\"));\r\n\r\n\t\tbuttonPanel.add(btnLogin);\r\n\t\tbuttonPanel.add(btnWrite);\r\n\t\tbuttonPanel.add(btnRead);\r\n\t\tbuttonPanel.add(btnClear);\r\n\r\n\t\tactionPanel.add(buttonPanel, BorderLayout.SOUTH);\r\n\t\tmainPanel.add(actionPanel, BorderLayout.CENTER);\r\n\r\n\t\t\/\/ ========== Log-Panel (unten) ==========\r\n\t\ttxtLog = new JTextArea(15, 60);\r\n\t\ttxtLog.setEditable(false);\r\n\t\ttxtLog.setBorder(BorderFactory.createTitledBorder(\"Log-Ausgabe\"));\r\n\t\tJScrollPane scrollLog = new JScrollPane(txtLog);\r\n\t\tscrollLog.setPreferredSize(new Dimension(800, 300));\r\n\t\tmainPanel.add(scrollLog, BorderLayout.SOUTH);\r\n\r\n\t\tadd(mainPanel);\r\n\t}\r\n\r\n\t\/**\r\n\t * Richtet Change Listener f\u00fcr alle TLS-relevanten Konfigurationsfelder ein. Bei\r\n\t * \u00c4nderungen wird der HttpClient invalidiert und muss neu initialisiert werden.\r\n\t * Dies stellt sicher, dass Konfigurations\u00e4nderungen sofort wirksam werden.\r\n\t *\/\r\n\tprivate void setupConfigChangeListeners() {\r\n\t\t\/\/ DocumentListener f\u00fcr Textfelder, die TLS-Konfiguration betreffen\r\n\t\tDocumentListener configChangeListener = new DocumentListener() {\r\n\t\t\t@Override\r\n\t\t\tpublic void insertUpdate(DocumentEvent e) {\r\n\t\t\t\tmarkConfigChanged();\r\n\t\t\t}\r\n\r\n\t\t\t@Override\r\n\t\t\tpublic void removeUpdate(DocumentEvent e) {\r\n\t\t\t\tmarkConfigChanged();\r\n\t\t\t}\r\n\r\n\t\t\t@Override\r\n\t\t\tpublic void changedUpdate(DocumentEvent e) {\r\n\t\t\t\tmarkConfigChanged();\r\n\t\t\t}\r\n\t\t};\r\n\r\n\t\t\/\/ TLS-relevante Felder \u00fcberwachen\r\n\t\ttxtVaultAddr.getDocument().addDocumentListener(configChangeListener);\r\n\t\ttxtCaCert.getDocument().addDocumentListener(configChangeListener);\r\n\t\ttxtCaPath.getDocument().addDocumentListener(configChangeListener);\r\n\t\ttxtClientP12.getDocument().addDocumentListener(configChangeListener);\r\n\t\ttxtClientP12Password.getDocument().addDocumentListener(configChangeListener);\r\n\r\n\t\tlog(\"Konfigurations\u00fcberwachung aktiviert: \u00c4nderungen werden automatisch \u00fcbernommen.\");\r\n\t}\r\n\r\n\t\/**\r\n\t * Markiert die Konfiguration als ge\u00e4ndert. Der HttpClient wird beim n\u00e4chsten\r\n\t * API-Aufruf neu initialisiert.\r\n\t *\/\r\n\tprivate void markConfigChanged() {\r\n\t\tif (!configChanged) {\r\n\t\t\tconfigChanged = true;\r\n\t\t\tlog(\"Konfiguration ge\u00e4ndert - HttpClient wird bei n\u00e4chster Operation neu initialisiert.\");\r\n\t\t}\r\n\t}\r\n\r\n\t\/**\r\n\t * Hilfsmethode zum Hinzuf\u00fcgen von Label und Textfeld in einem GridBagLayout.\r\n\t * \r\n\t * @param panel     Das Panel, dem die Komponenten hinzugef\u00fcgt werden\r\n\t * @param gbc       GridBagConstraints f\u00fcr Layout-Konfiguration\r\n\t * @param row       Die Zeile, in der Label und Textfeld platziert werden\r\n\t * @param labelText Der Text des Labels\r\n\t * @param field     Das Textfeld oder Passwortfeld\r\n\t *\/\r\n\tprivate void addLabelAndField(JPanel panel, GridBagConstraints gbc, int row, String labelText, JTextField field) {\r\n\t\tgbc.gridx = 0;\r\n\t\tgbc.gridy = row;\r\n\t\tgbc.weightx = 0.0;\r\n\t\tpanel.add(new JLabel(labelText), gbc);\r\n\r\n\t\tgbc.gridx = 1;\r\n\t\tgbc.weightx = 1.0;\r\n\t\tpanel.add(field, gbc);\r\n\t}\r\n\r\n\t\/**\r\n\t * Hilfsmethode zum Hinzuf\u00fcgen von Label und Passwortfeld mit Toggle-Button f\u00fcr\r\n\t * Sichtbarkeit. Der Toggle-Button erm\u00f6glicht das Umschalten zwischen maskierter\r\n\t * und Klartextanzeige.\r\n\t * \r\n\t * @param panel     Das Panel, dem die Komponenten hinzugef\u00fcgt werden\r\n\t * @param gbc       GridBagConstraints f\u00fcr Layout-Konfiguration\r\n\t * @param row       Die Zeile, in der Label, Passwortfeld und Button platziert\r\n\t *                  werden\r\n\t * @param labelText Der Text des Labels\r\n\t * @param field     Das Passwortfeld\r\n\t *\/\r\n\tprivate void addLabelAndPasswordField(JPanel panel, GridBagConstraints gbc, int row, String labelText, JPasswordField field) {\r\n\t\tgbc.gridx = 0;\r\n\t\tgbc.gridy = row;\r\n\t\tgbc.weightx = 0.0;\r\n\t\tpanel.add(new JLabel(labelText), gbc);\r\n\r\n\t\t\/\/ Panel f\u00fcr Passwortfeld + Toggle-Button\r\n\t\tJPanel passwordPanel = new JPanel(new BorderLayout(5, 0));\r\n\t\tpasswordPanel.add(field, BorderLayout.CENTER);\r\n\r\n\t\t\/\/ Toggle-Button f\u00fcr Passwort-Sichtbarkeit\r\n\t\tJButton btnToggle = new JButton(\"&#x1f441;\");\r\n\t\tbtnToggle.setToolTipText(\"Passwort anzeigen\/verbergen\");\r\n\t\tbtnToggle.setPreferredSize(new Dimension(45, field.getPreferredSize().height));\r\n\t\tbtnToggle.setFocusable(false); \/\/ Verhindert Fokus-Sprung vom Passwortfeld\r\n\r\n\t\t\/\/ ActionListener f\u00fcr Toggle-Funktionalit\u00e4t\r\n\t\tbtnToggle.addActionListener(e -&gt; {\r\n\t\t\tif (field.getEchoChar() == 0) {\r\n\t\t\t\t\/\/ Aktuell sichtbar -&gt; Verstecken\r\n\t\t\t\tfield.setEchoChar('\u25cf'); \/\/ Unicode-Zeichen f\u00fcr gef\u00fcllten Kreis\r\n\t\t\t\tbtnToggle.setText(\"&#x1f441;\");\r\n\t\t\t\tbtnToggle.setToolTipText(\"Passwort anzeigen\");\r\n\t\t\t} else {\r\n\t\t\t\t\/\/ Aktuell versteckt -&gt; Anzeigen\r\n\t\t\t\tfield.setEchoChar((char) 0); \/\/ 0 deaktiviert die Maskierung\r\n\t\t\t\tbtnToggle.setText(\"&#x1f512;\");\r\n\t\t\t\tbtnToggle.setToolTipText(\"Passwort verbergen\");\r\n\t\t\t}\r\n\t\t});\r\n\r\n\t\tpasswordPanel.add(btnToggle, BorderLayout.EAST);\r\n\r\n\t\tgbc.gridx = 1;\r\n\t\tgbc.weightx = 1.0;\r\n\t\tpanel.add(passwordPanel, gbc);\r\n\t}\r\n\r\n\t\/**\r\n\t * Schreibt eine Log-Nachricht in den Log-Textbereich. F\u00fcgt automatisch einen\r\n\t * Zeilenumbruch hinzu und scrollt zur neuesten Nachricht.\r\n\t * \r\n\t * @param message Die zu loggende Nachricht\r\n\t *\/\r\n\tprivate void log(String message) {\r\n\t\tSwingUtilities.invokeLater(() -&gt; {\r\n\t\t\ttxtLog.append(message + \"\\n\");\r\n\t\t\ttxtLog.setCaretPosition(txtLog.getDocument().getLength());\r\n\t\t});\r\n\t}\r\n\r\n\t\/**\r\n\t * Zeigt einen Fehlerdialog mit detaillierten Informationen zur Exception.\r\n\t * Zus\u00e4tzlich wird der Fehler im Log-Bereich ausgegeben.\r\n\t * \r\n\t * Der Dialog bietet zwei Ansichten: - Kurze Fehlermeldung f\u00fcr den Benutzer -\r\n\t * Optional: Detaillierte Stack Trace Information\r\n\t * \r\n\t * @param title     Der Titel des Fehlerdialogs\r\n\t * @param message   Die Fehlermeldung f\u00fcr den Benutzer\r\n\t * @param exception Die aufgetretene Exception (kann null sein)\r\n\t *\/\r\n\tprivate void showErrorDialog(String title, String message, Throwable exception) {\r\n\t\t\/\/ Log-Ausgabe f\u00fcr Entwickler\/Debugging\r\n\t\tlog(\"=== FEHLER: \" + title + \" ===\");\r\n\t\tlog(message);\r\n\t\tif (exception != null) {\r\n\t\t\tlog(\"Exception: \" + exception.getClass().getName());\r\n\t\t\tlog(\"Message: \" + exception.getMessage());\r\n\r\n\t\t\t\/\/ Stack Trace in StringBuilder sammeln\r\n\t\t\tStringBuilder stackTrace = new StringBuilder();\r\n\t\t\tstackTrace.append(exception.toString()).append(\"\\n\");\r\n\t\t\tfor (StackTraceElement element : exception.getStackTrace()) {\r\n\t\t\t\tstackTrace.append(\"\\tat \").append(element.toString()).append(\"\\n\");\r\n\t\t\t}\r\n\t\t\tlog(stackTrace.toString());\r\n\t\t}\r\n\r\n\t\t\/\/ Swing-Dialog im Event Dispatch Thread anzeigen\r\n\t\tSwingUtilities.invokeLater(() -&gt; {\r\n\t\t\tif (exception != null) {\r\n\t\t\t\t\/\/ Detaillierter Dialog mit Exception-Details\r\n\t\t\t\tJPanel panel = new JPanel(new BorderLayout(10, 10));\r\n\t\t\t\tpanel.add(new JLabel(\"&lt;html&gt;&lt;body style='width: 400px'&gt;\" + message.replace(\"\\n\", \"&lt;br&gt;\") + \"&lt;\/body&gt;&lt;\/html&gt;\"), BorderLayout.NORTH);\r\n\r\n\t\t\t\t\/\/ TextArea f\u00fcr Stack Trace\r\n\t\t\t\tJTextArea stackTraceArea = new JTextArea(15, 50);\r\n\t\t\t\tstackTraceArea.setEditable(false);\r\n\t\t\t\tstackTraceArea.setText(getStackTraceAsString(exception));\r\n\t\t\t\tstackTraceArea.setCaretPosition(0);\r\n\t\t\t\tJScrollPane scrollPane = new JScrollPane(stackTraceArea);\r\n\t\t\t\tscrollPane.setBorder(BorderFactory.createTitledBorder(\"Stack Trace Details\"));\r\n\r\n\t\t\t\t\/\/ Panel mit Details\r\n\t\t\t\tJPanel detailsPanel = new JPanel(new BorderLayout());\r\n\t\t\t\tdetailsPanel.add(new JLabel(\"Exception: \" + exception.getClass().getName()), BorderLayout.NORTH);\r\n\t\t\t\tdetailsPanel.add(scrollPane, BorderLayout.CENTER);\r\n\r\n\t\t\t\tpanel.add(detailsPanel, BorderLayout.CENTER);\r\n\r\n\t\t\t\t\/\/ Fehler-Dialog mit Icon anzeigen\r\n\t\t\t\tJOptionPane.showMessageDialog(this, panel, title, JOptionPane.ERROR_MESSAGE);\r\n\t\t\t} else {\r\n\t\t\t\t\/\/ Einfacher Fehlerdialog ohne Exception-Details\r\n\t\t\t\tJOptionPane.showMessageDialog(this, message, title, JOptionPane.ERROR_MESSAGE);\r\n\t\t\t}\r\n\t\t});\r\n\t}\r\n\r\n\t\/**\r\n\t * Konvertiert einen Stack Trace in einen String zur Anzeige.\r\n\t * \r\n\t * @param throwable Die Exception, deren Stack Trace extrahiert werden soll\r\n\t * @return Der Stack Trace als formatierter String\r\n\t *\/\r\n\tprivate String getStackTraceAsString(Throwable throwable) {\r\n\t\tStringBuilder sb = new StringBuilder();\r\n\t\tsb.append(throwable.toString()).append(\"\\n\");\r\n\t\tfor (StackTraceElement element : throwable.getStackTrace()) {\r\n\t\t\tsb.append(\"\\tat \").append(element.toString()).append(\"\\n\");\r\n\t\t}\r\n\r\n\t\t\/\/ Cause (verkettete Exceptions) ebenfalls ausgeben\r\n\t\tThrowable cause = throwable.getCause();\r\n\t\tif (cause != null) {\r\n\t\t\tsb.append(\"\\nCaused by: \");\r\n\t\t\tsb.append(getStackTraceAsString(cause));\r\n\t\t}\r\n\r\n\t\treturn sb.toString();\r\n\t}\r\n\r\n\t\/**\r\n\t * Alternative kompakte Fehleranzeige ohne Stack Trace Details. N\u00fctzlich f\u00fcr\r\n\t * erwartete Fehler wie Validierungsfehler.\r\n\t * \r\n\t * @param title   Der Titel des Fehlerdialogs\r\n\t * @param message Die Fehlermeldung\r\n\t *\/\r\n\tprivate void showSimpleErrorDialog(String title, String message) {\r\n\t\tlog(\"=== FEHLER: \" + title + \" ===\");\r\n\t\tlog(message);\r\n\r\n\t\tSwingUtilities.invokeLater(() -&gt; {\r\n\t\t\tJOptionPane.showMessageDialog(this, message, title, JOptionPane.ERROR_MESSAGE);\r\n\t\t});\r\n\t}\r\n\r\n\t\/**\r\n\t * Initialisiert den HttpClient mit custom SSLContext. Der SSLContext wird\r\n\t * basierend auf den aktuellen Konfigurationswerten erstellt. Diese Methode wird\r\n\t * bei jeder Operation aufgerufen, wenn sich die Konfiguration ge\u00e4ndert hat.\r\n\t * \r\n\t * @throws Exception bei Fehlern w\u00e4hrend der SSLContext-Initialisierung\r\n\t *\/\r\n\tprivate void initHttpClient() throws Exception {\r\n\t\ttry {\r\n\t\t\tlog(\"Initialisiere HTTP-Client mit SSL-Context...\");\r\n\t\t\tlog(\"  Vault Adresse: \" + txtVaultAddr.getText().trim());\r\n\t\t\tlog(\"  CA Cert: \" + (txtCaCert.getText().trim().isEmpty() ? \"(nicht gesetzt)\" : txtCaCert.getText().trim()));\r\n\t\t\tlog(\"  CA Path: \" + (txtCaPath.getText().trim().isEmpty() ? \"(nicht gesetzt)\" : txtCaPath.getText().trim()));\r\n\t\t\tlog(\"  Client P12: \" + (txtClientP12.getText().trim().isEmpty() ? \"(nicht gesetzt)\" : txtClientP12.getText().trim()));\r\n\r\n\t\t\tSSLContext sslContext = buildSslContext();\r\n\t\t\thttpClient = HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(5)).sslContext(sslContext).build();\r\n\t\t\tconfigChanged = false; \/\/ Konfiguration ist nun synchronisiert\r\n\t\t\tlog(\"HTTP-Client erfolgreich initialisiert.\");\r\n\t\t} catch (Exception ex) {\r\n\t\t\tshowErrorDialog(\"HTTP Client Initialisierung Fehlgeschlagen\", \"Der HTTP Client konnte nicht initialisiert werden.\\n\\n\" + \"M\u00f6gliche Ursachen:\\n\"\r\n\t\t\t\t\t+ \"\u2022 Fehlerhafte TLS-Zertifikate\\n\" + \"\u2022 CA-Cert Datei nicht gefunden\\n\" + \"\u2022 Client P12 Keystore ung\u00fcltig oder Passwort falsch\", ex);\r\n\t\t\tthrow ex; \/\/ Re-throw f\u00fcr Caller\r\n\t\t}\r\n\t}\r\n\r\n\t\/**\r\n\t * Stellt sicher, dass ein aktueller HttpClient mit den neuesten\r\n\t * Konfigurationswerten vorhanden ist. Bei Konfigurations\u00e4nderungen wird der\r\n\t * HttpClient neu initialisiert.\r\n\t * \r\n\t * @throws Exception bei Fehlern w\u00e4hrend der HttpClient-Initialisierung\r\n\t *\/\r\n\tprivate void ensureHttpClient() throws Exception {\r\n\t\tif (httpClient == null || configChanged) {\r\n\t\t\tinitHttpClient();\r\n\t\t}\r\n\t}\r\n\r\n\t\/**\r\n\t * Erstellt einen SSLContext mit custom TrustStore und optional KeyStore f\u00fcr\r\n\t * mTLS. Verwendet die aktuellen Werte aus den GUI-Feldern.\r\n\t * \r\n\t * @return SSLContext f\u00fcr HTTPS-Verbindungen mit custom Zertifikaten\r\n\t * @throws Exception bei Fehlern beim Laden der Zertifikate\r\n\t *\/\r\n\tprivate SSLContext buildSslContext() throws Exception {\r\n\t\t\/\/ TrustManagerFactory initialisieren\r\n\t\tTrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());\r\n\r\n\t\t\/\/ Leeren KeyStore f\u00fcr TrustStore erstellen\r\n\t\tKeyStore trustKs = KeyStore.getInstance(KeyStore.getDefaultType());\r\n\t\ttrustKs.load(null, null);\r\n\r\n\t\tboolean addedAny = false;\r\n\t\tString caCert = txtCaCert.getText().trim();\r\n\t\tString caPath = txtCaPath.getText().trim();\r\n\r\n\t\t\/\/ CA-Zertifikat aus einzelner PEM-Datei laden (VAULT_CACERT)\r\n\t\tif (!caCert.isEmpty()) {\r\n\t\t\tlog(\"Lade CA-Zertifikat aus: \" + caCert);\r\n\t\t\taddPemToTrustStore(trustKs, Path.of(caCert), \"vault-ca-\");\r\n\t\t\taddedAny = true;\r\n\t\t}\r\n\r\n\t\t\/\/ CA-Zertifikate aus Verzeichnis laden (VAULT_CAPATH)\r\n\t\tif (!caPath.isEmpty()) {\r\n\t\t\tlog(\"Lade CA-Zertifikate aus Verzeichnis: \" + caPath);\r\n\t\t\tFiles.list(Path.of(caPath)).filter(p -&gt; Files.isRegularFile(p)).forEach(p -&gt; {\r\n\t\t\t\ttry {\r\n\t\t\t\t\tlog(\"  -&gt; \" + p.getFileName());\r\n\t\t\t\t\taddPemToTrustStore(trustKs, p, \"vault-capath-\");\r\n\t\t\t\t} catch (Exception e) {\r\n\t\t\t\t\tthrow new RuntimeException(\"Fehler beim Laden von: \" + p, e);\r\n\t\t\t\t}\r\n\t\t\t});\r\n\t\t\taddedAny = true;\r\n\t\t}\r\n\r\n\t\t\/\/ TrustManagerFactory initialisieren: Custom TrustStore oder System-Default\r\n\t\tif (addedAny) {\r\n\t\t\ttmf.init(trustKs);\r\n\t\t\tlog(\"Custom TrustStore erfolgreich geladen.\");\r\n\t\t} else {\r\n\t\t\ttmf.init((KeyStore) null);\r\n\t\t\tlog(\"Verwende System-TrustStore (keine custom CA-Zertifikate konfiguriert).\");\r\n\t\t}\r\n\r\n\t\t\/\/ Optional: KeyManagerFactory f\u00fcr Client-Zertifikat (mTLS)\r\n\t\tKeyManager[] kms = null;\r\n\t\tString clientP12 = txtClientP12.getText().trim();\r\n\t\tString clientP12Pw = new String(txtClientP12Password.getPassword());\r\n\r\n\t\tif (!clientP12.isEmpty()) {\r\n\t\t\tlog(\"Lade Client-Zertifikat (mTLS): \" + clientP12);\r\n\t\t\tKeyStore clientKs = KeyStore.getInstance(\"PKCS12\");\r\n\t\t\ttry (var in = Files.newInputStream(Path.of(clientP12))) {\r\n\t\t\t\tclientKs.load(in, clientP12Pw.toCharArray());\r\n\t\t\t}\r\n\t\t\tKeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());\r\n\t\t\tkmf.init(clientKs, clientP12Pw.toCharArray());\r\n\t\t\tkms = kmf.getKeyManagers();\r\n\t\t\tlog(\"Client-Zertifikat erfolgreich geladen (mTLS aktiviert).\");\r\n\t\t}\r\n\r\n\t\t\/\/ SSLContext mit TrustManagers und optional KeyManagers initialisieren\r\n\t\tSSLContext sc = SSLContext.getInstance(\"TLS\");\r\n\t\tsc.init(kms, tmf.getTrustManagers(), null);\r\n\t\treturn sc;\r\n\t}\r\n\r\n\t\/**\r\n\t * L\u00e4dt ein oder mehrere X.509 Zertifikate aus einer PEM-Datei und f\u00fcgt sie dem\r\n\t * KeyStore hinzu. Eine PEM-Datei kann mehrere Zertifikate enthalten (z.B.\r\n\t * Certificate Chain).\r\n\t * \r\n\t * @param ks      Der KeyStore, dem die Zertifikate hinzugef\u00fcgt werden\r\n\t * @param pemPath Der Pfad zur PEM-Datei\r\n\t * @param prefix  Ein Pr\u00e4fix f\u00fcr die Alias-Namen der Zertifikate im KeyStore\r\n\t * @throws Exception bei Fehlern beim Lesen oder Parsen der PEM-Datei\r\n\t *\/\r\n\tprivate void addPemToTrustStore(KeyStore ks, Path pemPath, String prefix) throws Exception {\r\n\t\tCertificateFactory cf = CertificateFactory.getInstance(\"X.509\");\r\n\t\ttry (var in = Files.newInputStream(pemPath)) {\r\n\t\t\t\/\/ Eine PEM-Datei kann mehrere Zertifikate enthalten\r\n\t\t\tCollection&lt;? extends Certificate&gt; certs = cf.generateCertificates(in);\r\n\t\t\tint i = 1;\r\n\t\t\tfor (Certificate c : certs) {\r\n\t\t\t\tString alias = prefix + pemPath.getFileName() + \"-\" + i++;\r\n\t\t\t\tks.setCertificateEntry(alias, c);\r\n\t\t\t}\r\n\t\t}\r\n\t}\r\n\r\n\t\/**\r\n\t * F\u00fchrt AppRole Login durch. Authentifiziert sich beim Vault Server mit Role ID\r\n\t * und Secret ID. Das erhaltene Token wird im Token-Feld gespeichert f\u00fcr weitere\r\n\t * Operationen.\r\n\t *\/\r\n\tprivate void performAppRoleLogin() {\r\n\t\tnew Thread(() -&gt; {\r\n\t\t\ttry {\r\n\t\t\t\tensureHttpClient();\r\n\r\n\t\t\t\tString roleId = txtRoleId.getText().trim();\r\n\t\t\t\tString secretId = new String(txtSecretId.getPassword()).trim();\r\n\r\n\t\t\t\tif (roleId.isEmpty() || secretId.isEmpty()) {\r\n\t\t\t\t\tshowSimpleErrorDialog(\"AppRole Login - Validierungsfehler\", \"Role ID und Secret ID m\u00fcssen ausgef\u00fcllt sein!\");\r\n\t\t\t\t\treturn;\r\n\t\t\t\t}\r\n\r\n\t\t\t\tlog(\"=== AppRole Login ===\");\r\n\t\t\t\tlog(\"Role ID: \" + roleId);\r\n\t\t\t\tlog(\"Secret ID: \" + secretId.substring(0, Math.min(secretId.length(), 4)) + \"***\");\r\n\r\n\t\t\t\tString token = approleLogin(roleId, secretId);\r\n\r\n\t\t\t\t\/\/ Token im Feld speichern f\u00fcr weitere Operationen\r\n\t\t\t\tSwingUtilities.invokeLater(() -&gt; txtToken.setText(token));\r\n\t\t\t\tlog(\"AppRole-Login erfolgreich!\");\r\n\t\t\t\tlog(\"Token erhalten: \" + token.substring(0, Math.min(token.length(), 10)) + \"...\");\r\n\r\n\t\t\t\t\/\/ Erfolgs-Nachricht\r\n\t\t\t\tSwingUtilities.invokeLater(() -&gt; {\r\n\t\t\t\t\tJOptionPane.showMessageDialog(this, \"AppRole-Login erfolgreich!\\nToken wurde gespeichert.\", \"Login Erfolgreich\", JOptionPane.INFORMATION_MESSAGE);\r\n\t\t\t\t});\r\n\r\n\t\t\t} catch (Exception ex) {\r\n\t\t\t\tshowErrorDialog(\"AppRole Login Fehlgeschlagen\", \"Der AppRole-Login konnte nicht durchgef\u00fchrt werden.\\n\\n\" + \"Bitte \u00fcberpr\u00fcfen Sie:\\n\"\r\n\t\t\t\t\t\t+ \"\u2022 Vault Server Adresse (erreichbar?)\\n\" + \"\u2022 Role ID und Secret ID (korrekt?)\\n\" + \"\u2022 TLS-Zertifikate (vertrauensw\u00fcrdig?)\\n\"\r\n\t\t\t\t\t\t+ \"\u2022 Netzwerkverbindung\", ex);\r\n\t\t\t}\r\n\t\t}).start();\r\n\t}\r\n\r\n\t\/**\r\n\t * AppRole Login API-Aufruf. Verwendet die aktuellen Werte aus dem GUI-Feld f\u00fcr\r\n\t * Vault Adresse.\r\n\t *\/\r\n\tprivate String approleLogin(String roleId, String secretId) throws Exception {\r\n\t\tString vaultAddr = txtVaultAddr.getText().trim();\r\n\t\tURI uri = URI.create(vaultAddr + \"\/v1\/auth\/approle\/login\");\r\n\r\n\t\tString body = \"{\\\"role_id\\\":\\\"\" + escape(roleId) + \"\\\",\\\"secret_id\\\":\\\"\" + escape(secretId) + \"\\\"}\";\r\n\r\n\t\tHttpRequest req = HttpRequest.newBuilder(uri).timeout(Duration.ofSeconds(10)).header(\"Content-Type\", \"application\/json\")\r\n\t\t\t\t.POST(HttpRequest.BodyPublishers.ofString(body, StandardCharsets.UTF_8)).build();\r\n\r\n\t\tlog(\"Sende AppRole Login Request an: \" + uri);\r\n\t\tHttpResponse&lt;String&gt; res = httpClient.send(req, HttpResponse.BodyHandlers.ofString());\r\n\r\n\t\tlog(\"Response Status: \" + res.statusCode());\r\n\t\tlog(\"Response Body: \" + res.body());\r\n\r\n\t\tif (res.statusCode() \/ 100 != 2) {\r\n\t\t\tString errorDetail = parseVaultError(res.body());\r\n\t\t\tthrow new RuntimeException(\"AppRole-Login fehlgeschlagen: HTTP \" + res.statusCode() + \"\\n\" + errorDetail);\r\n\t\t}\r\n\r\n\t\tPattern p = Pattern.compile(\"\\\"client_token\\\"\\\\s*:\\\\s*\\\"([^\\\"]+)\\\"\");\r\n\t\tMatcher m = p.matcher(res.body());\r\n\t\tif (m.find()) {\r\n\t\t\treturn m.group(1);\r\n\t\t}\r\n\r\n\t\tthrow new RuntimeException(\"client_token nicht in der AppRole-Login-Antwort gefunden.\\n\" + \"Server-Antwort: \" + res.body());\r\n\t}\r\n\r\n\t\/**\r\n\t * Schreibt ein Secret in Vault. Verwendet die aktuellen Konfigurationswerte aus\r\n\t * den GUI-Feldern.\r\n\t *\/\r\n\tprivate void performWriteSecret() {\r\n\t\tnew Thread(() -&gt; {\r\n\t\t\ttry {\r\n\t\t\t\tensureHttpClient();\r\n\r\n\t\t\t\tString token = new String(txtToken.getPassword()).trim();\r\n\t\t\t\tif (token.isEmpty()) {\r\n\t\t\t\t\tshowSimpleErrorDialog(\"Secret Schreiben - Authentifizierung fehlt\", \"Kein Token vorhanden!\\n\\n\" + \"Bitte f\u00fchren Sie zuerst einen AppRole-Login durch\\n\"\r\n\t\t\t\t\t\t\t+ \"oder geben Sie einen g\u00fcltigen Token ein.\");\r\n\t\t\t\t\treturn;\r\n\t\t\t\t}\r\n\r\n\t\t\t\tString jsonData = txtSecretData.getText().trim();\r\n\t\t\t\tif (jsonData.isEmpty()) {\r\n\t\t\t\t\tshowSimpleErrorDialog(\"Secret Schreiben - Validierungsfehler\", \"Secret-Daten d\u00fcrfen nicht leer sein!\");\r\n\t\t\t\t\treturn;\r\n\t\t\t\t}\r\n\r\n\t\t\t\tif (!jsonData.startsWith(\"{\") || !jsonData.endsWith(\"}\")) {\r\n\t\t\t\t\tshowSimpleErrorDialog(\"Secret Schreiben - Formatfehler\", \"Secret-Daten m\u00fcssen im JSON-Format vorliegen.\\n\"\r\n\t\t\t\t\t\t\t+ \"Beispiel: {\\\"key\\\":\\\"value\\\",\\\"username\\\":\\\"demo\\\"}\");\r\n\t\t\t\t\treturn;\r\n\t\t\t\t}\r\n\r\n\t\t\t\tlog(\"=== Secret Schreiben ===\");\r\n\t\t\t\twriteSecret(token, jsonData);\r\n\r\n\t\t\t\tSwingUtilities.invokeLater(() -&gt; {\r\n\t\t\t\t\tJOptionPane.showMessageDialog(this, \"Secret wurde erfolgreich in Vault gespeichert:\\n\" + txtKvMount.getText() + \"\/data\/\"\r\n\t\t\t\t\t\t\t+ txtSecretPath.getText(), \"Secret Erfolgreich Gespeichert\", JOptionPane.INFORMATION_MESSAGE);\r\n\t\t\t\t});\r\n\r\n\t\t\t} catch (Exception ex) {\r\n\t\t\t\tshowErrorDialog(\"Secret Schreiben Fehlgeschlagen\", \"Das Secret konnte nicht in Vault geschrieben werden.\\n\\n\" + \"M\u00f6gliche Ursachen:\\n\"\r\n\t\t\t\t\t\t+ \"\u2022 Token abgelaufen oder ung\u00fcltig\\n\" + \"\u2022 Keine Schreibberechtigung f\u00fcr den Pfad\\n\" + \"\u2022 Ung\u00fcltiges JSON-Format\\n\"\r\n\t\t\t\t\t\t+ \"\u2022 Vault Server nicht erreichbar\", ex);\r\n\t\t\t}\r\n\t\t}).start();\r\n\t}\r\n\r\n\t\/**\r\n\t * Secret schreiben API-Aufruf. Liest alle Konfigurationswerte direkt aus den\r\n\t * GUI-Feldern.\r\n\t *\/\r\n\tprivate void writeSecret(String token, String jsonKeyValues) throws Exception {\r\n\t\tString vaultAddr = txtVaultAddr.getText().trim();\r\n\t\tString kvMount = txtKvMount.getText().trim();\r\n\t\tString secretPath = txtSecretPath.getText().trim();\r\n\r\n\t\tString mountPunkt = vaultAddr + \"\/v1\/\" + kvMount + \"\/data\/\" + secretPath;\r\n\t\tlog(\"Mount-Punkt: \" + mountPunkt);\r\n\r\n\t\tURI uri = URI.create(mountPunkt);\r\n\t\tString body = \"{\\\"data\\\":\" + jsonKeyValues + \"}\";\r\n\r\n\t\tHttpRequest req = HttpRequest.newBuilder(uri).timeout(Duration.ofSeconds(10)).header(\"X-Vault-Token\", token).header(\"Content-Type\", \"application\/json\")\r\n\t\t\t\t.POST(HttpRequest.BodyPublishers.ofString(body, StandardCharsets.UTF_8)).build();\r\n\r\n\t\tlog(\"Sende WRITE Request...\");\r\n\t\tHttpResponse&lt;String&gt; res = httpClient.send(req, HttpResponse.BodyHandlers.ofString());\r\n\r\n\t\tlog(\"WRITE Status: \" + res.statusCode());\r\n\t\tlog(\"WRITE Response: \" + res.body());\r\n\r\n\t\tif (res.statusCode() \/ 100 != 2) {\r\n\t\t\tString errorDetail = parseVaultError(res.body());\r\n\t\t\tthrow new RuntimeException(\"Schreiben fehlgeschlagen: HTTP \" + res.statusCode() + \"\\n\" + errorDetail);\r\n\t\t}\r\n\r\n\t\tlog(\"Secret erfolgreich geschrieben!\");\r\n\t}\r\n\r\n\t\/**\r\n\t * Liest ein Secret aus Vault. Verwendet die aktuellen Konfigurationswerte aus\r\n\t * den GUI-Feldern.\r\n\t *\/\r\n\tprivate void performReadSecret() {\r\n\t\tnew Thread(() -&gt; {\r\n\t\t\ttry {\r\n\t\t\t\tensureHttpClient();\r\n\r\n\t\t\t\tString token = new String(txtToken.getPassword()).trim();\r\n\t\t\t\tif (token.isEmpty()) {\r\n\t\t\t\t\tshowSimpleErrorDialog(\"Secret Lesen - Authentifizierung fehlt\", \"Kein Token vorhanden!\\n\\n\" + \"Bitte f\u00fchren Sie zuerst einen AppRole-Login durch\\n\"\r\n\t\t\t\t\t\t\t+ \"oder geben Sie einen g\u00fcltigen Token ein.\");\r\n\t\t\t\t\treturn;\r\n\t\t\t\t}\r\n\r\n\t\t\t\tlog(\"=== Secret Lesen ===\");\r\n\t\t\t\treadSecret(token);\r\n\r\n\t\t\t\tSwingUtilities.invokeLater(() -&gt; {\r\n\t\t\t\t\tJOptionPane.showMessageDialog(this, \"Secret wurde erfolgreich aus Vault gelesen.\\n\"\r\n\t\t\t\t\t\t\t+ \"Details siehe Log-Ausgabe unten.\", \"Secret Erfolgreich Gelesen\", JOptionPane.INFORMATION_MESSAGE);\r\n\t\t\t\t});\r\n\r\n\t\t\t} catch (Exception ex) {\r\n\t\t\t\tshowErrorDialog(\"Secret Lesen Fehlgeschlagen\", \"Das Secret konnte nicht aus Vault gelesen werden.\\n\\n\" + \"M\u00f6gliche Ursachen:\\n\"\r\n\t\t\t\t\t\t+ \"\u2022 Token abgelaufen oder ung\u00fcltig\\n\" + \"\u2022 Keine Leseberechtigung f\u00fcr den Pfad\\n\" + \"\u2022 Secret existiert nicht am angegebenen Pfad\\n\"\r\n\t\t\t\t\t\t+ \"\u2022 Vault Server nicht erreichbar\", ex);\r\n\t\t\t}\r\n\t\t}).start();\r\n\t}\r\n\r\n\t\/**\r\n\t * Secret lesen API-Aufruf. Liest alle Konfigurationswerte direkt aus den\r\n\t * GUI-Feldern.\r\n\t *\/\r\n\tprivate void readSecret(String token) throws Exception {\r\n\t\tString vaultAddr = txtVaultAddr.getText().trim();\r\n\t\tString kvMount = txtKvMount.getText().trim();\r\n\t\tString secretPath = txtSecretPath.getText().trim();\r\n\r\n\t\tURI uri = URI.create(vaultAddr + \"\/v1\/\" + kvMount + \"\/data\/\" + secretPath);\r\n\r\n\t\tHttpRequest req = HttpRequest.newBuilder(uri).timeout(Duration.ofSeconds(10)).header(\"X-Vault-Token\", token).GET().build();\r\n\r\n\t\tlog(\"Sende READ Request an: \" + uri);\r\n\t\tHttpResponse&lt;String&gt; res = httpClient.send(req, HttpResponse.BodyHandlers.ofString());\r\n\r\n\t\tlog(\"READ Status: \" + res.statusCode());\r\n\t\tlog(\"READ Response: \" + res.body());\r\n\r\n\t\tif (res.statusCode() \/ 100 != 2) {\r\n\t\t\tString errorDetail = parseVaultError(res.body());\r\n\t\t\tthrow new RuntimeException(\"Lesen fehlgeschlagen: HTTP \" + res.statusCode() + \"\\n\" + errorDetail);\r\n\t\t}\r\n\r\n\t\tlog(\"Secret erfolgreich gelesen!\");\r\n\t}\r\n\r\n\t\/**\r\n\t * Extrahiert die Fehlermeldung aus einer Vault API Error Response.\r\n\t *\/\r\n\tprivate String parseVaultError(String responseBody) {\r\n\t\tif (responseBody == null || responseBody.isEmpty()) {\r\n\t\t\treturn \"Keine Fehlerdetails verf\u00fcgbar\";\r\n\t\t}\r\n\r\n\t\tPattern p = Pattern.compile(\"\\\"errors\\\"\\\\s*:\\\\s*\\\\[([^\\\\]]+)\\\\]\");\r\n\t\tMatcher m = p.matcher(responseBody);\r\n\t\tif (m.find()) {\r\n\t\t\tString errors = m.group(1).replace(\"\\\"\", \"\").replace(\",\", \"\\n\u2022 \");\r\n\t\t\treturn \"Vault Fehler:\\n\u2022 \" + errors;\r\n\t\t}\r\n\r\n\t\tif (responseBody.length() &gt; 200) {\r\n\t\t\treturn responseBody.substring(0, 200) + \"...\";\r\n\t\t}\r\n\t\treturn responseBody;\r\n\t}\r\n\r\n\t\/**\r\n\t * Escaped JSON-Sonderzeichen in Strings.\r\n\t *\/\r\n\tprivate String escape(String s) {\r\n\t\treturn s.replace(\"\\\\\", \"\\\\\\\\\").replace(\"\\\"\", \"\\\\\\\"\");\r\n\t}\r\n\r\n\t\/**\r\n\t * Hauptmethode zum Starten der Anwendung.\r\n\t *\/\r\n\tpublic static void main(String[] args) {\r\n\t\tSwingUtilities.invokeLater(() -&gt; {\r\n\t\t\tVaultDemoSwingGUI frame = new VaultDemoSwingGUI();\r\n\t\t\tframe.setVisible(true);\r\n\t\t});\r\n\t}\r\n}\r\n<\/pre>\n<p>Es k\u00f6nnen die Parameter beim Start \u00fcbergeben werden, oder auch in der GUI angepasst werden, dann werden die Werte verwendet.<br \/>\nWie der Vault konfiguriert werden muss, um mit der GUI zu interagieren, werde ich aber nicht mehr heute beschreiben, das kommt dann in Teil 3.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Heute mal eine Java Anwendung, die auf ein Hashi Corp Vault zugreift. Was ein HashiCorpVault macht, wird auf Kleinhirn.eu zusammengefasst. Im Teil 1 hatte ich ja die Installation auf einem Raspberry Pi mit dem Workflow beschrieben. Nach dem Start der Anwendung, \u00f6ffnet sich diese Swing GUI: Hier der Java-Code, auch neueste Version im GitLab-Repo:<\/p>\n","protected":false},"author":2,"featured_media":0,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[220,4606,3897,5,5328,79,1319],"tags":[6244,6234,2178,6245,6235],"class_list":["post-22916","post","type-post","status-publish","format-standard","hentry","category-anleitung","category-crypto","category-java-programmierung","category-java","category-java-19","category-programmierung","category-sicherheit-2","tag-demo-anwendung","tag-hashicorp","tag-java","tag-swing-gui","tag-vault"],"_links":{"self":[{"href":"http:\/\/blog.wenzlaff.de\/index.php?rest_route=\/wp\/v2\/posts\/22916","targetHints":{"allow":["GET"]}}],"collection":[{"href":"http:\/\/blog.wenzlaff.de\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"http:\/\/blog.wenzlaff.de\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"http:\/\/blog.wenzlaff.de\/index.php?rest_route=\/wp\/v2\/users\/2"}],"replies":[{"embeddable":true,"href":"http:\/\/blog.wenzlaff.de\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=22916"}],"version-history":[{"count":0,"href":"http:\/\/blog.wenzlaff.de\/index.php?rest_route=\/wp\/v2\/posts\/22916\/revisions"}],"wp:attachment":[{"href":"http:\/\/blog.wenzlaff.de\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=22916"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"http:\/\/blog.wenzlaff.de\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=22916"},{"taxonomy":"post_tag","embeddable":true,"href":"http:\/\/blog.wenzlaff.de\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=22916"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}