summaryrefslogtreecommitdiff
path: root/pdf-over-gui/src/main/java/at/asit/pdfover/gui/bku
diff options
context:
space:
mode:
authorJakob Heher <jakob.heher@iaik.tugraz.at>2022-10-05 11:39:07 +0200
committerJakob Heher <jakob.heher@iaik.tugraz.at>2022-10-05 11:39:07 +0200
commit4269338d2e11028a880c99eb906c93a397fd0c1f (patch)
treeaf3ab0f0988fe088e81fc946c38cf47fbaf47e07 /pdf-over-gui/src/main/java/at/asit/pdfover/gui/bku
parentd6f4b34eae2e977cdd0339fb17302976fdae0574 (diff)
downloadpdf-over-4269338d2e11028a880c99eb906c93a397fd0c1f.tar.gz
pdf-over-4269338d2e11028a880c99eb906c93a397fd0c1f.tar.bz2
pdf-over-4269338d2e11028a880c99eb906c93a397fd0c1f.zip
FIDO2 support once again
Diffstat (limited to 'pdf-over-gui/src/main/java/at/asit/pdfover/gui/bku')
-rw-r--r--pdf-over-gui/src/main/java/at/asit/pdfover/gui/bku/MobileBKUConnector.java120
-rw-r--r--pdf-over-gui/src/main/java/at/asit/pdfover/gui/bku/OLDmobile/ATrustHandler.java15
-rw-r--r--pdf-over-gui/src/main/java/at/asit/pdfover/gui/bku/mobile/ATrustParser.java90
3 files changed, 159 insertions, 66 deletions
diff --git a/pdf-over-gui/src/main/java/at/asit/pdfover/gui/bku/MobileBKUConnector.java b/pdf-over-gui/src/main/java/at/asit/pdfover/gui/bku/MobileBKUConnector.java
index 14971ce1..1c07376c 100644
--- a/pdf-over-gui/src/main/java/at/asit/pdfover/gui/bku/MobileBKUConnector.java
+++ b/pdf-over-gui/src/main/java/at/asit/pdfover/gui/bku/MobileBKUConnector.java
@@ -5,12 +5,16 @@ import java.net.URI;
import java.net.URISyntaxException;
import java.nio.charset.Charset;
import java.util.ArrayList;
+import java.util.Base64;
import java.util.List;
import java.util.Optional;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
+import javax.annotation.CheckForNull;
import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+import javax.swing.text.html.HTML.Tag;
import org.apache.hc.client5.http.classic.methods.HttpGet;
import org.apache.hc.client5.http.classic.methods.HttpPost;
@@ -20,6 +24,7 @@ import org.apache.hc.client5.http.entity.mime.MultipartEntityBuilder;
import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
import org.apache.hc.client5.http.impl.classic.CloseableHttpResponse;
import org.apache.hc.client5.http.impl.classic.HttpClients;
+import org.apache.hc.client5.http.impl.classic.RequestFailedException;
import org.apache.hc.core5.http.ClassicHttpRequest;
import org.apache.hc.core5.http.ContentType;
import org.apache.hc.core5.http.Header;
@@ -46,6 +51,10 @@ import at.asit.pdfover.signer.BkuSlConnector;
import at.asit.pdfover.signer.SignatureException;
import at.asit.pdfover.signer.UserCancelledException;
import at.asit.pdfover.signer.pdfas.PdfAs4SLRequest;
+import at.asit.webauthn.PublicKeyCredentialRequestOptions;
+import at.asit.webauthn.WebAuthN;
+import at.asit.webauthn.exceptions.WebAuthNOperationFailed;
+import at.asit.webauthn.exceptions.WebAuthNUserCancelled;
import static at.asit.pdfover.commons.Constants.ISNOTNULL;
@@ -80,6 +89,9 @@ public class MobileBKUConnector implements BkuSlConnector {
}
}
+ /* some anti-infinite-loop safeguards so we don't murder the atrust servers by accident */
+ private int loopHTTPRequestCounter = 0;
+ private Long lastHTTPRequestTime = null;
/**
* Sends the specified request, following redirects (including meta-tag redirects) recursively
* @return The JSOUP document retrieved
@@ -89,6 +101,16 @@ public class MobileBKUConnector implements BkuSlConnector {
* @throws InterruptedException
*/
private @Nonnull ATrustParser.Result sendHTTPRequest(CloseableHttpClient httpClient, ClassicHttpRequest request) throws IOException, ProtocolException, URISyntaxException {
+ long now = System.nanoTime();
+ if ((lastHTTPRequestTime != null) && ((now - lastHTTPRequestTime) < 2e+9)) { /* less than 2s since last request */
+ ++loopHTTPRequestCounter;
+ if (loopHTTPRequestCounter > 250)
+ throw new IOException("Infinite loop protection triggered");
+ } else {
+ loopHTTPRequestCounter = 0;
+ }
+ lastHTTPRequestTime = now;
+
log.debug("Sending request to '{}'...", request.getUri().toString());
try (final CloseableHttpResponse response = httpClient.execute(request)) {
int httpStatus = response.getCode();
@@ -176,17 +198,30 @@ public class MobileBKUConnector implements BkuSlConnector {
return post;
}
- private static @Nonnull ClassicHttpRequest buildFormSubmit(@Nonnull ATrustParser.HTMLResult html, @Nonnull String submitButtonId) {
+ private static @Nonnull ClassicHttpRequest buildFormSubmit(@Nonnull ATrustParser.HTMLResult html, @CheckForNull String submitButton) {
HttpPost post = new HttpPost(html.formTarget);
var builder = MultipartEntityBuilder.create();
for (var pair : html.iterateFormOptions())
builder.addTextBody(pair.getKey(), pair.getValue());
- if (html.submitButtons.containsKey(submitButtonId)) {
- var submitInfo = html.submitButtons.get(submitButtonId);
- builder.addTextBody(submitInfo.name, submitInfo.value);
- } else {
- log.warn("Attempted to use submit button #{} which does not exist. Omitting.", submitButtonId);
+
+ if (submitButton != null) {
+ var submitButtonElm = html.htmlDocument.selectFirst(submitButton);
+ if (submitButtonElm != null) {
+ if ("input".equalsIgnoreCase(submitButtonElm.tagName())) {
+ if ("submit".equalsIgnoreCase(submitButtonElm.attr("type"))) {
+ String name = submitButtonElm.attr("name");
+ if (!name.isEmpty())
+ builder.addTextBody(name, submitButtonElm.attr("value"));
+ } else {
+ log.warn("Skipped specified submitButton {}, type is {} (not submit)", submitButton, submitButtonElm.attr("type"));
+ }
+ } else {
+ log.warn("Skipped specified submitButton {}, tag name is {} (not input)", submitButton, submitButtonElm.tagName());
+ }
+ } else {
+ log.warn("Skipped specified submitButton {}, element not found", submitButton);
+ }
}
post.setEntity(builder.build());
@@ -198,26 +233,41 @@ public class MobileBKUConnector implements BkuSlConnector {
* @return the next request to make, or null if the current response should be returned
*/
private @Nonnull ClassicHttpRequest presentResponseToUserAndReturnNextRequest(@Nonnull ATrustParser.HTMLResult html) throws UserCancelledException {
+ if (html.errorBlock != null) {
+ try {
+ this.credentials.password = null;
+ this.state.clearRememberedPassword();
+
+ if (html.errorBlock.isRecoverable)
+ this.state.showRecoverableError(html.errorBlock.errorText);
+ else
+ this.state.showUnrecoverableError(html.errorBlock.errorText);
+ return buildFormSubmit(html, "#Button_Back");
+ } catch (UserCancelledException e) {
+ return buildFormSubmit(html, "#Button_Cancel");
+ }
+ }
if (html.usernamePasswordBlock != null) {
try {
while ((this.credentials.username == null) || (this.credentials.password == null)) {
this.state.getCredentialsFromUserTo(this.credentials, null); // TODO error message
}
html.usernamePasswordBlock.setUsernamePassword(this.credentials.username, this.credentials.password);
- return buildFormSubmit(html, "Button_Identification");
+ return buildFormSubmit(html, "#Button_Identification");
} catch (UserCancelledException e) {
- return buildFormSubmit(html, "Button_Cancel");
+ return buildFormSubmit(html, "#Button_Cancel");
}
}
if (html.qrCodeBlock != null) {
try (final CloseableHttpClient httpClient = HttpClients.custom().disableRedirectHandling().build()) {
- final HttpGet request = new HttpGet(html.qrCodeBlock.qrCodeURI);
+ final HttpGet request = new HttpGet(html.qrCodeBlock.pollingURI);
boolean[] done = new boolean[1];
done[0] = false;
Thread longPollThread = new Thread(() -> {
+ long timeout = System.nanoTime() + (300l * 1000l * 1000l * 1000l); /* a-trust timeout is 5 minutes */
log.debug("longPollThread hello");
while (!done[0]) {
- try (final CloseableHttpResponse response = httpClient.execute(new HttpGet(html.qrCodeBlock.pollingURI))) {
+ try (final CloseableHttpResponse response = httpClient.execute(request)) {
JSONObject jsonResponse = new JSONObject(EntityUtils.toString(response.getEntity()));
if (jsonResponse.getBoolean("Fin"))
state.signalQRScanned();
@@ -233,8 +283,11 @@ public class MobileBKUConnector implements BkuSlConnector {
break;
}
} catch (NoHttpResponseException e) {
- continue;
+ if (timeout <= System.nanoTime())
+ state.signalQRScanned(); /* reload to find the timeout error */
+ continue; /* httpclient timeout */
} catch (IOException | ParseException | IllegalStateException e) {
+ if (done[0]) break;
log.warn("QR code long polling exception", e);
/* sleep so we don't hammer a-trust too hard in case this goes wrong */
try { Thread.sleep(5000); } catch (InterruptedException e2) {}
@@ -244,19 +297,54 @@ public class MobileBKUConnector implements BkuSlConnector {
});
try {
longPollThread.start();
- MobileBKUState.QRResult result = this.state.showQRCode(html.qrCodeBlock.referenceValue, html.qrCodeBlock.qrCodeURI, null);
+ MobileBKUState.QRResult result = this.state.showQRCode(html.qrCodeBlock.referenceValue, html.qrCodeBlock.qrCodeURI, html.signatureDataLink, html.smsTanLink != null, html.fido2Link != null, null);
switch (result) {
- case UPDATE: return new HttpGet(html.htmlDocument.baseUri());
- case TO_FIDO2: throw new IllegalStateException();
- case TO_SMS: throw new IllegalStateException();
+ case UPDATE: break;
+ case TO_FIDO2: if (html.fido2Link != null) return new HttpGet(html.fido2Link); break;
+ case TO_SMS: if (html.smsTanLink != null) return new HttpGet(html.smsTanLink); break;
}
+ return new HttpGet(html.htmlDocument.baseUri());
} finally {
done[0] = true;
request.abort();
try { longPollThread.join(1000); } catch (InterruptedException e) {}
}
} catch (IOException e) {
- log.warn("closing CloseableHttpClient threw exception", e);
+ log.warn("closing long-polling HttpClient threw exception", e);
+ }
+ }
+ if (html.fido2Block != null) {
+ // TODO composite for this
+ if (WebAuthN.isAvailable()) {
+ try {
+ var fido2Assertion = PublicKeyCredentialRequestOptions.FromJSONString(html.fido2Block.fidoOptions).get("https://service.a-trust.at");
+
+ Base64.Encoder base64 = Base64.getEncoder();
+
+ JSONObject aTrustAssertion = new JSONObject();
+ aTrustAssertion.put("id", fido2Assertion.id);
+ aTrustAssertion.put("rawId", base64.encodeToString(fido2Assertion.rawId));
+ aTrustAssertion.put("type", fido2Assertion.type);
+ aTrustAssertion.put("extensions", new JSONObject()); // TODO fix extensions in library
+
+ JSONObject aTrustAssertionResponse = new JSONObject();
+ aTrustAssertion.put("response", aTrustAssertionResponse);
+ aTrustAssertionResponse.put("authenticatorData", base64.encodeToString(fido2Assertion.response.authenticatorData));
+ aTrustAssertionResponse.put("clientDataJson", base64.encodeToString(fido2Assertion.response.clientDataJSON));
+ aTrustAssertionResponse.put("signature", base64.encodeToString(fido2Assertion.response.signature));
+ if (fido2Assertion.response.userHandle != null)
+ aTrustAssertionResponse.put("userHandle", base64.encodeToString(fido2Assertion.response.userHandle));
+ else
+ aTrustAssertionResponse.put("userHandle", JSONObject.NULL);
+
+ html.fido2Block.setFIDOResult(aTrustAssertion.toString());
+ return buildFormSubmit(html, "#FidoContinue");
+ } catch (WebAuthNUserCancelled e) {
+ log.debug("WebAuthN authentication cancelled by user");
+ throw new UserCancelledException();
+ } catch (WebAuthNOperationFailed e) {
+ log.warn("WebAuthN authentication failed", e);
+ }
}
}
throw new IllegalStateException("No top-level block is set? Something has gone terribly wrong.");
diff --git a/pdf-over-gui/src/main/java/at/asit/pdfover/gui/bku/OLDmobile/ATrustHandler.java b/pdf-over-gui/src/main/java/at/asit/pdfover/gui/bku/OLDmobile/ATrustHandler.java
index 2e69e779..ab85645b 100644
--- a/pdf-over-gui/src/main/java/at/asit/pdfover/gui/bku/OLDmobile/ATrustHandler.java
+++ b/pdf-over-gui/src/main/java/at/asit/pdfover/gui/bku/OLDmobile/ATrustHandler.java
@@ -57,6 +57,7 @@ import at.asit.pdfover.gui.controls.Dialog.BUTTONS;
import at.asit.pdfover.gui.controls.Dialog.ICON;
import at.asit.pdfover.gui.exceptions.ATrustConnectionException;
import at.asit.pdfover.gui.utils.FileUploadSource;
+import at.asit.pdfover.gui.utils.SWTUtils;
import at.asit.pdfover.commons.Messages;
import at.asit.pdfover.gui.workflow.states.LocalBKUState;
import at.asit.pdfover.gui.workflow.states.MobileBKUState;
@@ -399,25 +400,13 @@ public class ATrustHandler {
status.errorMessage = null;
- final Document responseDocument = Jsoup.parse(responseData);
-
if (responseData.contains("ExpiresInfo.aspx?sid=")) {
// Certificate expiration interstitial - skip
if (!expiryNoticeDisplayed) {
Display.getDefault().syncExec(()-> {
Dialog d = new Dialog(ATrustHandler.this.shell, Messages.getString("common.info"), Messages.getString("mobileBKU.certExpiresSoon"), BUTTONS.YES_NO, ICON.WARNING);
if (d.open() == SWT.YES) {
- log.debug("Trying to open " + ACTIVATION_URL);
- if (Desktop.isDesktopSupported()) {
- try {
- Desktop.getDesktop().browse(new URI(ACTIVATION_URL));
- return;
- } catch (Exception e) {
- log.debug("Error opening URL", e);
- }
- }
- log.info("SWT Desktop is not supported on this platform");
- Program.launch(ACTIVATION_URL);
+ SWTUtils.openURL(ACTIVATION_URL);
}
});
expiryNoticeDisplayed = true;
diff --git a/pdf-over-gui/src/main/java/at/asit/pdfover/gui/bku/mobile/ATrustParser.java b/pdf-over-gui/src/main/java/at/asit/pdfover/gui/bku/mobile/ATrustParser.java
index 890ffad1..16f571a3 100644
--- a/pdf-over-gui/src/main/java/at/asit/pdfover/gui/bku/mobile/ATrustParser.java
+++ b/pdf-over-gui/src/main/java/at/asit/pdfover/gui/bku/mobile/ATrustParser.java
@@ -24,8 +24,8 @@ public class ATrustParser {
private static class ComponentParseFailed extends Exception {}
private static class TopLevelFormBlock {
- protected @Nonnull org.jsoup.nodes.Document htmlDocument;
- protected @Nonnull Map<String, String> formOptions;
+ protected final @Nonnull org.jsoup.nodes.Document htmlDocument;
+ protected final @Nonnull Map<String, String> formOptions;
protected TopLevelFormBlock(@Nonnull org.jsoup.nodes.Document d, @Nonnull Map<String,String> fO) { this.htmlDocument = d; this.formOptions = fO; }
protected void abortIfElementMissing(@Nonnull String selector) throws ComponentParseFailed {
@@ -56,30 +56,49 @@ public class ATrustParser {
}
}
+ public static class ErrorBlock extends TopLevelFormBlock {
+ public final boolean isRecoverable;
+ public final @Nonnull String errorText;
+
+ private ErrorBlock(@Nonnull org.jsoup.nodes.Document htmlDocument, @Nonnull URI formTarget, @Nonnull Map<String, String> formOptions) throws ComponentParseFailed {
+ super(htmlDocument, formOptions);
+ if (!formTarget.getPath().contains("/error.aspx"))
+ throw new ComponentParseFailed();
+
+ this.isRecoverable = (htmlDocument.selectFirst("#Button_Back") != null);
+
+ String errorText = getElementEnsureNotNull("#Label1").ownText();
+ if (errorText.startsWith("Fehler:"))
+ errorText = errorText.substring(7);
+ this.errorText = ISNOTNULL(errorText.trim());
+ }
+ }
+
public static class UsernamePasswordBlock extends TopLevelFormBlock {
- private @Nonnull String usernameKey;
- private @Nonnull String passwordKey;
- public @CheckForNull String errorMessage;
+ private final @Nonnull String usernameKey;
+ private final @Nonnull String passwordKey;
+ public final @CheckForNull String errorMessage;
public void setUsernamePassword(String username, String password) {
formOptions.put(usernameKey, username); formOptions.put(passwordKey, password);
}
- private UsernamePasswordBlock(@Nonnull org.jsoup.nodes.Document htmlDocument, @Nonnull Map<String, String> formOptions) throws ComponentParseFailed {
+ private UsernamePasswordBlock(@Nonnull org.jsoup.nodes.Document htmlDocument, @Nonnull URI formTarget, @Nonnull Map<String, String> formOptions) throws ComponentParseFailed {
super(htmlDocument, formOptions);
abortIfElementMissing("#handynummer");
this.usernameKey = getAttributeEnsureNotNull("#handynummer", "name");
this.passwordKey = getAttributeEnsureNotNull("#signaturpasswort", "name");
+ this.errorMessage = null;
}
}
public static class QRCodeBlock extends TopLevelFormBlock {
- public @Nonnull String referenceValue;
- public @Nonnull URI qrCodeURI;
- public @Nonnull URI pollingURI;
- public @Nullable String errorMessage;
+ public final @Nonnull String referenceValue;
+ public final @Nonnull URI qrCodeURI;
+ public final @Nonnull URI pollingURI;
+ public final @Nullable String errorMessage;
- private QRCodeBlock(@Nonnull org.jsoup.nodes.Document htmlDocument, @Nonnull Map<String, String> formOptions) throws ComponentParseFailed {
+ private QRCodeBlock(@Nonnull org.jsoup.nodes.Document htmlDocument, @Nonnull URI formTarget, @Nonnull Map<String, String> formOptions) throws ComponentParseFailed {
super(htmlDocument, formOptions);
abortIfElementMissing("#qrimage");
@@ -102,17 +121,18 @@ public class ATrustParser {
log.warn("URI '{}' could not be parsed", pollingUriString);
throw new ComponentParseFailed();
}
+
+ this.errorMessage = null;
}
}
public static class Fido2Block extends TopLevelFormBlock {
- private @Nonnull String fidoOptions;
- private @Nonnull String credentialResultKey;
+ public final @Nonnull String fidoOptions;
+ private final @Nonnull String credentialResultKey;
- public @Nonnull String getFIDOOptions() { return fidoOptions; }
public void setFIDOResult(String result) { formOptions.put(credentialResultKey, result); }
- private Fido2Block(@Nonnull org.jsoup.nodes.Document htmlDocument, @Nonnull Map<String, String> formOptions) throws ComponentParseFailed {
+ private Fido2Block(@Nonnull org.jsoup.nodes.Document htmlDocument, @Nonnull URI formTarget, @Nonnull Map<String, String> formOptions) throws ComponentParseFailed {
super(htmlDocument, formOptions);
abortIfElementMissing("#fidoBlock");
this.fidoOptions = getAttributeEnsureNotNull("#credentialOptions", "value");
@@ -125,22 +145,15 @@ public class ATrustParser {
public final @Nonnull URI formTarget;
public final @Nonnull Map<String, String> formOptions = new HashMap<>();
- public static class NameValuePair {
- public final @Nonnull String name;
- public final @Nonnull String value;
- public NameValuePair(@Nonnull String n, @Nonnull String v) { name = n; value = v; }
- }
- /**
- * map: id -> (name, value)
- */
- public final @Nonnull Map<String, @CheckForNull NameValuePair> submitButtons = new HashMap<>();
-
public @Nonnull Iterable<Map.Entry<String, String>> iterateFormOptions() { return ISNOTNULL(formOptions.entrySet()); }
- /* optional mode switch links (any number may or may not be null) */
+ /* optional links (any number may or may not be null) */
+ public final @CheckForNull URI signatureDataLink;
+ public final @CheckForNull URI smsTanLink;
public final @CheckForNull URI fido2Link;
/* top-level blocks (exactly one is not null) */
+ public final @CheckForNull ErrorBlock errorBlock;
public final @CheckForNull UsernamePasswordBlock usernamePasswordBlock;
public final @CheckForNull QRCodeBlock qrCodeBlock;
public final @CheckForNull Fido2Block fido2Block;
@@ -148,6 +161,7 @@ public class ATrustParser {
private void validate() {
Set<String> populated = new HashSet<>();
+ if (errorBlock != null) populated.add("errorBlock");
if (usernamePasswordBlock != null) populated.add("usernamePasswordBlock");
if (qrCodeBlock != null) populated.add("qrCodeBlock");
if (fido2Block != null) populated.add("fido2Block");
@@ -178,7 +192,7 @@ public class ATrustParser {
*/
private <T extends TopLevelFormBlock> @Nullable T TryParseMainBlock(Class<T> clazz) {
try {
- return clazz.getDeclaredConstructor(org.jsoup.nodes.Document.class, Map.class).newInstance(this.htmlDocument, this.formOptions);
+ return clazz.getDeclaredConstructor(org.jsoup.nodes.Document.class, URI.class, Map.class).newInstance(this.htmlDocument, this.formTarget, this.formOptions);
} catch (NoSuchMethodException | InstantiationException | IllegalAccessException | IllegalArgumentException | SecurityException e) {
log.error("Internal parser error; check your method signatures?", e);
return null;
@@ -194,6 +208,7 @@ public class ATrustParser {
}
private HTMLResult(@Nonnull org.jsoup.nodes.Document htmlDocument) {
+ log.trace("Now parsing:\n{}", htmlDocument.toString());
this.htmlDocument = htmlDocument;
var forms = htmlDocument.getElementsByTag("form");
@@ -214,20 +229,21 @@ public class ATrustParser {
for (var input : mainForm.select("input")) {
String name = input.attr("name");
- /* special handling for submit inputs, they only get sent if they are "clicked" */
- if ("submit".equals(input.attr("type"))) {
- if (name.isEmpty())
- this.submitButtons.put(input.attr("id"), null);
- else
- this.submitButtons.put(input.attr("id"), new NameValuePair(name, ISNOTNULL(input.attr("value"))));
- } else {
- if (!name.isEmpty())
- this.formOptions.put(name, input.attr("value"));
- }
+ if (name.isEmpty())
+ continue;
+
+ /* submit inputs omitted here, they only get sent if they are "clicked", cf. MobileBKUConnector::buildFormSubmit */
+ if ("submit".equalsIgnoreCase(input.attr("type")))
+ continue;
+
+ this.formOptions.put(name, input.attr("value"));
}
+ this.signatureDataLink = getHrefIfExists("#LinkList a[href*=\"ShowSigobj.aspx\"]"); /* grr, they didn't give it an ID */
+ this.smsTanLink = getHrefIfExists("#SmsButton");
this.fido2Link = getHrefIfExists("#FidoButton");
+ this.errorBlock = TryParseMainBlock(ErrorBlock.class);
this.usernamePasswordBlock = TryParseMainBlock(UsernamePasswordBlock.class);
this.qrCodeBlock = TryParseMainBlock(QRCodeBlock.class);
this.fido2Block = TryParseMainBlock(Fido2Block.class);