neue impl
This commit is contained in:
parent
7fe009c23c
commit
9f6d86d328
5 changed files with 514 additions and 29 deletions
|
|
@ -1,13 +1,21 @@
|
|||
# KSeF - Converter
|
||||
|
||||
Die Idee ist es auf Basis der KSeF FA(2) XSD-Spezifikation einen Java-Konverter zu entwickeln.
|
||||
|
||||
## Github
|
||||
Die Idee ist es Rechnungen nach Erhalt aus cargosoft über die KSeF FA(2) API an die Behörde zu senden.
|
||||
|
||||
# API
|
||||
|
||||
Dokumentation: https://ksef-test.mf.gov.pl/docs/v2/index.html
|
||||
|
||||
Die Basis url ist: "https://ksef-test.mf.gov.pl/api/v2";
|
||||
|
||||
Der Ablauf für die Übermittlung ist folgender:
|
||||
|
||||
- vom Polnischen Goverment (bzw. AGL/Taxman) haben wir ein Secret bekommen (KSeFToken)
|
||||
- wir machen den call `/security/public-key-certificates` um Security Zertifikate zu erhalten
|
||||
- wir machen den call `/auth/challenge` um eine Challenge ID zu erhalten
|
||||
- wir machen den call `/auth/ksef-token` um eine ReferenceID und einen AccessToken zu erhalten
|
||||
- wir senden die Rechnung - mit dem öffentlichen Schlüssel verschlüsselt zusammen mit unserer ReferenceID und dem AccessToken an das Goverment.
|
||||
|
||||
## Generieren einer Referenznummer.
|
||||
|
||||
Man benötigt eine Referenznummer (Session ID). Dise kann man wie folgt generieren:
|
||||
|
|
@ -24,6 +32,8 @@ Details:
|
|||
|
||||
https://github.com/CIRFMF/ksef-docs/blob/main/sesja-interaktywna.md#2-wys%C5%82anie-faktury
|
||||
|
||||
Man muss die XML Datei mit dem öffentlichen Schlüssel der Behörde vor dem Versand verschlüsseln
|
||||
|
||||
### Das Rechnungsformat:
|
||||
|
||||
XML Datei
|
||||
|
|
|
|||
|
|
@ -5,11 +5,16 @@ import java.net.URI;
|
|||
import java.net.http.HttpClient;
|
||||
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.time.Duration;
|
||||
import java.util.Base64;
|
||||
import java.util.Optional;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
import javax.crypto.Cipher;
|
||||
|
||||
import org.eclipse.microprofile.config.inject.ConfigProperty;
|
||||
import org.imixs.workflow.exceptions.InvalidAccessException;
|
||||
import org.imixs.workflow.exceptions.PluginException;
|
||||
|
|
@ -65,8 +70,19 @@ public class KSeFAuthManager {
|
|||
private String challenge = "";
|
||||
private String refNumber = null;
|
||||
private String authToken = null;
|
||||
private String accessToken = null;
|
||||
private String refreshToken = null;
|
||||
private String symmetricCertBase64; // der Zertifikat-String aus der API
|
||||
private PublicKey symmetricPublicKey; // der extrahierte RSA Public Key
|
||||
private String sessionRefNumber = null;
|
||||
private String sessionValidUntil = null;
|
||||
private String ksefTokenEncryptionCertBase64 = null;
|
||||
private PublicKey ksefTokenEncryptionPublicKey;
|
||||
private String challengeTimestamp = null;
|
||||
|
||||
private byte[] sessionAesKey;
|
||||
|
||||
private byte[] sessionIv;
|
||||
|
||||
@PostConstruct
|
||||
void init() {
|
||||
|
|
@ -94,6 +110,10 @@ public class KSeFAuthManager {
|
|||
// this.jsonb = JsonbBuilder.create();
|
||||
}
|
||||
|
||||
public HttpClient getHttpClient() {
|
||||
return httpClient;
|
||||
}
|
||||
|
||||
public void setKsefToken(String token) {
|
||||
this.ksefToken = Optional.ofNullable(token);
|
||||
}
|
||||
|
|
@ -115,12 +135,13 @@ public class KSeFAuthManager {
|
|||
*
|
||||
* @return
|
||||
*/
|
||||
public String getEncodedToken() {
|
||||
// public String getEncodedToken() {
|
||||
|
||||
String result = java.util.Base64.getEncoder().encodeToString(ksefToken.orElse("").getBytes());
|
||||
// String result =
|
||||
// java.util.Base64.getEncoder().encodeToString(ksefToken.orElse("").getBytes());
|
||||
|
||||
return result;
|
||||
}
|
||||
// return result;
|
||||
// }
|
||||
|
||||
public String getBaseURI() {
|
||||
return baseURI;
|
||||
|
|
@ -182,9 +203,12 @@ public class KSeFAuthManager {
|
|||
// Annahme: response.body() ist ein String mit JSON-Inhalt
|
||||
JsonObject jsonObject = jsonb.fromJson(response.body(), JsonObject.class);
|
||||
challenge = jsonObject.getString("challenge");
|
||||
challengeTimestamp = jsonObject.getString("timestamp");
|
||||
if (debug) {
|
||||
logger.info("│ ├── challenge: " + challenge);
|
||||
logger.info("│ ├── timestamp: " + challengeTimestamp);
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
logger.severe("├── ⚠️ Error parsing JSON response" + e.getMessage());
|
||||
throw new PluginException(KSeFAuthManager.class.getSimpleName(), ERROR_API,
|
||||
|
|
@ -210,6 +234,26 @@ public class KSeFAuthManager {
|
|||
logger.info("├── KSeF API auth ksef-token...");
|
||||
String uri = baseURI + "/auth/ksef-token";
|
||||
logger.info("│ ├── Endpoint: " + uri);
|
||||
|
||||
if (ksefTokenEncryptionPublicKey == null) {
|
||||
throw new PluginException(KSeFAuthManager.class.getSimpleName(), ERROR_CONFIG,
|
||||
"Missing public key for KSeF token encryption (KsefTokenEncryption).");
|
||||
}
|
||||
|
||||
// token|timestamp (aus challenge)
|
||||
String token = ksefToken.orElseThrow(() -> new PluginException(
|
||||
KSeFAuthManager.class.getSimpleName(), ERROR_CONFIG, "Missing KSeF API token"));
|
||||
|
||||
String tokenWithTimestamp = token + "|" + challengeTimestamp;
|
||||
|
||||
logger.info("│ ├── token|timestamp = " + tokenWithTimestamp);
|
||||
|
||||
// Verschlüsseln mit RSA-OAEP SHA-256
|
||||
String encryptedToken = encryptWithPublicKey(tokenWithTimestamp, ksefTokenEncryptionPublicKey);
|
||||
|
||||
logger.info("│ ├── Encrypted token (Base64): " + encryptedToken);
|
||||
|
||||
// JSON payload zusammenbauen
|
||||
String jsonPayload = String.format(
|
||||
"{" +
|
||||
"\"challenge\": \"%s\"," +
|
||||
|
|
@ -219,7 +263,9 @@ public class KSeFAuthManager {
|
|||
"}," +
|
||||
"\"encryptedToken\": \"%s\"" +
|
||||
"}",
|
||||
challenge, ksefNip.get(), getEncodedToken());
|
||||
challenge,
|
||||
ksefNip.get(),
|
||||
encryptedToken);
|
||||
|
||||
HttpRequest request = HttpRequest.newBuilder()
|
||||
.uri(URI.create(uri))
|
||||
|
|
@ -229,55 +275,102 @@ public class KSeFAuthManager {
|
|||
.build();
|
||||
|
||||
HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
|
||||
|
||||
logger.info("│ ├── Response Code = " + response.statusCode());
|
||||
if (debug) {
|
||||
logger.info("Response: " + response.body());
|
||||
}
|
||||
|
||||
// Prüfen, dass wir eine API-Antwort bekommen
|
||||
if (response.statusCode() >= 500) {
|
||||
throw new PluginException(KSeFAuthManager.class.getSimpleName(), ERROR_CONFIG,
|
||||
"Unable to access KSeF API: response=" + response.statusCode());
|
||||
}
|
||||
|
||||
// JSON parsen und Werte speichern
|
||||
if (response.statusCode() == 200 || response.statusCode() == 202) {
|
||||
try (Jsonb jsonb = JsonbBuilder.create()) {
|
||||
JsonObject jsonObject = jsonb.fromJson(response.body(), JsonObject.class);
|
||||
|
||||
// Reference Number speichern
|
||||
refNumber = jsonObject.getString("referenceNumber");
|
||||
logger.info("Extracted referenceNumber: " + refNumber);
|
||||
|
||||
// Authentication Token speichern
|
||||
JsonObject authTokenObject = jsonObject.getJsonObject("authenticationToken");
|
||||
authToken = authTokenObject.getString("token");
|
||||
logger.info("Extracted authenticationToken: " + authToken);
|
||||
|
||||
} 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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Lock(LockType.WRITE)
|
||||
public void redeemAuthToken() throws Exception {
|
||||
logger.info("├── KSeF API redeem authentication token...");
|
||||
String uri = baseURI + "/auth/token/redeem";
|
||||
logger.info("│ ├── Endpoint: " + uri);
|
||||
|
||||
HttpRequest request = HttpRequest.newBuilder()
|
||||
.uri(URI.create(uri))
|
||||
.header("Accept", "application/json")
|
||||
.header("Content-Type", "application/json")
|
||||
.header("Authorization", "Bearer " + authToken) // authToken aus generateAuthToken()
|
||||
.POST(HttpRequest.BodyPublishers.noBody()) // Payload leer, API erwartet nur Bearer im Header
|
||||
.build();
|
||||
|
||||
HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
|
||||
logger.info("│ ├── Response Code = " + response.statusCode());
|
||||
if (debug) {
|
||||
logger.info("Response: " + response.body());
|
||||
}
|
||||
|
||||
// Prüfen auf Fehler
|
||||
if (response.statusCode() >= 500) {
|
||||
throw new PluginException(KSeFAuthManager.class.getSimpleName(), ERROR_CONFIG,
|
||||
"Unable to access KSeF API: response=" + response.statusCode());
|
||||
}
|
||||
|
||||
// JSON parsen und AccessToken + RefreshToken speichern
|
||||
if (response.statusCode() == 200 || response.statusCode() == 201) {
|
||||
try (Jsonb jsonb = JsonbBuilder.create()) {
|
||||
JsonObject jsonObject = jsonb.fromJson(response.body(), JsonObject.class);
|
||||
|
||||
JsonObject accessTokenObj = jsonObject.getJsonObject("accessToken");
|
||||
accessToken = accessTokenObj.getString("token"); // finaler Bearer für alle weiteren API-Calls
|
||||
logger.info("Extracted accessToken: " + accessToken);
|
||||
|
||||
JsonObject refreshTokenObj = jsonObject.getJsonObject("refreshToken");
|
||||
refreshToken = refreshTokenObj.getString("token"); // optional zum späteren Refresh
|
||||
if (debug) {
|
||||
logger.info("Extracted refreshToken: " + refreshToken);
|
||||
}
|
||||
|
||||
} 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());
|
||||
}
|
||||
} else {
|
||||
throw new PluginException(KSeFAuthManager.class.getSimpleName(), ERROR_API,
|
||||
"Failed to redeem authentication token: " + response.statusCode());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper Method to read the certificates
|
||||
*
|
||||
* @throws Exception
|
||||
*/
|
||||
@Lock(LockType.WRITE)
|
||||
public void getKsefPublicKeys() throws Exception {
|
||||
public void loadKsefPublicKeys() throws Exception {
|
||||
|
||||
logger.info("├── KSeF API public-key-certificates...");
|
||||
logger.info("├── KSeF API load public-key-certificates...");
|
||||
String uri = baseURI + "/security/public-key-certificates";
|
||||
logger.info("│ ├── Endpoint: " + uri);
|
||||
|
||||
HttpRequest request = HttpRequest.newBuilder()
|
||||
.uri(URI.create(uri))
|
||||
.header("Accept", "application/json")
|
||||
.header("Authorization", "Bearer " + authToken)
|
||||
.GET()
|
||||
.build();
|
||||
|
||||
|
|
@ -290,26 +383,34 @@ public class KSeFAuthManager {
|
|||
|
||||
if (response.statusCode() != 200) {
|
||||
throw new PluginException(KSeFAuthManager.class.getSimpleName(), ERROR_API,
|
||||
"Unexpected response from KSeF certificates endpoint: " + response.statusCode());
|
||||
"Unexpected response: " + response.statusCode());
|
||||
}
|
||||
|
||||
// JSON parsen
|
||||
try (Jsonb jsonb = JsonbBuilder.create()) {
|
||||
|
||||
var certArray = jsonb.fromJson(response.body(), JsonObject[].class);
|
||||
for (JsonObject entry : certArray) {
|
||||
// usage = ["KsefTokenEncryption"] oder ["SymmetricKeyEncryption"]
|
||||
// WICHTIG: JsonArray lesen – NICHT JsonObject[]
|
||||
jakarta.json.JsonArray certArray = jsonb.fromJson(response.body(), jakarta.json.JsonArray.class);
|
||||
|
||||
for (var entryValue : certArray) {
|
||||
|
||||
JsonObject entry = entryValue.asJsonObject();
|
||||
var usages = entry.getJsonArray("usage");
|
||||
if (usages.stream().anyMatch(v -> v.toString().contains("SymmetricKeyEncryption"))) {
|
||||
symmetricCertBase64 = entry.getString("certificate");
|
||||
logger.info("│ ├── SymmetricKeyEncryption certificate found.");
|
||||
// Public Key extrahieren
|
||||
symmetricPublicKey = extractPublicKeyFromCertificate(symmetricCertBase64);
|
||||
logger.info("│ ├── ✓ RSA PublicKey successfully extracted.");
|
||||
return; // gefunden – wir sind fertig
|
||||
|
||||
if (usages != null &&
|
||||
usages.stream().anyMatch(v -> v.toString().contains("KsefTokenEncryption"))) {
|
||||
|
||||
ksefTokenEncryptionCertBase64 = entry.getString("certificate");
|
||||
logger.info("│ ├── Found certificate with usage = KsefTokenEncryption");
|
||||
|
||||
ksefTokenEncryptionPublicKey = extractPublicKeyFromCertificate(ksefTokenEncryptionCertBase64);
|
||||
|
||||
logger.info("│ ├── ✓ RSA PublicKey for KSeF Token Encryption extracted.");
|
||||
return;
|
||||
}
|
||||
}
|
||||
logger.warning("│ ├── ⚠️ No certificate with usage 'SymmetricKeyEncryption' found!");
|
||||
|
||||
throw new PluginException(KSeFAuthManager.class.getSimpleName(), ERROR_API,
|
||||
"No certificate with usage = KsefTokenEncryption found");
|
||||
|
||||
} catch (Exception e) {
|
||||
throw new PluginException(KSeFAuthManager.class.getSimpleName(), ERROR_API,
|
||||
|
|
@ -317,6 +418,193 @@ public class KSeFAuthManager {
|
|||
}
|
||||
}
|
||||
|
||||
@Lock(LockType.WRITE)
|
||||
public void loadSymmetricKeyPublicKey() throws Exception {
|
||||
|
||||
logger.info("├── KSeF API load symmetric public key...");
|
||||
String uri = baseURI + "/security/public-key";
|
||||
logger.info("│ ├── Endpoint: " + uri);
|
||||
|
||||
HttpRequest request = HttpRequest.newBuilder()
|
||||
.uri(URI.create(uri))
|
||||
.header("Accept", "application/json")
|
||||
.GET()
|
||||
.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() != 200) {
|
||||
throw new PluginException(KSeFAuthManager.class.getSimpleName(), ERROR_API,
|
||||
"Unexpected response: " + response.statusCode());
|
||||
}
|
||||
|
||||
try (Jsonb jsonb = JsonbBuilder.create()) {
|
||||
|
||||
JsonObject json = jsonb.fromJson(response.body(), JsonObject.class);
|
||||
|
||||
symmetricCertBase64 = json.getString("certificate");
|
||||
logger.info("│ ├── Symmetric key certificate loaded");
|
||||
|
||||
symmetricPublicKey = extractPublicKeyFromCertificate(symmetricCertBase64);
|
||||
|
||||
logger.info("│ ├── ✓ RSA PublicKey for session encryption extracted.");
|
||||
|
||||
} catch (Exception e) {
|
||||
throw new PluginException(KSeFAuthManager.class.getSimpleName(), ERROR_API,
|
||||
"Error parsing public key JSON: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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());
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper Method to analyze the auth status
|
||||
*
|
||||
* @return
|
||||
* @throws Exception
|
||||
*/
|
||||
public JsonObject getAuthStatus() throws Exception {
|
||||
if (refNumber == null) {
|
||||
throw new IllegalStateException("Reference number is missing. Call generateAuthToken() first.");
|
||||
}
|
||||
|
||||
logger.info("├── KSeF API check authentication status...");
|
||||
|
||||
logger.info("Using public key algorithm: " + symmetricPublicKey.getAlgorithm());
|
||||
logger.info("Key format: " + symmetricPublicKey.getFormat());
|
||||
|
||||
String uri = baseURI + "/auth/" + refNumber;
|
||||
logger.info("│ ├── Endpoint: " + uri);
|
||||
|
||||
HttpRequest request = HttpRequest.newBuilder()
|
||||
.uri(URI.create(uri))
|
||||
.header("Accept", "application/json")
|
||||
.header("Authorization", "Bearer " + authToken)
|
||||
.GET()
|
||||
.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() >= 500) {
|
||||
throw new PluginException(KSeFAuthManager.class.getSimpleName(), ERROR_CONFIG,
|
||||
"Unable to access KSeF API: response=" + response.statusCode());
|
||||
}
|
||||
|
||||
try (Jsonb jsonb = JsonbBuilder.create()) {
|
||||
JsonObject jsonObject = jsonb.fromJson(response.body(), JsonObject.class);
|
||||
Boolean isRedeemed = jsonObject.containsKey("isTokenRedeemed") ? jsonObject.getBoolean("isTokenRedeemed")
|
||||
: null;
|
||||
logger.info("│ ├── isTokenRedeemed: " + isRedeemed);
|
||||
return jsonObject;
|
||||
} 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());
|
||||
}
|
||||
}
|
||||
|
||||
private PublicKey extractPublicKeyFromCertificate(String base64Cert) throws Exception {
|
||||
|
||||
byte[] der = java.util.Base64.getDecoder().decode(base64Cert);
|
||||
|
|
@ -329,4 +617,12 @@ public class KSeFAuthManager {
|
|||
return certificate.getPublicKey();
|
||||
}
|
||||
|
||||
private String encryptWithPublicKey(String data, PublicKey publicKey) throws Exception {
|
||||
Cipher cipher = Cipher.getInstance("RSA/ECB/OAEPWithSHA-256AndMGF1Padding");
|
||||
cipher.init(Cipher.ENCRYPT_MODE, publicKey);
|
||||
byte[] encryptedBytes = cipher.doFinal(data.getBytes(StandardCharsets.UTF_8));
|
||||
return Base64.getEncoder().encodeToString(encryptedBytes);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -92,4 +92,34 @@ public class KSeFAuthManagerTest {
|
|||
|
||||
System.out.println("Received KSeF Token OK");
|
||||
}
|
||||
|
||||
@Test
|
||||
@Order(4)
|
||||
@DisplayName("Upload Invoice XML (AES-256 + RSA)")
|
||||
public void testOpenInteractiveSession() throws Exception {
|
||||
|
||||
// ---------- 1) Vorbereitung ----------
|
||||
// API initialisieren
|
||||
manager.loadKsefPublicKeys();
|
||||
manager.getChallenge();
|
||||
manager.generateAuthToken(); // erzeugt authToken + refNumber
|
||||
manager.getAuthStatus();
|
||||
manager.redeemAuthToken();
|
||||
|
||||
assertNotNull(manager.getAuthToken(), "AccessToken fehlt");
|
||||
assertNotNull(manager.getRefNumber(), "ReferenceNumber fehlt");
|
||||
|
||||
manager.openInteractiveSession();
|
||||
}
|
||||
|
||||
@Test
|
||||
@Order(5)
|
||||
@DisplayName("Upload Invoice XML (AES-256 + RSA)")
|
||||
public void testUploadInvoice() throws Exception {
|
||||
|
||||
// ---------- 1) Vorbereitung ----------
|
||||
// API initialisieren
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,138 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Faktura
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns:xsd="http://www.w3.org/2001/XMLSchema"
|
||||
xmlns:etd="http://crd.gov.pl/xml/schematy/dziedzinowe/mf/2022/01/05/eD/DefinicjeTypy/"
|
||||
xmlns="http://crd.gov.pl/wzor/2025/06/25/13775/">
|
||||
<Naglowek>
|
||||
<KodFormularza kodSystemowy="FA (3)" wersjaSchemy="1-0E">FA</KodFormularza>
|
||||
<WariantFormularza>3</WariantFormularza>
|
||||
<DataWytworzeniaFa>2026-06-05T02:45:33.422866Z</DataWytworzeniaFa>
|
||||
<SystemInfo>Generator danych</SystemInfo>
|
||||
</Naglowek>
|
||||
<Podmiot1>
|
||||
<DaneIdentyfikacyjne>
|
||||
<NIP>9552521552</NIP>
|
||||
<Nazwa>Alexander Global Logistics GmbH</Nazwa>
|
||||
</DaneIdentyfikacyjne>
|
||||
<Adres>
|
||||
<KodKraju>PL</KodKraju>
|
||||
<AdresL1>Museumstr. 2-6</AdresL1>
|
||||
<AdresL2>28195 Bremen</AdresL2>
|
||||
</Adres>
|
||||
<DaneKontaktowe>
|
||||
<Email>Lucja17@example.net</Email>
|
||||
<Telefon>86-588-03-53</Telefon>
|
||||
</DaneKontaktowe>
|
||||
</Podmiot1>
|
||||
<Podmiot2>
|
||||
<DaneIdentyfikacyjne>
|
||||
<NIP>3861610227</NIP>
|
||||
<Nazwa>Błaszczak - Lenart</Nazwa>
|
||||
</DaneIdentyfikacyjne>
|
||||
<Adres>
|
||||
<KodKraju>PL</KodKraju>
|
||||
<AdresL1>al. Szewczyk 7247</AdresL1>
|
||||
<AdresL2>84-642 Żelechów</AdresL2>
|
||||
</Adres>
|
||||
<DaneKontaktowe>
|
||||
<Email>Jakubina82@gmail.com</Email>
|
||||
<Telefon>75-461-31-12</Telefon>
|
||||
</DaneKontaktowe>
|
||||
<NrKlienta>KL-2936</NrKlienta>
|
||||
<JST>2</JST>
|
||||
<GV>2</GV>
|
||||
</Podmiot2>
|
||||
<Fa>
|
||||
<KodWaluty>PLN</KodWaluty>
|
||||
<P_1>#invoicing_date#</P_1>
|
||||
<P_1M>Lidzbark</P_1M>
|
||||
<P_2>FA/GRQMB-#invoice_number#/05/2025</P_2>
|
||||
<P_6>2025-07-11</P_6>
|
||||
<P_13_1>10652.38</P_13_1>
|
||||
<P_14_1>2450.05</P_14_1>
|
||||
<P_15>13102.43</P_15>
|
||||
<Adnotacje>
|
||||
<P_16>2</P_16>
|
||||
<P_17>2</P_17>
|
||||
<P_18>2</P_18>
|
||||
<P_18A>2</P_18A>
|
||||
<Zwolnienie>
|
||||
<P_19N>1</P_19N>
|
||||
</Zwolnienie>
|
||||
<NoweSrodkiTransportu>
|
||||
<P_22N>1</P_22N>
|
||||
</NoweSrodkiTransportu>
|
||||
<P_23>2</P_23>
|
||||
<PMarzy>
|
||||
<P_PMarzyN>1</P_PMarzyN>
|
||||
</PMarzy>
|
||||
</Adnotacje>
|
||||
<RodzajFaktury>VAT</RodzajFaktury>
|
||||
<FaWiersz>
|
||||
<NrWierszaFa>1</NrWierszaFa>
|
||||
<UU_ID>66465935-48e0-b7d9-ea88-d9d7ff4c6023</UU_ID>
|
||||
<P_7>Refined Concrete Pants</P_7>
|
||||
<P_8A>szt.</P_8A>
|
||||
<P_8B>8</P_8B>
|
||||
<P_9A>295.81</P_9A>
|
||||
<P_11>2366.48</P_11>
|
||||
<P_12>23</P_12>
|
||||
</FaWiersz>
|
||||
<FaWiersz>
|
||||
<NrWierszaFa>2</NrWierszaFa>
|
||||
<UU_ID>16d35141-c43f-98fc-5249-27d7b2845cf7</UU_ID>
|
||||
<P_7>Sleek Steel Pants</P_7>
|
||||
<P_8A>szt.</P_8A>
|
||||
<P_8B>10</P_8B>
|
||||
<P_9A>402.96</P_9A>
|
||||
<P_11>4029.60</P_11>
|
||||
<P_12>23</P_12>
|
||||
</FaWiersz>
|
||||
<FaWiersz>
|
||||
<NrWierszaFa>3</NrWierszaFa>
|
||||
<UU_ID>8647a6b5-f12a-3817-869e-010e4353686a</UU_ID>
|
||||
<P_7>Incredible Steel Salad</P_7>
|
||||
<P_8A>szt.</P_8A>
|
||||
<P_8B>8</P_8B>
|
||||
<P_9A>11.13</P_9A>
|
||||
<P_11>89.04</P_11>
|
||||
<P_12>23</P_12>
|
||||
</FaWiersz>
|
||||
<FaWiersz>
|
||||
<NrWierszaFa>4</NrWierszaFa>
|
||||
<UU_ID>6a9b993d-33c1-eba2-f06c-7d8f22e7430b</UU_ID>
|
||||
<P_7>Ergonomic Soft Fish</P_7>
|
||||
<P_8A>szt.</P_8A>
|
||||
<P_8B>8</P_8B>
|
||||
<P_9A>34.03</P_9A>
|
||||
<P_11>272.24</P_11>
|
||||
<P_12>23</P_12>
|
||||
</FaWiersz>
|
||||
<FaWiersz>
|
||||
<NrWierszaFa>5</NrWierszaFa>
|
||||
<UU_ID>05e42590-9bfb-10c2-a946-fd1641f705e7</UU_ID>
|
||||
<P_7>Intelligent Frozen Cheese</P_7>
|
||||
<P_8A>szt.</P_8A>
|
||||
<P_8B>3</P_8B>
|
||||
<P_9A>501.86</P_9A>
|
||||
<P_11>1505.58</P_11>
|
||||
<P_12>23</P_12>
|
||||
</FaWiersz>
|
||||
<FaWiersz>
|
||||
<NrWierszaFa>6</NrWierszaFa>
|
||||
<UU_ID>dbb7aed5-fedb-96d7-0afd-c3d0c23740a7</UU_ID>
|
||||
<P_7>Ergonomic Metal Bike</P_7>
|
||||
<P_8A>szt.</P_8A>
|
||||
<P_8B>6</P_8B>
|
||||
<P_9A>398.24</P_9A>
|
||||
<P_11>2389.44</P_11>
|
||||
<P_12>23</P_12>
|
||||
</FaWiersz>
|
||||
<Platnosc>
|
||||
<Zaplacono>1</Zaplacono>
|
||||
<DataZaplaty>2025-07-17</DataZaplaty>
|
||||
<FormaPlatnosci>7</FormaPlatnosci>
|
||||
</Platnosc>
|
||||
</Fa>
|
||||
</Faktura>
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"formCode": {
|
||||
"systemCode": "FA (3)",
|
||||
"schemaVersion": "1-0E",
|
||||
"value": "FA"
|
||||
},
|
||||
"encryption": {
|
||||
"encryptedSymmetricKey": "bmrsRHK0uyMRB31FPLOocExKRbQMyiaqN0fzUVnwqiWWAnIySB/89iqHpGWOAiOkixrHIHRFQPHu8aUV33rv+SlCSfv/UIAuLO6FN3VgzVnXITUXFvWUE7Exi1GwJOHOPHp2+tk81Ca5WbPF3Hg3vQQoRo4YSYT7GRV63PVBSNrhHayXDRTT40dJEfLJ25/QzX7EuhV0PQU8sz/4j+kHFVEGTG6KkMN+sqctSRItauoUq7oNjoCw6zqPISvAu/FKxVmq5DxKMj2yFIJQfv9S+xIIpwQDYEPdI1s5ZD3vWtoApWUq4nH9Kbon1ma8yId5aV6PvlDG9oqKN+xvbpodzQ==",
|
||||
"initializationVector": "81LUdc7vjDZtG239K+1WKg=="
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue