229 lines
8.9 KiB
Java
229 lines
8.9 KiB
Java
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<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 {
|
|
|
|
// 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);
|
|
}
|
|
|
|
}
|