This commit is contained in:
Ralph Soika 2025-11-16 11:03:05 +01:00
parent cfd974ecd2
commit 1bce7db64e
5 changed files with 586 additions and 259 deletions

View file

@ -25,7 +25,6 @@ import org.imixs.workflow.ItemCollection;
import org.imixs.workflow.engine.DocumentService;
import org.imixs.workflow.exceptions.PluginException;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.security.DeclareRoles;
import jakarta.annotation.security.RolesAllowed;
import jakarta.annotation.security.RunAs;
@ -60,11 +59,6 @@ public class KSeFAPIService {
@Inject
DocumentService documentService;
@PostConstruct
void init() {
}
/**
* This method uploads an KSeF Invoice document (XML).
* The response of the uplaod is a 'referenceNumber' which is stored into the
@ -117,10 +111,7 @@ public class KSeFAPIService {
IvParameterSpec iv = new IvParameterSpec(ivBytes);
// ---------- 4) XML per AES-256-CBC verschlüsseln ----------
javax.crypto.Cipher aesCipher;
aesCipher = javax.crypto.Cipher.getInstance("AES/CBC/PKCS5Padding");
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);
@ -198,6 +189,9 @@ public class KSeFAPIService {
} catch (Exception e) {
logger.severe("├── ⚠️ Error parsing JSON response" + e.getMessage());
kseFAuthManager.deleteCurrentSession();
throw new PluginException(KSeFAuthManager.class.getSimpleName(), ERROR_API,
"Error parsing JSON response: " + e.getMessage());
@ -220,6 +214,8 @@ public class KSeFAPIService {
fileData.setAttribute("ksef.referenceNumber", textlist);
kseFAuthManager.closeSession();
kseFAuthManager.deleteCurrentSession();
return referenceNumber;
}

View file

@ -15,7 +15,9 @@ import java.security.spec.MGF1ParameterSpec;
import java.time.Duration;
import java.time.Instant;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.Base64;
import java.util.List;
import java.util.Optional;
import java.util.logging.Logger;
@ -36,6 +38,7 @@ import jakarta.ejb.Lock;
import jakarta.ejb.LockType;
import jakarta.ejb.Singleton;
import jakarta.inject.Inject;
import jakarta.json.JsonArray;
import jakarta.json.JsonObject;
import jakarta.json.bind.Jsonb;
import jakarta.json.bind.JsonbBuilder;
@ -195,14 +198,22 @@ public class KSeFAuthManager {
logger.info("│ ├── open new interactive KSeF Session...");
// open new session
this.loadPublicKeyCertificates();
this.authChallenge();
this.authKSeFToken();
this.waitSomeTime(3000);
// Jetzt Status abwarten
this.waitForAuthStatus();
waitSomeTime(5000);
this.redeemToken();
this.openInteractiveSession();
// Delete deprecated sessions
this.deleteDeprecatedSessions();
this.checkTokenStatus();
this.redeemToken();
this.openInteractiveSession();
} else {
logger.warning("│ ├── ! Session already exists - reuse existing KSeF Session...");
}
@ -262,10 +273,8 @@ public class KSeFAuthManager {
logger.info("├── ♻️ KSeF API load public-key-certificates...");
String uri = baseURI + "/security/public-key-certificates";
logger.info("│ ├── GET: " + uri);
if (debug) {
logger.info("│ ├── Endpoint: " + uri);
}
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(uri))
.header("Accept", "application/json")
@ -279,9 +288,9 @@ public class KSeFAuthManager {
throw new PluginException(KSeFAuthManager.class.getSimpleName(), ERROR_API,
"Unable to send request for KSeF certificates endpoint: " + e.getMessage());
}
logger.info("│ ├── HTTP Response: " + response.statusCode());
if (debug) {
logger.info("│ ├── Response Code = " + response.statusCode());
logger.info("Response: " + response.body());
}
@ -342,9 +351,7 @@ public class KSeFAuthManager {
logger.info("├── 🛅 KSeF API Challenge...");
String uri = baseURI + "/auth/challenge";
if (debug) {
logger.info("│ ├── Endpoint: " + uri);
}
logger.info("│ ├── POST: " + uri);
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(uri)) // nur Endpoint checken
.header("Accept", "application/json")
@ -354,8 +361,9 @@ public class KSeFAuthManager {
HttpResponse<String> response;
try {
response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
logger.info("│ ├── HTTP Response: " + response.statusCode());
if (debug) {
logger.info("│ ├── Response Code = " + response.statusCode());
logger.info("Response: " + response.body());
}
// Prüfen, dass wir eine API-Antwort bekommen, kein HTML
@ -400,9 +408,8 @@ public class KSeFAuthManager {
logger.info("├── 🛃 KSeF API auth ksef-token...");
String uri = baseURI + "/auth/ksef-token";
if (debug) {
logger.info("│ ├── Endpoint: " + uri);
}
logger.info("│ ├── POST: " + uri);
HttpResponse<String> response = null;
try {
String jsonPayload = String.format(
@ -429,8 +436,8 @@ public class KSeFAuthManager {
"API Error: Unable to request ksef-token");
}
logger.info("│ ├── HTTP Response: " + response.statusCode());
if (debug) {
logger.info("│ ├── Response Code = " + response.statusCode());
logger.info("Response: " + response.body());
}
@ -463,6 +470,113 @@ public class KSeFAuthManager {
}
}
/**
* Wartet auf den Korrekten Auth Status...
*
* @throws PluginException
*/
public void waitForAuthStatus() throws PluginException {
logger.info("├── 🔜 Waiting for Auth status 200...");
// Now we need to wait until auth/{AuthRef} will return 200
long start = System.currentTimeMillis();
long timeout = 10_000; // 10 Sekunden
while (System.currentTimeMillis() - start < timeout) {
if (this.checkTokenStatus() == 200) {
break; // Erfolg
}
// kleine Pause, um CPU zu schonen
waitSomeTime(200);
}
// Optional: prüfen, ob Timeout erreicht wurde
if (System.currentTimeMillis() - start >= timeout) {
logger.info("├── ⚠️ Timeout reached after 10 seconds!");
}
}
/**
* waits some time..
*/
private void waitSomeTime(long millis) {
logger.info("│ ├── waiting " + millis + "ms....");
try {
Thread.sleep(millis);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
/**
* Redeems the KSeF token and returns a session access token to be used to open
* an Interactive Session
*
* POST /auth/token/redeem
*
* @return JsonObject with referenceNumber and sessionToken
* @throws Exception
*/
@Lock(LockType.WRITE)
public JsonObject redeemToken() throws PluginException {
logger.info("├── 🛂 KSeF API redeem token...");
String uri = baseURI + "/auth/token/redeem";
logger.info("│ ├── POST: " + uri);
HttpResponse<String> response = null;
try {
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(uri))
.header("Accept", "application/json")
.header("Authorization", "Bearer " + authToken)
.POST(HttpRequest.BodyPublishers.noBody())
.build();
response = httpClient.send(request,
HttpResponse.BodyHandlers.ofString());
} catch (IOException | InterruptedException e) {
throw new PluginException(KSeFAuthManager.class.getSimpleName(), ERROR_API,
"API Error - unable to redeemToken: " + e.getMessage());
}
logger.info("│ ├── HTTP Response: " + response.statusCode());
if (debug) {
logger.info("│ ├── Response: " + response.body());
}
if (response.statusCode() >= 400) {
throw new PluginException(KSeFAuthManager.class.getSimpleName(),
ERROR_API,
"Token redeem failed: " + response.statusCode()
+ " - " + response.body());
}
try (Jsonb jsonb = JsonbBuilder.create()) {
JsonObject jsonObject = jsonb.fromJson(response.body(), JsonObject.class);
// Access Token extrahieren und speichern
JsonObject accessTokenObj = jsonObject.getJsonObject("accessToken");
this.accessToken = accessTokenObj.getString("token");
String validUntil = accessTokenObj.getString("validUntil");
logger.info("│ ├── ✓ Access Token received");
logger.info("│ └── ✓ validUntil: " + validUntil);
return jsonObject;
} catch (Exception e) {
logger.severe("├── ⚠️ Error parsing JSON response: " + e.getMessage());
throw new PluginException(KSeFAuthManager.class.getSimpleName(),
ERROR_API,
"Error parsing response: " + e.getMessage());
}
}
/**
* Helper Method to open an interactive session. The method fetches a
* sessionRefNumber which is mandatory to upload an invoice.
@ -479,9 +593,8 @@ public class KSeFAuthManager {
logger.info("├── 🔐 KSeF API open interactive session...");
String uri = baseURI + "/sessions/online";
if (debug) {
logger.info("│ ├── Endpoint: " + uri);
}
logger.info("│ ├── POST: " + uri);
HttpResponse<String> response = null;
try {
// --- AES Schlüssel erzeugen ---
@ -529,8 +642,9 @@ public class KSeFAuthManager {
response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
logger.info("│ ├── HTTP Response: " + response.statusCode());
if (debug) {
logger.info("│ ├── Response Code = " + response.statusCode());
logger.info("Response: " + response.body());
}
@ -546,8 +660,8 @@ public class KSeFAuthManager {
sessionRefNumber = jsonObject.getString("referenceNumber");
sessionValidUntil = jsonObject.getString("validUntil");
if (true) {
logger.info("│ ├── Session ReferenceNumber = " + sessionRefNumber);
logger.info("│ └── Session ValidUntil = " + sessionValidUntil);
logger.info("│ ├── Session ReferenceNumber = " + sessionRefNumber);
logger.info("│ └── Session ValidUntil = " + sessionValidUntil);
}
// IMPORTANT: Store the AES key - this key is mandatory to upload a invoice
@ -566,6 +680,14 @@ public class KSeFAuthManager {
"API Error openInteractiveSession: " + e.getMessage());
}
// logger.info("│ ├── Sleep 5 sec...");
// try {
// Thread.sleep(5000);
// } catch (InterruptedException e) {
// Thread.currentThread().interrupt();
// }
}
/**
@ -598,8 +720,6 @@ public class KSeFAuthManager {
long timestampMillis = instant.toEpochMilli();
logger.info("│ ├── Timestamp (ms): " + timestampMillis);
// 2. Format erstellen: token|timestamp
String plaintext = ksefToken.get() + "|" + timestampMillis;
@ -634,71 +754,6 @@ public class KSeFAuthManager {
return result;
}
/**
* Redeems the KSeF token and returns a session access token to be used to open
* an Interactive Session
*
* POST /auth/token/redeem
*
* @return JsonObject with referenceNumber and sessionToken
* @throws Exception
*/
@Lock(LockType.WRITE)
public JsonObject redeemToken() throws PluginException {
logger.info("├── 🛂 KSeF API redeem token...");
String uri = baseURI + "/auth/token/redeem";
if (debug) {
logger.info("│ ├── Endpoint: " + uri);
}
HttpResponse<String> response = null;
try {
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(uri))
.header("Accept", "application/json")
.header("Authorization", "Bearer " + authToken)
.POST(HttpRequest.BodyPublishers.noBody())
.build();
response = httpClient.send(request,
HttpResponse.BodyHandlers.ofString());
} catch (IOException | InterruptedException e) {
throw new PluginException(KSeFAuthManager.class.getSimpleName(), ERROR_API,
"API Error - unable to redeemToken: " + e.getMessage());
}
if (debug) {
logger.info("│ ├── Response Code = " + response.statusCode());
logger.info("│ ├── Response: " + response.body());
}
if (response.statusCode() >= 400) {
throw new PluginException(KSeFAuthManager.class.getSimpleName(),
ERROR_API,
"Token redeem failed: " + response.statusCode()
+ " - " + response.body());
}
try (Jsonb jsonb = JsonbBuilder.create()) {
JsonObject jsonObject = jsonb.fromJson(response.body(), JsonObject.class);
// Access Token extrahieren und speichern
JsonObject accessTokenObj = jsonObject.getJsonObject("accessToken");
this.accessToken = accessTokenObj.getString("token");
logger.info("│ └── ☑️ Access Token received");
return jsonObject;
} catch (Exception e) {
logger.severe("├── ⚠️ Error parsing JSON response: " + e.getMessage());
throw new PluginException(KSeFAuthManager.class.getSimpleName(),
ERROR_API,
"Error parsing response: " + e.getMessage());
}
}
private PublicKey extractPublicKeyFromCertificate(String base64Cert) throws Exception {
byte[] der = java.util.Base64.getDecoder().decode(base64Cert);
java.security.cert.CertificateFactory factory = java.security.cert.CertificateFactory.getInstance("X.509");
@ -714,15 +769,15 @@ public class KSeFAuthManager {
*
* https://ksef-test.mf.gov.pl/docs/v2/index.html#tag/Uzyskiwanie-dostepu/paths/~1api~1v2~1auth~1%7BreferenceNumber%7D/get
*
* @return HTTP Response Code
* @throws PluginException
*/
public void checkTokenStatus() throws PluginException {
logger.info("├── 🌡 KSeF API check auth status...");
public int checkTokenStatus() throws PluginException {
logger.info("├── KSeF API check auth status...");
String uri = baseURI + "/auth/" + authRefNumber;
int httpResponse = -1;
if (debug) {
logger.info("│ ├── Endpoint: " + uri);
}
logger.info("│ ├── GET: " + uri);
HttpResponse<String> response = null;
try {
HttpRequest request = HttpRequest.newBuilder()
@ -737,10 +792,11 @@ public class KSeFAuthManager {
throw new PluginException(KSeFAuthManager.class.getSimpleName(), ERROR_API,
"API Error: Unable to request auth status: " + e.getMessage());
}
if (true) {
logger.info("│ ├── Response Code = " + response.statusCode());
logger.info("Response: " + response.body());
}
httpResponse = response.statusCode();
logger.info("│ ├── HTTP Response: " + httpResponse);
logger.info("│ ├── " + response.body());
try (Jsonb jsonb = JsonbBuilder.create()) {
JsonObject jsonObject = jsonb.fromJson(response.body(), JsonObject.class);
@ -767,6 +823,163 @@ public class KSeFAuthManager {
throw new PluginException(KSeFAuthManager.class.getSimpleName(), ERROR_API,
"Error parsing JSON response: " + e.getMessage());
}
return httpResponse;
}
/**
* This method returns all active sessions
*
* https://ksef-test.mf.gov.pl/docs/v2/index.html#tag/Aktywne-sesje/paths/~1api~1v2~1auth~1sessions/get
*
* @throws PluginException
*/
public JsonObject getActiveSessions() throws PluginException {
logger.info("├── 🚸 KSeF API active sessions...");
String uri = baseURI + "/auth/sessions";
if (debug) {
logger.info("│ ├── GET: " + uri);
}
HttpResponse<String> response = null;
try {
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(uri))
.header("Accept", "application/json")
.header("Content-Type", "application/json")
.header("Authorization", "Bearer " + accessToken)
.GET()
.build();
response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
} catch (IOException | InterruptedException e) {
throw new PluginException(KSeFAuthManager.class.getSimpleName(), ERROR_API,
"API Error: Unable to request auth status: " + e.getMessage());
}
if (true) {
logger.info("│ ├── HTTP Response: " + response.statusCode());
// logger.info("Response: " + response.body());
}
try (Jsonb jsonb = JsonbBuilder.create()) {
JsonObject jsonObject = jsonb.fromJson(response.body(), JsonObject.class);
return jsonObject;
} 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());
}
}
/**
* Deletes all old sessions
*
* The method first asks for old sessions and if we have more then 2 sessions we
* delete the deprecated ones.
*
* @param sessionData
* @throws PluginException
*/
public void deleteDeprecatedSessions() throws PluginException {
int maxIterations = 10; // Sicherheits-Begrenzung
for (int iteration = 0; iteration < maxIterations; iteration++) {
JsonObject currentSessions = this.getActiveSessions();
// Get list of refNumbers
List<String> referenceNumbers = new ArrayList<>();
JsonArray items = currentSessions.getJsonArray("items");
for (int i = 0; i < items.size(); i++) {
JsonObject item = items.getJsonObject(i);
referenceNumbers.add(item.getString("referenceNumber"));
}
// Wenn nur noch eine Session da ist, abbrechen
if (referenceNumbers.size() <= 1) {
if (iteration == 0) {
logger.info("│ ├── Only one session found, no deprecated sessions");
} else {
logger.info("├── 🎉 All deprecated sessions deleted after " + iteration + " iterations");
}
break;
}
// delete old sessions....
logger.info("├── 🚮 KSeF API delete deprecated sessions (iteration " + (iteration + 1) + ")...");
String uri = baseURI + "/auth/sessions/";
// Alle Einträge ab Index 1 (also ab dem zweiten Eintrag) durchgehen
for (int i = 1; i < referenceNumbers.size(); i++) {
String referenceNumber = referenceNumbers.get(i);
logger.info("│ ├── delete deprecated session: " + referenceNumber);
HttpResponse<String> response = null;
try {
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(uri + referenceNumber))
.header("Accept", "application/json")
.header("Content-Type", "application/json")
.header("Authorization", "Bearer " + accessToken)
.DELETE()
.build();
response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
} catch (IOException | InterruptedException e) {
throw new PluginException(KSeFAuthManager.class.getSimpleName(), ERROR_API,
"API Error: Unable to request auth status: " + e.getMessage());
}
logger.info("│ ├── HTTP Response: " + response.statusCode());
}
logger.info("│ ├── Deleted " + (referenceNumbers.size() - 1) + " sessions in this iteration");
// Kurze Pause vor der nächsten Iteration
if (referenceNumbers.size() > 1 && iteration < maxIterations - 1) {
try {
Thread.sleep(500);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
break;
}
}
// Wenn wir die maximale Iteration erreicht haben aber immer noch Sessions da
// sind
if (iteration == maxIterations - 1 && referenceNumbers.size() > 1) {
logger.warning("├── ⚠️ Maximum iterations reached, but " + (referenceNumbers.size() - 1)
+ " sessions still remain");
}
}
}
/**
* Deletes the current session
*
* @throws PluginException
*/
public void deleteCurrentSession() throws PluginException {
logger.info("├── 🚮 KSeF API delete current session...");
String uri = baseURI + "/auth/sessions/current";
HttpResponse<String> response = null;
try {
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(uri))
.header("Accept", "application/json")
.header("Content-Type", "application/json")
.header("Authorization", "Bearer " + accessToken)
.DELETE()
.build();
response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
} catch (IOException | InterruptedException e) {
throw new PluginException(KSeFAuthManager.class.getSimpleName(), ERROR_API,
"API Error: Unable to request auth status: " + e.getMessage());
}
logger.info("│ ├── HTTP Response: " + response.statusCode());
}
/**
@ -785,9 +998,7 @@ public class KSeFAuthManager {
// https://ksef-test.mf.gov.pl/api/v2/sessions/online/{referenceNumber}/close
String uri = baseURI + "/sessions/online/" + sessionRefNumber + "/close";
if (debug) {
logger.info("│ ├── Endpoint: " + uri);
}
logger.info("│ ├── POST: " + uri);
if (sessionRefNumber == null || sessionRefNumber.isEmpty()) {
throw new PluginException(KSeFAuthManager.class.getSimpleName(), ERROR_API,
@ -809,7 +1020,7 @@ public class KSeFAuthManager {
"API Error: Unable to request auth status: " + e.getMessage());
}
if (true) {
logger.info("│ ├── Response Code = " + response.statusCode());
logger.info("│ ├── HTTP Response: " + response.statusCode());
logger.info("Response: " + response.body());
}

View file

@ -13,7 +13,6 @@ import java.util.logging.Logger;
import org.imixs.workflow.FileData;
import org.imixs.workflow.ItemCollection;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.MethodOrderer;
import org.junit.jupiter.api.Order;
@ -24,7 +23,7 @@ import org.mockito.junit.jupiter.MockitoExtension;
import com.alexanderlogistics.TestLoggerConfig;
@Disabled
//@Disabled
@ExtendWith(MockitoExtension.class)
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
public class KSeFAPIServiceTest {

View file

@ -18,7 +18,6 @@ import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.MethodOrderer;
import org.junit.jupiter.api.Order;
@ -27,155 +26,160 @@ import org.junit.jupiter.api.TestMethodOrder;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.junit.jupiter.MockitoExtension;
@Disabled
import com.alexanderlogistics.TestLoggerConfig;
@ExtendWith(MockitoExtension.class)
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
public class KSeFAuthManagerTest {
private static Logger logger = Logger.getLogger(KSeFAuthManagerTest.class.getName());
private static Logger logger = Logger.getLogger(KSeFAuthManagerTest.class.getName());
private KSeFAuthManager manager;
private KSeFAuthManager manager;
@BeforeEach
void setup() throws Exception {
manager = new KSeFAuthManager();
@BeforeEach
void setup() throws Exception {
TestLoggerConfig.setupTestLogger();
manager = new KSeFAuthManager();
// Test config
manager.setKsefToken(Params.TOKEN);
manager.setKsefNip(Params.NIP);
manager.setKsefEndpoint("https://ksef-test.mf.gov.pl/api/v2");
manager.setDebug(false);
// Test config
manager.setKsefToken(Params.TOKEN);
manager.setKsefNip(Params.NIP);
manager.setKsefEndpoint("https://ksef-test.mf.gov.pl/api/v2");
manager.setDebug(false);
// init() ausführen
manager.init();
// init() ausführen
manager.init();
}
// ------------------------------------------------------------
// 1) Challenge abholen
// ------------------------------------------------------------
@Test
@Order(1)
@DisplayName("POST /api/v2/auth/token/redeem")
void testRedeemToken() throws Exception {
manager.loadPublicKeyCertificates();
manager.authChallenge();
manager.authKSeFToken();
manager.redeemToken();
assertNotNull(
manager.getAuthRefNumber(),
"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.waitForAuthStatus();
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);
if (manager.isDebug()) {
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();
// ------------------------------------------------------------
// 1) Challenge abholen
// ------------------------------------------------------------
@Test
@Order(1)
@DisplayName("POST /api/v2/auth/token/redeem")
void testRedeemToken() throws Exception {
// ---------- 9) Call ausführen ----------
var response = manager.getHttpClient().send(request,
java.net.http.HttpResponse.BodyHandlers.ofString());
manager.loadPublicKeyCertificates();
manager.authChallenge();
manager.authKSeFToken();
manager.redeemToken();
System.out.println("Upload Response:");
System.out.println(response.statusCode());
System.out.println(response.body());
assertNotNull(
manager.getAuthRefNumber(),
"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);
if (manager.isDebug()) {
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");
}
// ---------- 10) Basic Assertions ----------
assertTrue(response.statusCode() == 202,
"Upload muss erfolgreich sein");
}
}

File diff suppressed because one or more lines are too long