neuer Test Code - bessere Key Verwaltung
This commit is contained in:
parent
1bce7db64e
commit
6c4e72fe82
6 changed files with 555 additions and 413 deletions
|
|
@ -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;
|
||||
|
||||
|
|
@ -60,27 +54,22 @@ public class KSeFAPIService {
|
|||
DocumentService documentService;
|
||||
|
||||
/**
|
||||
* This method uploads an KSeF Invoice document (XML).
|
||||
* The response of the uplaod is a 'referenceNumber' which is stored into the
|
||||
* 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 companyName
|
||||
* @param contactType
|
||||
* @param accessToken
|
||||
* @return
|
||||
* @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 {
|
||||
String referenceNumber = null;
|
||||
public String uploadInvoice(ItemCollection workitem, String fileName) throws PluginException {
|
||||
|
||||
// First open an interactive session (reuses existing if valid)
|
||||
kseFAuthManager.openSession();
|
||||
logger.info("├── 📤 Upload Invoice...");
|
||||
|
||||
// First open an interactive Session. The KSeFAuthManager automatically reuses
|
||||
// an existing session
|
||||
|
||||
kseFAuthManager.openSession();
|
||||
|
||||
// Validate session
|
||||
if (kseFAuthManager.getAccessToken() == null) {
|
||||
throw new PluginException(KSeFAuthManager.class.getSimpleName(), ERROR_API,
|
||||
"API Error - missing AccessToken!");
|
||||
|
|
@ -89,80 +78,54 @@ public class KSeFAPIService {
|
|||
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
|
||||
// Load invoice XML
|
||||
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();
|
||||
// Get the session encryption keys (already sent to KSeF during session
|
||||
// creation!)
|
||||
SessionEncryption encryption = kseFAuthManager.getSessionEncryption();
|
||||
|
||||
byte[] aesKeyBytes = new byte[32]; // 256-bit AES key
|
||||
random.nextBytes(aesKeyBytes);
|
||||
// Encrypt invoice with session keys
|
||||
byte[] encryptedInvoice = encryptInvoiceWithSessionKeys(invoiceXml, encryption);
|
||||
|
||||
byte[] ivBytes = new byte[16]; // 128-bit IV
|
||||
random.nextBytes(ivBytes);
|
||||
// Calculate hashes
|
||||
String invoiceHash = calculateSHA256Hash(invoiceXml);
|
||||
String encryptedInvoiceHash = calculateSHA256Hash(encryptedInvoice);
|
||||
|
||||
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("{ " +
|
||||
// Build JSON request body
|
||||
String jsonBody = String.format(
|
||||
"{" +
|
||||
"\"invoiceHash\": \"%s\"," +
|
||||
"\"invoiceSize\": \"%s\", " +
|
||||
"\"invoiceSize\": %d," +
|
||||
"\"encryptedInvoiceHash\": \"%s\"," +
|
||||
"\"encryptedInvoiceSize\": \"%s\", " +
|
||||
"\"encryptedInvoiceSize\": %d," +
|
||||
"\"encryptedInvoiceContent\": \"%s\"," +
|
||||
"\"offlineMode\": false" +
|
||||
"}",
|
||||
invoiceHash,
|
||||
invoiceXml.length,
|
||||
encryptedInvoiceHash,
|
||||
encryptedInvoiceXml.length,
|
||||
Base64.getEncoder().encodeToString(encryptedInvoiceXml));
|
||||
encryptedInvoice.length,
|
||||
Base64.getEncoder().encodeToString(encryptedInvoice));
|
||||
|
||||
// ---------- 8) HTTP POST vorbereiten ----------
|
||||
// String uri = manager.getBaseURI() + "/sessions/online/" +
|
||||
// manager.getRefNumber() + "/invoices";
|
||||
// Send upload request
|
||||
String uri = kseFAuthManager.getBaseURI() + "/sessions/online/"
|
||||
+ kseFAuthManager.getSessionRefNumber()
|
||||
+ "/invoices";
|
||||
logger.info("│ ├── Endpoint: " + uri);
|
||||
+ kseFAuthManager.getSessionRefNumber() + "/invoices";
|
||||
|
||||
logger.info("│ ├── POST: " + uri);
|
||||
|
||||
if (kseFAuthManager.isDebug()) {
|
||||
logger.info("Request: " + jsonBody);
|
||||
logger.info("│ ├── Request Body: " + jsonBody);
|
||||
}
|
||||
|
||||
HttpRequest request = HttpRequest.newBuilder()
|
||||
.uri(URI.create(uri))
|
||||
.header("Content-Type", "application/json")
|
||||
|
|
@ -170,53 +133,94 @@ public class KSeFAPIService {
|
|||
.POST(HttpRequest.BodyPublishers.ofString(jsonBody))
|
||||
.build();
|
||||
|
||||
// ---------- 9) Call ausführen ----------
|
||||
var response = kseFAuthManager.getHttpClient().send(request,
|
||||
java.net.http.HttpResponse.BodyHandlers.ofString());
|
||||
HttpResponse<String> response = kseFAuthManager.getHttpClient().send(
|
||||
request, HttpResponse.BodyHandlers.ofString());
|
||||
|
||||
System.out.println("Upload Response:");
|
||||
System.out.println(response.statusCode());
|
||||
System.out.println(response.body());
|
||||
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()) {
|
||||
// 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);
|
||||
}
|
||||
logger.info("│ ├── Reference Number: " + 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());
|
||||
|
||||
"Error parsing upload 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>();
|
||||
// Store reference number in file metadata
|
||||
List<Object> textlist = new ArrayList<>();
|
||||
textlist.add(referenceNumber);
|
||||
fileData.setAttribute("ksef.referenceNumber", textlist);
|
||||
|
||||
kseFAuthManager.closeSession();
|
||||
kseFAuthManager.deleteCurrentSession();
|
||||
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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
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;
|
||||
} 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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
@ -100,13 +99,18 @@ public class KSeFAuthManagerTest {
|
|||
byte[] invoiceXml = Files.readAllBytes(xmlPath);
|
||||
|
||||
// ---------- 3) AES Key generieren ----------
|
||||
SecureRandom random = new SecureRandom();
|
||||
// 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);
|
||||
|
||||
byte[] aesKeyBytes = new byte[32]; // 256-bit AES key
|
||||
random.nextBytes(aesKeyBytes);
|
||||
|
||||
byte[] ivBytes = new byte[16]; // 128-bit IV
|
||||
random.nextBytes(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);
|
||||
|
|
@ -118,7 +122,7 @@ public class KSeFAuthManagerTest {
|
|||
byte[] encryptedInvoiceXml = aesCipher.doFinal(invoiceXml);
|
||||
|
||||
// ---------- 5) AES Key per RSA verschlüsseln ----------
|
||||
PublicKey ksefPublicKey = manager.getSymmetricPublicKey();
|
||||
PublicKey ksefPublicKey = manager.getSessionEncryptionPublicKey();
|
||||
|
||||
javax.crypto.Cipher rsaCipher = javax.crypto.Cipher
|
||||
.getInstance("RSA/ECB/OAEPWithSHA-256AndMGF1Padding");
|
||||
|
|
|
|||
Loading…
Reference in a new issue