korrektur

This commit is contained in:
Ralph Soika 2025-11-15 10:51:27 +01:00
parent 11b28ef9c3
commit bb058306f0
7 changed files with 370 additions and 189 deletions

76
doc/KSeF/KSEF_CLIENT.md Normal file
View file

@ -0,0 +1,76 @@
# ksef-client
Um die ksef-client Library nutzen zu können muss man eine relativ komplexe prozedur durchlaufen um Zugrifff darauf zu erhalen:
## 1. Personal Access Token (PAT) erstellen
Du brauchst einen GitHub-Token, damit dein Projekt das Paket aus GitHub Packages herunterladen darf.
So machst du das:
1. Klicke oben rechts auf GitHub auf dein Profil.
2. Gehe zu Settings.
3. Links unten: Developer settings.
4. Dann: Personal access tokens.
5. Dann: Tokens (classic).
6. Klicke: Generate new token (classic).
**Beim Erstellen:**
- Vergib irgendeinen Namen (z. B. „ksef-client access“).
- Wähle nur ein einziges Häkchen:
👉 read:packages
- Token generieren → Token kopieren! Du siehst ihn nur einmal.
## 2. Maven für GitHub Packages konfigurieren
Damit Maven die Pakete laden kann, musst du GitHub Packages in der Datei `~/.m2/settings.xml` eintragen.
👉 Inhalt, den du einfügen musst:
```xml
<settings xmlns="http://maven.apache.org/SETTINGS/1.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/SETTINGS/1.0.0
https://maven.apache.org/xsd/settings-1.0.0.xsd">
<servers>
<server>
<id>github-cirfmf</id>
<username>rsoika</username>
<password>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</password>
</server>
</servers>
</settings>
```
## 3. Repository in deiner pom.xml eintragen
Füge das in deine pom.xml ein:
```
<repositories>
<repository>
<id>github-cirfmf</id>
<url>https://maven.pkg.github.com/CIRFMF/ksef-client-java</url>
</repository>
</repositories>
```
## 4. Abhängigkeit hinzufügen
Auch in die pom.xml:
```xml
..
<dependency>
<groupId>pl.akmf.ksef-sdk</groupId>
<artifactId>ksef-client</artifactId>
<version>3.0.4</version>
</dependency>
...
```

View file

@ -400,12 +400,18 @@
<!-- KSeF 2.0 Client -->
<dependency>
<groupId>io.alapierre.ksef-sdk</groupId>
<groupId>pl.akmf.ksef-sdk</groupId>
<artifactId>ksef-client</artifactId>
<version>2.1.8</version>
<version>3.0.4</version>
<scope>test</scope>
</dependency>
</dependencies>
<repositories>
<repository>
<id>github-cirfmf</id>
<url>https://maven.pkg.github.com/CIRFMF/ksef-client-java</url>
</repository>
</repositories>
</project>

View file

@ -7,7 +7,11 @@ import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.PublicKey;
import java.security.SecureRandom;
import java.util.ArrayList;
import java.util.Base64;
import java.util.List;
import java.util.logging.Logger;
import javax.crypto.BadPaddingException;
@ -18,6 +22,7 @@ import javax.crypto.spec.SecretKeySpec;
import org.imixs.workflow.FileData;
import org.imixs.workflow.ItemCollection;
import org.imixs.workflow.engine.DocumentService;
import org.imixs.workflow.exceptions.PluginException;
import jakarta.annotation.PostConstruct;
@ -52,19 +57,24 @@ public class KSeFAPIService {
@Inject
KSeFAuthManager kseFAuthManager;
@Inject
DocumentService documentService;
@PostConstruct
void init() {
}
/**
* This method uploads a KSeF Invoice document (XML).
* The response of the upload is a 'referenceNumber' which is stored into the
* This method uploads an KSeF Invoice document (XML).
* The response of the uplaod is a 'referenceNumber' which is stored into the
* item ksef.referenceNumber
*
* @param workitem - the workitem containing the invoice file
* @param fileName - the name of the invoice file to upload
* @return referenceNumber - the KSeF reference number for the uploaded invoice
*
* @param companyName
* @param contactType
* @param accessToken
* @return
* @throws PluginException
*/
public String uploadInvoice(ItemCollection workitem, String fileName)
@ -74,6 +84,7 @@ public class KSeFAPIService {
// First open an interactive Session. The KSeFAuthManager automatically reuses
// an existing session
kseFAuthManager.openSession();
if (kseFAuthManager.getAccessToken() == null) {
@ -85,55 +96,62 @@ public class KSeFAPIService {
"API Error - missing SessionRefNumber!");
}
// Get session encryption keys
byte[] aesKeyBytes = kseFAuthManager.getSessionAesKey();
byte[] ivBytes = kseFAuthManager.getSessionIv();
if (aesKeyBytes == null || ivBytes == null) {
throw new PluginException(KSeFAuthManager.class.getSimpleName(), ERROR_API,
"API Error - missing session encryption keys!");
}
// Load invoice file
// load invoice
FileData fileData = workitem.getFileData(fileName);
if (fileData == null) {
throw new PluginException(KSeFAPIService.class.getSimpleName(), ERROR_API,
"File not found: " + fileName);
}
byte[] invoiceXml = fileData.getContent();
logger.info("│ ├── XML Invoice loaded - " + invoiceXml.length + " bytes");
logger.info("│ ├── XML Invoice loaded - " + invoiceXml.length + " bytes");
try {
// ---------- 1) Use session AES key and IV ----------
logger.info("│ ├── Using session AES Key...");
// ---------- 3) AES Key generieren ----------
logger.info("│ ├── Generate AES Key...");
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);
// ---------- 2) Encrypt XML with AES-256-CBC ----------
logger.info("│ ├── Encrypting invoice with AES...");
javax.crypto.Cipher aesCipher = javax.crypto.Cipher.getInstance("AES/CBC/PKCS5Padding");
// ---------- 4) XML per AES-256-CBC verschlüsseln ----------
javax.crypto.Cipher aesCipher;
aesCipher = javax.crypto.Cipher.getInstance("AES/CBC/PKCS5Padding");
aesCipher.init(javax.crypto.Cipher.ENCRYPT_MODE, aesKey, iv);
byte[] encryptedInvoiceXml = aesCipher.doFinal(invoiceXml);
// ---------- 3) Calculate SHA-256 hashes ----------
logger.info("│ ├── Calculating hashes...");
// ---------- 5) AES Key per RSA verschlüsseln ----------
PublicKey ksefPublicKey = kseFAuthManager.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 of original invoice
// hash origin invoice
byte[] xmlHashBytes = sha256.digest(invoiceXml);
String invoiceHash = Base64.getEncoder().encodeToString(xmlHashBytes);
// Hash of encrypted invoice
// hash encrypted invoice
xmlHashBytes = sha256.digest(encryptedInvoiceXml);
String encryptedInvoiceHash = Base64.getEncoder().encodeToString(xmlHashBytes);
// ---------- 4) Build request body ----------
// ---------- 7) Request-Body bauen ----------
// Struktur gemäß KSeF-Doku
String jsonBody = String.format("{ " +
"\"invoiceHash\": \"%s\", " +
"\"invoiceSize\": %d, " +
"\"invoiceSize\": \"%s\", " +
"\"encryptedInvoiceHash\": \"%s\", " +
"\"encryptedInvoiceSize\": %d, " +
"\"encryptedInvoiceSize\": \"%s\", " +
"\"encryptedInvoiceContent\": \"%s\", " +
"\"offlineMode\": false " +
"}",
@ -143,15 +161,17 @@ public class KSeFAPIService {
encryptedInvoiceXml.length,
Base64.getEncoder().encodeToString(encryptedInvoiceXml));
// ---------- 5) Prepare HTTP POST request ----------
// ---------- 8) HTTP POST vorbereiten ----------
// String uri = manager.getBaseURI() + "/sessions/online/" +
// manager.getRefNumber() + "/invoices";
String uri = kseFAuthManager.getBaseURI() + "/sessions/online/"
+ kseFAuthManager.getSessionRefNumber() + "/invoices";
+ kseFAuthManager.getSessionRefNumber()
+ "/invoices";
logger.info("│ ├── Endpoint: " + uri);
if (kseFAuthManager.isDebug()) {
logger.info("│ ├── Request body: " + jsonBody);
logger.info("Request: " + jsonBody);
}
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(uri))
.header("Content-Type", "application/json")
@ -159,47 +179,45 @@ public class KSeFAPIService {
.POST(HttpRequest.BodyPublishers.ofString(jsonBody))
.build();
// ---------- 6) Execute request ----------
logger.info("│ ├── Sending request...");
// ---------- 9) Call ausführen ----------
var response = kseFAuthManager.getHttpClient().send(request,
java.net.http.HttpResponse.BodyHandlers.ofString());
logger.info("│ ├── Response Code: " + response.statusCode());
System.out.println("Upload Response:");
System.out.println(response.statusCode());
System.out.println(response.body());
if (kseFAuthManager.isDebug()) {
logger.info("│ ├── Response Body: " + response.body());
}
// ---------- 7) Validate response status ----------
if (response.statusCode() != 202) {
throw new PluginException(KSeFAPIService.class.getSimpleName(), ERROR_API,
"Invoice upload failed: " + response.statusCode() + " - " + response.body());
}
// ---------- 8) Parse response and extract reference number ----------
try (Jsonb jsonb = JsonbBuilder.create()) {
// Annahme: response.body() ist ein String mit JSON-Inhalt
JsonObject jsonObject = jsonb.fromJson(response.body(), JsonObject.class);
referenceNumber = jsonObject.getString("referenceNumber");
logger.info("│ ├── Reference Number: " + referenceNumber);
logger.info("├── ✅ Invoice upload successful");
if (kseFAuthManager.isDebug()) {
logger.info("│ ├── referenceNumber: " + referenceNumber);
}
} catch (Exception e) {
logger.severe("├── ⚠️ Error parsing JSON response: " + e.getMessage());
throw new PluginException(KSeFAPIService.class.getSimpleName(), ERROR_API,
logger.severe("├── ⚠️ Error parsing JSON response" + e.getMessage());
throw new PluginException(KSeFAuthManager.class.getSimpleName(), ERROR_API,
"Error parsing JSON response: " + e.getMessage());
}
} catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException
| IllegalBlockSizeException | BadPaddingException | InvalidAlgorithmParameterException e) {
logger.severe("├── ⚠️ Encryption error: " + e.getMessage());
throw new PluginException(KSeFAPIService.class.getSimpleName(), ERROR_API,
"Encryption error during invoice upload: " + e.getMessage());
} catch (IOException | InterruptedException e) {
logger.severe("├── ⚠️ HTTP request error: " + e.getMessage());
throw new PluginException(KSeFAPIService.class.getSimpleName(), ERROR_API,
"HTTP error during invoice upload: " + e.getMessage());
if (response.statusCode() == 202) {
logger.info("├── ✅ Upload successful ");
} else {
logger.info("├── ⚠️ Upload failed! ");
}
} catch (NoSuchAlgorithmException | NoSuchPaddingException | IOException | InterruptedException
| InvalidKeyException | IllegalBlockSizeException | BadPaddingException
| InvalidAlgorithmParameterException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
// finally set the referenceNumber
List<Object> textlist = new ArrayList<Object>();
textlist.add(referenceNumber);
fileData.setAttribute("ksef.referenceNumber", textlist);
return referenceNumber;
}

View file

@ -185,8 +185,9 @@ public class KSeFAuthManager {
* @throws PluginException
*/
public void openSession() throws PluginException {
logger.info("├── open new KSeF Session...");
logger.info("├── open Session...");
if (!hasValidSession()) {
logger.info("│ ├── open new interactive KSeF Session...");
// open new session
this.loadPublicKeyCertificates();
@ -195,7 +196,7 @@ public class KSeFAuthManager {
this.redeemToken();
this.openInteractiveSession();
} else {
logger.info("├── reuse existing KSeF Session...");
logger.info("├── reuse existing KSeF Session...");
}
}
@ -205,40 +206,44 @@ public class KSeFAuthManager {
* @return true if a valid session exists, false otherwise
*/
public boolean hasValidSession() {
// Check if session reference number exists
if (sessionRefNumber == null || sessionRefNumber.isEmpty()) {
return false;
}
// Check if valid until timestamp exists
if (sessionValidUntil == null || sessionValidUntil.isEmpty()) {
return false;
}
// // Check if session reference number exists
// if (sessionRefNumber == null || sessionRefNumber.isEmpty()) {
// return false;
// }
try {
// Parse the validUntil timestamp (ISO 8601 format with UTC)
DateTimeFormatter formatter = DateTimeFormatter.ISO_OFFSET_DATE_TIME;
Instant validUntilInstant = Instant.from(formatter.parse(sessionValidUntil));
// // Check if valid until timestamp exists
// if (sessionValidUntil == null || sessionValidUntil.isEmpty()) {
// return false;
// }
// Get current time in UTC
Instant now = Instant.now();
// try {
// // Parse the validUntil timestamp (ISO 8601 format with UTC)
// DateTimeFormatter formatter = DateTimeFormatter.ISO_OFFSET_DATE_TIME;
// Instant validUntilInstant = Instant.from(formatter.parse(sessionValidUntil));
// Check if session is still valid
boolean isValid = now.isBefore(validUntilInstant);
// // Get current time in UTC
// Instant now = Instant.now();
if (debug && isValid) {
logger.info("├── Session is still valid until: " + sessionValidUntil);
} else if (debug) {
logger.info("├── Session expired at: " + sessionValidUntil);
}
// // Check if session is still valid
// boolean isValid = now.isBefore(validUntilInstant);
return isValid;
// if (debug && isValid) {
// logger.info("├── Session is still valid until: " + sessionValidUntil);
// } else if (debug) {
// logger.info("├── Session expired at: " + sessionValidUntil);
// }
} catch (Exception e) {
// If parsing fails, assume session is invalid
logger.warning("├── ⚠️ Error parsing sessionValidUntil timestamp: " + e.getMessage());
return false;
}
// return isValid;
// } catch (Exception e) {
// // If parsing fails, assume session is invalid
// logger.warning("├── ⚠️ Error parsing sessionValidUntil timestamp: " +
// e.getMessage());
// return false;
// }
}
/**
@ -447,7 +452,7 @@ public class KSeFAuthManager {
JsonObject jsonObject = jsonb.fromJson(response.body(), JsonObject.class);
sessionRefNumber = jsonObject.getString("referenceNumber");
sessionValidUntil = jsonObject.getString("validUntil");
if (debug) {
if (true) {
logger.info("│ ├── Session ReferenceNumber = " + sessionRefNumber);
logger.info("│ └── Session ValidUntil = " + sessionValidUntil);
}

View file

@ -1,5 +1,6 @@
package com.alexanderlogistics.ksef;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
@ -37,13 +38,20 @@ public class KSeFAPIServiceTest {
manager.setKsefToken(Params.TOKEN);
manager.setKsefNip(Params.NIP);
manager.setKsefEndpoint("https://ksef-test.mf.gov.pl/api/v2");
manager.setDebug(false);
manager.setDebug(true);
// init() ausführen
manager.init();
// Open Session
manager.openSession();
// manager.loadPublicKeyCertificates();
// manager.authChallenge();
// manager.authKSeFToken();
// manager.redeemToken();
// manager.openInteractiveSession();
// Create API Service and inject manager
apiService = new KSeFAPIService();
apiService.kseFAuthManager = manager;
@ -66,6 +74,10 @@ public class KSeFAPIServiceTest {
assertFalse(referenceNumber.isEmpty(), "Reference number should not be empty");
logger.info("✅ Upload successful - Reference Number: " + referenceNumber);
FileData file = workitem.getFileData("example-invoice-01.xml");
ItemCollection fileAttributes = new ItemCollection(file.getAttributes());
assertEquals(referenceNumber, fileAttributes.getItemValueString("ksef.referenceNumber"));
}
private ItemCollection createWorkitem() throws IOException {

View file

@ -0,0 +1,64 @@
package com.alexanderlogistics.ksef;
import java.util.logging.Logger;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.MethodOrderer;
import org.junit.jupiter.api.TestMethodOrder;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.junit.jupiter.MockitoExtension;
import pl.akmf.ksef.sdk.api.services.DefaultCryptographyService;
import pl.akmf.ksef.sdk.client.interfaces.KSeFClient;
/**
* Dieser Test testet den offiziellen KSeF Client
*
* <dependency>
* <groupId>pl.akmf.ksef-sdk</groupId>
* <artifactId>ksef-client</artifactId>
* <version>3.0.4</version>
* <scope>test</scope>
* </dependency>
*/
@ExtendWith(MockitoExtension.class)
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
public class KSeFClientTest {
private static Logger logger = Logger.getLogger(KSeFClientTest.class.getName());
private DefaultCryptographyService cryptographyService;
private KSeFClient ksefClient;
private String accessToken;
@BeforeEach
void setup() throws Exception {
// Simply instantiate as POJOs
// cryptographyService = new DefaultCryptographyService();
}
// ------------------------------------------------------------
// 1) Challenge abholen
// ------------------------------------------------------------
// @Test
// @Order(1)
// @DisplayName("POST /api/v2/auth/token/redeem")
// void testRedeemToken() throws Exception {
// EncryptionData encryptionData = cryptographyService.getEncryptionData();
// OpenOnlineSessionRequest request = new OpenOnlineSessionRequestBuilder()
// .withFormCode(new FormCode(SystemCode.FA_3, SchemaVersion.VERSION_1_0E,
// SessionValue.FA))
// .withEncryptionInfo(encryptionData.encryptionInfo())
// .build();
// OpenOnlineSessionResponse openOnlineSessionResponse =
// ksefClient.openOnlineSession(request,
// accessToken);
// logger.info("OK");
// }
}

View file

@ -22,7 +22,7 @@
</Adres>
<DaneKontaktowe>
<Email>Lucja17@example.net</Email>
<Telefon>86-588-03-553</Telefon>
<Telefon>86-588-03-5531</Telefon>
</DaneKontaktowe>
</Podmiot1>
<Podmiot2>
@ -92,7 +92,7 @@
<FaWiersz>
<NrWierszaFa>3</NrWierszaFa>
<UU_ID>8647a6b5-f12a-3817-869e-010e4353686a</UU_ID>
<P_7>Incredible Steel Salad</P_7>
<P_7>Incredible Steel Salad blaasdff</P_7>
<P_8A>szt.</P_8A>
<P_8B>8</P_8B>
<P_9A>11.13</P_9A>