package com.alexanderlogistics.ksef.api; import java.net.URI; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.util.ArrayList; import java.util.Base64; import java.util.List; import java.util.logging.Logger; import javax.crypto.Cipher; import javax.crypto.spec.IvParameterSpec; 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.security.DeclareRoles; import jakarta.annotation.security.RolesAllowed; import jakarta.annotation.security.RunAs; import jakarta.ejb.Singleton; import jakarta.inject.Inject; import jakarta.json.JsonObject; import jakarta.json.bind.Jsonb; import jakarta.json.bind.JsonbBuilder; /** * THe KSeFAPIService provides methods to open an interactive session and upload * an invoice. * */ @DeclareRoles({ "org.imixs.ACCESSLEVEL.NOACCESS", "org.imixs.ACCESSLEVEL.READERACCESS", "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" }) @Singleton @RunAs("org.imixs.ACCESSLEVEL.MANAGERACCESS") public class KSeFAPIService { private static Logger logger = Logger.getLogger(KSeFAPIService.class.getName()); public static final String ERROR_API = "API_ERROR"; @Inject KSeFAuthManager kseFAuthManager; @Inject DocumentService documentService; /** * 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 (reuses existing if valid) kseFAuthManager.openSession(); logger.info("├── 📤 Upload Invoice..."); // 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 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 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 { // finally we close the session kseFAuthManager.closeInteractiveSession(); kseFAuthManager.deleteCurrentAuthSession(); } } /** * 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); } }