774 lines
30 KiB
Java
774 lines
30 KiB
Java
package com.alexanderlogistics.ksef;
|
||
|
||
import java.io.IOException;
|
||
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.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;
|
||
import java.time.format.DateTimeFormatter;
|
||
import java.util.Base64;
|
||
import java.util.Optional;
|
||
import java.util.logging.Logger;
|
||
|
||
import javax.crypto.BadPaddingException;
|
||
import javax.crypto.Cipher;
|
||
import javax.crypto.IllegalBlockSizeException;
|
||
import javax.crypto.NoSuchPaddingException;
|
||
import javax.crypto.spec.OAEPParameterSpec;
|
||
import javax.crypto.spec.PSource;
|
||
|
||
import org.eclipse.microprofile.config.inject.ConfigProperty;
|
||
import org.imixs.workflow.exceptions.InvalidAccessException;
|
||
import org.imixs.workflow.exceptions.PluginException;
|
||
|
||
import jakarta.annotation.PostConstruct;
|
||
import jakarta.annotation.security.RunAs;
|
||
import jakarta.ejb.Lock;
|
||
import jakarta.ejb.LockType;
|
||
import jakarta.ejb.Singleton;
|
||
import jakarta.inject.Inject;
|
||
import jakarta.json.JsonObject;
|
||
import jakarta.json.bind.Jsonb;
|
||
import jakarta.json.bind.JsonbBuilder;
|
||
|
||
/**
|
||
* Der KSeFAuthManager stellt Methoden bereit um einen Access Token für das KSeF
|
||
* System in Polen zu erstellen.
|
||
*
|
||
*
|
||
*/
|
||
@Singleton
|
||
@RunAs("org.imixs.ACCESSLEVEL.MANAGERACCESS")
|
||
public class KSeFAuthManager {
|
||
|
||
private static Logger logger = Logger.getLogger(KSeFAuthManager.class.getName());
|
||
|
||
public static final String ENV_KSEF_API_TOKEN = "ksef.api.token";
|
||
public static final String ENV_KSEF_API_NIP = "ksef.api.nip";
|
||
public static final String ENV_KSEF_API_ENDPOINT = "ksef.api.endpoint";
|
||
public static final String ENV_KSEF_API_DEBUG = "ksef.api.debug";
|
||
|
||
public static final String ERROR_CONFIG = "CONFIG_ERROR";
|
||
public static final String ERROR_API = "API_ERROR";
|
||
|
||
@Inject
|
||
@ConfigProperty(name = ENV_KSEF_API_TOKEN)
|
||
Optional<String> ksefToken;
|
||
|
||
@Inject
|
||
@ConfigProperty(name = ENV_KSEF_API_NIP)
|
||
Optional<String> ksefNip;
|
||
|
||
@Inject
|
||
@ConfigProperty(name = ENV_KSEF_API_ENDPOINT)
|
||
Optional<String> ksefEndpoint;
|
||
|
||
@Inject
|
||
@ConfigProperty(name = ENV_KSEF_API_DEBUG, defaultValue = "false")
|
||
boolean debug;
|
||
|
||
private HttpClient httpClient;
|
||
private String baseURI = "";
|
||
|
||
// public keys
|
||
private String symmetricCertBase64;
|
||
private String ksefTokenCertBase64;
|
||
private PublicKey symmetricPublicKey;
|
||
private PublicKey ksefTokenPublicKey;
|
||
|
||
// Auth information
|
||
private String challenge = "";
|
||
private String authRefNumber = null;
|
||
private String authToken = null;
|
||
private String challengeTimestamp = null;
|
||
|
||
// Session information
|
||
private String accessToken = null;
|
||
private String sessionRefNumber;
|
||
private String sessionValidUntil;
|
||
private byte[] sessionAesKey;
|
||
private byte[] sessionIv;
|
||
|
||
@PostConstruct
|
||
void init() {
|
||
|
||
if (!ksefToken.isPresent()) {
|
||
throw new InvalidAccessException("Missing environment parameter ksef.api.token!");
|
||
}
|
||
if (!ksefEndpoint.isPresent()) {
|
||
throw new InvalidAccessException("Missing environment parameter ksef.api.endpoint!");
|
||
}
|
||
if (!ksefNip.isPresent()) {
|
||
throw new InvalidAccessException("Missing environment parameter ksef.api.nip!");
|
||
}
|
||
|
||
baseURI = ksefEndpoint.get();
|
||
if (baseURI.endsWith("/")) {
|
||
baseURI = baseURI.substring(0, baseURI.length() - 1);
|
||
}
|
||
|
||
httpClient = HttpClient.newBuilder()
|
||
.version(HttpClient.Version.HTTP_2)
|
||
.connectTimeout(Duration.ofSeconds(10))
|
||
.build();
|
||
|
||
}
|
||
|
||
public HttpClient getHttpClient() {
|
||
return httpClient;
|
||
}
|
||
|
||
public boolean isDebug() {
|
||
return debug;
|
||
}
|
||
|
||
public void setKsefToken(String token) {
|
||
this.ksefToken = Optional.ofNullable(token);
|
||
}
|
||
|
||
public String getAccessToken() {
|
||
return accessToken;
|
||
}
|
||
|
||
public void setKsefNip(String nip) {
|
||
this.ksefNip = Optional.ofNullable(nip);
|
||
}
|
||
|
||
public void setKsefEndpoint(String endpoint) {
|
||
this.ksefEndpoint = Optional.ofNullable(endpoint);
|
||
}
|
||
|
||
public String getSessionRefNumber() {
|
||
return sessionRefNumber;
|
||
}
|
||
|
||
public void setDebug(boolean debug) {
|
||
this.debug = debug;
|
||
}
|
||
|
||
public String getBaseURI() {
|
||
return baseURI;
|
||
}
|
||
|
||
public String getAuthRefNumber() {
|
||
return authRefNumber;
|
||
}
|
||
|
||
public String getAuthToken() {
|
||
return authToken;
|
||
}
|
||
|
||
public PublicKey getSymmetricPublicKey() {
|
||
return symmetricPublicKey;
|
||
}
|
||
|
||
public String getChallengeValue() {
|
||
return challenge;
|
||
}
|
||
|
||
public byte[] getSessionAesKey() {
|
||
return sessionAesKey;
|
||
}
|
||
|
||
public byte[] getSessionIv() {
|
||
return sessionIv;
|
||
}
|
||
|
||
/**
|
||
* This method opens a new interactive session to upload invoices.
|
||
* you can call hasValidSession() to verify if a session already exists.
|
||
*
|
||
* @throws PluginException
|
||
*/
|
||
public void openSession() throws PluginException {
|
||
logger.info("├── open Session...");
|
||
if (!hasValidSession()) {
|
||
logger.info("│ ├── open new interactive KSeF Session...");
|
||
// open new session
|
||
this.loadPublicKeyCertificates();
|
||
|
||
this.authChallenge();
|
||
this.authKSeFToken();
|
||
|
||
this.checkTokenStatus();
|
||
|
||
this.redeemToken();
|
||
this.openInteractiveSession();
|
||
} else {
|
||
logger.info("│ ├── reuse existing KSeF Session...");
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Verifies if sessionValidUntil and sessionRefNumber is valid
|
||
*
|
||
* @return true if a valid session exists, false otherwise
|
||
*/
|
||
public boolean hasValidSession() {
|
||
|
||
return false;
|
||
|
||
// // Check if session reference number exists
|
||
// if (sessionRefNumber == null || sessionRefNumber.isEmpty()) {
|
||
// return false;
|
||
// }
|
||
|
||
// // Check if valid until timestamp exists
|
||
// if (sessionValidUntil == null || sessionValidUntil.isEmpty()) {
|
||
// return false;
|
||
// }
|
||
|
||
// try {
|
||
// // Parse the validUntil timestamp (ISO 8601 format with UTC)
|
||
// DateTimeFormatter formatter = DateTimeFormatter.ISO_OFFSET_DATE_TIME;
|
||
// Instant validUntilInstant = Instant.from(formatter.parse(sessionValidUntil));
|
||
|
||
// // Get current time in UTC
|
||
// Instant now = Instant.now();
|
||
|
||
// // Check if session is still valid
|
||
// boolean isValid = now.isBefore(validUntilInstant);
|
||
|
||
// if (debug && isValid) {
|
||
// logger.info("├── Session is still valid until: " + sessionValidUntil);
|
||
// } else if (debug) {
|
||
// logger.info("├── Session expired at: " + sessionValidUntil);
|
||
// }
|
||
|
||
// return isValid;
|
||
|
||
// } catch (Exception e) {
|
||
// // If parsing fails, assume session is invalid
|
||
// logger.warning("├── ⚠️ Error parsing sessionValidUntil timestamp: " +
|
||
// e.getMessage());
|
||
// return false;
|
||
// }
|
||
}
|
||
|
||
/**
|
||
* Helper Method to read the certificates
|
||
*
|
||
* @throws Exception
|
||
*/
|
||
@Lock(LockType.WRITE)
|
||
public void loadPublicKeyCertificates() throws PluginException {
|
||
|
||
logger.info("├── ♻️ KSeF API load public-key-certificates...");
|
||
String uri = baseURI + "/security/public-key-certificates";
|
||
|
||
if (debug) {
|
||
logger.info("│ ├── Endpoint: " + uri);
|
||
}
|
||
HttpRequest request = HttpRequest.newBuilder()
|
||
.uri(URI.create(uri))
|
||
.header("Accept", "application/json")
|
||
.GET()
|
||
.build();
|
||
|
||
HttpResponse<String> response = null;
|
||
try {
|
||
response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
|
||
} catch (IOException | InterruptedException e) {
|
||
throw new PluginException(KSeFAuthManager.class.getSimpleName(), ERROR_API,
|
||
"Unable to send request for KSeF certificates endpoint: " + e.getMessage());
|
||
}
|
||
|
||
if (debug) {
|
||
logger.info("│ ├── Response Code = " + response.statusCode());
|
||
logger.info("Response: " + response.body());
|
||
}
|
||
|
||
if (response.statusCode() != 200) {
|
||
throw new PluginException(KSeFAuthManager.class.getSimpleName(), ERROR_API,
|
||
"Unexpected response from KSeF certificates endpoint: " + 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"]
|
||
var usages = entry.getJsonArray("usage");
|
||
|
||
if (usages.stream().anyMatch(v -> v.toString().contains("KsefTokenEncryption"))) {
|
||
ksefTokenCertBase64 = entry.getString("certificate");
|
||
if (debug) {
|
||
logger.info("│ ├── ✓ KsefTokenEncryption certificate found.");
|
||
} // Public Key extrahieren
|
||
ksefTokenPublicKey = extractPublicKeyFromCertificate(ksefTokenCertBase64);
|
||
logger.info("│ ├── ✓ RSA PublicKey successfully extracted.");
|
||
|
||
}
|
||
|
||
if (usages.stream().anyMatch(v -> v.toString().contains("SymmetricKeyEncryption"))) {
|
||
symmetricCertBase64 = entry.getString("certificate");
|
||
if (debug) {
|
||
logger.info("│ ├── ✓ SymmetricKeyEncryption certificate found.");
|
||
}
|
||
// Public Key extrahieren
|
||
symmetricPublicKey = extractPublicKeyFromCertificate(symmetricCertBase64);
|
||
logger.info("│ ├── ✓ Symetric PublicKey successfully extracted.");
|
||
|
||
}
|
||
}
|
||
if (ksefTokenPublicKey == null) {
|
||
logger.warning("│ ├── ⚠️ No certificate with usage 'ksefTokenPublicKey' found!");
|
||
}
|
||
if (symmetricPublicKey == null) {
|
||
logger.warning("│ ├── ⚠️ No certificate with usage 'symmetricPublicKey' found!");
|
||
}
|
||
} catch (Exception e) {
|
||
throw new PluginException(KSeFAuthManager.class.getSimpleName(), ERROR_API,
|
||
"Error parsing certificates JSON: " + e.getMessage());
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Diese method führt den challenge Request durch
|
||
*
|
||
* @throws Exception
|
||
*/
|
||
// @Lock(LockType.READ)
|
||
@Lock(LockType.WRITE)
|
||
public void authChallenge() throws PluginException {
|
||
logger.info("├── 🛅 KSeF API Challenge...");
|
||
String uri = baseURI + "/auth/challenge";
|
||
|
||
if (debug) {
|
||
logger.info("│ ├── Endpoint: " + uri);
|
||
}
|
||
HttpRequest request = HttpRequest.newBuilder()
|
||
.uri(URI.create(uri)) // nur Endpoint checken
|
||
.header("Accept", "application/json")
|
||
.POST(HttpRequest.BodyPublishers.noBody()) //
|
||
.build();
|
||
|
||
HttpResponse<String> response;
|
||
try {
|
||
response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
|
||
if (debug) {
|
||
logger.info("│ ├── Response Code = " + response.statusCode());
|
||
logger.info("Response: " + response.body());
|
||
}
|
||
// Prüfen, dass wir eine API-Antwort bekommen, kein HTML
|
||
if (response.statusCode() >= 500) {
|
||
throw new PluginException(KSeFAuthManager.class.getSimpleName(), ERROR_CONFIG,
|
||
"Unable to access KSeF API: response=" + response.statusCode());
|
||
}
|
||
|
||
// Challenge aus Response extrahieren und speichern
|
||
if (response.statusCode() == 200) {
|
||
try (Jsonb jsonb = JsonbBuilder.create()) {
|
||
// 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");
|
||
|
||
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,
|
||
"Error parsing JSON response: " + e.getMessage());
|
||
|
||
}
|
||
|
||
}
|
||
} catch (IOException | InterruptedException e) {
|
||
throw new PluginException(KSeFAuthManager.class.getSimpleName(), ERROR_API,
|
||
"API Error: Unable to parse KSeF API response");
|
||
}
|
||
|
||
}
|
||
|
||
/**
|
||
* Helper method to request the KSeF Auth Token and referenceNumber
|
||
*
|
||
* @throws Exception
|
||
*/
|
||
@Lock(LockType.WRITE)
|
||
public void authKSeFToken() throws PluginException {
|
||
logger.info("├── 🛃 KSeF API auth ksef-token...");
|
||
String uri = baseURI + "/auth/ksef-token";
|
||
|
||
if (debug) {
|
||
logger.info("│ ├── Endpoint: " + uri);
|
||
}
|
||
HttpResponse<String> response = null;
|
||
try {
|
||
String jsonPayload = String.format(
|
||
"{" +
|
||
"\"challenge\": \"%s\"," +
|
||
"\"contextIdentifier\": {" +
|
||
" \"type\": \"Nip\"," +
|
||
" \"value\": \"%s\"" +
|
||
"}," +
|
||
"\"encryptedToken\": \"%s\"" +
|
||
"}",
|
||
challenge, ksefNip.get(), getEncryptedToken());
|
||
|
||
HttpRequest request = HttpRequest.newBuilder()
|
||
.uri(URI.create(uri))
|
||
.header("Accept", "application/json")
|
||
.header("Content-Type", "application/json")
|
||
.POST(HttpRequest.BodyPublishers.ofString(jsonPayload))
|
||
.build();
|
||
|
||
response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
|
||
} catch (IOException | InterruptedException e) {
|
||
throw new PluginException(KSeFAuthManager.class.getSimpleName(), ERROR_API,
|
||
"API Error: Unable to request ksef-token");
|
||
}
|
||
|
||
if (debug) {
|
||
logger.info("│ ├── Response Code = " + response.statusCode());
|
||
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
|
||
authRefNumber = jsonObject.getString("referenceNumber");
|
||
logger.info("│ ├── ✓ referenceNumber: " + authRefNumber);
|
||
|
||
// Authentication Token speichern
|
||
JsonObject authTokenObject = jsonObject.getJsonObject("authenticationToken");
|
||
authToken = authTokenObject.getString("token");
|
||
if (debug) {
|
||
logger.info("│ ├── ✓ 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());
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Helper Method to open an interactive session. The method fetches a
|
||
* sessionRefNumber which is mandatory to upload an invoice.
|
||
*
|
||
* @throws Exception
|
||
*/
|
||
@Lock(LockType.WRITE)
|
||
public void openInteractiveSession() throws PluginException {
|
||
|
||
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";
|
||
if (debug) {
|
||
logger.info("│ ├── Endpoint: " + 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);
|
||
|
||
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();
|
||
|
||
response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
|
||
|
||
if (debug) {
|
||
logger.info("│ ├── Response Code = " + response.statusCode());
|
||
logger.info("Response: " + response.body());
|
||
}
|
||
|
||
if (response.statusCode() != 201) {
|
||
throw new PluginException(KSeFAuthManager.class.getSimpleName(), ERROR_API,
|
||
"Failed to open interactive session: " + response.statusCode());
|
||
}
|
||
|
||
// parse JSON
|
||
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());
|
||
throw new PluginException(KSeFAuthManager.class.getSimpleName(), ERROR_API,
|
||
"Error parsing JSON response: " + e.getMessage());
|
||
|
||
}
|
||
} catch (NoSuchAlgorithmException | NoSuchPaddingException | IOException | InterruptedException
|
||
| InvalidKeyException | IllegalBlockSizeException | BadPaddingException e) {
|
||
logger.severe("├── ⚠️ API Error: " + e.getMessage());
|
||
throw new PluginException(KSeFAuthManager.class.getSimpleName(), ERROR_API,
|
||
"API Error openInteractiveSession: " + e.getMessage());
|
||
}
|
||
|
||
}
|
||
|
||
/**
|
||
* Verschlüsselt den KSeF-Token mit RSA-OAEP (SHA-256) für die Authentifizierung
|
||
*
|
||
* Format: token|timestamp_in_milliseconds
|
||
*
|
||
* @return Base64-kodierter verschlüsselter Token
|
||
* @throws NoSuchPaddingException
|
||
* @throws NoSuchAlgorithmException
|
||
* @throws InvalidAlgorithmParameterException
|
||
* @throws InvalidKeyException
|
||
* @throws Exception
|
||
*/
|
||
public String getEncryptedToken() throws PluginException {
|
||
String result = null;
|
||
if (ksefTokenPublicKey == null) {
|
||
throw new IllegalStateException("Public Key not loaded! Call loadPublicKeyCertificates() first.");
|
||
}
|
||
|
||
if (challengeTimestamp == null || challengeTimestamp.isEmpty()) {
|
||
throw new IllegalStateException("Challenge timestamp is missing! Call authChallenge() first.");
|
||
}
|
||
try {
|
||
// 1. Timestamp parsen - KSeF liefert zu viele Dezimalstellen
|
||
// Format: "2025-11-14T14:18:35.606507+00:00"
|
||
// Flexibler Parser der verschiedene Dezimalstellen akzeptiert
|
||
DateTimeFormatter formatter = DateTimeFormatter.ISO_OFFSET_DATE_TIME;
|
||
Instant instant = Instant.from(formatter.parse(challengeTimestamp));
|
||
|
||
long timestampMillis = instant.toEpochMilli();
|
||
|
||
logger.info("│ ├── Timestamp (ms): " + timestampMillis);
|
||
|
||
// 2. Format erstellen: token|timestamp
|
||
String plaintext = ksefToken.get() + "|" + timestampMillis;
|
||
|
||
if (debug) {
|
||
logger.info("│ ├── Plaintext format: [token]|[" + timestampMillis + "]");
|
||
}
|
||
|
||
// 3. RSA-OAEP Verschlüsselung mit SHA-256
|
||
Cipher cipher;
|
||
|
||
cipher = Cipher.getInstance("RSA/ECB/OAEPWithSHA-256AndMGF1Padding");
|
||
|
||
OAEPParameterSpec oaepParams = new OAEPParameterSpec(
|
||
"SHA-256",
|
||
"MGF1",
|
||
MGF1ParameterSpec.SHA256,
|
||
PSource.PSpecified.DEFAULT);
|
||
cipher.init(Cipher.ENCRYPT_MODE, ksefTokenPublicKey, oaepParams);
|
||
|
||
byte[] encrypted = cipher.doFinal(plaintext.getBytes(StandardCharsets.UTF_8));
|
||
|
||
// 4. Base64 kodieren
|
||
result = Base64.getEncoder().encodeToString(encrypted);
|
||
|
||
logger.info("│ ├── ✓ Token encrypted with RSA-OAEP");
|
||
} catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException
|
||
| InvalidAlgorithmParameterException | IllegalBlockSizeException | BadPaddingException e) {
|
||
throw new PluginException(KSeFAuthManager.class.getSimpleName(), ERROR_API,
|
||
"API Error - unable to getEncryptedToken: " + e.getMessage());
|
||
|
||
}
|
||
return result;
|
||
}
|
||
|
||
/**
|
||
* Redeems the KSeF token and returns a session access token to be used to open
|
||
* an Interactive Session
|
||
*
|
||
* POST /auth/token/redeem
|
||
*
|
||
* @return JsonObject with referenceNumber and sessionToken
|
||
* @throws Exception
|
||
*/
|
||
@Lock(LockType.WRITE)
|
||
public JsonObject redeemToken() throws PluginException {
|
||
|
||
logger.info("├── 🛂 KSeF API redeem token...");
|
||
String uri = baseURI + "/auth/token/redeem";
|
||
|
||
if (debug) {
|
||
logger.info("│ ├── Endpoint: " + uri);
|
||
}
|
||
HttpResponse<String> response = null;
|
||
try {
|
||
HttpRequest request = HttpRequest.newBuilder()
|
||
.uri(URI.create(uri))
|
||
.header("Accept", "application/json")
|
||
.header("Authorization", "Bearer " + authToken)
|
||
.POST(HttpRequest.BodyPublishers.noBody())
|
||
.build();
|
||
|
||
response = httpClient.send(request,
|
||
HttpResponse.BodyHandlers.ofString());
|
||
} catch (IOException | InterruptedException e) {
|
||
throw new PluginException(KSeFAuthManager.class.getSimpleName(), ERROR_API,
|
||
"API Error - unable to redeemToken: " + e.getMessage());
|
||
}
|
||
|
||
if (debug) {
|
||
logger.info("│ ├── Response Code = " + response.statusCode());
|
||
logger.info("│ ├── Response: " + response.body());
|
||
}
|
||
|
||
if (response.statusCode() >= 400) {
|
||
throw new PluginException(KSeFAuthManager.class.getSimpleName(),
|
||
ERROR_API,
|
||
"Token redeem failed: " + response.statusCode()
|
||
+ " - " + response.body());
|
||
}
|
||
|
||
try (Jsonb jsonb = JsonbBuilder.create()) {
|
||
JsonObject jsonObject = jsonb.fromJson(response.body(), JsonObject.class);
|
||
|
||
// Access Token extrahieren und speichern
|
||
JsonObject accessTokenObj = jsonObject.getJsonObject("accessToken");
|
||
this.accessToken = accessTokenObj.getString("token");
|
||
|
||
logger.info("│ └── ☑️ Access Token received");
|
||
|
||
return jsonObject;
|
||
|
||
} catch (Exception e) {
|
||
logger.severe("├── ⚠️ Error parsing JSON response: " + e.getMessage());
|
||
throw new PluginException(KSeFAuthManager.class.getSimpleName(),
|
||
ERROR_API,
|
||
"Error parsing response: " + e.getMessage());
|
||
}
|
||
}
|
||
|
||
private PublicKey extractPublicKeyFromCertificate(String base64Cert) throws Exception {
|
||
byte[] der = java.util.Base64.getDecoder().decode(base64Cert);
|
||
java.security.cert.CertificateFactory factory = java.security.cert.CertificateFactory.getInstance("X.509");
|
||
var certificate = (java.security.cert.X509Certificate) factory
|
||
.generateCertificate(new java.io.ByteArrayInputStream(der));
|
||
return certificate.getPublicKey();
|
||
}
|
||
|
||
/**
|
||
* 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)
|
||
*
|
||
* https://ksef-test.mf.gov.pl/docs/v2/index.html#tag/Uzyskiwanie-dostepu/paths/~1api~1v2~1auth~1%7BreferenceNumber%7D/get
|
||
*
|
||
* @throws PluginException
|
||
*/
|
||
public void checkTokenStatus() throws PluginException {
|
||
logger.info("├── 🌡️ KSeF API check auth status...");
|
||
String uri = baseURI + "/auth/" + authRefNumber;
|
||
|
||
if (debug) {
|
||
logger.info("│ ├── Endpoint: " + uri);
|
||
}
|
||
HttpResponse<String> response = null;
|
||
try {
|
||
HttpRequest request = HttpRequest.newBuilder()
|
||
.uri(URI.create(uri))
|
||
.header("Accept", "application/json")
|
||
.header("Content-Type", "application/json")
|
||
.header("Authorization", "Bearer " + authToken)
|
||
.GET()
|
||
.build();
|
||
response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
|
||
} catch (IOException | InterruptedException e) {
|
||
throw new PluginException(KSeFAuthManager.class.getSimpleName(), ERROR_API,
|
||
"API Error: Unable to request auth status: " + e.getMessage());
|
||
}
|
||
if (true) {
|
||
logger.info("│ ├── Response Code = " + response.statusCode());
|
||
logger.info("Response: " + response.body());
|
||
}
|
||
|
||
try (Jsonb jsonb = JsonbBuilder.create()) {
|
||
JsonObject jsonObject = jsonb.fromJson(response.body(), JsonObject.class);
|
||
Boolean isRedeemed = jsonObject.containsKey("isTokenRedeemed") ? jsonObject.getBoolean("isTokenRedeemed")
|
||
: null;
|
||
|
||
String lastTokenRefreshDate = jsonObject.containsKey("lastTokenRefreshDate")
|
||
? jsonObject.getString("lastTokenRefreshDate")
|
||
: null;
|
||
String refreshTokenValidUntil = jsonObject.containsKey("refreshTokenValidUntil")
|
||
? jsonObject.getString("refreshTokenValidUntil")
|
||
: null;
|
||
|
||
logger.info("│ ├── › lastTokenRefreshDate: " + lastTokenRefreshDate);
|
||
if (isRedeemed) {
|
||
logger.info("│ ├── ✓ isTokenRedeemed: " + isRedeemed);
|
||
logger.info("│ ├── ✓ refreshTokenValidUntil: " + refreshTokenValidUntil);
|
||
} else {
|
||
logger.info("│ ├── 𐄂 isTokenRedeemed: " + isRedeemed);
|
||
}
|
||
|
||
} 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());
|
||
}
|
||
}
|
||
|
||
}
|