verbesserte auth methodik
This commit is contained in:
parent
9f6d86d328
commit
5ead936783
2 changed files with 153 additions and 324 deletions
|
|
@ -7,13 +7,17 @@ import java.net.http.HttpRequest;
|
||||||
import java.net.http.HttpResponse;
|
import java.net.http.HttpResponse;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.security.PublicKey;
|
import java.security.PublicKey;
|
||||||
import java.security.SecureRandom;
|
import java.security.spec.MGF1ParameterSpec;
|
||||||
import java.time.Duration;
|
import java.time.Duration;
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.time.format.DateTimeFormatter;
|
||||||
import java.util.Base64;
|
import java.util.Base64;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.logging.Logger;
|
import java.util.logging.Logger;
|
||||||
|
|
||||||
import javax.crypto.Cipher;
|
import javax.crypto.Cipher;
|
||||||
|
import javax.crypto.spec.OAEPParameterSpec;
|
||||||
|
import javax.crypto.spec.PSource;
|
||||||
|
|
||||||
import org.eclipse.microprofile.config.inject.ConfigProperty;
|
import org.eclipse.microprofile.config.inject.ConfigProperty;
|
||||||
import org.imixs.workflow.exceptions.InvalidAccessException;
|
import org.imixs.workflow.exceptions.InvalidAccessException;
|
||||||
|
|
@ -70,19 +74,11 @@ public class KSeFAuthManager {
|
||||||
private String challenge = "";
|
private String challenge = "";
|
||||||
private String refNumber = null;
|
private String refNumber = null;
|
||||||
private String authToken = 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 String challengeTimestamp = null;
|
||||||
|
private String symmetricCertBase64;
|
||||||
private byte[] sessionAesKey;
|
private String ksefTokenCertBase64;
|
||||||
|
private PublicKey symmetricPublicKey;
|
||||||
private byte[] sessionIv;
|
private PublicKey ksefTokenPublicKey;
|
||||||
|
|
||||||
@PostConstruct
|
@PostConstruct
|
||||||
void init() {
|
void init() {
|
||||||
|
|
@ -135,13 +131,12 @@ public class KSeFAuthManager {
|
||||||
*
|
*
|
||||||
* @return
|
* @return
|
||||||
*/
|
*/
|
||||||
// public String getEncodedToken() {
|
public String getEncodedToken() {
|
||||||
|
|
||||||
// String result =
|
String result = java.util.Base64.getEncoder().encodeToString(ksefToken.orElse("").getBytes());
|
||||||
// java.util.Base64.getEncoder().encodeToString(ksefToken.orElse("").getBytes());
|
|
||||||
|
|
||||||
// return result;
|
return result;
|
||||||
// }
|
}
|
||||||
|
|
||||||
public String getBaseURI() {
|
public String getBaseURI() {
|
||||||
return baseURI;
|
return baseURI;
|
||||||
|
|
@ -155,14 +150,6 @@ public class KSeFAuthManager {
|
||||||
return authToken;
|
return authToken;
|
||||||
}
|
}
|
||||||
|
|
||||||
public PublicKey getSymmetricPublicKey() {
|
|
||||||
return symmetricPublicKey;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getSymmetricCertBase64() {
|
|
||||||
return symmetricCertBase64;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getChallengeValue() {
|
public String getChallengeValue() {
|
||||||
return challenge;
|
return challenge;
|
||||||
}
|
}
|
||||||
|
|
@ -174,7 +161,7 @@ public class KSeFAuthManager {
|
||||||
*/
|
*/
|
||||||
// @Lock(LockType.READ)
|
// @Lock(LockType.READ)
|
||||||
@Lock(LockType.WRITE)
|
@Lock(LockType.WRITE)
|
||||||
public void getChallenge() throws PluginException {
|
public void authChallenge() throws PluginException {
|
||||||
logger.info("├── KSeF API Challenge...");
|
logger.info("├── KSeF API Challenge...");
|
||||||
String uri = baseURI + "/auth/challenge";
|
String uri = baseURI + "/auth/challenge";
|
||||||
logger.info("│ ├── Endpoint: " + uri);
|
logger.info("│ ├── Endpoint: " + uri);
|
||||||
|
|
@ -230,30 +217,10 @@ public class KSeFAuthManager {
|
||||||
* @throws Exception
|
* @throws Exception
|
||||||
*/
|
*/
|
||||||
@Lock(LockType.WRITE)
|
@Lock(LockType.WRITE)
|
||||||
public void generateAuthToken() throws Exception {
|
public void authKSeFToken() throws Exception {
|
||||||
logger.info("├── KSeF API auth ksef-token...");
|
logger.info("├── KSeF API auth ksef-token...");
|
||||||
String uri = baseURI + "/auth/ksef-token";
|
String uri = baseURI + "/auth/ksef-token";
|
||||||
logger.info("│ ├── Endpoint: " + uri);
|
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(
|
String jsonPayload = String.format(
|
||||||
"{" +
|
"{" +
|
||||||
"\"challenge\": \"%s\"," +
|
"\"challenge\": \"%s\"," +
|
||||||
|
|
@ -263,9 +230,7 @@ public class KSeFAuthManager {
|
||||||
"}," +
|
"}," +
|
||||||
"\"encryptedToken\": \"%s\"" +
|
"\"encryptedToken\": \"%s\"" +
|
||||||
"}",
|
"}",
|
||||||
challenge,
|
challenge, ksefNip.get(), getEncryptedToken());
|
||||||
ksefNip.get(),
|
|
||||||
encryptedToken);
|
|
||||||
|
|
||||||
HttpRequest request = HttpRequest.newBuilder()
|
HttpRequest request = HttpRequest.newBuilder()
|
||||||
.uri(URI.create(uri))
|
.uri(URI.create(uri))
|
||||||
|
|
@ -275,84 +240,146 @@ public class KSeFAuthManager {
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
|
HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
|
||||||
|
|
||||||
logger.info("│ ├── Response Code = " + response.statusCode());
|
logger.info("│ ├── Response Code = " + response.statusCode());
|
||||||
if (debug) {
|
if (debug) {
|
||||||
logger.info("Response: " + response.body());
|
logger.info("Response: " + response.body());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Prüfen, dass wir eine API-Antwort bekommen
|
||||||
if (response.statusCode() >= 500) {
|
if (response.statusCode() >= 500) {
|
||||||
throw new PluginException(KSeFAuthManager.class.getSimpleName(), ERROR_CONFIG,
|
throw new PluginException(KSeFAuthManager.class.getSimpleName(), ERROR_CONFIG,
|
||||||
"Unable to access KSeF API: response=" + response.statusCode());
|
"Unable to access KSeF API: response=" + response.statusCode());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// JSON parsen und Werte speichern
|
||||||
if (response.statusCode() == 200 || response.statusCode() == 202) {
|
if (response.statusCode() == 200 || response.statusCode() == 202) {
|
||||||
try (Jsonb jsonb = JsonbBuilder.create()) {
|
try (Jsonb jsonb = JsonbBuilder.create()) {
|
||||||
JsonObject jsonObject = jsonb.fromJson(response.body(), JsonObject.class);
|
JsonObject jsonObject = jsonb.fromJson(response.body(), JsonObject.class);
|
||||||
|
|
||||||
|
// Reference Number speichern
|
||||||
refNumber = jsonObject.getString("referenceNumber");
|
refNumber = jsonObject.getString("referenceNumber");
|
||||||
logger.info("Extracted referenceNumber: " + refNumber);
|
logger.info("Extracted referenceNumber: " + refNumber);
|
||||||
|
|
||||||
|
// Authentication Token speichern
|
||||||
JsonObject authTokenObject = jsonObject.getJsonObject("authenticationToken");
|
JsonObject authTokenObject = jsonObject.getJsonObject("authenticationToken");
|
||||||
authToken = authTokenObject.getString("token");
|
authToken = authTokenObject.getString("token");
|
||||||
logger.info("Extracted authenticationToken: " + authToken);
|
logger.info("Extracted authenticationToken: " + authToken);
|
||||||
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
|
logger.severe("├── ⚠️ Error parsing JSON response" + e.getMessage());
|
||||||
throw new PluginException(KSeFAuthManager.class.getSimpleName(), ERROR_API,
|
throw new PluginException(KSeFAuthManager.class.getSimpleName(), ERROR_API,
|
||||||
"Error parsing JSON response: " + e.getMessage());
|
"Error parsing JSON response: " + 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 Exception
|
||||||
|
*/
|
||||||
|
public String getEncryptedToken() throws Exception {
|
||||||
|
|
||||||
|
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.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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.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
|
||||||
|
String result = Base64.getEncoder().encodeToString(encrypted);
|
||||||
|
|
||||||
|
logger.info("│ ├── ✓ Token encrypted with RSA-OAEP");
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Redeems the KSeF token and returns a session token
|
||||||
|
*
|
||||||
|
* POST /auth/token/redeem
|
||||||
|
*
|
||||||
|
* @return JsonObject with referenceNumber and sessionToken
|
||||||
|
* @throws Exception
|
||||||
|
*/
|
||||||
@Lock(LockType.WRITE)
|
@Lock(LockType.WRITE)
|
||||||
public void redeemAuthToken() throws Exception {
|
public JsonObject redeemToken() throws Exception {
|
||||||
logger.info("├── KSeF API redeem authentication token...");
|
|
||||||
|
logger.info("├── KSeF API redeem token...");
|
||||||
String uri = baseURI + "/auth/token/redeem";
|
String uri = baseURI + "/auth/token/redeem";
|
||||||
logger.info("│ ├── Endpoint: " + uri);
|
logger.info("│ ├── Endpoint: " + uri);
|
||||||
|
|
||||||
HttpRequest request = HttpRequest.newBuilder()
|
HttpRequest request = HttpRequest.newBuilder()
|
||||||
.uri(URI.create(uri))
|
.uri(URI.create(uri))
|
||||||
.header("Accept", "application/json")
|
.header("Accept", "application/json")
|
||||||
.header("Content-Type", "application/json")
|
.header("Authorization", "Bearer " + authToken)
|
||||||
.header("Authorization", "Bearer " + authToken) // authToken aus generateAuthToken()
|
.POST(HttpRequest.BodyPublishers.noBody())
|
||||||
.POST(HttpRequest.BodyPublishers.noBody()) // Payload leer, API erwartet nur Bearer im Header
|
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
|
HttpResponse<String> response = httpClient.send(request,
|
||||||
|
HttpResponse.BodyHandlers.ofString());
|
||||||
logger.info("│ ├── Response Code = " + response.statusCode());
|
logger.info("│ ├── Response Code = " + response.statusCode());
|
||||||
|
|
||||||
if (debug) {
|
if (debug) {
|
||||||
logger.info("Response: " + response.body());
|
logger.info("│ └── Response: " + response.body());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Prüfen auf Fehler
|
if (response.statusCode() >= 400) {
|
||||||
if (response.statusCode() >= 500) {
|
throw new PluginException(KSeFAuthManager.class.getSimpleName(),
|
||||||
throw new PluginException(KSeFAuthManager.class.getSimpleName(), ERROR_CONFIG,
|
ERROR_API,
|
||||||
"Unable to access KSeF API: response=" + response.statusCode());
|
"Token redeem failed: " + response.statusCode()
|
||||||
|
+ " - " + response.body());
|
||||||
}
|
}
|
||||||
|
|
||||||
// JSON parsen und AccessToken + RefreshToken speichern
|
try (Jsonb jsonb = JsonbBuilder.create()) {
|
||||||
if (response.statusCode() == 200 || response.statusCode() == 201) {
|
JsonObject jsonObject = jsonb.fromJson(response.body(), JsonObject.class);
|
||||||
try (Jsonb jsonb = JsonbBuilder.create()) {
|
|
||||||
JsonObject jsonObject = jsonb.fromJson(response.body(), JsonObject.class);
|
|
||||||
|
|
||||||
JsonObject accessTokenObj = jsonObject.getJsonObject("accessToken");
|
// Access Token extrahieren und speichern
|
||||||
accessToken = accessTokenObj.getString("token"); // finaler Bearer für alle weiteren API-Calls
|
JsonObject accessTokenObj = jsonObject.getJsonObject("accessToken");
|
||||||
logger.info("Extracted accessToken: " + accessToken);
|
this.authToken = accessTokenObj.getString("token");
|
||||||
|
|
||||||
JsonObject refreshTokenObj = jsonObject.getJsonObject("refreshToken");
|
logger.info("│ └── Access Token received");
|
||||||
refreshToken = refreshTokenObj.getString("token"); // optional zum späteren Refresh
|
|
||||||
if (debug) {
|
|
||||||
logger.info("Extracted refreshToken: " + refreshToken);
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (Exception e) {
|
return jsonObject;
|
||||||
logger.severe("├── ⚠️ Error parsing JSON response: " + e.getMessage());
|
|
||||||
throw new PluginException(KSeFAuthManager.class.getSimpleName(), ERROR_API,
|
} catch (Exception e) {
|
||||||
"Error parsing JSON response: " + e.getMessage());
|
logger.severe("├── ⚠️ Error parsing JSON response: " + e.getMessage());
|
||||||
}
|
throw new PluginException(KSeFAuthManager.class.getSimpleName(),
|
||||||
} else {
|
ERROR_API,
|
||||||
throw new PluginException(KSeFAuthManager.class.getSimpleName(), ERROR_API,
|
"Error parsing response: " + e.getMessage());
|
||||||
"Failed to redeem authentication token: " + response.statusCode());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -362,7 +389,7 @@ public class KSeFAuthManager {
|
||||||
* @throws Exception
|
* @throws Exception
|
||||||
*/
|
*/
|
||||||
@Lock(LockType.WRITE)
|
@Lock(LockType.WRITE)
|
||||||
public void loadKsefPublicKeys() throws Exception {
|
public void loadPublicKeyCertificates() throws Exception {
|
||||||
|
|
||||||
logger.info("├── KSeF API load public-key-certificates...");
|
logger.info("├── KSeF API load public-key-certificates...");
|
||||||
String uri = baseURI + "/security/public-key-certificates";
|
String uri = baseURI + "/security/public-key-certificates";
|
||||||
|
|
@ -383,81 +410,53 @@ public class KSeFAuthManager {
|
||||||
|
|
||||||
if (response.statusCode() != 200) {
|
if (response.statusCode() != 200) {
|
||||||
throw new PluginException(KSeFAuthManager.class.getSimpleName(), ERROR_API,
|
throw new PluginException(KSeFAuthManager.class.getSimpleName(), ERROR_API,
|
||||||
"Unexpected response: " + response.statusCode());
|
"Unexpected response from KSeF certificates endpoint: " + response.statusCode());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// JSON parsen
|
||||||
try (Jsonb jsonb = JsonbBuilder.create()) {
|
try (Jsonb jsonb = JsonbBuilder.create()) {
|
||||||
|
|
||||||
// WICHTIG: JsonArray lesen – NICHT JsonObject[]
|
var certArray = jsonb.fromJson(response.body(), JsonObject[].class);
|
||||||
jakarta.json.JsonArray certArray = jsonb.fromJson(response.body(), jakarta.json.JsonArray.class);
|
for (JsonObject entry : certArray) {
|
||||||
|
// usage = ["KsefTokenEncryption"] oder ["SymmetricKeyEncryption"]
|
||||||
for (var entryValue : certArray) {
|
|
||||||
|
|
||||||
JsonObject entry = entryValue.asJsonObject();
|
|
||||||
var usages = entry.getJsonArray("usage");
|
var usages = entry.getJsonArray("usage");
|
||||||
|
|
||||||
if (usages != null &&
|
if (usages.stream().anyMatch(v -> v.toString().contains("KsefTokenEncryption"))) {
|
||||||
usages.stream().anyMatch(v -> v.toString().contains("KsefTokenEncryption"))) {
|
ksefTokenCertBase64 = entry.getString("certificate");
|
||||||
|
logger.info("│ ├── KsefTokenEncryption certificate found.");
|
||||||
|
// Public Key extrahieren
|
||||||
|
ksefTokenPublicKey = extractPublicKeyFromCertificate(ksefTokenCertBase64);
|
||||||
|
logger.info("│ ├── ✓ RSA PublicKey successfully extracted.");
|
||||||
|
|
||||||
ksefTokenEncryptionCertBase64 = entry.getString("certificate");
|
}
|
||||||
logger.info("│ ├── Found certificate with usage = KsefTokenEncryption");
|
|
||||||
|
|
||||||
ksefTokenEncryptionPublicKey = extractPublicKeyFromCertificate(ksefTokenEncryptionCertBase64);
|
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.");
|
||||||
|
|
||||||
logger.info("│ ├── ✓ RSA PublicKey for KSeF Token Encryption extracted.");
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (ksefTokenPublicKey == null) {
|
||||||
throw new PluginException(KSeFAuthManager.class.getSimpleName(), ERROR_API,
|
logger.warning("│ ├── ⚠️ No certificate with usage 'ksefTokenPublicKey' found!");
|
||||||
"No certificate with usage = KsefTokenEncryption found");
|
}
|
||||||
|
if (symmetricPublicKey == null) {
|
||||||
|
logger.warning("│ ├── ⚠️ No certificate with usage 'symmetricPublicKey' found!");
|
||||||
|
}
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
throw new PluginException(KSeFAuthManager.class.getSimpleName(), ERROR_API,
|
throw new PluginException(KSeFAuthManager.class.getSimpleName(), ERROR_API,
|
||||||
"Error parsing certificates JSON: " + e.getMessage());
|
"Error parsing certificates JSON: " + e.getMessage());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Lock(LockType.WRITE)
|
private PublicKey extractPublicKeyFromCertificate(String base64Cert) throws Exception {
|
||||||
public void loadSymmetricKeyPublicKey() throws Exception {
|
byte[] der = java.util.Base64.getDecoder().decode(base64Cert);
|
||||||
|
java.security.cert.CertificateFactory factory = java.security.cert.CertificateFactory.getInstance("X.509");
|
||||||
logger.info("├── KSeF API load symmetric public key...");
|
var certificate = (java.security.cert.X509Certificate) factory
|
||||||
String uri = baseURI + "/security/public-key";
|
.generateCertificate(new java.io.ByteArrayInputStream(der));
|
||||||
logger.info("│ ├── Endpoint: " + uri);
|
return certificate.getPublicKey();
|
||||||
|
|
||||||
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());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -468,90 +467,11 @@ public class KSeFAuthManager {
|
||||||
@Lock(LockType.WRITE)
|
@Lock(LockType.WRITE)
|
||||||
public void openInteractiveSession() throws Exception {
|
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...");
|
logger.info("├── KSeF API open interactive session...");
|
||||||
String uri = baseURI + "/sessions/online";
|
String uri = baseURI + "/sessions/online";
|
||||||
logger.info("│ ├── Endpoint: " + uri);
|
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());
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -568,9 +488,6 @@ public class KSeFAuthManager {
|
||||||
|
|
||||||
logger.info("├── KSeF API check authentication status...");
|
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;
|
String uri = baseURI + "/auth/" + refNumber;
|
||||||
logger.info("│ ├── Endpoint: " + uri);
|
logger.info("│ ├── Endpoint: " + uri);
|
||||||
|
|
||||||
|
|
@ -605,24 +522,4 @@ public class KSeFAuthManager {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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();
|
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
package com.alexanderlogistics.ksef;
|
package com.alexanderlogistics.ksef;
|
||||||
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||||
|
|
||||||
import org.junit.jupiter.api.BeforeEach;
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
|
@ -37,88 +36,21 @@ public class KSeFAuthManagerTest {
|
||||||
// ------------------------------------------------------------
|
// ------------------------------------------------------------
|
||||||
@Test
|
@Test
|
||||||
@Order(1)
|
@Order(1)
|
||||||
@DisplayName("GET /auth/challenge should return a challenge")
|
@DisplayName("POST /api/v2/auth/token/redeem")
|
||||||
void testChallenge() throws Exception {
|
void testRedeemToken() throws Exception {
|
||||||
|
|
||||||
manager.getChallenge();
|
manager.loadPublicKeyCertificates();
|
||||||
|
manager.authChallenge();
|
||||||
|
manager.authKSeFToken();
|
||||||
|
manager.redeemToken();
|
||||||
|
|
||||||
assertNotNull(
|
assertNotNull(
|
||||||
manager.getChallengeValue(),
|
manager.getRefNumber(),
|
||||||
"Challenge darf nicht NULL sein");
|
"RefNumber darf nicht NULL sein");
|
||||||
|
|
||||||
assertFalse(
|
assertNotNull(
|
||||||
manager.getChallengeValue().isBlank(),
|
manager.getAuthToken(),
|
||||||
"Challenge darf nicht leer sein");
|
"AuthToken darf nicht leer sein");
|
||||||
|
|
||||||
System.out.println("Challenge = " + manager.getChallengeValue());
|
|
||||||
}
|
|
||||||
|
|
||||||
// ------------------------------------------------------------
|
|
||||||
// 2) Zertifikate holen (nur Public Keys)
|
|
||||||
// ------------------------------------------------------------
|
|
||||||
@Test
|
|
||||||
@Order(2)
|
|
||||||
@DisplayName("GET /security/public-key-certificates should return symmetric key certificate")
|
|
||||||
void testPublicKeys() throws Exception {
|
|
||||||
|
|
||||||
manager.getKsefPublicKeys();
|
|
||||||
|
|
||||||
assertNotNull(manager.getSymmetricPublicKey(), "Symmetric RSA PublicKey muss vorhanden sein");
|
|
||||||
assertNotNull(manager.getSymmetricCertBase64(), "Base64 Zertifikat muss vorhanden sein");
|
|
||||||
|
|
||||||
System.out.println("Symmetric PublicKey OK");
|
|
||||||
}
|
|
||||||
|
|
||||||
// ------------------------------------------------------------
|
|
||||||
// 3) KSeF Token Request
|
|
||||||
// ------------------------------------------------------------
|
|
||||||
@Test
|
|
||||||
@Order(3)
|
|
||||||
@DisplayName("POST /auth/ksef-token should return a token")
|
|
||||||
void testKsefToken() throws Exception {
|
|
||||||
|
|
||||||
// Challenge muss vorher geholt worden sein
|
|
||||||
manager.getChallenge();
|
|
||||||
|
|
||||||
// Token verschlüsseln
|
|
||||||
manager.generateAuthToken();
|
|
||||||
|
|
||||||
// Token-Call durchführen
|
|
||||||
String authToken = manager.getAuthToken();
|
|
||||||
assertNotNull(authToken, "auth token darf nicht NULL sein");
|
|
||||||
|
|
||||||
String refNumber = manager.getRefNumber();
|
|
||||||
assertNotNull(refNumber, "refNumber darf nicht NULL sein");
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue