neuer Test Code - bessere Key Verwaltung

This commit is contained in:
Ralph Soika 2025-11-16 15:31:28 +01:00
parent 1bce7db64e
commit 6c4e72fe82
6 changed files with 555 additions and 413 deletions

View file

@ -1,22 +1,16 @@
package com.alexanderlogistics.ksef.api;
import java.io.IOException;
import java.net.URI;
import java.net.http.HttpRequest;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.net.http.HttpResponse;
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;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
@ -40,183 +34,193 @@ import jakarta.json.bind.JsonbBuilder;
*
*/
@DeclareRoles({ "org.imixs.ACCESSLEVEL.NOACCESS", "org.imixs.ACCESSLEVEL.READERACCESS",
"org.imixs.ACCESSLEVEL.AUTHORACCESS", "org.imixs.ACCESSLEVEL.EDITORACCESS",
"org.imixs.ACCESSLEVEL.MANAGERACCESS" })
"org.imixs.ACCESSLEVEL.AUTHORACCESS", "org.imixs.ACCESSLEVEL.EDITORACCESS",
"org.imixs.ACCESSLEVEL.MANAGERACCESS" })
@RolesAllowed({ "org.imixs.ACCESSLEVEL.NOACCESS", "org.imixs.ACCESSLEVEL.READERACCESS",
"org.imixs.ACCESSLEVEL.AUTHORACCESS", "org.imixs.ACCESSLEVEL.EDITORACCESS",
"org.imixs.ACCESSLEVEL.MANAGERACCESS" })
"org.imixs.ACCESSLEVEL.AUTHORACCESS", "org.imixs.ACCESSLEVEL.EDITORACCESS",
"org.imixs.ACCESSLEVEL.MANAGERACCESS" })
@Singleton
@RunAs("org.imixs.ACCESSLEVEL.MANAGERACCESS")
public class KSeFAPIService {
private static Logger logger = Logger.getLogger(KSeFAPIService.class.getName());
private static Logger logger = Logger.getLogger(KSeFAPIService.class.getName());
public static final String ERROR_API = "API_ERROR";
public static final String ERROR_API = "API_ERROR";
@Inject
KSeFAuthManager kseFAuthManager;
@Inject
KSeFAuthManager kseFAuthManager;
@Inject
DocumentService documentService;
@Inject
DocumentService documentService;
/**
* 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 companyName
* @param contactType
* @param accessToken
* @return
* @throws PluginException
*/
public String uploadInvoice(ItemCollection workitem, String fileName)
throws PluginException {
String referenceNumber = null;
logger.info("├── 📤 Upload Invoice...");
/**
* This method uploads a KSeF Invoice document (XML).
* The response of the upload is a 'referenceNumber' which is stored into the
* item ksef.referenceNumber
*
* @param workitem workitem containing the invoice
* @param fileName name of the XML file to upload
* @return referenceNumber from KSeF
* @throws PluginException
*/
public String uploadInvoice(ItemCollection workitem, String fileName) throws PluginException {
// First open an interactive Session. The KSeFAuthManager automatically reuses
// an existing session
// First open an interactive session (reuses existing if valid)
kseFAuthManager.openSession();
logger.info("├── 📤 Upload Invoice...");
kseFAuthManager.openSession();
if (kseFAuthManager.getAccessToken() == null) {
throw new PluginException(KSeFAuthManager.class.getSimpleName(), ERROR_API,
"API Error - missing AccessToken!");
}
if (kseFAuthManager.getSessionRefNumber() == null) {
throw new PluginException(KSeFAuthManager.class.getSimpleName(), ERROR_API,
"API Error - missing SessionRefNumber!");
}
// load invoice
FileData fileData = workitem.getFileData(fileName);
byte[] invoiceXml = fileData.getContent();
logger.info("│ ├── XML Invoice loaded - " + invoiceXml.length + " bytes");
try {
// ---------- 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);
// ---------- 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 = 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 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 = kseFAuthManager.getBaseURI() + "/sessions/online/"
+ kseFAuthManager.getSessionRefNumber()
+ "/invoices";
logger.info("│ ├── Endpoint: " + uri);
if (kseFAuthManager.isDebug()) {
logger.info("Request: " + jsonBody);
}
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(uri))
.header("Content-Type", "application/json")
.header("Authorization", "Bearer " + kseFAuthManager.getAccessToken())
.POST(HttpRequest.BodyPublishers.ofString(jsonBody))
.build();
// ---------- 9) Call ausführen ----------
var response = kseFAuthManager.getHttpClient().send(request,
java.net.http.HttpResponse.BodyHandlers.ofString());
System.out.println("Upload Response:");
System.out.println(response.statusCode());
System.out.println(response.body());
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");
if (kseFAuthManager.isDebug()) {
logger.info("│ ├── referenceNumber: " + referenceNumber);
}
} 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());
}
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);
kseFAuthManager.closeSession();
kseFAuthManager.deleteCurrentSession();
return referenceNumber;
// Validate session
if (kseFAuthManager.getAccessToken() == null) {
throw new PluginException(KSeFAuthManager.class.getSimpleName(), ERROR_API,
"API Error - missing AccessToken!");
}
if (kseFAuthManager.getSessionRefNumber() == null) {
throw new PluginException(KSeFAuthManager.class.getSimpleName(), ERROR_API,
"API Error - missing SessionRefNumber!");
}
if (kseFAuthManager.getSessionEncryption() == null) {
throw new PluginException(KSeFAuthManager.class.getSimpleName(), ERROR_API,
"API Error - missing Session Encryption!");
}
// Load invoice XML
FileData fileData = workitem.getFileData(fileName);
byte[] invoiceXml = fileData.getContent();
logger.info("│ ├── XML Invoice loaded - " + invoiceXml.length + " bytes");
try {
// Get the session encryption keys (already sent to KSeF during session
// creation!)
SessionEncryption encryption = kseFAuthManager.getSessionEncryption();
// Encrypt invoice with session keys
byte[] encryptedInvoice = encryptInvoiceWithSessionKeys(invoiceXml, encryption);
// Calculate hashes
String invoiceHash = calculateSHA256Hash(invoiceXml);
String encryptedInvoiceHash = calculateSHA256Hash(encryptedInvoice);
// Build JSON request body
String jsonBody = String.format(
"{" +
"\"invoiceHash\": \"%s\"," +
"\"invoiceSize\": %d," +
"\"encryptedInvoiceHash\": \"%s\"," +
"\"encryptedInvoiceSize\": %d," +
"\"encryptedInvoiceContent\": \"%s\"," +
"\"offlineMode\": false" +
"}",
invoiceHash,
invoiceXml.length,
encryptedInvoiceHash,
encryptedInvoice.length,
Base64.getEncoder().encodeToString(encryptedInvoice));
// Send upload request
String uri = kseFAuthManager.getBaseURI() + "/sessions/online/"
+ kseFAuthManager.getSessionRefNumber() + "/invoices";
logger.info("│ ├── POST: " + uri);
if (kseFAuthManager.isDebug()) {
logger.info("│ ├── Request Body: " + jsonBody);
}
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(uri))
.header("Content-Type", "application/json")
.header("Authorization", "Bearer " + kseFAuthManager.getAccessToken())
.POST(HttpRequest.BodyPublishers.ofString(jsonBody))
.build();
HttpResponse<String> response = kseFAuthManager.getHttpClient().send(
request, HttpResponse.BodyHandlers.ofString());
logger.info("│ ├── HTTP Response: " + response.statusCode());
// if (kseFAuthManager.isDebug()) {
logger.info("│ ├── Response Body: " + response.body());
// }
if (response.statusCode() != 202) {
throw new PluginException(KSeFAuthManager.class.getSimpleName(), ERROR_API,
"Invoice upload failed with status: " + response.statusCode() +
" - " + response.body());
}
// Parse response and get reference number
String referenceNumber = null;
try (Jsonb jsonb = JsonbBuilder.create()) {
JsonObject jsonObject = jsonb.fromJson(response.body(), JsonObject.class);
referenceNumber = jsonObject.getString("referenceNumber");
logger.info("│ ├── Reference Number: " + referenceNumber);
} catch (Exception e) {
throw new PluginException(KSeFAuthManager.class.getSimpleName(), ERROR_API,
"Error parsing upload response: " + e.getMessage());
}
// Store reference number in file metadata
List<Object> textlist = new ArrayList<>();
textlist.add(referenceNumber);
fileData.setAttribute("ksef.referenceNumber", textlist);
logger.info("├── ✅ Upload successful - Reference: " + referenceNumber);
return referenceNumber;
} catch (Exception e) {
logger.severe("├── ⚠️ Invoice upload failed: " + e.getMessage());
throw new PluginException(KSeFAuthManager.class.getSimpleName(), ERROR_API,
"Invoice upload failed: " + e.getMessage());
} finally {
kseFAuthManager.closeSession();
kseFAuthManager.deleteCurrentSession();
}
}
/**
* Encrypt invoice XML with the session encryption keys
*
* @param invoiceXml the invoice XML content
* @param encryption the session encryption containing AES key and IV
* @return encrypted invoice bytes
* @throws Exception if encryption fails
*/
private byte[] encryptInvoiceWithSessionKeys(byte[] invoiceXml, SessionEncryption encryption)
throws Exception {
logger.info("│ ├── Encrypting invoice with session keys...");
// Use the SAME keys that were sent during session creation!
SecretKeySpec aesKey = new SecretKeySpec(encryption.getAesKeyBytes(), "AES");
IvParameterSpec iv = new IvParameterSpec(encryption.getInitializationVector());
// Encrypt with AES-256-CBC
Cipher aesCipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
aesCipher.init(Cipher.ENCRYPT_MODE, aesKey, iv);
byte[] encryptedData = aesCipher.doFinal(invoiceXml);
logger.info("│ ├── Invoice encrypted - " + encryptedData.length + " bytes");
return encryptedData;
}
/**
* Calculate SHA-256 hash of data
*
* @param data byte array to hash
* @return Base64-encoded hash
* @throws NoSuchAlgorithmException if SHA-256 is not available
*/
private String calculateSHA256Hash(byte[] data) throws NoSuchAlgorithmException {
MessageDigest sha256 = MessageDigest.getInstance("SHA-256");
byte[] hashBytes = sha256.digest(data);
return Base64.getEncoder().encodeToString(hashBytes);
}
}

View file

@ -10,7 +10,6 @@ import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.PublicKey;
import java.security.SecureRandom;
import java.security.spec.MGF1ParameterSpec;
import java.time.Duration;
import java.time.Instant;
@ -83,10 +82,10 @@ public class KSeFAuthManager {
private String baseURI = "";
// public keys
private String symmetricCertBase64;
private String ksefTokenCertBase64;
private PublicKey symmetricPublicKey;
private PublicKey ksefTokenPublicKey;
private String sessionEncryptionCertBase64;
private String tokenEncryptionCertBase64;
private PublicKey sessionEncryptionPublicKey;
private PublicKey tokenEncryptionPublicKey;
// Auth information
private String challenge = "";
@ -95,11 +94,10 @@ public class KSeFAuthManager {
private String challengeTimestamp = null;
// Session information
private SessionEncryption encryption = null;
private String accessToken = null;
private String sessionRefNumber;
private String sessionValidUntil;
private byte[] sessionAesKey;
private byte[] sessionIv;
@PostConstruct
void init() {
@ -142,6 +140,10 @@ public class KSeFAuthManager {
return accessToken;
}
public SessionEncryption getSessionEncryption() {
return encryption;
}
public void setKsefNip(String nip) {
this.ksefNip = Optional.ofNullable(nip);
}
@ -170,20 +172,8 @@ public class KSeFAuthManager {
return authToken;
}
public PublicKey getSymmetricPublicKey() {
return symmetricPublicKey;
}
public String getChallengeValue() {
return challenge;
}
public byte[] getSessionAesKey() {
return sessionAesKey;
}
public byte[] getSessionIv() {
return sessionIv;
public PublicKey getSessionEncryptionPublicKey() {
return sessionEncryptionPublicKey;
}
/**
@ -200,12 +190,12 @@ public class KSeFAuthManager {
this.loadPublicKeyCertificates();
this.authChallenge();
this.authKSeFToken();
this.waitSomeTime(3000);
// this.waitSomeTime(3000);
// Jetzt Status abwarten
this.waitForAuthStatus();
waitSomeTime(5000);
// waitSomeTime(5000);
this.redeemToken();
this.openInteractiveSession();
@ -308,30 +298,30 @@ public class KSeFAuthManager {
var usages = entry.getJsonArray("usage");
if (usages.stream().anyMatch(v -> v.toString().contains("KsefTokenEncryption"))) {
ksefTokenCertBase64 = entry.getString("certificate");
tokenEncryptionCertBase64 = entry.getString("certificate");
if (debug) {
logger.info("│ ├── ✓ KsefTokenEncryption certificate found.");
} // Public Key extrahieren
ksefTokenPublicKey = extractPublicKeyFromCertificate(ksefTokenCertBase64);
tokenEncryptionPublicKey = extractPublicKeyFromCertificate(tokenEncryptionCertBase64);
logger.info("│ ├── ✓ RSA PublicKey successfully extracted.");
}
if (usages.stream().anyMatch(v -> v.toString().contains("SymmetricKeyEncryption"))) {
symmetricCertBase64 = entry.getString("certificate");
sessionEncryptionCertBase64 = entry.getString("certificate");
if (debug) {
logger.info("│ ├── ✓ SymmetricKeyEncryption certificate found.");
}
// Public Key extrahieren
symmetricPublicKey = extractPublicKeyFromCertificate(symmetricCertBase64);
sessionEncryptionPublicKey = extractPublicKeyFromCertificate(sessionEncryptionCertBase64);
logger.info("│ ├── ✓ Symetric PublicKey successfully extracted.");
}
}
if (ksefTokenPublicKey == null) {
if (tokenEncryptionPublicKey == null) {
logger.warning("│ ├── ⚠️ No certificate with usage 'ksefTokenPublicKey' found!");
}
if (symmetricPublicKey == null) {
if (sessionEncryptionPublicKey == null) {
logger.warning("│ ├── ⚠️ No certificate with usage 'symmetricPublicKey' found!");
}
} catch (Exception e) {
@ -595,27 +585,16 @@ public class KSeFAuthManager {
String uri = baseURI + "/sessions/online";
logger.info("│ ├── POST: " + uri);
HttpResponse<String> response = null;
try {
// --- 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;
rsa = Cipher.getInstance("RSA/ECB/OAEPWithSHA-1AndMGF1Padding");
rsa.init(Cipher.ENCRYPT_MODE, this.symmetricPublicKey);
byte[] encryptedAesKey = rsa.doFinal(aesKeyBytes);
// Generate encryption data using helper
encryption = KSeFEncryptionHelper
.createSessionEncryption(this.sessionEncryptionPublicKey);
// Build JSON payload
String jsonPayload = String.format(
"{" +
"\"formCode\": {" +
" \"systemCode\": \"FA (3)\"," +
" \"systemCode\": \"FA (2)\"," +
" \"schemaVersion\": \"1-0E\"," +
" \"value\": \"FA\"" +
"}," +
@ -624,28 +603,27 @@ public class KSeFAuthManager {
" \"initializationVector\": \"%s\"" +
"}" +
"}",
Base64.getEncoder().encodeToString(encryptedAesKey),
Base64.getEncoder().encodeToString(ivBytes));
encryption.getEncryptedAesKeyBase64(), encryption.getIvBase64());
if (debug) {
logger.info("│ ├── Payload: " + jsonPayload);
}
// Send HTTP request
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();
response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
HttpResponse<String> response = httpClient.send(request,
HttpResponse.BodyHandlers.ofString());
logger.info("│ ├── HTTP Response: " + response.statusCode());
logger.info("│ ├── HTTP Response: " + response.statusCode());
if (debug) {
logger.info("Response: " + response.body());
logger.info("│ ├── Response Body: " + response.body());
}
if (response.statusCode() != 201) {
@ -653,41 +631,31 @@ public class KSeFAuthManager {
"Failed to open interactive session: " + response.statusCode());
}
// parse JSON
// Parse and store session data
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 (true) {
logger.info("│ ├── Session ReferenceNumber = " + sessionRefNumber);
logger.info("│ └── Session ValidUntil = " + sessionValidUntil);
}
// IMPORTANT: Store the AES key - this key is mandatory to upload a invoice
this.sessionAesKey = aesKeyBytes;
this.sessionIv = ivBytes;
logger.info("│ ├── Session ReferenceNumber = " + sessionRefNumber);
logger.info("│ └── Session ValidUntil = " + sessionValidUntil);
} catch (Exception e) {
logger.severe("├── ⚠️ Error parsing JSON response" + e.getMessage());
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 | IOException | InterruptedException
| InvalidKeyException | IllegalBlockSizeException | BadPaddingException e) {
// WICHTIG: Warte kurz, damit die Session vollständig initialisiert wird
// logger.info("│ ├── Waiting for session to be ready...");
// waitSomeTime(30000); // 2 Sekunden warten
} catch (Exception e) {
logger.severe("├── ⚠️ API Error: " + e.getMessage());
throw new PluginException(KSeFAuthManager.class.getSimpleName(), ERROR_API,
"API Error openInteractiveSession: " + e.getMessage());
}
// logger.info("│ ├── Sleep 5 sec...");
// try {
// Thread.sleep(5000);
// } catch (InterruptedException e) {
// Thread.currentThread().interrupt();
// }
}
/**
@ -704,7 +672,7 @@ public class KSeFAuthManager {
*/
public String getEncryptedToken() throws PluginException {
String result = null;
if (ksefTokenPublicKey == null) {
if (tokenEncryptionPublicKey == null) {
throw new IllegalStateException("Public Key not loaded! Call loadPublicKeyCertificates() first.");
}
@ -737,7 +705,7 @@ public class KSeFAuthManager {
"MGF1",
MGF1ParameterSpec.SHA256,
PSource.PSpecified.DEFAULT);
cipher.init(Cipher.ENCRYPT_MODE, ksefTokenPublicKey, oaepParams);
cipher.init(Cipher.ENCRYPT_MODE, tokenEncryptionPublicKey, oaepParams);
byte[] encrypted = cipher.doFinal(plaintext.getBytes(StandardCharsets.UTF_8));
@ -766,6 +734,7 @@ public class KSeFAuthManager {
* This method verifies the actual status of the authentication process for the
* current auth token (this is the token received from the start of the auth
* procedure)
* The method returns the aut status code (not the HTTP Response code!)
*
* https://ksef-test.mf.gov.pl/docs/v2/index.html#tag/Uzyskiwanie-dostepu/paths/~1api~1v2~1auth~1%7BreferenceNumber%7D/get
*
@ -776,7 +745,7 @@ public class KSeFAuthManager {
logger.info("├── ↩️ KSeF API check auth status...");
String uri = baseURI + "/auth/" + authRefNumber;
int httpResponse = -1;
int statusCode = -1;
logger.info("│ ├── GET: " + uri);
HttpResponse<String> response = null;
try {
@ -800,8 +769,19 @@ public class KSeFAuthManager {
try (Jsonb jsonb = JsonbBuilder.create()) {
JsonObject jsonObject = jsonb.fromJson(response.body(), JsonObject.class);
// Status Code aus dem status-Objekt extrahieren
if (jsonObject.containsKey("status")) {
JsonObject statusObject = jsonObject.getJsonObject("status");
if (statusObject.containsKey("code")) {
statusCode = statusObject.getInt("code");
logger.info("│ ├── Status Code: " + statusCode);
logger.info("│ ├── Status Description: " + statusObject.getString("description"));
}
}
Boolean isRedeemed = jsonObject.containsKey("isTokenRedeemed") ? jsonObject.getBoolean("isTokenRedeemed")
: null;
: false;
String lastTokenRefreshDate = jsonObject.containsKey("lastTokenRefreshDate")
? jsonObject.getString("lastTokenRefreshDate")
@ -824,7 +804,7 @@ public class KSeFAuthManager {
"Error parsing JSON response: " + e.getMessage());
}
return httpResponse;
return statusCode;
}
/**

View file

@ -0,0 +1,129 @@
package com.alexanderlogistics.ksef.api;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.PublicKey;
import java.security.SecureRandom;
import java.util.Base64;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.KeyGenerator;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
/**
* Helper class for KSeF encryption operations.
* Handles AES-256 symmetric key generation and RSA encryption.
*/
public class KSeFEncryptionHelper {
private static final String AES_ALGORITHM = "AES";
private static final String RSA_TRANSFORMATION = "RSA/ECB/OAEPWithSHA-1AndMGF1Padding";
private static final int AES_KEY_SIZE = 256;
private static final int IV_SIZE = 16;
/**
* Generate a new AES-256 symmetric key for session encryption
*
* @return SecretKey instance containing the generated AES-256 key
* @throws NoSuchAlgorithmException if AES algorithm is not available
*/
public static SecretKey generateAesKey() throws NoSuchAlgorithmException {
KeyGenerator keyGen = KeyGenerator.getInstance(AES_ALGORITHM);
keyGen.init(AES_KEY_SIZE, new SecureRandom());
return keyGen.generateKey();
}
/**
* Generate a random initialization vector for AES encryption
*
* @return byte array containing 16 random bytes
*/
public static byte[] generateInitializationVector() {
byte[] iv = new byte[IV_SIZE];
new SecureRandom().nextBytes(iv);
return iv;
}
/**
* Encrypt a symmetric AES key with KSeF's RSA public key
*
* @param aesKey the AES symmetric key to encrypt
* @param ksefPublicKey KSeF's RSA public key
* @return Base64-encoded encrypted AES key
* @throws NoSuchAlgorithmException if RSA algorithm is not available
* @throws NoSuchPaddingException if padding scheme is not available
* @throws InvalidKeyException if the public key is invalid
* @throws IllegalBlockSizeException if the key size is invalid
* @throws BadPaddingException if padding is incorrect
*/
public static String encryptAesKey(SecretKey aesKey, PublicKey ksefPublicKey)
throws NoSuchAlgorithmException, NoSuchPaddingException,
InvalidKeyException, IllegalBlockSizeException, BadPaddingException {
Cipher rsaCipher = Cipher.getInstance(RSA_TRANSFORMATION);
rsaCipher.init(Cipher.ENCRYPT_MODE, ksefPublicKey);
byte[] encryptedKey = rsaCipher.doFinal(aesKey.getEncoded());
return Base64.getEncoder().encodeToString(encryptedKey);
}
/**
* Encode byte array to Base64 string
*
* @param data byte array to encode
* @return Base64-encoded string
*/
public static String toBase64(byte[] data) {
return Base64.getEncoder().encodeToString(data);
}
/**
* Decode Base64 string to byte array
*
* @param base64 Base64-encoded string
* @return decoded byte array
*/
public static byte[] fromBase64(String base64) {
return Base64.getDecoder().decode(base64);
}
/**
* Recreate SecretKey from raw bytes (useful when loading from storage)
*
* @param keyBytes raw AES key bytes
* @return SecretKey instance
*/
public static SecretKey bytesToAesKey(byte[] keyBytes) {
return new SecretKeySpec(keyBytes, AES_ALGORITHM);
}
/**
* Generate complete session encryption data
* Creates AES key, IV, and encrypts the AES key with KSeF's public key
*
* @param ksefPublicKey KSeF's RSA public key
* @return SessionEncryption containing all encryption data
* @throws NoSuchAlgorithmException if algorithm is not available
* @throws NoSuchPaddingException if padding scheme is not available
* @throws InvalidKeyException if the public key is invalid
* @throws IllegalBlockSizeException if the key size is invalid
* @throws BadPaddingException if padding is incorrect
*/
public static SessionEncryption createSessionEncryption(PublicKey ksefPublicKey)
throws NoSuchAlgorithmException, NoSuchPaddingException,
InvalidKeyException, IllegalBlockSizeException, BadPaddingException {
// Generate AES key and IV
SecretKey aesKey = generateAesKey();
byte[] iv = generateInitializationVector();
// Encrypt AES key with KSeF's public key
String encryptedAesKey = encryptAesKey(aesKey, ksefPublicKey);
return new SessionEncryption(aesKey, iv, encryptedAesKey);
}
}

View file

@ -0,0 +1,40 @@
package com.alexanderlogistics.ksef.api;
import java.util.Base64;
import javax.crypto.SecretKey;
/**
* Container class for session encryption data
*/
public class SessionEncryption {
private final SecretKey aesKey;
private final byte[] initializationVector;
private final String encryptedAesKeyBase64;
public SessionEncryption(SecretKey aesKey, byte[] iv, String encryptedAesKeyBase64) {
this.aesKey = aesKey;
this.initializationVector = iv;
this.encryptedAesKeyBase64 = encryptedAesKeyBase64;
}
public SecretKey getAesKey() {
return aesKey;
}
public byte[] getAesKeyBytes() {
return aesKey.getEncoded();
}
public byte[] getInitializationVector() {
return initializationVector;
}
public String getIvBase64() {
return Base64.getEncoder().encodeToString(initializationVector);
}
public String getEncryptedAesKeyBase64() {
return encryptedAesKeyBase64;
}
}

View file

@ -14,18 +14,13 @@ import org.imixs.workflow.FileData;
import org.imixs.workflow.ItemCollection;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.MethodOrderer;
import org.junit.jupiter.api.Order;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestMethodOrder;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.junit.jupiter.MockitoExtension;
import com.alexanderlogistics.TestLoggerConfig;
//@Disabled
@ExtendWith(MockitoExtension.class)
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
//@ExtendWith(MockitoExtension.class)
// @TestMethodOrder(MethodOrderer.OrderAnnotation.class)
public class KSeFAPIServiceTest {
private static Logger logger = Logger.getLogger(KSeFAPIServiceTest.class.getName());
@ -49,16 +44,6 @@ public class KSeFAPIServiceTest {
// init() ausführen
manager.init();
// Open Session
// manager.openSession();
// manager.checkTokenStatus();
// manager.loadPublicKeyCertificates();
// manager.authChallenge();
// manager.authKSeFToken();
// manager.redeemToken();
// manager.openInteractiveSession();
// Create API Service and inject manager
apiService = new KSeFAPIService();
apiService.kseFAuthManager = manager;
@ -66,7 +51,6 @@ public class KSeFAPIServiceTest {
}
@Test
@Order(4)
@DisplayName("Upload Invoice XML (AES-256 + RSA)")
public void testUploadInvoice() throws Exception {
logger.info("==> Test: Upload Invoice XML");
@ -74,6 +58,7 @@ public class KSeFAPIServiceTest {
ItemCollection workitem = createWorkitem();
// Execute upload
String referenceNumber = apiService.uploadInvoice(workitem, "example-invoice-01.xml");
// Verify result

View file

@ -10,7 +10,6 @@ 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;
@ -32,154 +31,159 @@ import com.alexanderlogistics.TestLoggerConfig;
@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 {
TestLoggerConfig.setupTestLogger();
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();
}
// ------------------------------------------------------------
// 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);
// init() ausführen
manager.init();
}
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());
// ------------------------------------------------------------
// 1) Challenge abholen
// ------------------------------------------------------------
@Test
@Order(1)
@DisplayName("POST /api/v2/auth/token/redeem")
void testRedeemToken() throws Exception {
System.out.println("Upload Response:");
System.out.println(response.statusCode());
System.out.println(response.body());
manager.loadPublicKeyCertificates();
manager.authChallenge();
manager.authKSeFToken();
manager.redeemToken();
// ---------- 10) Basic Assertions ----------
assertTrue(response.statusCode() == 202,
"Upload muss erfolgreich sein");
}
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);
// Statt neue Keys zu generieren:
SessionEncryption encryption = manager.getSessionEncryption();
byte[] aesKeyBytes = encryption.getAesKeyBytes();
byte[] ivBytes = encryption.getInitializationVector();
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.getSessionEncryptionPublicKey();
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");
}
}