diff --git a/office-alexander-logistics-app/src/main/java/com/alexanderlogistics/ksef/KSeFAuthManager.java b/office-alexander-logistics-app/src/main/java/com/alexanderlogistics/ksef/KSeFAuthManager.java index 6a710b2..7cae417 100644 --- a/office-alexander-logistics-app/src/main/java/com/alexanderlogistics/ksef/KSeFAuthManager.java +++ b/office-alexander-logistics-app/src/main/java/com/alexanderlogistics/ksef/KSeFAuthManager.java @@ -7,6 +7,7 @@ import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.nio.charset.StandardCharsets; import java.security.PublicKey; +import java.security.SecureRandom; import java.security.spec.MGF1ParameterSpec; import java.time.Duration; import java.time.Instant; @@ -74,11 +75,17 @@ public class KSeFAuthManager { private String challenge = ""; private String refNumber = null; private String authToken = null; + private String accessToken = null; private String challengeTimestamp = null; private String symmetricCertBase64; private String ksefTokenCertBase64; private PublicKey symmetricPublicKey; private PublicKey ksefTokenPublicKey; + private String sessionRefNumber; + private String sessionValidUntil; + + private byte[] sessionAesKey; + private byte[] sessionIv; @PostConstruct void init() { @@ -114,6 +121,10 @@ public class KSeFAuthManager { this.ksefToken = Optional.ofNullable(token); } + public String getAccessToken() { + return accessToken; + } + public void setKsefNip(String nip) { this.ksefNip = Optional.ofNullable(nip); } @@ -122,6 +133,10 @@ public class KSeFAuthManager { this.ksefEndpoint = Optional.ofNullable(endpoint); } + public String getSessionRefNumber() { + return sessionRefNumber; + } + public void setDebug(boolean debug) { this.debug = debug; } @@ -150,6 +165,10 @@ public class KSeFAuthManager { return authToken; } + public PublicKey getSymmetricPublicKey() { + return symmetricPublicKey; + } + public String getChallengeValue() { return challenge; } @@ -273,6 +292,101 @@ public class KSeFAuthManager { } } + /** + * Helper Method to open an interactive session + * + * @throws Exception + */ + @Lock(LockType.WRITE) + public void openInteractiveSession() throws Exception { + + if (accessToken == null) { + throw new PluginException(KSeFAuthManager.class.getSimpleName(), ERROR_API, + "Error - missing access token!"); + } + + logger.info("├── KSeF API open interactive session..."); + String uri = baseURI + "/sessions/online"; + logger.info("│ ├── Endpoint: " + uri); + + // --- AES Schlüssel erzeugen --- + SecureRandom rnd = new SecureRandom(); + byte[] aesKeyBytes = new byte[32]; // 256-bit AES key + rnd.nextBytes(aesKeyBytes); + byte[] ivBytes = new byte[16]; // 128-bit IV + rnd.nextBytes(ivBytes); + + // --- AES Key mit RSA encrypten --- + // Cipher rsa = Cipher.getInstance("RSA/ECB/OAEPWithSHA-256AndMGF1Padding"); + Cipher rsa = Cipher.getInstance("RSA/ECB/OAEPWithSHA-1AndMGF1Padding"); + rsa.init(Cipher.ENCRYPT_MODE, this.symmetricPublicKey); + byte[] encryptedAesKey = rsa.doFinal(aesKeyBytes); + + String jsonPayload = String.format( + "{" + + "\"formCode\": {" + + " \"systemCode\": \"FA (3)\"," + + " \"schemaVersion\": \"1-0E\"," + + " \"value\": \"FA\"" + + "}," + + "\"encryption\": {" + + " \"encryptedSymmetricKey\": \"%s\"," + + " \"initializationVector\": \"%s\"" + + "}" + + "}", + Base64.getEncoder().encodeToString(encryptedAesKey), + Base64.getEncoder().encodeToString(ivBytes)); + + if (debug) { + logger.info("│ ├── Payload: " + jsonPayload); + } + + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create(uri)) + // .header("Accept-Charset", "UTF-8") + .header("Accept", "application/json") + .header("Content-Type", "application/json") + .header("Authorization", "Bearer " + accessToken) + .POST(HttpRequest.BodyPublishers.ofString(jsonPayload)) + .build(); + + HttpResponse response = httpClient.send(request, HttpResponse.BodyHandlers.ofString()); + + logger.info("│ ├── Response Code = " + response.statusCode()); + if (debug) { + logger.info("Response: " + response.body()); + } + + if (response.statusCode() != 201) { + throw new PluginException(KSeFAuthManager.class.getSimpleName(), ERROR_API, + "Failed to open interactive session: " + response.statusCode()); + } + + // JSON parsen + // Session RefNumber aus Response extrahieren und speichern + + try (Jsonb jsonb = JsonbBuilder.create()) { + // Annahme: response.body() ist ein String mit JSON-Inhalt + JsonObject jsonObject = jsonb.fromJson(response.body(), JsonObject.class); + sessionRefNumber = jsonObject.getString("referenceNumber"); + sessionValidUntil = jsonObject.getString("validUntil"); + if (debug) { + logger.info("│ ├── Session ReferenceNumber = " + sessionRefNumber); + logger.info("│ └── Session ValidUntil = " + sessionValidUntil); + } + + // --- WICHTIG: AES Schlüssel speichern, wir brauchen ihn beim Upload --- + this.sessionAesKey = aesKeyBytes; + this.sessionIv = ivBytes; + } catch (Exception e) { + logger.severe("├── ⚠️ Error parsing JSON response" + e.getMessage()); + throw new PluginException(KSeFAuthManager.class.getSimpleName(), ERROR_API, + "Error parsing JSON response: " + e.getMessage()); + + } + + } + /** * Verschlüsselt den KSeF-Token mit RSA-OAEP (SHA-256) für die Authentifizierung * @@ -369,7 +483,7 @@ public class KSeFAuthManager { // Access Token extrahieren und speichern JsonObject accessTokenObj = jsonObject.getJsonObject("accessToken"); - this.authToken = accessTokenObj.getString("token"); + this.accessToken = accessTokenObj.getString("token"); logger.info("│ └── Access Token received"); @@ -459,22 +573,6 @@ public class KSeFAuthManager { return certificate.getPublicKey(); } - /** - * Helper Method to open an interactive session - * - * @throws Exception - */ - @Lock(LockType.WRITE) - public void openInteractiveSession() throws Exception { - - logger.info("├── KSeF API open interactive session..."); - String uri = baseURI + "/sessions/online"; - logger.info("│ ├── Endpoint: " + uri); - - // ............ - - } - /** * Helper Method to analyze the auth status * diff --git a/office-alexander-logistics-app/src/test/java/com/alexanderlogistics/ksef/KSeFAuthManagerTest.java b/office-alexander-logistics-app/src/test/java/com/alexanderlogistics/ksef/KSeFAuthManagerTest.java index 571a823..39b3db4 100644 --- a/office-alexander-logistics-app/src/test/java/com/alexanderlogistics/ksef/KSeFAuthManagerTest.java +++ b/office-alexander-logistics-app/src/test/java/com/alexanderlogistics/ksef/KSeFAuthManagerTest.java @@ -1,6 +1,21 @@ package com.alexanderlogistics.ksef; import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.net.URI; +import java.net.http.HttpRequest; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.security.MessageDigest; +import java.security.PublicKey; +import java.security.SecureRandom; +import java.util.Base64; +import java.util.logging.Logger; + +import javax.crypto.spec.IvParameterSpec; +import javax.crypto.spec.SecretKeySpec; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; @@ -15,43 +30,147 @@ import org.mockito.junit.jupiter.MockitoExtension; @TestMethodOrder(MethodOrderer.OrderAnnotation.class) public class KSeFAuthManagerTest { - private KSeFAuthManager manager; + private static Logger logger = Logger.getLogger(KSeFAuthManagerTest.class.getName()); - @BeforeEach - void setup() throws Exception { - manager = new KSeFAuthManager(); + private KSeFAuthManager manager; - // Test config - manager.setKsefToken(Params.TOKEN); - manager.setKsefNip(Params.NIP); - manager.setKsefEndpoint("https://ksef-test.mf.gov.pl/api/v2"); - manager.setDebug(true); + @BeforeEach + void setup() throws Exception { + manager = new KSeFAuthManager(); - // init() ausführen - manager.init(); - } + // Test config + manager.setKsefToken(Params.TOKEN); + manager.setKsefNip(Params.NIP); + manager.setKsefEndpoint("https://ksef-test.mf.gov.pl/api/v2"); + manager.setDebug(true); - // ------------------------------------------------------------ - // 1) Challenge abholen - // ------------------------------------------------------------ - @Test - @Order(1) - @DisplayName("POST /api/v2/auth/token/redeem") - void testRedeemToken() throws Exception { + // init() ausführen + manager.init(); + } - manager.loadPublicKeyCertificates(); - manager.authChallenge(); - manager.authKSeFToken(); - manager.redeemToken(); + // ------------------------------------------------------------ + // 1) Challenge abholen + // ------------------------------------------------------------ + @Test + @Order(1) + @DisplayName("POST /api/v2/auth/token/redeem") + void testRedeemToken() throws Exception { - assertNotNull( - manager.getRefNumber(), - "RefNumber darf nicht NULL sein"); + manager.loadPublicKeyCertificates(); + manager.authChallenge(); + manager.authKSeFToken(); + manager.redeemToken(); - assertNotNull( - manager.getAuthToken(), - "AuthToken darf nicht leer sein"); + assertNotNull( + manager.getRefNumber(), + "RefNumber darf nicht NULL sein"); - } + assertNotNull( + manager.getAuthToken(), + "AuthToken darf nicht leer sein"); + + } + + @Test + @Order(4) + @DisplayName("Upload Invoice XML (AES-256 + RSA)") + public void testUploadInvoice() throws Exception { + + // ---------- 1) Vorbereitung ---------- + // API initialisieren + manager.loadPublicKeyCertificates(); + manager.authChallenge(); + manager.authKSeFToken(); + manager.redeemToken(); + + manager.openInteractiveSession(); + + assertNotNull(manager.getAccessToken(), "AccessToken fehlt"); + // assertNotNull(manager.getRefNumber(), "ReferenceNumber fehlt"); + assertNotNull(manager.getSessionRefNumber(), "SessionReferenceNumber fehlt"); + + // ---------- 2) XML laden ---------- + Path xmlPath = Paths.get("src/test/resources/ksef/example-invoice-01.xml"); + byte[] invoiceXml = Files.readAllBytes(xmlPath); + + // ---------- 3) AES Key generieren ---------- + SecureRandom random = new SecureRandom(); + + byte[] aesKeyBytes = new byte[32]; // 256-bit AES key + random.nextBytes(aesKeyBytes); + + byte[] ivBytes = new byte[16]; // 128-bit IV + random.nextBytes(ivBytes); + + SecretKeySpec aesKey = new SecretKeySpec(aesKeyBytes, "AES"); + IvParameterSpec iv = new IvParameterSpec(ivBytes); + + // ---------- 4) XML per AES-256-CBC verschlüsseln ---------- + javax.crypto.Cipher aesCipher = javax.crypto.Cipher.getInstance("AES/CBC/PKCS5Padding"); + aesCipher.init(javax.crypto.Cipher.ENCRYPT_MODE, aesKey, iv); + + byte[] encryptedInvoiceXml = aesCipher.doFinal(invoiceXml); + + // ---------- 5) AES Key per RSA verschlüsseln ---------- + PublicKey ksefPublicKey = manager.getSymmetricPublicKey(); + + javax.crypto.Cipher rsaCipher = javax.crypto.Cipher + .getInstance("RSA/ECB/OAEPWithSHA-256AndMGF1Padding"); + rsaCipher.init(javax.crypto.Cipher.ENCRYPT_MODE, ksefPublicKey); + + byte[] encryptedAesKey = rsaCipher.doFinal(aesKeyBytes); + + // ---------- 6) SHA-256 Hash des Klartext-XML berechnen ---------- + MessageDigest sha256 = MessageDigest.getInstance("SHA-256"); + + // hash origin invoice + byte[] xmlHashBytes = sha256.digest(invoiceXml); + String invoiceHash = Base64.getEncoder().encodeToString(xmlHashBytes); + + // hash encrypted invoice + xmlHashBytes = sha256.digest(encryptedInvoiceXml); + String encryptedInvoiceHash = Base64.getEncoder().encodeToString(xmlHashBytes); + + // ---------- 7) Request-Body bauen ---------- + // Struktur gemäß KSeF-Doku + String jsonBody = String.format("{ " + + "\"invoiceHash\": \"%s\", " + + "\"invoiceSize\": \"%s\", " + + "\"encryptedInvoiceHash\": \"%s\", " + + "\"encryptedInvoiceSize\": \"%s\", " + + "\"encryptedInvoiceContent\": \"%s\", " + + "\"offlineMode\": false " + + "}", + invoiceHash, + invoiceXml.length, + encryptedInvoiceHash, + encryptedInvoiceXml.length, + Base64.getEncoder().encodeToString(encryptedInvoiceXml)); + + // ---------- 8) HTTP POST vorbereiten ---------- + // String uri = manager.getBaseURI() + "/sessions/online/" + + // manager.getRefNumber() + "/invoices"; + String uri = manager.getBaseURI() + "/sessions/online/" + manager.getSessionRefNumber() + "/invoices"; + logger.info("│ ├── Endpoint: " + uri); + logger.info("Request: " + jsonBody); + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create(uri)) + .header("Content-Type", "application/json") + .header("Authorization", "Bearer " + manager.getAccessToken()) + .POST(HttpRequest.BodyPublishers.ofString(jsonBody)) + .build(); + + // ---------- 9) Call ausführen ---------- + var response = manager.getHttpClient().send(request, + java.net.http.HttpResponse.BodyHandlers.ofString()); + + System.out.println("Upload Response:"); + System.out.println(response.statusCode()); + System.out.println(response.body()); + + // ---------- 10) Basic Assertions ---------- + assertTrue(response.statusCode() == 202, + "Upload muss erfolgreich sein"); + } } diff --git a/office-alexander-logistics-app/src/test/resources/ksef/test.json b/office-alexander-logistics-app/src/test/resources/ksef/test.json index 7a11fad..8583387 100644 --- a/office-alexander-logistics-app/src/test/resources/ksef/test.json +++ b/office-alexander-logistics-app/src/test/resources/ksef/test.json @@ -1,11 +1,8 @@ { - "formCode": { - "systemCode": "FA (3)", - "schemaVersion": "1-0E", - "value": "FA" - }, - "encryption": { - "encryptedSymmetricKey": "bmrsRHK0uyMRB31FPLOocExKRbQMyiaqN0fzUVnwqiWWAnIySB/89iqHpGWOAiOkixrHIHRFQPHu8aUV33rv+SlCSfv/UIAuLO6FN3VgzVnXITUXFvWUE7Exi1GwJOHOPHp2+tk81Ca5WbPF3Hg3vQQoRo4YSYT7GRV63PVBSNrhHayXDRTT40dJEfLJ25/QzX7EuhV0PQU8sz/4j+kHFVEGTG6KkMN+sqctSRItauoUq7oNjoCw6zqPISvAu/FKxVmq5DxKMj2yFIJQfv9S+xIIpwQDYEPdI1s5ZD3vWtoApWUq4nH9Kbon1ma8yId5aV6PvlDG9oqKN+xvbpodzQ==", - "initializationVector": "81LUdc7vjDZtG239K+1WKg==" - } + "invoiceHash": "W5UcWtOuOI7Ebe9nMjGK6Y1sgxKHMRrIwqvc13xanVQ=", + "invoiceSize": "4642", + "encryptedInvoiceHash": "iJug90d6T8AmonxnHWgMA8QA4y5zbKTSIOh1fyAjf8g=", + "encryptedInvoiceSize": "4656", + "encryptedInvoiceContent": "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", + "offlineMode": false } \ No newline at end of file