From f39ab43fc0120b7fa97028d40acd7851de8d4a99 Mon Sep 17 00:00:00 2001 From: Jakob Heher Date: Thu, 24 Nov 2022 14:14:37 +0100 Subject: Repository moved to GitHub: https://github.com/a-sit/pdf-over --- .../java/at/asit/pdfover/gui/bku/BKUHelper.java | 105 ----- .../at/asit/pdfover/gui/bku/LocalBKUConnector.java | 122 ------ .../asit/pdfover/gui/bku/MobileBKUConnector.java | 439 --------------------- .../asit/pdfover/gui/bku/mobile/ATrustParser.java | 379 ------------------ .../pdfover/gui/bku/mobile/MobileBKUValidator.java | 93 ----- 5 files changed, 1138 deletions(-) delete mode 100644 pdf-over-gui/src/main/java/at/asit/pdfover/gui/bku/BKUHelper.java delete mode 100644 pdf-over-gui/src/main/java/at/asit/pdfover/gui/bku/LocalBKUConnector.java delete mode 100644 pdf-over-gui/src/main/java/at/asit/pdfover/gui/bku/MobileBKUConnector.java delete mode 100644 pdf-over-gui/src/main/java/at/asit/pdfover/gui/bku/mobile/ATrustParser.java delete mode 100644 pdf-over-gui/src/main/java/at/asit/pdfover/gui/bku/mobile/MobileBKUValidator.java (limited to 'pdf-over-gui/src/main/java/at/asit/pdfover/gui/bku') diff --git a/pdf-over-gui/src/main/java/at/asit/pdfover/gui/bku/BKUHelper.java b/pdf-over-gui/src/main/java/at/asit/pdfover/gui/bku/BKUHelper.java deleted file mode 100644 index 382a3d24..00000000 --- a/pdf-over-gui/src/main/java/at/asit/pdfover/gui/bku/BKUHelper.java +++ /dev/null @@ -1,105 +0,0 @@ -/* - * Copyright 2012 by A-SIT, Secure Information Technology Center Austria - * - * Licensed under the EUPL, Version 1.1 or - as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the "Licence"); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * http://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - */ -package at.asit.pdfover.gui.bku; - -import org.apache.commons.httpclient.HttpClient; -import org.apache.commons.httpclient.UsernamePasswordCredentials; -// Imports -import org.apache.commons.httpclient.auth.AuthScope; -import org.apache.http.client.config.CookieSpecs; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import at.asit.pdfover.commons.Constants; - -/** - * - */ -public class BKUHelper { - /** - * SLF4J Logger instance - **/ - @SuppressWarnings("unused") - private static final Logger log = LoggerFactory.getLogger(BKUHelper.class); - - /* public static HttpClient getHttpClient(boolean useProxy) { - HttpClient client = new HttpClient(); - client.getParams().setParameter("http.useragent", - Constants.APP_NAME_VERSION); - - if (useProxy) { - String host = System.getProperty("http.proxyHost"); - String port = System.getProperty("http.proxyPort"); - if (host != null && !host.isEmpty() && - port != null && !port.isEmpty()) { - int p = Integer.parseInt(port); - client.getHostConfiguration().setProxy(host, p); - String user = System.getProperty("http.proxyUser"); - String pass = System.getProperty("http.proxyPassword"); - if (user != null && !user.isEmpty() && pass != null) { - client.getState().setProxyCredentials(new AuthScope(host, p), - new UsernamePasswordCredentials(user, pass)); - } - } - } - - return client; - }*/ - - /** - * Get a HTTP Client instance - * - * @param useProxy - * whether to use a potentially set proxy - * @return the HttpClient - */ - @SuppressWarnings("deprecation") - public static HttpClient getHttpClient(boolean useProxy) { - HttpClient client = new HttpClient(); - client.getParams().setParameter("http.useragent", - Constants.APP_NAME_VERSION); - - - client.getParams().setParameter("http.protocol.cookie-policy", CookieSpecs.BROWSER_COMPATIBILITY); - - if (useProxy) { - String host = System.getProperty("http.proxyHost");// - String port = System.getProperty("http.proxyPort");// - if (host != null && !host.isEmpty() && port != null && !port.isEmpty()) { - int p = Integer.parseInt(port); - client.getHostConfiguration().setProxy(host, p); - String user = System.getProperty("http.proxyUser");// - String pass = System.getProperty("http.proxyPassword");// - if (user != null && !user.isEmpty() && pass != null) { - client.getState().setProxyCredentials(new AuthScope(host, p), - new UsernamePasswordCredentials(user, pass)); - } - } - } - - return client; - - } - - /** - * Get a HTTP Client instance - * - * @return the HttpClient - */ - public static HttpClient getHttpClient() { - return getHttpClient(false); - } -} diff --git a/pdf-over-gui/src/main/java/at/asit/pdfover/gui/bku/LocalBKUConnector.java b/pdf-over-gui/src/main/java/at/asit/pdfover/gui/bku/LocalBKUConnector.java deleted file mode 100644 index 1f68a020..00000000 --- a/pdf-over-gui/src/main/java/at/asit/pdfover/gui/bku/LocalBKUConnector.java +++ /dev/null @@ -1,122 +0,0 @@ -/* - * Copyright 2012 by A-SIT, Secure Information Technology Center Austria - * - * Licensed under the EUPL, Version 1.1 or - as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the "Licence"); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * http://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - */ -package at.asit.pdfover.gui.bku; - -// Imports -import java.io.IOException; -import java.net.Socket; - -import org.apache.commons.httpclient.HttpClient; -import org.apache.commons.httpclient.HttpException; -import org.apache.commons.httpclient.HttpStatus; -import org.apache.commons.httpclient.methods.PostMethod; -import org.apache.commons.httpclient.methods.multipart.FilePart; -import org.apache.commons.httpclient.methods.multipart.MultipartRequestEntity; -import org.apache.commons.httpclient.methods.multipart.Part; -import org.apache.commons.httpclient.methods.multipart.StringPart; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import at.asit.pdfover.commons.Constants; -import at.asit.pdfover.gui.utils.FileUploadSource; -import at.asit.pdfover.signer.BkuSlConnector; -import at.asit.pdfover.signer.SignatureException; -import at.asit.pdfover.signer.pdfas.PdfAs4SLRequest; - -/** - * - */ -public class LocalBKUConnector implements BkuSlConnector { - /** - * SLF4J Logger instance - **/ - private static final Logger log = LoggerFactory.getLogger(LocalBKUConnector.class); - - private static boolean isAvailable = false; - public static boolean IsAvailable() { return isAvailable; } - private static Thread pollingThread = new Thread(() -> { - while (true) { - try { Thread.sleep(isAvailable ? 30000 : 5000); } catch (InterruptedException e) {} - try (Socket socket = new Socket("127.0.0.1", 3495)) { - isAvailable = true; - } catch (IOException e) { - isAvailable = false; - } - } - }, "LocalBKUProbeThread"); - static { - pollingThread.setDaemon(true); - pollingThread.start(); - } - - - /** - * HTTP Response server HEADER - */ - public final static String BKU_RESPONSE_HEADER_SERVER = "server"; - - /** - * HTTP Response user-agent HEADER - */ - public final static String BKU_RESPONSE_HEADER_USERAGENT = "user-agent"; - - /** - * HTTP Response SignatureLayout HEADER - */ - public final static String BKU_RESPONSE_HEADER_SIGNATURE_LAYOUT = "SignatureLayout"; - - /* (non-Javadoc) - * @see at.asit.pdfover.signator.BkuSlConnector#handleSLRequest(java.lang.String) - */ - @Override - public String handleSLRequest(PdfAs4SLRequest request) throws SignatureException { - try { - HttpClient client = BKUHelper.getHttpClient(); - PostMethod method = new PostMethod(Constants.LOCAL_BKU_URL); - - String sl_request = request.xmlRequest; - if (request.signatureData == null) { - method.addParameter("XMLRequest", sl_request); - } else { - StringPart xmlpart = new StringPart( - "XMLRequest", sl_request, "UTF-8"); - - FilePart filepart = new FilePart("fileupload", new FileUploadSource(request.signatureData)); - - Part[] parts = { xmlpart, filepart }; - - method.setRequestEntity(new MultipartRequestEntity(parts, method - .getParams())); - } - log.trace("SL REQUEST: " + sl_request); - - int returnCode = client.executeMethod(method); - - if (returnCode != HttpStatus.SC_OK) { - throw new HttpException( - method.getResponseBodyAsString()); - } - - return method.getResponseBodyAsString(); - } catch (HttpException e) { - log.error("LocalBKUConnector: ", e); - throw new SignatureException(e); - } catch (IOException e) { - log.error("LocalBKUConnector: ", e); - throw new SignatureException(e); - } - } -} 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 deleted file mode 100644 index 779e24c6..00000000 --- a/pdf-over-gui/src/main/java/at/asit/pdfover/gui/bku/MobileBKUConnector.java +++ /dev/null @@ -1,439 +0,0 @@ -package at.asit.pdfover.gui.bku; - -import java.io.IOException; -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 org.apache.hc.client5.http.classic.methods.HttpGet; -import org.apache.hc.client5.http.classic.methods.HttpPost; -import org.apache.hc.client5.http.entity.UrlEncodedFormEntity; -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.core5.http.ClassicHttpRequest; -import org.apache.hc.core5.http.ContentType; -import org.apache.hc.core5.http.Header; -import org.apache.hc.core5.http.HttpEntity; -import org.apache.hc.core5.http.HttpStatus; -import org.apache.hc.core5.http.NameValuePair; -import org.apache.hc.core5.http.NoHttpResponseException; -import org.apache.hc.core5.http.ParseException; -import org.apache.hc.core5.http.ProtocolException; -import org.apache.hc.core5.http.io.entity.EntityUtils; -import org.apache.hc.core5.http.message.BasicNameValuePair; -import org.json.JSONObject; -import org.jsoup.Jsoup; -import org.jsoup.nodes.Document; -import org.jsoup.nodes.Element; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import at.asit.pdfover.commons.Constants; -import at.asit.pdfover.commons.Messages; -import at.asit.pdfover.gui.bku.mobile.ATrustParser; -import at.asit.pdfover.gui.workflow.states.MobileBKUState; -import at.asit.pdfover.gui.workflow.states.MobileBKUState.UsernameAndPassword; -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.WebAuthN; - -import static at.asit.pdfover.commons.Constants.ISNOTNULL; - -public class MobileBKUConnector implements BkuSlConnector { - private static final Logger log = LoggerFactory.getLogger(MobileBKUConnector.class); - - private final @Nonnull MobileBKUState state; - public MobileBKUConnector(@Nonnull MobileBKUState state) { - this.state = state; - this.wantsFido2Default = WebAuthN.isAvailable() && state.getConfig().getFido2ByDefault(); - state.storeRememberedCredentialsTo(this.credentials); - } - - private class UserDisplayedError extends Exception { - private final @Nonnull String msg; - @Override public @Nonnull String getMessage() { return this.msg; } - private UserDisplayedError(@Nonnull String s) { this.msg = s; } - } - - public @Nonnull UsernameAndPassword credentials = new UsernameAndPassword(); - - /** - * This method takes the SLRequest from PDF-AS, and blocks until it has obtained a response - */ - @Override - public String handleSLRequest(PdfAs4SLRequest slRequest) throws SignatureException, UserCancelledException { - log.debug("Got security layer request: (has file part: {})\n{}", (slRequest.signatureData != null), slRequest.xmlRequest); - try (final CloseableHttpClient httpClient = HttpClients.custom().disableRedirectHandling().build()) { - ClassicHttpRequest currentRequest = buildInitialRequest(slRequest); - ATrustParser.Result response; - while ((response = sendHTTPRequest(httpClient, currentRequest)).slResponse == null) - currentRequest = presentResponseToUserAndReturnNextRequest(ISNOTNULL(response.html)); - log.debug("Returning security layer response:\n{}", response.slResponse); - return response.slResponse; - } catch (UserDisplayedError e) { - state.showUnrecoverableError(e.getMessage()); - throw new IllegalStateException("unreachable", e); /* showUnrecoverableError always throws */ - } catch (UserCancelledException e) { - throw e; - } catch (Exception e) { - throw new SignatureException(e); - } - } - - /* 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 - * @throws IOException on HTTP error codes - * @throws ProtocolException - * @throws URISyntaxException - * @throws InterruptedException - */ - private @Nonnull ATrustParser.Result sendHTTPRequest(CloseableHttpClient httpClient, ClassicHttpRequest request) throws IOException, ProtocolException, URISyntaxException, UserDisplayedError { - 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.getMethod(), request.getUri().toString()); - try (final CloseableHttpResponse response = httpClient.execute(request)) { - int httpStatus = response.getCode(); - if ((httpStatus == HttpStatus.SC_MOVED_PERMANENTLY) || (httpStatus == HttpStatus.SC_MOVED_TEMPORARILY)) { - Header redirectPath = response.getHeader("location"); - if (redirectPath == null) - throw new IOException("Received HTTP redirect, but no Location header."); - return sendHTTPRequest(httpClient, buildRedirectedRequest(request.getUri(), redirectPath.getValue())); - } - - if (httpStatus != HttpStatus.SC_OK) { - switch (httpStatus) { - case HttpStatus.SC_REQUEST_TOO_LONG: throw new UserDisplayedError(Messages.getString("atrusterror.http_413")); - default: throw new UserDisplayedError(Messages.formatString("atrusterror.http_generic", httpStatus, Optional.ofNullable(response.getReasonPhrase()).orElse("(null)"))); - } - } - - Header refreshHeader = response.getHeader("refresh"); - if (refreshHeader != null) - return sendHTTPRequest(httpClient, buildRefreshHeaderRequest(request.getUri(), refreshHeader.getValue())); - - HttpEntity responseEntity = response.getEntity(); - if (responseEntity == null) - throw new IOException("Did not get a HTTP body (entity == null)"); - - ContentType contentType = ContentType.parse(responseEntity.getContentType()); - String entityBody = EntityUtils.toString(response.getEntity(),contentType.getCharset()); - if (entityBody == null) - throw new IOException("Did not get a HTTP body (entity content == null)"); - - if ("text/html".equals(contentType.getMimeType())) { - Document resultDocument = Jsoup.parse(entityBody, request.getUri().toASCIIString()); - if (resultDocument == null) - { - log.error("Failed to parse A-Trust server response as HTML:\n{}", entityBody); - throw new IOException("Failed to parse HTML"); - } - - Element metaRefresh = resultDocument.selectFirst("meta[http-equiv=\"refresh\"i]"); - if (metaRefresh != null) { - String refreshContent = metaRefresh.attr("content"); - if (!refreshContent.isEmpty()) - return sendHTTPRequest(httpClient, buildRefreshHeaderRequest(request.getUri(), refreshContent)); - } - return ATrustParser.Parse(resultDocument); - } else { - return ATrustParser.Parse(request.getUri(), contentType.getMimeType(), entityBody); - } - } - } - - /** - * Builds a HttpRequest for the given base URI and (potentially relative) redirect path - */ - private static @Nonnull ClassicHttpRequest buildRedirectedRequest(URI baseURI, String redirectLocation) { - log.debug("following redirect: {}", redirectLocation); - return new HttpGet(baseURI.resolve(redirectLocation)); - } - - /** - * Builds a HttpRequest for redirection to a given Refresh header value - */ - private static @Nonnull ClassicHttpRequest buildRefreshHeaderRequest(URI baseURI, String refreshHeader) throws IOException { - // refresh value is delay in seconds, semicolon, URL=, url - Pattern pattern = Pattern.compile("^\\s*[0-9\\.]+\\s*;\\s*(?:[uU][rR][lL]\s*=\s*)(.+)$"); - Matcher matcher = pattern.matcher(refreshHeader); - if (!matcher.matches()) - throw new IOException("Got invalid Refresh header with value \"" + refreshHeader + "\"."); - String redirectURL = matcher.group(1); - return buildRedirectedRequest(baseURI, redirectURL); - } - - /** - * Builds the initial request to A-Trust based on the specified SL request - */ - private static @Nonnull ClassicHttpRequest buildInitialRequest(PdfAs4SLRequest slRequest) { - HttpPost post = new HttpPost(Constants.MOBILE_BKU_URL); - if (slRequest.signatureData != null) { - post.setEntity(MultipartEntityBuilder.create() - .addBinaryBody("fileupload", slRequest.signatureData.getByteArray(), ContentType.APPLICATION_PDF, "sign.pdf") - .addTextBody("XMLRequest", slRequest.xmlRequest) - .build()); - } else { - post.setEntity(UrlEncodedFormEntityBuilder.create() - .add("XMLRequest", slRequest.xmlRequest) - .build()); - } - return post; - } - - 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 (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()); - return post; - } - - private static class LongPollThread extends Thread implements AutoCloseable { - - private final CloseableHttpClient httpClient = HttpClients.createDefault(); - private final HttpGet request; - private final Runnable signal; - private boolean done = false; - - @Override - public void run() { - long timeout = System.nanoTime() + (300l * 1000l * 1000l * 1000l); /* a-trust timeout is 5 minutes */ - log.debug("longPollThread hello"); - while (!done) { - try (final CloseableHttpResponse response = httpClient.execute(request)) { - JSONObject jsonResponse = new JSONObject(EntityUtils.toString(response.getEntity())); - if (jsonResponse.getBoolean("Fin")) - signal.run(); - else if (jsonResponse.getBoolean("Wait")) - { - log.debug("longPollThread continue..."); - continue; - } - else if (jsonResponse.getBoolean("Error")) - signal.run(); /* will trigger reload and find error; this is the same thing a-trust does */ - else { - log.warn("Unknown long poll response:\n{}", jsonResponse.toString(2)); - break; - } - } catch (NoHttpResponseException e) { - if (timeout <= System.nanoTime()) - signal.run(); /* reload to find the timeout error */ - continue; /* httpclient timeout */ - } catch (IOException | ParseException | IllegalStateException e) { - if (done) 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) {} - } - } - log.debug("longPollThread goodbye"); - } - - public LongPollThread(URI uri, Runnable signal) { - this.request = new HttpGet(uri); - this.signal = signal; - } - - @Override - public void close() { - done = true; - if (this.request != null) - this.request.abort(); - - if (this.isAlive()) - try { this.join(1000); } catch (InterruptedException e) {} - - if (this.httpClient != null) - try { this.httpClient.close(); } catch (IOException e) { log.warn("Auto-close of long-poll HTTP client threw exception", e); } - } - - } - - private boolean wantsFido2Default; - /** - * Main lifting function for MobileBKU UX - * @return the next request to make - */ - private @Nonnull ClassicHttpRequest presentResponseToUserAndReturnNextRequest(@Nonnull ATrustParser.HTMLResult html) throws UserCancelledException { - if ((html.errorBlock == null) && (html.usernamePasswordBlock == null)) { /* successful username/password auth */ - if ((this.credentials.username != null) && (this.credentials.password != null)) - state.rememberCredentialsIfNecessary(this.credentials); - } - - if (wantsFido2Default && (html.fido2Link != null)) { - wantsFido2Default = false; - return new HttpGet(html.fido2Link); - } - - if (html.autoSkipBlock != null) { - return buildFormSubmit(html, html.autoSkipBlock.submitButton); - } - if (html.interstitialBlock != null) { - this.state.showInformationMessage(html.interstitialBlock.interstitialMessage); - return buildFormSubmit(html, html.interstitialBlock.submitButton); - } - 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, html.usernamePasswordBlock.errorMessage); - } - html.usernamePasswordBlock.setUsernamePassword(this.credentials.username, this.credentials.password); - return buildFormSubmit(html, "#Button_Identification"); - } catch (UserCancelledException e) { - return buildFormSubmit(html, "#Button_Cancel"); - } - } - if (html.smsTanBlock != null) { - MobileBKUState.SMSTanResult result = this.state.getSMSTanFromUser( - html.smsTanBlock.referenceValue, html.signatureDataLink, - html.fido2Link != null, html.smsTanBlock.errorMessage); - - switch (result.type) { - case TO_FIDO2: if (html.fido2Link != null) return new HttpGet(html.fido2Link); - case SMSTAN: html.smsTanBlock.setTAN(result.smsTan); return buildFormSubmit(html, "#SignButton"); - } - return new HttpGet(html.htmlDocument.baseUri()); - } - if (html.qrCodeBlock != null) { - try (LongPollThread longPollThread = new LongPollThread(html.qrCodeBlock.pollingURI, () -> { this.state.signalQRScanned(); })) { - this.state.showQRCode(html.qrCodeBlock.referenceValue, html.qrCodeBlock.qrCodeURI, html.signatureDataLink, html.smsTanLink != null, html.fido2Link != null, html.qrCodeBlock.errorMessage); - longPollThread.start(); - var result = this.state.waitForQRCodeResult(); - switch (result) { - 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()); - } - } - if (html.waitingForAppBlock != null) { - try (LongPollThread longPollThread = new LongPollThread(html.waitingForAppBlock.pollingURI, () -> { this.state.signalAppOpened(); })) { - this.state.showWaitingForAppOpen(html.waitingForAppBlock.referenceValue, html.signatureDataLink, html.smsTanLink != null, html.fido2Link != null); - longPollThread.start(); - var result = this.state.waitForAppOpen(); - switch (result) { - 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()); - } - } - if (html.waitingForBiometryBlock != null) { - try (LongPollThread longPollThread = new LongPollThread(html.waitingForBiometryBlock.pollingURI, () -> { this.state.signalAppBiometryDone(); })) { - this.state.showWaitingForAppBiometry(html.waitingForBiometryBlock.referenceValue, html.signatureDataLink, html.smsTanLink != null, html.fido2Link != null); - longPollThread.start(); - var result = this.state.waitForAppBiometry(); - switch (result) { - 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()); - } - } - if (html.fido2Block != null) { - - var fido2Result = this.state.promptUserForFIDO2Auth(html.fido2Block.fidoOptions, html.signatureDataLink, html.smsTanLink != null); - - switch (fido2Result.type) { - case TO_SMS: return new HttpGet(html.smsTanLink); - case CREDENTIAL: break; - } - - var fido2Assertion = ISNOTNULL(fido2Result.credential); - - 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"); - } - throw new IllegalStateException("No top-level block is set? Something has gone terribly wrong."); - } - - private static class UrlEncodedFormEntityBuilder { - private UrlEncodedFormEntityBuilder() {} - private List values = new ArrayList<>(); - public static @Nonnull UrlEncodedFormEntityBuilder create() { return new UrlEncodedFormEntityBuilder(); } - public @Nonnull UrlEncodedFormEntityBuilder add(String key, String value) { values.add(new BasicNameValuePair(key, value)); return this; } - public @Nonnull UrlEncodedFormEntity build() { return new UrlEncodedFormEntity(values, Charset.forName("utf-8")); } - } -} 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 deleted file mode 100644 index 89f53629..00000000 --- a/pdf-over-gui/src/main/java/at/asit/pdfover/gui/bku/mobile/ATrustParser.java +++ /dev/null @@ -1,379 +0,0 @@ -package at.asit.pdfover.gui.bku.mobile; - -import java.lang.reflect.InvocationTargetException; -import java.net.URI; -import java.net.URISyntaxException; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Map; -import java.util.Set; - -import javax.annotation.CheckForNull; -import javax.annotation.Nonnull; -import javax.annotation.Nullable; - -import org.jsoup.Jsoup; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import static at.asit.pdfover.commons.Constants.ISNOTNULL; - -public class ATrustParser { - private static final Logger log = LoggerFactory.getLogger(ATrustParser.class); - - private static class ComponentParseFailed extends Exception {} - - private static class TopLevelFormBlock { - protected final @Nonnull org.jsoup.nodes.Document htmlDocument; - protected final @Nonnull Map formOptions; - protected TopLevelFormBlock(@Nonnull org.jsoup.nodes.Document d, @Nonnull Map fO) { this.htmlDocument = d; this.formOptions = fO; } - - protected void abortIfElementMissing(@Nonnull String selector) throws ComponentParseFailed { - if (this.htmlDocument.selectFirst(selector) != null) return; - log.debug("Tested for element {} -- not found.", selector); - throw new ComponentParseFailed(); - } - protected @Nonnull org.jsoup.nodes.Element getElementEnsureNotNull(@Nonnull String selector) throws ComponentParseFailed { - var elm = this.htmlDocument.selectFirst(selector); - if (elm == null) { log.warn("Expected element not found in response: {}", selector); throw new ComponentParseFailed(); } - return elm; - } - protected @Nonnull String getAttributeEnsureNotNull(@Nonnull String selector, @Nonnull String attribute) throws ComponentParseFailed { - var elm = getElementEnsureNotNull(selector); - if (!elm.hasAttr(attribute)) { log.warn("Element {} is missing expected attribute '{}'.", selector, attribute); throw new ComponentParseFailed(); } - return ISNOTNULL(elm.attr(attribute)); - } - protected @Nonnull URI getURIAttributeEnsureNotNull(@Nonnull String selector, @Nonnull String attribute) throws ComponentParseFailed { - String value = getAttributeEnsureNotNull(selector, attribute); - try { - return new URI(value); - } catch (URISyntaxException e) { - if (attribute.startsWith("abs:")) - attribute = ISNOTNULL(attribute.substring(4)); - log.warn("Element {} attribute {} is '{}', could not be parsed as URI", selector, attribute, getAttributeEnsureNotNull(selector, attribute)); - throw new ComponentParseFailed(); - } - } - protected @Nonnull URI getLongPollURI() throws ComponentParseFailed { - var pollingScriptElm = getElementEnsureNotNull("#jsLongPoll script"); - String pollingScript = pollingScriptElm.data(); - int startIdx = pollingScript.indexOf("qrpoll(\""); - if (startIdx < 0) { log.warn("Failed to find 'qrpoll(\"' in jsLongPoll script:\n{}", pollingScript); throw new ComponentParseFailed(); } - startIdx += 8; - - int endIdx = pollingScript.indexOf("\");", startIdx); - if (endIdx < 0) { log.warn("Failed to find qrpoll terminator '\");' in jsLongPoll script:\n{}", pollingScript); throw new ComponentParseFailed(); } - - String pollingUriString = pollingScript.substring(startIdx, endIdx); - try { - return ISNOTNULL(new URI(pollingScriptElm.baseUri()).resolve(pollingUriString)); - } catch (URISyntaxException e) { - log.warn("Long-poll URI '{}' could not be parsed", pollingUriString); - throw new ComponentParseFailed(); - } - } - } - - public static class AutoSkipBlock extends TopLevelFormBlock { - public final @Nonnull String submitButton; - - private AutoSkipBlock(@Nonnull org.jsoup.nodes.Document htmlDocument, @Nonnull Map formOptions) throws ComponentParseFailed { - super(htmlDocument, formOptions); - if (htmlDocument.baseUri().contains("/tanAppInfo.aspx")) { - this.submitButton = "#NextBtn"; - } else { throw new ComponentParseFailed(); } - } - } - - public static class InterstitialBlock extends TopLevelFormBlock { - public final @Nonnull String submitButton; - public final @Nonnull String interstitialMessage; - - private InterstitialBlock(@Nonnull org.jsoup.nodes.Document htmlDocument, @Nonnull Map formOptions) throws ComponentParseFailed { - super(htmlDocument, formOptions); - if (htmlDocument.baseUri().contains("/ExpiresInfo.aspx")) { - this.interstitialMessage = ISNOTNULL(getElementEnsureNotNull("#Label2").ownText()); - this.submitButton = "#Button_Next"; - } else { throw new ComponentParseFailed(); } - } - } - - public static class ErrorBlock extends TopLevelFormBlock { - public final boolean isRecoverable; - public final @Nonnull String errorText; - - private ErrorBlock(@Nonnull org.jsoup.nodes.Document htmlDocument, @Nonnull Map formOptions) throws ComponentParseFailed { - super(htmlDocument, formOptions); - - try { - String documentPath = new URI(htmlDocument.baseUri()).getPath(); - String aspxFile = documentPath.substring(documentPath.lastIndexOf('/')); - - // gods this is such a hack, why can't they have a proper error element or something - if (!(aspxFile.startsWith("/error") && aspxFile.endsWith(".aspx"))) - throw new ComponentParseFailed(); - } catch (URISyntaxException ex) { - log.warn("Failed to parse document base URI as URI? ({})", htmlDocument.baseUri()); - throw new ComponentParseFailed(); - } - - this.isRecoverable = (htmlDocument.selectFirst("#Button_Back") != null); - - StringBuilder errorText = new StringBuilder(getElementEnsureNotNull("#Label1").ownText().trim()); - var detailLabel = this.htmlDocument.selectFirst("#LabelDetail"); - if (detailLabel != null) - errorText.append("\n").append(detailLabel.ownText().trim()); - this.errorText = ISNOTNULL(errorText.toString()); - } - } - - public static class UsernamePasswordBlock extends TopLevelFormBlock { - 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 formOptions) throws ComponentParseFailed { - super(htmlDocument, formOptions); - abortIfElementMissing("#handynummer"); - this.usernameKey = getAttributeEnsureNotNull("#handynummer", "name"); - this.passwordKey = getAttributeEnsureNotNull("#signaturpasswort", "name"); - this.errorMessage = null; - } - } - - public static class SMSTanBlock extends TopLevelFormBlock { - private final @Nonnull String tanKey; - public final @Nonnull String referenceValue; - public final @CheckForNull String errorMessage; - - public void setTAN(String tan) { - formOptions.put(tanKey, tan); - } - - private SMSTanBlock(@Nonnull org.jsoup.nodes.Document htmlDocument, @Nonnull Map formOptions) throws ComponentParseFailed { - super(htmlDocument, formOptions); - abortIfElementMissing("#input_tan"); - this.tanKey = getAttributeEnsureNotNull("#input_tan", "name"); - this.referenceValue = ISNOTNULL(getElementEnsureNotNull("#vergleichswert").ownText()); - this.errorMessage = null; - } - } - - public static class QRCodeBlock extends TopLevelFormBlock { - 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 formOptions) throws ComponentParseFailed { - super(htmlDocument, formOptions); - abortIfElementMissing("#qrimage"); - - this.referenceValue = ISNOTNULL(getElementEnsureNotNull("#vergleichswert").ownText()); - this.qrCodeURI = getURIAttributeEnsureNotNull("#qrimage", "abs:src"); - this.pollingURI = getLongPollURI(); - - this.errorMessage = null; - } - } - - public static class WaitingForAppBlock extends TopLevelFormBlock { - public final @Nonnull String referenceValue; - public final @Nonnull URI pollingURI; - - private WaitingForAppBlock(@Nonnull org.jsoup.nodes.Document htmlDocument, @Nonnull Map formOptions) throws ComponentParseFailed { - super(htmlDocument, formOptions); - abortIfElementMissing("#smartphoneAnimation"); - - this.referenceValue = ISNOTNULL(getElementEnsureNotNull("#vergleichswert").ownText()); - this.pollingURI = getLongPollURI(); - } - } - - public static class WaitingForBiometryBlock extends TopLevelFormBlock { - public final @Nonnull String referenceValue; - public final @Nonnull URI pollingURI; - - private WaitingForBiometryBlock(@Nonnull org.jsoup.nodes.Document htmlDocument, @Nonnull Map formOptions) throws ComponentParseFailed { - super(htmlDocument, formOptions); - abortIfElementMissing("#biometricimage"); - - this.referenceValue = ISNOTNULL(getElementEnsureNotNull("#vergleichswert").ownText()); - this.pollingURI = getLongPollURI(); - } - } - - public static class Fido2Block extends TopLevelFormBlock { - public final @Nonnull String fidoOptions; - private final @Nonnull String credentialResultKey; - - public void setFIDOResult(String result) { formOptions.put(credentialResultKey, result); } - - private Fido2Block(@Nonnull org.jsoup.nodes.Document htmlDocument, @Nonnull Map formOptions) throws ComponentParseFailed { - super(htmlDocument, formOptions); - abortIfElementMissing("#fidoBlock"); - this.fidoOptions = getAttributeEnsureNotNull("#credentialOptions", "value"); - this.credentialResultKey = getAttributeEnsureNotNull("#credentialResult", "name"); - } - } - - public static class HTMLResult { - public final @Nonnull org.jsoup.nodes.Document htmlDocument; - public final @Nonnull URI formTarget; - public final @Nonnull Map formOptions = new HashMap<>(); - - public @Nonnull Iterable> iterateFormOptions() { return ISNOTNULL(formOptions.entrySet()); } - - /* 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 AutoSkipBlock autoSkipBlock; - public final @CheckForNull InterstitialBlock interstitialBlock; - public final @CheckForNull ErrorBlock errorBlock; - public final @CheckForNull UsernamePasswordBlock usernamePasswordBlock; - public final @CheckForNull SMSTanBlock smsTanBlock; - public final @CheckForNull QRCodeBlock qrCodeBlock; - public final @CheckForNull WaitingForAppBlock waitingForAppBlock; - public final @CheckForNull WaitingForBiometryBlock waitingForBiometryBlock; - public final @CheckForNull Fido2Block fido2Block; - - private void validate() { - Set populated = new HashSet<>(); - - if (autoSkipBlock != null) populated.add("autoSkipBlock"); - if (interstitialBlock != null) populated.add("interstitialBlock"); - if (errorBlock != null) populated.add("errorBlock"); - if (usernamePasswordBlock != null) populated.add("usernamePasswordBlock"); - if (smsTanBlock != null) populated.add("smsTanBlock"); - if (qrCodeBlock != null) populated.add("qrCodeBlock"); - if (waitingForAppBlock != null) populated.add("waitingForAppBlock"); - if (waitingForBiometryBlock != null) populated.add("waitingForBiometryBlock"); - if (fido2Block != null) populated.add("fido2Block"); - - switch (populated.size()) { - case 0: log.error("Did not find any top-level blocks.\n{}", this.htmlDocument.toString()); break; - case 1: /* passed */ return; - default: log.error("Found too many top-level blocks: {}\n", String.join(", ", populated), this.htmlDocument.toString()); break; - } - throw new IllegalArgumentException("Unknown A-Trust page reached?"); - } - - private @Nullable URI getHrefIfExists(String selector) { - var elm = htmlDocument.selectFirst(selector); - if (elm == null) return null; - - String url = elm.absUrl("href"); - try { - return new URI(url); - } catch (Exception e) { - log.warn("Invalid {} href attribute: {} ({})", selector, elm.attr("href"), url); - return null; - } - } - - /** - * tries to parse T using its constructor; if ComponentParseFailed is thrown, swallows it - */ - private @Nullable T TryParseMainBlock(Class clazz) { - try { - return clazz.getDeclaredConstructor(org.jsoup.nodes.Document.class, Map.class).newInstance(this.htmlDocument, this.formOptions); - } catch (NoSuchMethodException | InstantiationException | IllegalAccessException | IllegalArgumentException | SecurityException e) { - log.error("Internal parser error; check your method signatures?", e); - return null; - } catch (InvocationTargetException wrappedE) { - Throwable e = wrappedE.getCause(); - if (!(e instanceof ComponentParseFailed)) { - if (e instanceof RuntimeException) - throw (RuntimeException)e; - log.warn("Unexpected parser failure.", e); - } - return null; - } - } - - private HTMLResult(@Nonnull org.jsoup.nodes.Document htmlDocument) { - log.trace("Now parsing:\n{}", htmlDocument.toString()); - this.htmlDocument = htmlDocument; - - var forms = htmlDocument.getElementsByTag("form"); - if (forms.size() != 1) { - log.error("Found {} forms in A-Trust response document, expected 1. Document:\n{}", forms.size(), htmlDocument.toString()); - throw new IllegalArgumentException("Failed to parse A-Trust response page"); - } - - var mainForm = ISNOTNULL(forms.first()); /* size check above */ - String formAction = mainForm.absUrl("action"); - try { - this.formTarget = new URI(formAction); - } catch (URISyntaxException e) { - log.error("Invalid form target in page: {} ({})", mainForm.attr("action"), formAction, e); - throw new IllegalArgumentException("Failed to parse A-Trust response page"); - } - - for (var input : mainForm.select("input")) { - String name = input.attr("name"); - - 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"); // TODO hide the button if unsupported? - - this.autoSkipBlock = TryParseMainBlock(AutoSkipBlock.class); - this.interstitialBlock = TryParseMainBlock(InterstitialBlock.class); - this.errorBlock = TryParseMainBlock(ErrorBlock.class); - this.usernamePasswordBlock = TryParseMainBlock(UsernamePasswordBlock.class); - this.smsTanBlock = TryParseMainBlock(SMSTanBlock.class); - this.qrCodeBlock = TryParseMainBlock(QRCodeBlock.class); - this.waitingForAppBlock = TryParseMainBlock(WaitingForAppBlock.class); - this.waitingForBiometryBlock = TryParseMainBlock(WaitingForBiometryBlock.class); - this.fido2Block = TryParseMainBlock(Fido2Block.class); - - validate(); - } - } - - public static class Result { - public final @CheckForNull String slResponse; - public final @CheckForNull HTMLResult html; - - private Result(@Nonnull String slResponse) { this.slResponse = slResponse; this.html = null; } - private Result(@Nonnull org.jsoup.nodes.Document htmlDocument) { this.slResponse = null; this.html = new HTMLResult(htmlDocument); } - } - - public static @Nonnull Result Parse(@Nonnull org.jsoup.nodes.Document htmlDocument) { return new Result(htmlDocument); } - - public static @Nonnull Result Parse(URI baseURI, String contentType, @Nonnull String content) { - if (contentType.equals("text/html")) - { - var document = Jsoup.parse(content, baseURI.toASCIIString()); - if (document == null) - { - log.error("Failed to parse HTML (document == null):\n{}", content); - throw new IllegalArgumentException("A-Trust parsing failed"); - } - return Parse(document); - } - - if (contentType.endsWith("/xml")) - return new Result(content); - - log.error("Unknown content-type \"{}\" from URI {}", contentType, baseURI.toString()); - throw new IllegalArgumentException("Unknown A-Trust page reached?"); - } -} diff --git a/pdf-over-gui/src/main/java/at/asit/pdfover/gui/bku/mobile/MobileBKUValidator.java b/pdf-over-gui/src/main/java/at/asit/pdfover/gui/bku/mobile/MobileBKUValidator.java deleted file mode 100644 index 89dbdf4f..00000000 --- a/pdf-over-gui/src/main/java/at/asit/pdfover/gui/bku/mobile/MobileBKUValidator.java +++ /dev/null @@ -1,93 +0,0 @@ -/* - * Copyright 2012 by A-SIT, Secure Information Technology Center Austria - * - * Licensed under the EUPL, Version 1.1 or - as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the "Licence"); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * http://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - */ -package at.asit.pdfover.gui.bku.mobile; - -// Imports -import java.util.regex.Matcher; -import java.util.regex.Pattern; -import at.asit.pdfover.gui.exceptions.InvalidPasswordException; -import at.asit.pdfover.gui.exceptions.PasswordTooLongException; -import at.asit.pdfover.gui.exceptions.PasswordTooShortException; - -/** - * - */ -public class MobileBKUValidator { - - /** - * Regular expression for mobile phone numbers: this allows the entry of - * mobile numbers in the following formats: - * - * +(countryCode)99999999999 00(countryCode)99999999999 099999999999 - * 1030199999999999 (A-Trust Test bku) - */ - private static final String NUMBER_REGEX = "^((\\+[\\d]{2})|(00[\\d]{2})|(0)|(10301))([1-9][\\d]+)$"; - - /** - * Validates the Mobile phone number - * - * @param number - * @return the normalized Phone number - */ - public static String normalizeMobileNumber(String number) { - // Verify number and normalize - - number = number.trim(); - - String numberWithoutWhitespace = number.replaceAll("\\s",""); - // Compile and use regular expression - Pattern pattern = Pattern.compile(NUMBER_REGEX); - Matcher matcher = pattern.matcher(numberWithoutWhitespace); - - if (!matcher.find()) - return number; /* might be an idA username, return unchanged */ - - if (matcher.groupCount() != 6) { - return number; - } - - String countryCode = matcher.group(1); - - String normalNumber = matcher.group(6); - - if (countryCode.equals("10301")) { - // A-Trust Testnumber! Don't change - return numberWithoutWhitespace; - } - - countryCode = countryCode.replace("00", "+"); - - if (countryCode.equals("0")) { - countryCode = "+43"; - } - - return countryCode + normalNumber; - } - - /** - * Validate given Password for Mobile BKU - * - * @param password - * @throws InvalidPasswordException - */ - public static void validatePassword(String password) - throws InvalidPasswordException { - if (password.length() < 5) - throw new PasswordTooShortException(); - if (password.length() > 200) - throw new PasswordTooLongException(); - } -} -- cgit v1.2.3