test authManager

This commit is contained in:
Ralph Soika 2025-11-14 22:04:11 +01:00
parent 5ead936783
commit 26b08eb507
3 changed files with 270 additions and 56 deletions

View file

@ -7,6 +7,7 @@ import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.security.PublicKey;
import java.security.SecureRandom;
import java.security.spec.MGF1ParameterSpec;
import java.time.Duration;
import java.time.Instant;
@ -74,11 +75,17 @@ public class KSeFAuthManager {
private String challenge = "";
private String refNumber = null;
private String authToken = null;
private String accessToken = null;
private String challengeTimestamp = null;
private String symmetricCertBase64;
private String ksefTokenCertBase64;
private PublicKey symmetricPublicKey;
private PublicKey ksefTokenPublicKey;
private String sessionRefNumber;
private String sessionValidUntil;
private byte[] sessionAesKey;
private byte[] sessionIv;
@PostConstruct
void init() {
@ -114,6 +121,10 @@ public class KSeFAuthManager {
this.ksefToken = Optional.ofNullable(token);
}
public String getAccessToken() {
return accessToken;
}
public void setKsefNip(String nip) {
this.ksefNip = Optional.ofNullable(nip);
}
@ -122,6 +133,10 @@ public class KSeFAuthManager {
this.ksefEndpoint = Optional.ofNullable(endpoint);
}
public String getSessionRefNumber() {
return sessionRefNumber;
}
public void setDebug(boolean debug) {
this.debug = debug;
}
@ -150,6 +165,10 @@ public class KSeFAuthManager {
return authToken;
}
public PublicKey getSymmetricPublicKey() {
return symmetricPublicKey;
}
public String getChallengeValue() {
return challenge;
}
@ -273,6 +292,101 @@ public class KSeFAuthManager {
}
}
/**
* Helper Method to open an interactive session
*
* @throws Exception
*/
@Lock(LockType.WRITE)
public void openInteractiveSession() throws Exception {
if (accessToken == null) {
throw new PluginException(KSeFAuthManager.class.getSimpleName(), ERROR_API,
"Error - missing access token!");
}
logger.info("├── KSeF API open interactive session...");
String uri = baseURI + "/sessions/online";
logger.info("│ ├── Endpoint: " + uri);
// --- 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 = Cipher.getInstance("RSA/ECB/OAEPWithSHA-256AndMGF1Padding");
Cipher rsa = Cipher.getInstance("RSA/ECB/OAEPWithSHA-1AndMGF1Padding");
rsa.init(Cipher.ENCRYPT_MODE, this.symmetricPublicKey);
byte[] encryptedAesKey = rsa.doFinal(aesKeyBytes);
String jsonPayload = String.format(
"{" +
"\"formCode\": {" +
" \"systemCode\": \"FA (3)\"," +
" \"schemaVersion\": \"1-0E\"," +
" \"value\": \"FA\"" +
"}," +
"\"encryption\": {" +
" \"encryptedSymmetricKey\": \"%s\"," +
" \"initializationVector\": \"%s\"" +
"}" +
"}",
Base64.getEncoder().encodeToString(encryptedAesKey),
Base64.getEncoder().encodeToString(ivBytes));
if (debug) {
logger.info("│ ├── Payload: " + jsonPayload);
}
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();
HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
logger.info("│ ├── Response Code = " + response.statusCode());
if (debug) {
logger.info("Response: " + response.body());
}
if (response.statusCode() != 201) {
throw new PluginException(KSeFAuthManager.class.getSimpleName(), ERROR_API,
"Failed to open interactive session: " + response.statusCode());
}
// JSON parsen
// Session RefNumber aus Response extrahieren und speichern
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 (debug) {
logger.info("│ ├── Session ReferenceNumber = " + sessionRefNumber);
logger.info("│ └── Session ValidUntil = " + sessionValidUntil);
}
// --- WICHTIG: AES Schlüssel speichern, wir brauchen ihn beim Upload ---
this.sessionAesKey = aesKeyBytes;
this.sessionIv = ivBytes;
} catch (Exception e) {
logger.severe("├── ⚠️ Error parsing JSON response" + e.getMessage());
throw new PluginException(KSeFAuthManager.class.getSimpleName(), ERROR_API,
"Error parsing JSON response: " + e.getMessage());
}
}
/**
* Verschlüsselt den KSeF-Token mit RSA-OAEP (SHA-256) für die Authentifizierung
*
@ -369,7 +483,7 @@ public class KSeFAuthManager {
// Access Token extrahieren und speichern
JsonObject accessTokenObj = jsonObject.getJsonObject("accessToken");
this.authToken = accessTokenObj.getString("token");
this.accessToken = accessTokenObj.getString("token");
logger.info("│ └── Access Token received");
@ -459,22 +573,6 @@ public class KSeFAuthManager {
return certificate.getPublicKey();
}
/**
* Helper Method to open an interactive session
*
* @throws Exception
*/
@Lock(LockType.WRITE)
public void openInteractiveSession() throws Exception {
logger.info("├── KSeF API open interactive session...");
String uri = baseURI + "/sessions/online";
logger.info("│ ├── Endpoint: " + uri);
// ............
}
/**
* Helper Method to analyze the auth status
*

View file

@ -1,6 +1,21 @@
package com.alexanderlogistics.ksef;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.net.URI;
import java.net.http.HttpRequest;
import java.nio.file.Files;
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;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
@ -15,6 +30,8 @@ import org.mockito.junit.jupiter.MockitoExtension;
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
public class KSeFAuthManagerTest {
private static Logger logger = Logger.getLogger(KSeFAuthManagerTest.class.getName());
private KSeFAuthManager manager;
@BeforeEach
@ -54,4 +71,106 @@ public class KSeFAuthManagerTest {
}
@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.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);
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");
}
}

File diff suppressed because one or more lines are too long