diff options
16 files changed, 307 insertions, 387 deletions
| diff --git a/pdf-over-gui/pom.xml b/pdf-over-gui/pom.xml index 4cb083e3..125a3e33 100644 --- a/pdf-over-gui/pom.xml +++ b/pdf-over-gui/pom.xml @@ -74,6 +74,11 @@  			<groupId>com.google.code.findbugs</groupId>  			<artifactId>jsr305</artifactId>  		</dependency> +		<dependency> +			<groupId>at.a-sit</groupId> +			<artifactId>webauthn-java</artifactId> +			<version>0.0.1-SNAPSHOT</version> +		</dependency>  	</dependencies>  	<dependencyManagement>  		<dependencies> 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); diff --git a/pdf-over-gui/src/main/java/at/asit/pdfover/gui/composites/MobileBKUEnterNumberComposite.java b/pdf-over-gui/src/main/java/at/asit/pdfover/gui/composites/MobileBKUEnterNumberComposite.java index 5f228bcb..6c75b160 100644 --- a/pdf-over-gui/src/main/java/at/asit/pdfover/gui/composites/MobileBKUEnterNumberComposite.java +++ b/pdf-over-gui/src/main/java/at/asit/pdfover/gui/composites/MobileBKUEnterNumberComposite.java @@ -72,12 +72,11 @@ public class MobileBKUEnterNumberComposite extends StateComposite {  				MobileBKUEnterNumberComposite.this.btn_ok.setEnabled(false);
  			} catch(InvalidPasswordException ex) {
 -				log.error("Validating input for Mobile BKU failed!", ex);
 +				log.info("Validating input for Mobile BKU failed!", ex);
  				MobileBKUEnterNumberComposite.this.setErrorMessage(ex.getMessage());
  				MobileBKUEnterNumberComposite.this.txt_password.setFocus();
 -			}
 -			catch (Exception ex) {
 -				log.error("Validating input for Mobile BKU failed!", ex);
 +			} catch (Exception ex) {
 +				log.info("Validating input for Mobile BKU failed!", ex);
  				MobileBKUEnterNumberComposite.this.setErrorMessage(Messages.getString("error.InvalidPhoneNumber"));
  				MobileBKUEnterNumberComposite.this.txt_number.setFocus();
  				return;
 diff --git a/pdf-over-gui/src/main/java/at/asit/pdfover/gui/composites/MobileBKUEnterTANComposite.java b/pdf-over-gui/src/main/java/at/asit/pdfover/gui/composites/MobileBKUEnterTANComposite.java index 7fe40ffe..ab5f5962 100644 --- a/pdf-over-gui/src/main/java/at/asit/pdfover/gui/composites/MobileBKUEnterTANComposite.java +++ b/pdf-over-gui/src/main/java/at/asit/pdfover/gui/composites/MobileBKUEnterTANComposite.java @@ -41,6 +41,8 @@ import org.eclipse.swt.widgets.Text;  import org.slf4j.Logger;
  import org.slf4j.LoggerFactory;
 +import com.beust.jcommander.internal.Nullable;
 +
  import at.asit.pdfover.commons.Constants;
  import at.asit.pdfover.commons.Messages;
  import at.asit.pdfover.gui.utils.SWTUtils;
 @@ -55,11 +57,6 @@ public class MobileBKUEnterTANComposite extends StateComposite {  	 *
  	 */
  	private final class OkSelectionListener extends SelectionAdapter {
 -		/**
 -		 * Empty constructor
 -		 */
 -		public OkSelectionListener() {
 -		}
  		@Override
  		public void widgetSelected(SelectionEvent e) {
 @@ -127,25 +124,21 @@ public class MobileBKUEnterTANComposite extends StateComposite {  	String refVal;
 -	String signatureData;
 -
 -	/**
 -	 * @return the signatureData
 -	 */
 -	public String getSignatureData() {
 -		return this.signatureData;
 -	}
 +	URI signatureDataURI;
  	/**
  	 * @param signatureData
  	 *            the signatureData to set
  	 */
 -	public void setSignatureData(String signatureData) {
 -		this.signatureData = signatureData;
 +	public void setSignatureDataURI(@Nullable URI uri) {
 +		this.signatureDataURI = uri;
 +		this.lnk_sig_data.setEnabled(uri != null);
  	}
  	String tan;
 +	private Link lnk_sig_data;
 +
  	private Label lblTries;
  	private Label lblRefValLabel;
  	private Label lblTan;
 @@ -268,36 +261,6 @@ public class MobileBKUEnterTANComposite extends StateComposite {  	}
  	/**
 -	 * Selection Listener for open button
 -	 */
 -	private final class ShowSignatureDataListener extends SelectionAdapter {
 -		/**
 -		 * Empty constructor
 -		 */
 -		public ShowSignatureDataListener() {
 -		}
 -
 -		@Override
 -		public void widgetSelected(SelectionEvent e) {
 -			try {
 -				String signatureData = MobileBKUEnterTANComposite.this
 -						.getSignatureData();
 -				if (signatureData != null && !signatureData.equals("")) {
 -					log.debug("Trying to open " + signatureData);
 -					if (Desktop.isDesktopSupported()) {
 -						Desktop.getDesktop().browse(new URI(signatureData));
 -					} else {
 -						log.info("SWT Desktop is not supported on this platform");
 -						Program.launch(signatureData);
 -					}
 -				}
 -			} catch (Exception ex) {
 -				log.error("OpenSelectionListener: ", ex);
 -			}
 -		}
 -	}
 -
 -	/**
  	 * Create the composite.
  	 *
  	 * @param parent
 @@ -368,27 +331,22 @@ public class MobileBKUEnterTANComposite extends StateComposite {  				if (text.length() > 3
  						&& MobileBKUEnterTANComposite.this.getRefVal()
  								.startsWith(text.trim())) {
 -					MobileBKUEnterTANComposite.this.setMessage(Messages
 -							.getString("error.EnteredReferenceValue"));
 +					MobileBKUEnterTANComposite.this.setMessage(Messages.getString("error.EnteredReferenceValue"));
  				}
  			}
  		});
 -		Link lnk_sig_data = new Link(containerComposite, SWT.NATIVE | SWT.RESIZE);
 +		this.lnk_sig_data = new Link(containerComposite, SWT.NATIVE | SWT.RESIZE);
  		SWTUtils.anchor(lnk_sig_data).right(100,-20).top(0,20);
  		lnk_sig_data.setEnabled(true);
 -		lnk_sig_data.addSelectionListener(new ShowSignatureDataListener());
 -		SWTUtils.setLocalizedText(lnk_sig_data, "mobileBKU.show");
 -		SWTUtils.setLocalizedToolTipText(lnk_sig_data, "mobileBKU.show_tooltip");
 +		SWTUtils.addSelectionListener(lnk_sig_data, (e) -> { SWTUtils.openURL(this.signatureDataURI); });
  		this.btn_ok = new Button(containerComposite, SWT.NATIVE);
  		SWTUtils.anchor(btn_ok).right(100,-20).bottom(100,-20);
 -		SWTUtils.setLocalizedText(btn_ok, "common.Ok");
  		this.btn_ok.addSelectionListener(new OkSelectionListener());
  		this.btn_cancel = new Button(containerComposite, SWT.NATIVE);
  		SWTUtils.anchor(btn_cancel).right(btn_ok, -20).bottom(100, -20);
 -		SWTUtils.setLocalizedText(btn_cancel, "common.Cancel");
  		this.btn_cancel.addSelectionListener(new CancelSelectionListener());
  		this.lblTries = new Label(containerComposite, SWT.WRAP | SWT.NATIVE);
 @@ -417,7 +375,11 @@ public class MobileBKUEnterTANComposite extends StateComposite {  	 */
  	@Override
  	public void reloadResources() {
 +		SWTUtils.setLocalizedText(lnk_sig_data, "mobileBKU.show");
 +		SWTUtils.setLocalizedToolTipText(lnk_sig_data, "mobileBKU.show_tooltip");
  		SWTUtils.setLocalizedText(lblRefValLabel, "tanEnter.ReferenceValue");
  		SWTUtils.setLocalizedText(lblTan, "tanEnter.TAN");
 +		SWTUtils.setLocalizedText(btn_cancel, "common.Cancel");
 +		SWTUtils.setLocalizedText(btn_ok, "common.Ok");
  	}
  }
 diff --git a/pdf-over-gui/src/main/java/at/asit/pdfover/gui/composites/MobileBKUFingerprintComposite.java b/pdf-over-gui/src/main/java/at/asit/pdfover/gui/composites/MobileBKUFingerprintComposite.java index af630b8d..afa71f9f 100644 --- a/pdf-over-gui/src/main/java/at/asit/pdfover/gui/composites/MobileBKUFingerprintComposite.java +++ b/pdf-over-gui/src/main/java/at/asit/pdfover/gui/composites/MobileBKUFingerprintComposite.java @@ -201,37 +201,6 @@ public class MobileBKUFingerprintComposite extends StateComposite {  	}
 -
 -	/**
 -	 * Selection Listener for open button
 -	 */
 -	private final class ShowSignatureDataListener extends SelectionAdapter {
 -		/**
 -		 * Empty constructor
 -		 */
 -		public ShowSignatureDataListener() {
 -		}
 -
 -		@Override
 -		public void widgetSelected(SelectionEvent e) {
 -			try {
 -				String signatureData = MobileBKUFingerprintComposite.this
 -						.getSignatureData();
 -				if (signatureData != null && !signatureData.equals("")) {
 -					log.debug("Trying to open " + signatureData);
 -					if (Desktop.isDesktopSupported()) {
 -						Desktop.getDesktop().browse(new URI(signatureData));
 -					} else {
 -						log.info("SWT Desktop is not supported on this platform");
 -						Program.launch(signatureData);
 -					}
 -				}
 -			} catch (Exception ex) {
 -				log.error("OpenSelectionListener: ", ex);
 -			}
 -		}
 -	}
 -
  	/**
  	 * Create the composite.
  	 *
 @@ -279,7 +248,7 @@ public class MobileBKUFingerprintComposite extends StateComposite {  		this.lnk_sig_data = new Link(containerComposite, SWT.NATIVE | SWT.RESIZE);
  		SWTUtils.anchor(lnk_sig_data).right(100, -20).top(0, 20);
  		this.lnk_sig_data.setEnabled(true);
 -		this.lnk_sig_data.addSelectionListener(new ShowSignatureDataListener());
 +		SWTUtils.addSelectionListener(lnk_sig_data, (e) -> { SWTUtils.openURL(getSignatureData()); });
  		this.btn_cancel = new Button(containerComposite, SWT.NATIVE);
  		SWTUtils.anchor(btn_cancel).right(100, -20).bottom(100, -20);
 diff --git a/pdf-over-gui/src/main/java/at/asit/pdfover/gui/composites/MobileBKUQRComposite.java b/pdf-over-gui/src/main/java/at/asit/pdfover/gui/composites/MobileBKUQRComposite.java index 3f1aa04d..652baed4 100644 --- a/pdf-over-gui/src/main/java/at/asit/pdfover/gui/composites/MobileBKUQRComposite.java +++ b/pdf-over-gui/src/main/java/at/asit/pdfover/gui/composites/MobileBKUQRComposite.java @@ -20,6 +20,9 @@ import java.awt.Desktop;  import java.io.ByteArrayInputStream;
  import java.io.InputStream;
  import java.net.URI;
 +import java.util.Objects;
 +
 +import javax.annotation.Nullable;
  import org.eclipse.swt.SWT;
  import org.eclipse.swt.SWTException;
 @@ -51,52 +54,16 @@ import at.asit.pdfover.gui.workflow.states.State;  public class MobileBKUQRComposite extends StateComposite {
  	/**
 -	 *
 -	 */
 -	private final class SMSSelectionListener extends SelectionAdapter {
 -		/**
 -		 * Empty constructor
 -		 */
 -		public SMSSelectionListener() {
 -		}
 -
 -		@Override
 -		public void widgetSelected(SelectionEvent e) {
 -			if(!MobileBKUQRComposite.this.btn_sms.getEnabled()) {
 -				return;
 -			}
 -
 -			MobileBKUQRComposite.this.setUserSMS(true);
 -			MobileBKUQRComposite.this.btn_sms.setEnabled(false);
 -		}
 -	}
 -
 -	/**
 -	 *
 -	 */
 -	private final class CancelSelectionListener extends SelectionAdapter {
 -		/**
 -		 * Empty constructor
 -		 */
 -		public CancelSelectionListener() {
 -		}
 -
 -		@Override
 -		public void widgetSelected(SelectionEvent e) {
 -			MobileBKUQRComposite.this.setUserCancel(true);
 -		}
 -	}
 -
 -	/**
  	 * SLF4J Logger instance
  	 **/
  	static final Logger log = LoggerFactory.getLogger(MobileBKUQRComposite.class);
  	private Label lblQR;
 -	private boolean userCancel = false;
 -	private boolean userSMS = false;
 -	private boolean done = false;
 +	private boolean userCancelClicked = false;
 +	private boolean userSMSClicked = false;
 +	private boolean userFIDO2Clicked = false;
 +	private boolean pollingDone = false;
  	private Label lblRefVal;
 @@ -104,52 +71,24 @@ public class MobileBKUQRComposite extends StateComposite {  	private ImageData currentQRImage;
 -	private String signatureData;
 -
 -	/**
 -	 * @return the signatureData
 -	 */
 -	public String getSignatureData() {
 -		return this.signatureData;
 -	}
 -
 -	/**
 -	 * @param signatureData
 -	 *            the signatureData to set
 -	 */
 -	public void setSignatureData(String signatureData) {
 -		this.signatureData = signatureData;
 -	}
 +	private URI signatureDataURI;
  	private Label lblError;
  	private Label lblRefValLabel;
  	private Label lblQRLabel;
 +	private Button btn_fido2;
  	private Button btn_sms;
  	private Button btn_cancel;
  	private Link lnk_sig_data;
 -	/**
 -	 * @return the userCancel
 -	 */
 -	public boolean isUserCancel() {
 -		return this.userCancel;
 -	}
 -
 -	/**
 -	 * @return the userSMS
 -	 */
 -	public boolean isUserSMS() {
 -		return this.userSMS;
 -	}
 -
 -	/**
 -	 * @return the done
 -	 */
 -	public boolean isDone() {
 -		return this.done;
 -	}
 +	public void signalPollingDone() { this.pollingDone = true; getDisplay().wake(); }
 +	public boolean isDone() { return (this.userCancelClicked || this.userSMSClicked || this.userFIDO2Clicked || this.pollingDone); }
 +	public boolean wasCancelClicked() { return this.userCancelClicked; }
 +	public boolean wasSMSClicked() { return this.userSMSClicked; }
 +	public boolean wasFIDO2Clicked() { return this.userFIDO2Clicked; }
 +	public void reset() { this.userCancelClicked = this.userSMSClicked = this.userFIDO2Clicked = this.pollingDone = false; }
  	/**
  	 * Set an error message
 @@ -163,48 +102,10 @@ public class MobileBKUQRComposite extends StateComposite {  					Messages.getString("error.Title") + ": " + errorMessage);
  	}
 -	/**
 -	 * @param userCancel
 -	 *            the userCancel to set
 -	 */
 -	public void setUserCancel(boolean userCancel) {
 -		this.userCancel = userCancel;
 -	}
 -
 -	/**
 -	 * @param userSMS
 -	 *            the userSMS to set
 -	 */
 -	public void setUserSMS(boolean userSMS) {
 -		this.userSMS = userSMS;
 -	}
 -
 -	/**
 -	 * @param done
 -	 *            the done to set
 -	 */
 -	public void setDone(boolean done) {
 -		this.done = done;
 -	}
 -
 -	/**
 -	 * @return the reference value
 -	 */
 -	public String getRefVal() {
 -		return this.refVal;
 -	}
 -
 -	/**
 -	 * @param refVal
 -	 *            the reference value to set
 -	 */
 +	public String getRefVal() { return this.refVal; }
  	public void setRefVal(String refVal) {
 -		if (this.refVal != null) {
 -			this.refVal = refVal.trim();
 -			this.lblRefVal.setText(this.refVal);
 -		} else {
 -			this.lblRefVal.setText("");
 -		}
 +		this.refVal = (refVal != null) ? refVal.trim() : null;
 +		this.lblRefVal.setText(Objects.requireNonNullElse(this.refVal, ""));
  	}
  	private void updateQRImage() {
 @@ -238,34 +139,17 @@ public class MobileBKUQRComposite extends StateComposite {  		updateQRImage();
  	}
 -	/**
 -	 * Selection Listener for open button
 -	 */
 -	private final class ShowSignatureDataListener extends SelectionAdapter {
 -		/**
 -		 * Empty constructor
 -		 */
 -		public ShowSignatureDataListener() {
 -		}
 +	public void setSMSEnabled(boolean state) {
 +		this.btn_sms.setEnabled(state);
 +	}
 -		@Override
 -		public void widgetSelected(SelectionEvent e) {
 -			try {
 -				String signatureData = MobileBKUQRComposite.this
 -						.getSignatureData();
 -				if (signatureData != null && !signatureData.equals("")) {
 -					log.debug("Trying to open " + signatureData);
 -					if (Desktop.isDesktopSupported()) {
 -						Desktop.getDesktop().browse(new URI(signatureData));
 -					} else {
 -						log.info("SWT Desktop is not supported on this platform");
 -						Program.launch(signatureData);
 -					}
 -				}
 -			} catch (Exception ex) {
 -				log.error("OpenSelectionListener: ", ex);
 -			}
 -		}
 +	public void setFIDO2Enabled(boolean state) {
 +		this.btn_fido2.setEnabled(state);
 +	}
 +
 +	public void setSignatureDataURI(@Nullable URI uri) {
 +		this.signatureDataURI = uri;
 +		this.lnk_sig_data.setEnabled(uri != null);
  	}
  	/**
 @@ -317,16 +201,20 @@ public class MobileBKUQRComposite extends StateComposite {  		this.lnk_sig_data = new Link(containerComposite, SWT.NATIVE | SWT.RESIZE);
  		SWTUtils.anchor(lnk_sig_data).right(100, -20).top(0, 20);
 -		this.lnk_sig_data.setEnabled(true);
 -		this.lnk_sig_data.addSelectionListener(new ShowSignatureDataListener());
 +		SWTUtils.addSelectionListener(lnk_sig_data, (e) -> { SWTUtils.openURL(this.signatureDataURI); });
  		this.btn_cancel = new Button(containerComposite, SWT.NATIVE);
  		SWTUtils.anchor(btn_cancel).right(100, -20).bottom(100, -5);
 -		this.btn_cancel.addSelectionListener(new CancelSelectionListener());
 +		SWTUtils.addSelectionListener(btn_cancel, (e) -> { this.userCancelClicked = true; });
  		this.btn_sms = new Button(containerComposite, SWT.NATIVE);
  		SWTUtils.anchor(btn_sms).right(btn_cancel, -20).bottom(100, -5);
 -		this.btn_sms.addSelectionListener(new SMSSelectionListener());
 +		SWTUtils.addSelectionListener(btn_sms, (e) -> { this.userSMSClicked = true; });
 +
 +		this.btn_fido2 = new Button(containerComposite, SWT.NATIVE);
 +		SWTUtils.anchor(btn_fido2).right(btn_sms, -20).bottom(100, -5);
 +		SWTUtils.addSelectionListener(btn_fido2, (e) -> {this.userFIDO2Clicked = true; });
 +
  		SWTUtils.anchor(lblQR).left(50, 10).right(100, -20).top(lblRefVal, 10).bottom(btn_sms, -10);
 @@ -364,5 +252,6 @@ public class MobileBKUQRComposite extends StateComposite {  		SWTUtils.setLocalizedToolTipText(lnk_sig_data, "mobileBKU.show_tooltip");
  		SWTUtils.setLocalizedText(btn_cancel, "common.Cancel");
  		SWTUtils.setLocalizedText(btn_sms, "tanEnter.SMS");
 +		SWTUtils.setLocalizedText(btn_fido2, "tanEnter.FIDO2");
  	}
  }
 diff --git a/pdf-over-gui/src/main/java/at/asit/pdfover/gui/composites/configuration/AboutComposite.java b/pdf-over-gui/src/main/java/at/asit/pdfover/gui/composites/configuration/AboutComposite.java index 35b73e7b..b4675e2a 100644 --- a/pdf-over-gui/src/main/java/at/asit/pdfover/gui/composites/configuration/AboutComposite.java +++ b/pdf-over-gui/src/main/java/at/asit/pdfover/gui/composites/configuration/AboutComposite.java @@ -47,6 +47,7 @@ public class AboutComposite extends ConfigurationCompositeBase {  		this.lnkAbout = new Link(this, SWT.WRAP);  		SWTUtils.anchor(lnkAbout).top(0,5).right(100,-5).left(0,5);  		SWTUtils.setFontHeight(lnkAbout, Constants.TEXT_SIZE_NORMAL); +		SWTUtils.addSelectionListener(lnkAbout, (e) -> { SWTUtils.openURL(Messages.getString("config.LicenseURL")); });  		this.lblDataProtection = new Label(this, SWT.WRAP);  		SWTUtils.anchor(lblDataProtection).top(lnkAbout, 15).left(0,5).right(100,-5); @@ -56,6 +57,7 @@ public class AboutComposite extends ConfigurationCompositeBase {  		this.lnkDataProtection = new Link(this, SWT.WRAP);  		SWTUtils.anchor(lnkDataProtection).top(lblDataProtection,10).left(0,5).right(100,-5);  		SWTUtils.setFontHeight(lnkDataProtection, Constants.TEXT_SIZE_NORMAL); +		SWTUtils.addSelectionListener(lnkDataProtection, (e) -> { SWTUtils.openURL(Messages.getString("config.DataProtectionURL")); });  		this.lnkUpdateCheckStatus = new Link(this, SWT.NONE);  		SWTUtils.anchor(lnkUpdateCheckStatus).bottom(100, -5).left(0,5); @@ -84,56 +86,13 @@ public class AboutComposite extends ConfigurationCompositeBase {  		SWTUtils.reanchor(lnkDataProtection).bottom(btnUpdateCheck,-5); -		this.lnkAbout.addSelectionListener(new SelectionAdapter() { -			@Override -			public void widgetSelected(SelectionEvent e) { -				try { -					URI url = new URI(Messages.getString("config.LicenseURL")); -					log.debug("Trying to open " + url.toString()); -					if (Desktop.isDesktopSupported()) { -						Desktop.getDesktop().browse(url); -					} else { -						log.info("AWT Desktop is not supported on this platform"); -						Program.launch(url.toString()); -					} -				} catch (IOException ex) { -					log.error("AboutComposite: ", ex); -				} catch (URISyntaxException ex) { -					log.error("AboutComposite: ", ex); -				} -			} -		}); - -		this.lnkDataProtection.addSelectionListener(new SelectionAdapter() { -			@Override -			public void widgetSelected(SelectionEvent e) { -				try { -					URI url = new URI(Messages.getString("config.DataProtectionURL")); -					log.debug("Trying to open " + url.toString()); -					if (Desktop.isDesktopSupported()) { -						Desktop.getDesktop().browse(url); -					} else { -						log.info("AWT Desktop is not supported on this platform"); -						Program.launch(url.toString()); -					} -				} catch (IOException ex) { -					log.error("AboutComposite: ", ex); -				} catch (URISyntaxException ex) { -					log.error("AboutComposite: ", ex); -				} -			} -		}); - -		this.btnOpenLogDirectory.addSelectionListener(new SelectionAdapter() { -			@Override -			public void widgetSelected(SelectionEvent e) { -				try -				{ -					if (Desktop.isDesktopSupported()) -						Desktop.getDesktop().open(new File(Constants.CONFIG_DIRECTORY + File.separator + "logs")); -				} catch (Exception ex) { -					log.warn("Failed to open log directory: ", ex); -				} +		SWTUtils.addSelectionListener(btnOpenLogDirectory, (e) -> { +			try +			{ +				if (Desktop.isDesktopSupported()) +					Desktop.getDesktop().open(new File(Constants.CONFIG_DIRECTORY + File.separator + "logs")); +			} catch (Exception ex) { +				log.warn("Failed to open log directory: ", ex);  			}  		}); diff --git a/pdf-over-gui/src/main/java/at/asit/pdfover/gui/utils/SWTUtils.java b/pdf-over-gui/src/main/java/at/asit/pdfover/gui/utils/SWTUtils.java index ef93e3e9..d276ecd6 100644 --- a/pdf-over-gui/src/main/java/at/asit/pdfover/gui/utils/SWTUtils.java +++ b/pdf-over-gui/src/main/java/at/asit/pdfover/gui/utils/SWTUtils.java @@ -1,9 +1,14 @@  package at.asit.pdfover.gui.utils; +import java.awt.Desktop;  import java.lang.reflect.InvocationTargetException;  import java.lang.reflect.Method; +import java.net.URI; +import java.net.URISyntaxException;  import java.util.function.Consumer; +import javax.annotation.Nullable; +  import org.eclipse.swt.SWT;  import org.eclipse.swt.custom.ScrolledComposite;  import org.eclipse.swt.events.SelectionAdapter; @@ -14,6 +19,7 @@ import org.eclipse.swt.graphics.FontData;  import org.eclipse.swt.graphics.Point;  import org.eclipse.swt.layout.FormAttachment;  import org.eclipse.swt.layout.FormData; +import org.eclipse.swt.program.Program;  import org.eclipse.swt.widgets.Composite;  import org.eclipse.swt.widgets.Control;  import org.eclipse.swt.widgets.Event; @@ -171,4 +177,26 @@ public final class SWTUtils {  			log.error("Failed to add selection listener on object of type {}", swtObj.getClass().getSimpleName(), e);  		}  	} + +	public static void openURL(@Nullable URI uri) { +		try { +			if (uri == null) return; +			if (Desktop.isDesktopSupported()) { +				Desktop.getDesktop().browse(uri); +			} else { +				Program.launch(uri.toURL().toExternalForm()); +			} +		} catch (Exception e) { +			log.warn("Failed to open URI: {}", uri, e); +		} +	} + +	public static void openURL(@Nullable String uri) { +		if (uri == null) return; +		try { +			openURL(new URI(uri)); +		} catch (URISyntaxException e) { +			log.warn("Failed to open URI: {}", uri, e); +		} +	}  } diff --git a/pdf-over-gui/src/main/java/at/asit/pdfover/gui/utils/UpdateCheckManager.java b/pdf-over-gui/src/main/java/at/asit/pdfover/gui/utils/UpdateCheckManager.java index 92104176..133ecdcb 100644 --- a/pdf-over-gui/src/main/java/at/asit/pdfover/gui/utils/UpdateCheckManager.java +++ b/pdf-over-gui/src/main/java/at/asit/pdfover/gui/utils/UpdateCheckManager.java @@ -70,18 +70,7 @@ public final class UpdateCheckManager {                              BUTTONS.OK_CANCEL, ICON.INFORMATION);                      if (info.open() == SWT.OK) -                    { -                        if (Desktop.isDesktopSupported()) { -                            try { -                                Desktop.getDesktop().browse(new URI(Constants.UPDATE_URL)); -                            } catch (Exception e) { -                                log.error("Error opening update location ", e); -                            } -                        } else { -                            log.info("SWT Desktop is not supported on this platform"); -                            Program.launch(Constants.UPDATE_URL); -                        } -                    } +                        SWTUtils.openURL(Constants.UPDATE_URL);                  });              } diff --git a/pdf-over-gui/src/main/java/at/asit/pdfover/gui/workflow/states/MobileBKUState.java b/pdf-over-gui/src/main/java/at/asit/pdfover/gui/workflow/states/MobileBKUState.java index 9c3fc807..563b3b88 100644 --- a/pdf-over-gui/src/main/java/at/asit/pdfover/gui/workflow/states/MobileBKUState.java +++ b/pdf-over-gui/src/main/java/at/asit/pdfover/gui/workflow/states/MobileBKUState.java @@ -190,6 +190,32 @@ public class MobileBKUState extends State {  		});
  	}
 +	/**
 +	 * Show an error message to the user with "retry" or "cancel" as options
 +	 * returns normally on "retry", throws UserCancelledException on "cancel"
 +	 */
 +	public void showRecoverableError(final @Nonnull String errorMessage) throws UserCancelledException {
 +		Display.getDefault().syncCall(() -> {
 +			ErrorDialog error = new ErrorDialog(getStateMachine().getMainShell(), errorMessage, BUTTONS.RETRY_CANCEL);
 +			int result = error.open();
 +			if (result == SWT.CANCEL)
 +				throw new UserCancelledException();
 +			return true; /* dummy return */
 +		});
 +	}
 +
 +	/**
 +	 * Show an error message to the user with only an "ok" option
 +	 * throws UserCancelledException afterwards
 +	 */
 +	public void showUnrecoverableError(final @Nonnull String errorMessage) throws UserCancelledException {
 +		Display.getDefault().syncCall(() -> {
 +			ErrorDialog error = new ErrorDialog(getStateMachine().getMainShell(), errorMessage, BUTTONS.OK);
 +			error.open();
 +			throw new UserCancelledException();
 +		});
 +	}
 +
  	public static class UsernameAndPassword {
  		public @CheckForNull String username;
  		public @CheckForNull String password;
 @@ -313,7 +339,7 @@ public class MobileBKUState extends State {  			if (!tan.isUserAck()) {
  				// we need the TAN
  				tan.setRefVal(mobileStatus.refVal);
 -				tan.setSignatureData(mobileStatus.signatureDataURL);
 +				try { tan.setSignatureDataURI(new URI(mobileStatus.signatureDataURL)); } catch (URISyntaxException e) {}
  				tan.setErrorMessage(mobileStatus.errorMessage);
  				if (mobileStatus.tanTries < ATrustStatus.MOBILE_MAX_TAN_TRIES
  						&& mobileStatus.tanTries > 0) {
 @@ -367,7 +393,7 @@ public class MobileBKUState extends State {  	 * it is the responsibility of the caller to perform AJAX long polling
  	 * @return
  	 */
 -	public QRResult showQRCode(@Nonnull String referenceValue, @Nonnull URI qrCodeURI, @Nullable String errorMessage) throws UserCancelledException {
 +	public QRResult showQRCode(final @Nonnull String referenceValue, @Nonnull URI qrCodeURI, @Nullable URI signatureDataURI, final boolean showSmsTan, final boolean showFido2, final @Nullable String errorMessage) throws UserCancelledException {
  		byte[] qrCode;
  		try (final CloseableHttpClient httpClient = HttpClients.createDefault()) {
  			try (final CloseableHttpResponse response = httpClient.execute(new HttpGet(qrCodeURI))) {
 @@ -381,18 +407,18 @@ public class MobileBKUState extends State {  		final byte[] qrCodeCopy = qrCode; /* because java is silly */
  		return Display.getDefault().syncCall(() -> {
  			MobileBKUQRComposite qr = getMobileBKUQRComposite();
 -			qr.setUserCancel(false);
 -			qr.setUserSMS(false);
 -			qr.setDone(false);
 +			qr.reset();
 -			qr.setRefVal(status.refVal);
 -			qr.setSignatureData(status.signatureDataURL);
 -			qr.setErrorMessage(status.errorMessage);
 +			qr.setRefVal(referenceValue);
 +			qr.setSignatureDataURI(signatureDataURI);
 +			qr.setErrorMessage(errorMessage);
  			qr.setQR(qrCodeCopy);
 +			qr.setSMSEnabled(showSmsTan);
 +			qr.setFIDO2Enabled(showFido2);
  			getStateMachine().display(qr);
  			Display display = getStateMachine().getMainShell().getDisplay();
 -			while (!qr.isUserCancel() && !qr.isUserSMS() && !qr.isDone()) {
 +			while (!qr.isDone()) {
  				if (!display.readAndDispatch()) {
  					display.sleep();
  				}
 @@ -400,18 +426,18 @@ public class MobileBKUState extends State {  			getStateMachine().display(this.getWaitingComposite());
 -			if (qr.isUserCancel()) {
 +			if (qr.wasCancelClicked()) {
  				clearRememberedPassword();
  				throw new UserCancelledException();
  			}
 -			if (qr.isUserSMS())
 +			if (qr.wasSMSClicked())
  				return QRResult.TO_SMS;
 -
 -			if (qr.isDone())
 -				return QRResult.UPDATE;
 -			throw new RuntimeException("unexpected display wake");
 +			if (qr.wasFIDO2Clicked())
 +				return QRResult.TO_FIDO2;
 +
 +			return QRResult.UPDATE;
  		});
  	}
 @@ -420,10 +446,7 @@ public class MobileBKUState extends State {  	 * (any ongoing showQRCode call will then return)
  	 */
  	public void signalQRScanned() {
 -		getMobileBKUQRComposite().setDone(true);
 -		Display display = getStateMachine().
 -				getMainShell().getDisplay();
 -		display.wake();
 +		getMobileBKUQRComposite().signalPollingDone();
  	}
  	/**
 @@ -455,7 +478,7 @@ public class MobileBKUState extends State {  			}
  		}, 0, 5000);
 -		QRResult result = showQRCode(status.refVal, new URI(status.baseURL).resolve(status.qrCodeURL), status.errorMessage);
 +		QRResult result = showQRCode(status.refVal, new URI(status.baseURL).resolve(status.qrCodeURL), new URI(status.baseURL).resolve(status.signatureDataURL), true, false, status.errorMessage);
  		checkDone.cancel();
  		if (result == QRResult.TO_SMS)
  			status.qrCodeURL = null;
 diff --git a/pdf-over-gui/src/main/resources/at/asit/pdfover/gui/messages.properties b/pdf-over-gui/src/main/resources/at/asit/pdfover/gui/messages.properties index 65e353a1..2d5d74f1 100644 --- a/pdf-over-gui/src/main/resources/at/asit/pdfover/gui/messages.properties +++ b/pdf-over-gui/src/main/resources/at/asit/pdfover/gui/messages.properties @@ -253,6 +253,7 @@ tanEnter.FP=Please open the Handy-Signature app\nand confirm the signature.  tanEnter.QR=QR code\:  tanEnter.ReferenceValue=Reference value\:  tanEnter.SMS=Request &SMS +tanEnter.FIDO2=&FIDO2  tanEnter.TAN=TAN\:  tanEnter.tries=%d tries left\!  tanEnter.try=Only 1 try left\! diff --git a/pdf-over-gui/src/main/resources/at/asit/pdfover/gui/messages_de.properties b/pdf-over-gui/src/main/resources/at/asit/pdfover/gui/messages_de.properties index a2afff03..a75dfbb4 100644 --- a/pdf-over-gui/src/main/resources/at/asit/pdfover/gui/messages_de.properties +++ b/pdf-over-gui/src/main/resources/at/asit/pdfover/gui/messages_de.properties @@ -244,6 +244,7 @@ tanEnter.QR=QR Code\:  tanEnter.FP=Bitte öffnen Sie die Handy-Signatur App\nund bestätigen Sie die Signatur.  tanEnter.ReferenceValue=Vergleichswert\:  tanEnter.SMS=&SMS anfordern +tanEnter.FIDO2=&FIDO2  tanEnter.TAN=TAN\:  tanEnter.APPTAN=Bitte öffnen Sie die Handy-Signatur App\nTAN\:  tanEnter.tries=%d Versuche übrig\! diff --git a/pdf-over-gui/src/main/resources/logback.xml b/pdf-over-gui/src/main/resources/logback.xml index 866a82f7..4e33264a 100644 --- a/pdf-over-gui/src/main/resources/logback.xml +++ b/pdf-over-gui/src/main/resources/logback.xml @@ -22,6 +22,7 @@      </triggeringPolicy>    </appender>    <logger name="at.asit.pdfover" level="INFO"/> +  <logger name="at.asit.pdfover.gui.bku" level="TRACE"/>    <root level="WARN">      <appender-ref ref="STDOUT"/>      <appender-ref ref="LOGFILE"/> diff --git a/pdf-over-signer/src/main/java/at/asit/pdfover/signer/pdfas/PdfAs4Signer.java b/pdf-over-signer/src/main/java/at/asit/pdfover/signer/pdfas/PdfAs4Signer.java index c7be135f..561452a4 100644 --- a/pdf-over-signer/src/main/java/at/asit/pdfover/signer/pdfas/PdfAs4Signer.java +++ b/pdf-over-signer/src/main/java/at/asit/pdfover/signer/pdfas/PdfAs4Signer.java @@ -160,8 +160,9 @@ public class PdfAs4Signer {  				Throwable rootCause = e;  				while (rootCause.getCause() != null)  					rootCause = rootCause.getCause(); -				try { /* error code 6001 is user cancellation */ -					if (((SLPdfAsException)rootCause).getMessage().startsWith("6001 :")) +				try { /* error code 60xx is user cancellation */ +					int errorCode = Integer.parseInt(((SLPdfAsException)rootCause).getMessage().split(":",2)[0].trim()); +					if ((6000 <= errorCode) && (errorCode <= 6099))  						throw new UserCancelledException();  				} catch (ClassCastException e2) { /* fall through to wrapped throw */}  			} | 
