diff --git a/.vscode/launch.json b/.vscode/launch.json index 44c150235..898495d31 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -33,6 +33,28 @@ // this is because of some escaping problem with zsh "console": "externalTerminal" } + }, + { + "type": "java", + "name": "Autogram CLI", + "request": "launch", + "mainClass": "digital.slovensko.autogram.Main", + "projectName": "autogram", + "vmArgs": "--add-exports javafx.graphics/com.sun.javafx.tk=ALL-UNNAMED", + "args": "${input:commandLineArguments}", + "osx": { + // this is because of some escaping problem with zsh + "console": "integratedTerminal" + } + }, + + ], + "inputs": [ + { + "id": "commandLineArguments", + "type": "promptString", + "description": "Please enter command line arguments", + "default": "--cli --source src/test/resources/digital/slovensko/autogram/crystal_test_data/rozhodnutie_X4564-2.pdf --target target/out.pdf" } ] } diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 14af2b630..e133a1ab9 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -103,6 +103,25 @@ }, "problemMatcher": [], "type": "shell" + }, + { + "label": "Run Autogram CLI", + "command": "./mvnw exec:java -Dexec.mainClass=\"digital.slovensko.autogram.Main\" -Dexec.args=\"${input:commandLineArguments}\"", + "type": "shell", + "problemMatcher": [], + "options": { + "env": { + "JAVA_HOME": "${config:java.jdt.ls.java.home}" + } + } + } + ], + "inputs": [ + { + "id": "commandLineArguments", + "type": "promptString", + "description": "Please enter command line arguments", + "default": "--cli" } ] } diff --git a/DEVELOPER.md b/DEVELOPER.md index 129c517e3..faa3a4bb5 100644 --- a/DEVELOPER.md +++ b/DEVELOPER.md @@ -1,5 +1,13 @@ # Info for developers +# Trying out CLI mode + +Useful command how to run project from CLI. + +```bash +./mvnw exec:java -Dexec.mainClass="digital.slovensko.autogram.Main" -Dexec.args="--cli ..." +``` + # More info about inner workings of builds for MacOS To run signed mac build add follwing to `.vscode/settings.json` (or you can do unsigned build by setting `mac.sign=0` in `build.properties`) diff --git a/README.md b/README.md index 1deef457f..6aaba5d54 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Autogram -Autogram je multi-platformová (Windows, MacOS, Linux) desktopová JavaFX aplikácia, ktorá slúži na podpisovanie dokumentov v súlade s eIDAS reguláciou. Používateľ ňou môže podpisovať súbory priamo alebo je možné aplikáciu jednoducho zaintegrovať do vlastného (webového) informačného systému pomocou HTTP API. +Autogram je multi-platformová (Windows, MacOS, Linux) desktopová JavaFX aplikácia, ktorá slúži na podpisovanie dokumentov v súlade s eIDAS reguláciou. Používateľ ňou môže podpisovať súbory priamo alebo je možné aplikáciu jednoducho zaintegrovať do vlastného (webového) informačného systému pomocou HTTP API. Podpisovanie je možné spúšať aj z príkazového riadku, čo je vhodné pre hromadné podpisovanie veľkého množstva súborov naraz. **Inštalačné balíky pre Windows, MacOS a Linux sú dostupné v časti [Releases](https://github.com/slovensko-digital/autogram/releases).** Na použitie na existujúcich štátnych weboch bude potrebné doinštalovať aj [rozšírenie do prehliadača](https://github.com/slovensko-digital/autogram-extension#readme). @@ -12,6 +12,10 @@ Swagger dokumentácia pre HTTP API je [dostupná na githube](https://generator3. Vyvolať spustenie programu je možné priamo z webového prehliadača otvorením adresy so špeciálnym protokolom `autogram://`. Napríklad cez `autogram://go`. +## Konzolový mód + +Autogram je možné spúšťať aj z príkazového riadku (CLI mód), ktoré umožňuje aj hromadné podpisovanie súborov. Detailné informácie o prepínačoch je sú popísané v nápovede po spustení `autogram --help` + ### Štýlovanie Aplikácia momentálne podporuje len jeden štýl - štátny IDSK dizajn. Ďalšie štýly sú plánované. Štýlovanie sa však už teraz deje výhradne cez kaskádové štýly, viď [idsk.css](https://github.com/slovensko-digital/autogram/blob/main/src/main/resources/digital/slovensko/autogram/ui/gui/idsk.css) diff --git a/pom.xml b/pom.xml index 35805109c..e32474932 100644 --- a/pom.xml +++ b/pom.xml @@ -1,6 +1,6 @@ + xmlns="http://maven.apache.org/POM/4.0.0" + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> 4.0.0 @@ -24,7 +24,9 @@ 2.0.7 5.9.3 5.4.0 + 1.5.0 2.9.1 + 1.2 @@ -113,6 +115,11 @@ ${mockito.version} test + + commons-cli + commons-cli + ${commons-cli.version} + org.xmlunit @@ -126,6 +133,12 @@ ${xmlunit.version} test + + com.google.jimfs + jimfs + ${jimfs.version} + test + @@ -207,7 +220,7 @@ org.codehaus.mojo - versions-maven-plugin + versions-maven-plugin 2.16.0 @@ -259,8 +272,10 @@ download-single - https://cdn.jsdelivr.net/npm/pdfjs-dist@${pdfjs.version}/build/pdf.min.js - ${project.resources[0].directory}/digital/slovensko/autogram/ui/gui/vendor/pdfjs + + https://cdn.jsdelivr.net/npm/pdfjs-dist@${pdfjs.version}/build/pdf.min.js + + ${project.resources[0].directory}/digital/slovensko/autogram/ui/gui/vendor/pdfjs true @@ -271,8 +286,10 @@ download-single - https://cdn.jsdelivr.net/npm/pdfjs-dist@${pdfjs.version}/build/pdf.worker.min.js - ${project.resources[0].directory}/digital/slovensko/autogram/ui/gui/vendor/pdfjs + + https://cdn.jsdelivr.net/npm/pdfjs-dist@${pdfjs.version}/build/pdf.worker.min.js + + ${project.resources[0].directory}/digital/slovensko/autogram/ui/gui/vendor/pdfjs true @@ -285,7 +302,8 @@ https://cdn.jsdelivr.net/npm/pdfjs-dist@${pdfjs.version} cmaps - ${project.resources[0].directory}/digital/slovensko/autogram/ui/gui/vendor/pdfjs/cmaps + + ${project.resources[0].directory}/digital/slovensko/autogram/ui/gui/vendor/pdfjs/cmaps @@ -321,7 +339,8 @@ jdeps - + true ${project.build.directory}${file.separator}jdeps.out @@ -332,7 +351,8 @@ - + @@ -363,7 +383,8 @@ javafx.fxml javafx.graphics javafx.web - + jdk.unsupported jdk.httpserver @@ -409,13 +430,16 @@ 3.1.0 bash - ${project.build.scriptSourceDirectory}${file.separator}resources + + ${project.build.scriptSourceDirectory}${file.separator}resources ${project.build.scriptSourceDirectory}${file.separator}package.sh ${jlink.jdk.path}${file.separator}bin${file.separator}jpackage - ${project.build.directory}${file.separator}${project.artifactId}-${project.version}-${platform} + + ${project.build.directory}${file.separator}${project.artifactId}-${project.version}-${platform} ${project.build.directory}${file.separator}preparedJDK - ${project.basedir}${file.separator}src${file.separator}main${file.separator}resources${file.separator}digital${file.separator}slovensko${file.separator}autogram + + ${project.basedir}${file.separator}src${file.separator}main${file.separator}resources${file.separator}digital${file.separator}slovensko${file.separator}autogram ${platform} ${project.version} ${project.build.directory} @@ -492,4 +516,4 @@ - + \ No newline at end of file diff --git a/src/main/java/digital/slovensko/autogram/Main.java b/src/main/java/digital/slovensko/autogram/Main.java index 552a02dfb..6c6c383fc 100644 --- a/src/main/java/digital/slovensko/autogram/Main.java +++ b/src/main/java/digital/slovensko/autogram/Main.java @@ -1,17 +1,12 @@ package digital.slovensko.autogram; -import digital.slovensko.autogram.ui.gui.GUIApp; -import javafx.application.Application; - -import java.util.Arrays; +import digital.slovensko.autogram.core.AppStarter; import static java.util.Objects.requireNonNullElse; public class Main { public static void main(String[] args) { - System.out.println("Starting with args: " + Arrays.toString(args)); - - Application.launch(GUIApp.class, args); + AppStarter.start(args); } public static String getVersion() { diff --git a/src/main/java/digital/slovensko/autogram/core/AppStarter.java b/src/main/java/digital/slovensko/autogram/core/AppStarter.java new file mode 100644 index 000000000..a27cf2ea3 --- /dev/null +++ b/src/main/java/digital/slovensko/autogram/core/AppStarter.java @@ -0,0 +1,77 @@ +package digital.slovensko.autogram.core; + +import digital.slovensko.autogram.ui.cli.CliApp; +import digital.slovensko.autogram.ui.gui.GUIApp; +import javafx.application.Application; +import org.apache.commons.cli.*; + +import java.io.PrintWriter; + +public class AppStarter { + private static final Options options = new Options(). + addOptionGroup(new OptionGroup(). + addOption(new Option(null, "url", true, "Start in GUI mode with API server listening on given port and protocol (HTTP/HTTPS). Application starts minimised when is not empty.")). + addOption(new Option("c", "cli", false, "Run application in CLI mode.")) + ). + addOption("h", "help", false, "Print this command line help."). + addOption("u", "usage", false, "Print usage examples."). + addOption("s", "source", true, "Source file or directory of files to sign."). + addOption("t", "target", true, "Target file or directory for signed files. Type (file/directory) must match the source."). + addOption("f", "force", false, "Overwrite existing file(s)."). + addOption(null, "pdfa", false, "Check PDF/A compliance before signing."). + addOption(null, "parents", false, "Create all parent directories for target if needed."). + addOption("d", "driver", true, "PCKS driver for signing. Supported drivers: eid, secure_store, monet, gemalto."); + + public static void start(String[] args) { + try { + CommandLine cmd = new DefaultParser().parse(options, args); + + if (cmd.hasOption("h")) { + printHelp(); + } else if (cmd.hasOption("u")) { + printUsage(); + } else if (cmd.hasOption("c")) { + CliApp.start(cmd); + } else { + Application.launch(GUIApp.class, args); + } + } catch (ParseException e) { + System.err.println("Unable to parse program args"); + System.err.println(e); + } + } + + public static void printHelp() { + final HelpFormatter formatter = new HelpFormatter(); + final String syntax = "autogram"; + final String footer = """ + + In CLI mode, signed files are saved with the same name as the source file, but with the suffix "_signed" if no target is specified. If the source is a directory, the target must also be a directory. If the source is a file, the target must also be a file. If the source is a driectory and no target is specified, a target directory is created with the same name as the source directory, but with the suffix "_signed". + + If no target is specified and generated target name already exists, number is added to the target's name suffix if --force is not enabled. For example, if the source is "file.pdf" and the target is not specified, the target will be "file_signed.pdf". If the target already exists, the target will be "file_signed (1).pdf". If that target already exists, the target will be "file_signed (2).pdf", and so on. + + If --force is enabled, the target will be overwritten if it already exists. + + If target is specified with missing parent directories, they are created onyl if --parents is enabled. Otherwise, the signing fails. For example, if the source is "file.pdf" and the target is "target/file_signed.pdf", the target directory "target" must exist. If it does not exist, the signing fails. If --parents is enabled, the target directory "target" is created if it does not exist. + """; + + formatter.printHelp(80, syntax, "", options, footer, true); + } + + public static void printUsage() { + final HelpFormatter formatter = new HelpFormatter(); + final String syntax = """ + autogram [options] + autogram --url=http://localhost:32700 + autogram --cli [options] + autogram --cli -s target/directory-example/file-example.pdf -t target/output-example/out-example.pdf + autogram --cli -s target/directory-example -t target/output-example -f + autogram --cli -s target/directory-example -t target/non-existent-dir/output-example --parents + autogram --cli -s target/directory-example/file-example.pdf -pdfa + autogram --cli -s target/directory-example/file-example.pdf -d eid + """; + final PrintWriter pw = new PrintWriter(System.out); + formatter.printUsage(pw, 80, syntax); + pw.flush(); + } +} diff --git a/src/main/java/digital/slovensko/autogram/core/Autogram.java b/src/main/java/digital/slovensko/autogram/core/Autogram.java index 9a5cbcb29..9d72b753e 100644 --- a/src/main/java/digital/slovensko/autogram/core/Autogram.java +++ b/src/main/java/digital/slovensko/autogram/core/Autogram.java @@ -26,18 +26,19 @@ public Autogram(UI ui, DriverDetector driverDetector) { } public void sign(SigningJob job) { - ui.onUIThreadDo(() -> ui.startSigning(job, this)); - - if (job.shouldCheckPDFCompliance()) { - ui.onWorkThreadDo(() -> checkPDFACompliance(job)); - } + ui.onUIThreadDo(() + -> ui.startSigning(job, this)); } - private void checkPDFACompliance(SigningJob job) { - var result = new PDFAStructureValidator().validate(job.getDocument()); - if (!result.isCompliant()) { - ui.onUIThreadDo(() -> ui.onPDFAComplianceCheckFailed(job)); - } + public void checkPDFACompliance(SigningJob job) { + if(!job.shouldCheckPDFCompliance()) return; + + ui.onWorkThreadDo(() -> { + var result = new PDFAStructureValidator().validate(job.getDocument()); + if (!result.isCompliant()) { + ui.onUIThreadDo(() -> ui.onPDFAComplianceCheckFailed(job)); + } + }); } public void startVisualization(SigningJob job) { diff --git a/src/main/java/digital/slovensko/autogram/core/CliParameters.java b/src/main/java/digital/slovensko/autogram/core/CliParameters.java new file mode 100644 index 000000000..cebb6fe72 --- /dev/null +++ b/src/main/java/digital/slovensko/autogram/core/CliParameters.java @@ -0,0 +1,75 @@ +package digital.slovensko.autogram.core; + +import digital.slovensko.autogram.core.errors.SourceDoesNotExistException; +import digital.slovensko.autogram.core.errors.TokenDriverDoesNotExistException; +import digital.slovensko.autogram.drivers.TokenDriver; +import org.apache.commons.cli.CommandLine; + +import java.io.File; +import java.util.Optional; + +public class CliParameters { + private final File source; + private final String target; + private final boolean force; + private final TokenDriver driver; + private final boolean checkPDFACompliance; + private final boolean makeParentDirectories; + + + public CliParameters(CommandLine cmd) { + source = getValidSource(cmd.getOptionValue("s")); + target = cmd.getOptionValue("t"); + driver = getValidTokenDriver(cmd.getOptionValue("d")); + force = cmd.hasOption("f"); + checkPDFACompliance = cmd.hasOption("pdfa"); + makeParentDirectories = cmd.hasOption("parents"); + } + + public File getSource() { + return source; + } + + public TokenDriver getDriver() { + return driver; + } + + public boolean isForce() { + return force; + } + + public String getTarget() { + return target; + } + + public boolean shouldCheckPDFACompliance() { + return checkPDFACompliance; + } + + public boolean shouldMakeParentDirectories() { + return makeParentDirectories; + } + + private static File getValidSource(String sourcePath) { + if (sourcePath != null && !new File(sourcePath).exists()) + throw new SourceDoesNotExistException(sourcePath); + + return sourcePath == null ? null : new File(sourcePath); + } + + private static TokenDriver getValidTokenDriver(String driverName) { + if (driverName == null) + return null; + + Optional tokenDriver = new DefaultDriverDetector() + .getAvailableDrivers() + .stream() + .filter(d -> d.getShortname().equals(driverName)) + .findFirst(); + + if (tokenDriver.isEmpty()) + throw new TokenDriverDoesNotExistException(driverName); + + return tokenDriver.get(); + } +} diff --git a/src/main/java/digital/slovensko/autogram/core/DefaultDriverDetector.java b/src/main/java/digital/slovensko/autogram/core/DefaultDriverDetector.java index 03586a49b..f8a01478c 100644 --- a/src/main/java/digital/slovensko/autogram/core/DefaultDriverDetector.java +++ b/src/main/java/digital/slovensko/autogram/core/DefaultDriverDetector.java @@ -8,25 +8,32 @@ import java.util.List; public class DefaultDriverDetector implements DriverDetector { + public static class TokenDriverShortnames { + public static final String EID = "eid"; + public static final String SECURE_STORE = "secure_store"; + public static final String MONET = "monet"; + public static final String GEMALTO = "gemalto"; + } + public static final List LINUX_DRIVERS = List.of( - new PKCS11TokenDriver("Občiansky preukaz (eID klient)", Path.of("/usr/lib/eID_klient/libpkcs11_x64.so"), false), - new PKCS11TokenDriver("I.CA SecureStore", Path.of("/usr/lib/pkcs11/libICASecureStorePkcs11.so"), true), - new PKCS11TokenDriver("MONET+ ProID+Q", Path.of("/usr/lib/x86_64-linux-gnu/libproidqcm11.so"), true), - new PKCS11TokenDriver("Gemalto IDPrime 940", Path.of("/usr/lib/libIDPrimePKCS11.so"), true) + new PKCS11TokenDriver("Občiansky preukaz (eID klient)", Path.of("/usr/lib/eID_klient/libpkcs11_x64.so"), false, TokenDriverShortnames.EID), + new PKCS11TokenDriver("I.CA SecureStore", Path.of("/usr/lib/pkcs11/libICASecureStorePkcs11.so"), true, TokenDriverShortnames.SECURE_STORE), + new PKCS11TokenDriver("MONET+ ProID+Q", Path.of("/usr/lib/x86_64-linux-gnu/libproidqcm11.so"), true, TokenDriverShortnames.MONET), + new PKCS11TokenDriver("Gemalto IDPrime 940", Path.of("/usr/lib/libIDPrimePKCS11.so"), true, TokenDriverShortnames.GEMALTO) ); public static final List WINDOWS_DRIVERS = List.of( - new PKCS11TokenDriver("Občiansky preukaz (eID klient)", Path.of("C:\\Program Files (x86)\\eID_klient\\pkcs11_x64.dll"), false), - new PKCS11TokenDriver("I.CA SecureStore", Path.of("C:\\Windows\\System32\\SecureStorePkcs11.dll"), true), - new PKCS11TokenDriver("MONET+ ProID+Q", Path.of( "C:\\Windows\\system32\\proidqcm11.dll"), true), - new PKCS11TokenDriver("Gemalto IDPrime 940", Path.of("C:\\Windows\\System32\\eTPKCS11.dll"), true) + new PKCS11TokenDriver("Občiansky preukaz (eID klient)", Path.of("C:\\Program Files (x86)\\eID_klient\\pkcs11_x64.dll"), false, TokenDriverShortnames.EID), + new PKCS11TokenDriver("I.CA SecureStore", Path.of("C:\\Windows\\System32\\SecureStorePkcs11.dll"), true, TokenDriverShortnames.SECURE_STORE), + new PKCS11TokenDriver("MONET+ ProID+Q", Path.of( "C:\\Windows\\system32\\proidqcm11.dll"), true, TokenDriverShortnames.MONET), + new PKCS11TokenDriver("Gemalto IDPrime 940", Path.of("C:\\Windows\\System32\\eTPKCS11.dll"), true, TokenDriverShortnames.GEMALTO) ); public static final List MAC_DRIVERS = List.of( - new PKCS11TokenDriver("Občiansky preukaz (eID klient)", Path.of("/Applications/eID_klient.app/Contents/Frameworks/libPkcs11.dylib"), false), - new PKCS11TokenDriver("I.CA SecureStore", Path.of("/usr/local/lib/pkcs11/libICASecureStorePkcs11.dylib"), true), - new PKCS11TokenDriver("MONET+ ProID+Q", Path.of("/usr/local/lib/ProIDPlus/libproidqcm11.dylib"), true), - new PKCS11TokenDriver("Gemalto IDPrime 940", Path.of("/usr/local/lib/libIDPrimePKCS11.dylib"), true) + new PKCS11TokenDriver("Občiansky preukaz (eID klient)", Path.of("/Applications/eID_klient.app/Contents/Frameworks/libPkcs11.dylib"), false, TokenDriverShortnames.EID), + new PKCS11TokenDriver("I.CA SecureStore", Path.of("/usr/local/lib/pkcs11/libICASecureStorePkcs11.dylib"), true, TokenDriverShortnames.SECURE_STORE), + new PKCS11TokenDriver("MONET+ ProID+Q", Path.of("/usr/local/lib/ProIDPlus/libproidqcm11.dylib"), true, TokenDriverShortnames.MONET), + new PKCS11TokenDriver("Gemalto IDPrime 940", Path.of("/usr/local/lib/libIDPrimePKCS11.dylib"), true, TokenDriverShortnames.GEMALTO) ); public List getAvailableDrivers() { diff --git a/src/main/java/digital/slovensko/autogram/core/SigningJob.java b/src/main/java/digital/slovensko/autogram/core/SigningJob.java index 584c0a960..355ca8a18 100644 --- a/src/main/java/digital/slovensko/autogram/core/SigningJob.java +++ b/src/main/java/digital/slovensko/autogram/core/SigningJob.java @@ -3,7 +3,6 @@ import java.io.File; import digital.slovensko.autogram.core.errors.AutogramException; -import digital.slovensko.autogram.ui.SaveFileResponder; import eu.europa.esig.dss.asic.cades.signature.ASiCWithCAdESService; import eu.europa.esig.dss.asic.xades.signature.ASiCWithXAdESService; import eu.europa.esig.dss.cades.signature.CAdESService; @@ -47,7 +46,7 @@ public SigningParameters getParameters() { public int getVisualizationWidth() { return parameters.getVisualizationWidth(); } - + private boolean isDocumentXDC() { return document.getMimeType().equals(AutogramMimeType.XML_DATACONTAINER); } @@ -150,19 +149,18 @@ private DSSDocument signDocumentAsPAdeS(SigningKey key) { return service.signDocument(getDocument(), signatureParameters, signatureValue); } - public static SigningJob buildFromFile(File file, Autogram autogram) { + public static SigningJob buildFromFile(File file, Responder responder, boolean checkPDFACompliance) { var document = new FileDocument(file); SigningParameters parameters; var filename = file.getName(); if (filename.endsWith(".pdf")) { - parameters = SigningParameters.buildForPDF(filename); + parameters = SigningParameters.buildForPDF(filename, checkPDFACompliance); } else { parameters = SigningParameters.buildForASiCWithXAdES(filename); } - var responder = new SaveFileResponder(file, autogram); return new SigningJob(document, parameters, responder); } diff --git a/src/main/java/digital/slovensko/autogram/core/SigningParameters.java b/src/main/java/digital/slovensko/autogram/core/SigningParameters.java index f96c1e4c9..b760b0311 100644 --- a/src/main/java/digital/slovensko/autogram/core/SigningParameters.java +++ b/src/main/java/digital/slovensko/autogram/core/SigningParameters.java @@ -50,7 +50,6 @@ public SigningParameters(SignatureLevel level, ASiCContainerType container, this.visualizationWidth = preferredPreviewWidth; } - public ASiCWithXAdESSignatureParameters getASiCWithXAdESSignatureParameters() { var parameters = new ASiCWithXAdESSignatureParameters(); @@ -163,15 +162,21 @@ public String getKeyInfoCanonicalization() { : CanonicalizationMethod.INCLUSIVE; } - public static SigningParameters buildForPDF(String filename) { - return new SigningParameters(SignatureLevel.PAdES_BASELINE_B, null, null, null, - DigestAlgorithm.SHA256, false, null, null, null, null, null, "", false, 600); + public static SigningParameters buildForPDF(String filename, boolean checkPDFACompliance) { + return new SigningParameters( + SignatureLevel.PAdES_BASELINE_B, + null, + null, null, + DigestAlgorithm.SHA256, + false, null, + null, null, + null, null, "", checkPDFACompliance, 640); } public static SigningParameters buildForASiCWithXAdES(String filename) { return new SigningParameters(SignatureLevel.XAdES_BASELINE_B, ASiCContainerType.ASiC_E, null, SignaturePackaging.ENVELOPING, DigestAlgorithm.SHA256, false, null, null, - null, null, null, "", false, 600); + null, null, null, "", false, 640); } public String getIdentifier() { diff --git a/src/main/java/digital/slovensko/autogram/core/TargetPath.java b/src/main/java/digital/slovensko/autogram/core/TargetPath.java new file mode 100644 index 000000000..2b009b94b --- /dev/null +++ b/src/main/java/digital/slovensko/autogram/core/TargetPath.java @@ -0,0 +1,179 @@ +package digital.slovensko.autogram.core; + +import java.nio.file.FileSystem; +import java.nio.file.FileSystems; +import java.nio.file.Files; +import java.nio.file.Path; + +import digital.slovensko.autogram.core.errors.SourceAndTargetTypeMismatchException; +import digital.slovensko.autogram.core.errors.TargetAlreadyExistsException; +import digital.slovensko.autogram.core.errors.TargetDirectoryDoesNotExistException; +import digital.slovensko.autogram.core.errors.UnableToCreateDirectoryException; + +public class TargetPath { + private final Path targetDirectory; + private final String targetName; + private final Path sourceFile; + private final boolean isForce; + private final boolean isGenerated; + private final boolean isForMultipleFiles; + private final boolean isParents; + + private final FileSystem fs; + + public TargetPath(String target, Path source, boolean force, boolean parents, FileSystem fileSystem) { + fs = fileSystem; + sourceFile = source; + isForce = force; + isParents = parents; + + isGenerated = target == null; + isForMultipleFiles = Files.isDirectory(source); + if (isGenerated) { + if (isForMultipleFiles) { + + targetDirectory = fs.getPath( + generateUniqueName(source.toAbsolutePath().getParent().toString(), source.getFileName().toString() + "_signed", + "")); + targetName = null; + + } else { + targetDirectory = source.toAbsolutePath().getParent(); + targetName = null; + } + + } else { + var targetFile = fs.getPath(target); + if (!hasSourceAndTargetMatchingType(sourceFile, targetFile)) + throw new SourceAndTargetTypeMismatchException(); + + if (Files.exists(targetFile) && !isForce) + throw new TargetAlreadyExistsException(); + + if (isForMultipleFiles) { + targetDirectory = targetFile; + targetName = null; + + } else { + targetDirectory = targetFile.getParent(); + targetName = targetFile.getFileName().toString(); + } + } + } + + public static TargetPath fromParams(CliParameters params) { + return new TargetPath(params.getTarget(), params.getSource().toPath(), params.isForce(), + params.shouldMakeParentDirectories(), FileSystems.getDefault()); + } + + public static TargetPath fromSource(Path source) { + return new TargetPath(null, source, false, false, FileSystems.getDefault()); + } + + /** + * Create directory when we want to fill it out + */ + public void mkdirIfDir() { + if (targetDirectory == null || Files.exists(targetDirectory)) + return; + + if (isParents) { + try { + Files.createDirectories(targetDirectory); + } catch (Exception e) { + throw new UnableToCreateDirectoryException(); + } + + return; + } + + if (!isForMultipleFiles) + throw new TargetDirectoryDoesNotExistException(); + + try { + Files.createDirectory(targetDirectory); + } catch (Exception e) { + throw new UnableToCreateDirectoryException(); + } + } + + private static boolean hasSourceAndTargetMatchingType(Path source, Path target) { + if (!Files.exists(target)) + return true; + + var bothAreFiles = Files.isRegularFile(target) && Files.isRegularFile(source); + var bothAreDirectories = Files.isDirectory(target) && Files.isDirectory(source); + + return (bothAreDirectories || bothAreFiles); + } + + /* + * Use these functions to get concrete file to be saved to + * + */ + + public Path getSaveFilePath(Path singleSourceFile) { + var file = _getSaveFilePath(singleSourceFile); + + if (Files.exists(file) && !isForce) + throw new TargetAlreadyExistsException(); + + return file; + } + + private Path _getSaveFilePath(Path singleSourceFile) { + var targetName = this.targetName == null ? generateTargetName(singleSourceFile) : this.targetName; + var targetDirectoryPath = targetDirectory == null ? "" : targetDirectory.toString(); + Path targetSingleFile = fs.getPath(targetDirectoryPath, targetName); + + if (!Files.exists(targetSingleFile)) + return targetSingleFile; + + if (isForce) + return targetSingleFile; + + if (isGenerated) { + var parent = targetSingleFile.getParent(); + var baseName = com.google.common.io.Files + .getNameWithoutExtension(targetSingleFile.getFileName().toString()); + var extension = "." + + com.google.common.io.Files.getFileExtension(targetSingleFile.getFileName().toString()); + return fs.getPath(generateUniqueName(parent.toString(), baseName, extension)); + } + + throw new TargetAlreadyExistsException(); + } + + private String generateUniqueName(String parent, String baseName, String extension) { + var count = 1; + var newBaseName = baseName; + parent = parent == null ? "" : parent; + while (true) { + var newTargetFile = _generateUniqueNameGetNewTargetFile(parent, newBaseName, extension); + if (!Files.exists(newTargetFile)) + return newTargetFile.toString(); + + if (count > 1000) + throw new TargetAlreadyExistsException(); + + newBaseName = baseName + " (" + count + ")"; + count++; + } + } + + private Path _generateUniqueNameGetNewTargetFile(String parent, String baseName, String extension) { + return fs.getPath(parent, baseName + extension); + } + + private String generateTargetName(Path singleSourceFile) { + var extension = singleSourceFile.getFileName().toString().endsWith(".pdf") ? ".pdf" : ".asice"; + if (isGenerated || isForMultipleFiles) + return com.google.common.io.Files.getNameWithoutExtension(singleSourceFile.getFileName().toString()) + + "_signed" + + extension; + + else + return com.google.common.io.Files.getNameWithoutExtension(targetDirectory.getFileName().toString()) + + extension; + } +} diff --git a/src/main/java/digital/slovensko/autogram/core/errors/PDFAComplianceException.java b/src/main/java/digital/slovensko/autogram/core/errors/PDFAComplianceException.java new file mode 100644 index 000000000..7a46b55d2 --- /dev/null +++ b/src/main/java/digital/slovensko/autogram/core/errors/PDFAComplianceException.java @@ -0,0 +1,7 @@ +package digital.slovensko.autogram.core.errors; + +public class PDFAComplianceException extends AutogramException { + public PDFAComplianceException() { + super("Nastala chyba", "Dokument nie je vo formáte PDF/A", "Dokument, ktorý ste chceli podpísať je vo formáte, ktorý úrady nemusia akceptovať."); + } +} diff --git a/src/main/java/digital/slovensko/autogram/core/errors/SourceAndTargetTypeMismatchException.java b/src/main/java/digital/slovensko/autogram/core/errors/SourceAndTargetTypeMismatchException.java new file mode 100644 index 000000000..4656eca8d --- /dev/null +++ b/src/main/java/digital/slovensko/autogram/core/errors/SourceAndTargetTypeMismatchException.java @@ -0,0 +1,8 @@ +package digital.slovensko.autogram.core.errors; + +public class SourceAndTargetTypeMismatchException extends AutogramException { + public SourceAndTargetTypeMismatchException() { + super("Nastala chyba", "Zdrojové a cieľové umiestnenia sú rôzneho typu (súbor / adresár)", "Zadali ste zdrojové a cieľové umiestnenia, ktoré sú rôzneho typu (súbor / adresár)"); + } + +} diff --git a/src/main/java/digital/slovensko/autogram/core/errors/SourceDoesNotExistException.java b/src/main/java/digital/slovensko/autogram/core/errors/SourceDoesNotExistException.java new file mode 100644 index 000000000..1dc7fb13c --- /dev/null +++ b/src/main/java/digital/slovensko/autogram/core/errors/SourceDoesNotExistException.java @@ -0,0 +1,11 @@ +package digital.slovensko.autogram.core.errors; + +public class SourceDoesNotExistException extends AutogramException { + public SourceDoesNotExistException() { + super("Nastala chyba", "Zdrojový súbor / adresár neexistuje", "Zadali ste zdrojový súbor / adresár, ktorý neexistuje"); + } + + public SourceDoesNotExistException(String sourcePath) { + super("Nastala chyba", "Zdrojový súbor / adresár neexistuje", "Zadali ste zdrojový súbor / adresár \"" + sourcePath + "\", ktorý neexistuje"); + } +} diff --git a/src/main/java/digital/slovensko/autogram/core/errors/SourceNotDefindedException.java b/src/main/java/digital/slovensko/autogram/core/errors/SourceNotDefindedException.java new file mode 100644 index 000000000..d22b35830 --- /dev/null +++ b/src/main/java/digital/slovensko/autogram/core/errors/SourceNotDefindedException.java @@ -0,0 +1,8 @@ +package digital.slovensko.autogram.core.errors; + +public class SourceNotDefindedException extends AutogramException { + public SourceNotDefindedException() { + super("Nastala chyba", "Zdrojový súbor / adresár nebol definovaný", "Nezadali ste zdrojový súbor / adresár na podpis"); + } + +} diff --git a/src/main/java/digital/slovensko/autogram/core/errors/TargetAlreadyExistsException.java b/src/main/java/digital/slovensko/autogram/core/errors/TargetAlreadyExistsException.java new file mode 100644 index 000000000..08da262de --- /dev/null +++ b/src/main/java/digital/slovensko/autogram/core/errors/TargetAlreadyExistsException.java @@ -0,0 +1,7 @@ +package digital.slovensko.autogram.core.errors; + +public class TargetAlreadyExistsException extends AutogramException { + public TargetAlreadyExistsException() { + super("Nastala chyba", "Cieľový súbor / adresár už existuje", "Zadali ste cieľový súbor / adresár, ktorý už existuje"); + } +} diff --git a/src/main/java/digital/slovensko/autogram/core/errors/TargetDirectoryDoesNotExistException.java b/src/main/java/digital/slovensko/autogram/core/errors/TargetDirectoryDoesNotExistException.java new file mode 100644 index 000000000..a54324243 --- /dev/null +++ b/src/main/java/digital/slovensko/autogram/core/errors/TargetDirectoryDoesNotExistException.java @@ -0,0 +1,7 @@ +package digital.slovensko.autogram.core.errors; + +public class TargetDirectoryDoesNotExistException extends AutogramException { + public TargetDirectoryDoesNotExistException() { + super("Nastala chyba", "Cieľový adresár neexistuje", "Zadali ste cieľový adresár, ktorý neexistuje"); + } +} diff --git a/src/main/java/digital/slovensko/autogram/core/errors/TokenDriverDoesNotExistException.java b/src/main/java/digital/slovensko/autogram/core/errors/TokenDriverDoesNotExistException.java new file mode 100644 index 000000000..eb8b5ff30 --- /dev/null +++ b/src/main/java/digital/slovensko/autogram/core/errors/TokenDriverDoesNotExistException.java @@ -0,0 +1,11 @@ +package digital.slovensko.autogram.core.errors; + +public class TokenDriverDoesNotExistException extends AutogramException { + public TokenDriverDoesNotExistException() { + super("Nastala chyba", "Token driver neexistuje", "Zadali ste token driver, ktorý neexistuje"); + } + + public TokenDriverDoesNotExistException(String tokenDriver) { + super("Nastala chyba", "Token driver neexistuje", "Zadali ste token driver \"" + tokenDriver + "\", ktorý neexistuje"); + } +} diff --git a/src/main/java/digital/slovensko/autogram/core/errors/UnableToCreateDirectoryException.java b/src/main/java/digital/slovensko/autogram/core/errors/UnableToCreateDirectoryException.java new file mode 100644 index 000000000..21c611dd0 --- /dev/null +++ b/src/main/java/digital/slovensko/autogram/core/errors/UnableToCreateDirectoryException.java @@ -0,0 +1,8 @@ +package digital.slovensko.autogram.core.errors; + +public class UnableToCreateDirectoryException extends AutogramException { + public UnableToCreateDirectoryException() { + super("Nastala chyba", "Nepodarilo sa vytvoriť adresár", "Nepodarilo sa vytvoriť adresár"); + } + +} diff --git a/src/main/java/digital/slovensko/autogram/drivers/PKCS11TokenDriver.java b/src/main/java/digital/slovensko/autogram/drivers/PKCS11TokenDriver.java index c8d3c7d70..070b3e1c9 100644 --- a/src/main/java/digital/slovensko/autogram/drivers/PKCS11TokenDriver.java +++ b/src/main/java/digital/slovensko/autogram/drivers/PKCS11TokenDriver.java @@ -7,8 +7,8 @@ import java.security.KeyStore; public class PKCS11TokenDriver extends TokenDriver { - public PKCS11TokenDriver(String name, Path path, boolean needsPassword) { - super(name, path, needsPassword); + public PKCS11TokenDriver(String name, Path path, boolean needsPassword, String shortname) { + super(name, path, needsPassword, shortname); } @Override diff --git a/src/main/java/digital/slovensko/autogram/drivers/TokenDriver.java b/src/main/java/digital/slovensko/autogram/drivers/TokenDriver.java index df539da28..14397c939 100644 --- a/src/main/java/digital/slovensko/autogram/drivers/TokenDriver.java +++ b/src/main/java/digital/slovensko/autogram/drivers/TokenDriver.java @@ -1,20 +1,20 @@ package digital.slovensko.autogram.drivers; -import digital.slovensko.autogram.util.OperatingSystem; import eu.europa.esig.dss.token.AbstractKeyStoreTokenConnection; import java.nio.file.Path; -import java.util.List; public abstract class TokenDriver { protected final String name; private final Path path; private final boolean needsPassword; + private final String shortname; - public TokenDriver(String name, Path path, boolean needsPassword) { + public TokenDriver(String name, Path path, boolean needsPassword, String shortname) { this.name = name; this.path = path; this.needsPassword = needsPassword; + this.shortname = shortname; } public String getName() { @@ -36,4 +36,8 @@ public boolean isInstalled() { public boolean needsPassword() { return needsPassword; } + + public String getShortname() { + return shortname; + } } diff --git a/src/main/java/digital/slovensko/autogram/server/SignEndpoint.java b/src/main/java/digital/slovensko/autogram/server/SignEndpoint.java index 714be6a13..1782459da 100644 --- a/src/main/java/digital/slovensko/autogram/server/SignEndpoint.java +++ b/src/main/java/digital/slovensko/autogram/server/SignEndpoint.java @@ -5,7 +5,6 @@ import com.sun.net.httpserver.HttpHandler; import digital.slovensko.autogram.core.Autogram; import digital.slovensko.autogram.core.SigningJob; -import digital.slovensko.autogram.core.errors.AutogramException; import digital.slovensko.autogram.core.visualization.DocumentVisualizationBuilder; import digital.slovensko.autogram.server.dto.ErrorResponse; import digital.slovensko.autogram.server.dto.SignRequestBody; diff --git a/src/main/java/digital/slovensko/autogram/server/dto/ErrorResponse.java b/src/main/java/digital/slovensko/autogram/server/dto/ErrorResponse.java index 08f0346a7..afceb0c43 100644 --- a/src/main/java/digital/slovensko/autogram/server/dto/ErrorResponse.java +++ b/src/main/java/digital/slovensko/autogram/server/dto/ErrorResponse.java @@ -1,11 +1,6 @@ package digital.slovensko.autogram.server.dto; import digital.slovensko.autogram.core.errors.AutogramException; -import digital.slovensko.autogram.core.errors.SigningCanceledByUserException; -import digital.slovensko.autogram.core.errors.UnrecognizedException; -import digital.slovensko.autogram.server.errors.MalformedBodyException; -import digital.slovensko.autogram.server.errors.RequestValidationException; -import digital.slovensko.autogram.server.errors.UnsupportedSignatureLevelExceptionError; public class ErrorResponse { private final int statusCode; diff --git a/src/main/java/digital/slovensko/autogram/server/dto/InfoResponse.java b/src/main/java/digital/slovensko/autogram/server/dto/InfoResponse.java index cb9ae3c3f..36673af3e 100644 --- a/src/main/java/digital/slovensko/autogram/server/dto/InfoResponse.java +++ b/src/main/java/digital/slovensko/autogram/server/dto/InfoResponse.java @@ -1,8 +1,5 @@ package digital.slovensko.autogram.server.dto; -import digital.slovensko.autogram.Main; -import static java.util.Objects.requireNonNullElse; - public class InfoResponse { private final String version; private final String status; diff --git a/src/main/java/digital/slovensko/autogram/server/dto/ServerSigningParameters.java b/src/main/java/digital/slovensko/autogram/server/dto/ServerSigningParameters.java index 69f8b9c99..f50099e02 100644 --- a/src/main/java/digital/slovensko/autogram/server/dto/ServerSigningParameters.java +++ b/src/main/java/digital/slovensko/autogram/server/dto/ServerSigningParameters.java @@ -11,12 +11,7 @@ import digital.slovensko.autogram.server.errors.UnsupportedSignatureLevelExceptionError; import eu.europa.esig.dss.enumerations.*; -import javax.xml.crypto.dsig.CanonicalizationMethod; import java.nio.charset.StandardCharsets; -import java.util.Arrays; -import java.util.Base64; - -import static digital.slovensko.autogram.server.dto.ServerSigningParameters.LocalCanonicalizationMethod.*; public class ServerSigningParameters { public enum LocalCanonicalizationMethod { diff --git a/src/main/java/digital/slovensko/autogram/ui/SaveFileResponder.java b/src/main/java/digital/slovensko/autogram/ui/SaveFileResponder.java index b249ff3b6..fe00721d0 100644 --- a/src/main/java/digital/slovensko/autogram/ui/SaveFileResponder.java +++ b/src/main/java/digital/slovensko/autogram/ui/SaveFileResponder.java @@ -1,29 +1,34 @@ package digital.slovensko.autogram.ui; -import com.google.common.io.Files; import digital.slovensko.autogram.core.Autogram; import digital.slovensko.autogram.core.Responder; import digital.slovensko.autogram.core.SignedDocument; +import digital.slovensko.autogram.core.TargetPath; import digital.slovensko.autogram.core.errors.AutogramException; import java.io.File; import java.io.IOException; -import java.nio.file.Paths; public class SaveFileResponder extends Responder { private final File file; private final Autogram autogram; + private final TargetPath targetPathBuilder; public SaveFileResponder(File file, Autogram autogram) { + this(file, autogram, TargetPath.fromSource(file.toPath())); + } + + public SaveFileResponder(File file, Autogram autogram, TargetPath targetPathBuilder) { this.file = file; this.autogram = autogram; + this.targetPathBuilder = targetPathBuilder; } public void onDocumentSigned(SignedDocument signedDocument) { try { - var targetFile = getTargetFile(); - signedDocument.getDocument().save(targetFile.getPath()); - autogram.onDocumentSaved(targetFile); + var targetFile = targetPathBuilder.getSaveFilePath(file.toPath()); + signedDocument.getDocument().save(targetFile.toString()); + autogram.onDocumentSaved(targetFile.toFile()); } catch (IOException e) { throw new RuntimeException(e); } @@ -32,25 +37,4 @@ public void onDocumentSigned(SignedDocument signedDocument) { public void onDocumentSignFailed(AutogramException error) { System.err.println("Sign failed error occurred: " + error.toString()); } - - private File getTargetFile() { - var directory = file.getParent(); - var name = Files.getNameWithoutExtension(file.getName()); - - var extension = ".asice"; - if (file.getName().endsWith(".pdf")) - extension = ".pdf"; - - var baseName = Paths.get(directory, name + "_signed").toString(); - var newBaseName = baseName; - - var count = 1; - while(true) { - var targetFile = new File(newBaseName + extension); - if(!targetFile.exists()) return targetFile; - - newBaseName = baseName + " (" + count + ")"; - count++; - } - } } diff --git a/src/main/java/digital/slovensko/autogram/ui/cli/CliApp.java b/src/main/java/digital/slovensko/autogram/ui/cli/CliApp.java new file mode 100644 index 000000000..d2f53bc7d --- /dev/null +++ b/src/main/java/digital/slovensko/autogram/ui/cli/CliApp.java @@ -0,0 +1,58 @@ +package digital.slovensko.autogram.ui.cli; + +import digital.slovensko.autogram.core.Autogram; +import digital.slovensko.autogram.core.CliParameters; +import digital.slovensko.autogram.core.SigningJob; +import digital.slovensko.autogram.core.errors.SourceNotDefindedException; +import digital.slovensko.autogram.core.TargetPath; +import digital.slovensko.autogram.core.errors.AutogramException; +import digital.slovensko.autogram.core.errors.SourceDoesNotExistException; +import digital.slovensko.autogram.ui.SaveFileResponder; + +import java.io.File; +import java.util.Arrays; +import java.util.Collections; + +import org.apache.commons.cli.CommandLine; + +public class CliApp { + public static void start(CommandLine cmd) { + var ui = new CliUI(); + + try { + var params = new CliParameters(cmd); + + var autogram = params.getDriver() == null ? new Autogram(ui) + : new Autogram(ui, () -> Collections.singletonList(params.getDriver())); + + if (params.getSource() == null) + throw new SourceNotDefindedException(); + + if (!params.getSource().exists()) + throw new SourceDoesNotExistException(); + + var targetPathBuilder = TargetPath.fromParams(params); + targetPathBuilder.mkdirIfDir(); + + var source = params.getSource(); + var sourceList = source.isDirectory() ? source.listFiles() : new File[] { source }; + var jobs = Arrays + .stream(sourceList).filter(f -> f.isFile()).map(f -> SigningJob.buildFromFile(f, + new SaveFileResponder(f, autogram, targetPathBuilder), params.shouldCheckPDFACompliance())) + .toList(); + if (params.shouldCheckPDFACompliance()) { + jobs.forEach(job -> { + System.out.println("Checking PDF/A file compatibility for " + job.getDocument().getName()); + autogram.checkPDFACompliance(job); + }); + } + + ui.setJobsCount(jobs.size()); + + jobs.forEach(autogram::sign); + + } catch (AutogramException e) { + ui.showError(e); + } + } +} diff --git a/src/main/java/digital/slovensko/autogram/ui/cli/CliUI.java b/src/main/java/digital/slovensko/autogram/ui/cli/CliUI.java index ae63bd625..f8dcb147c 100644 --- a/src/main/java/digital/slovensko/autogram/ui/cli/CliUI.java +++ b/src/main/java/digital/slovensko/autogram/ui/cli/CliUI.java @@ -1,14 +1,14 @@ package digital.slovensko.autogram.ui.cli; -import digital.slovensko.autogram.core.Autogram; -import digital.slovensko.autogram.core.SigningJob; -import digital.slovensko.autogram.core.SigningKey; -import digital.slovensko.autogram.core.errors.AutogramException; -import digital.slovensko.autogram.core.visualization.Visualization; +import digital.slovensko.autogram.Main; +import digital.slovensko.autogram.core.*; +import digital.slovensko.autogram.core.errors.*; import digital.slovensko.autogram.drivers.TokenDriver; import digital.slovensko.autogram.ui.UI; +import digital.slovensko.autogram.core.visualization.Visualization; import digital.slovensko.autogram.ui.gui.IgnorableException; -import digital.slovensko.autogram.util.DSSUtils; +import static digital.slovensko.autogram.util.DSSUtils.parseCN; + import eu.europa.esig.dss.token.DSSPrivateKeyEntry; import java.io.File; @@ -18,29 +18,42 @@ public class CliUI implements UI { SigningKey activeKey; + int nJobsSigned = 1; + int nJobsTotal = 0; @Override public void startSigning(SigningJob job, Autogram autogram) { - System.out.println("Starting signing for " + job); if (activeKey == null) { autogram.pickSigningKeyAndThen(key -> { activeKey = key; - autogram.sign(job, activeKey); + sign(job, autogram); }); } else { - autogram.sign(job, activeKey); + sign(job, autogram); } } + private void sign(SigningJob job, Autogram autogram) { + System.out.println("Starting signing file \"%s\" [%d/%d]".formatted(job.getDocument().getName(), nJobsSigned++, nJobsTotal)); + autogram.sign(job, activeKey); + } + + public void setJobsCount(int nJobsTotal) { + this.nJobsTotal = nJobsTotal; + } + @Override public void pickTokenDriverAndThen(List drivers, Consumer callback) { TokenDriver pickedDriver; - if (drivers.size() == 1) { + if (drivers.isEmpty()) { + showError(new NoDriversDetectedException()); + return; + } else if (drivers.size() == 1) { pickedDriver = drivers.get(0); } else { var i = new AtomicInteger(1); - System.out.println("Vyberte ulozisko certifikatov"); + System.out.println("Pick driver:"); drivers.forEach(driver -> { System.out.print("[" + i + "] "); System.out.println(driver.getName()); @@ -57,19 +70,34 @@ public void requestPasswordAndThen(TokenDriver driver, Consumer callback callback.accept(null); return; } - System.out.println("Zadajte bezpecnostny kod k ulozisku certifikatov: "); - callback.accept(CliUtils.readLine()); // TODO do not show pin + + // Read password from CLI + var password = System.console().readPassword("Enter security code for driver (hidden): "); + callback.accept(password); } @Override public void pickKeyAndThen(List keys, Consumer callback) { - if (keys.size() > 1) { - System.out.println("Found multiple keys:"); - keys.forEach(key -> System.out.println(DSSUtils.buildTooltipLabel(key))); + if (keys.isEmpty()) { + showError(new NoKeysDetectedException()); + return; } - System.out.println("Picking key: " + DSSUtils.buildTooltipLabel(keys.get(0))); - callback.accept(keys.get(0)); + if (keys.size() == 1) { + callback.accept(keys.get(0)); + return; + } + + var i = new AtomicInteger(1); + System.out.println("Pick Key:"); + keys.forEach(key -> { + System.out.print("[" + i + "] "); + System.out.println(parseCN(key.getCertificate().getSubject().getRFC2253())); + i.addAndGet(1); + }); + var pickedKey = keys.get(CliUtils.readInteger() - 1); + + callback.accept(pickedKey); } @Override @@ -84,36 +112,50 @@ public void onUIThreadDo(Runnable callback) { @Override public void onSigningSuccess(SigningJob job) { - System.out.println("Success for " + job); + } @Override public void onSigningFailed(AutogramException e) { - System.err.println(e); + throw e; } @Override - public void onDocumentSaved(File filename) { - + public void onDocumentSaved(File file) { + var directory = file.getParent() != null ? " in \"%s\"".formatted(file.getParent()) : ""; + System.out.println("File successfully signed. Signed file saved as \"%s\"".formatted(file.getName()) + directory); } @Override public void onPickSigningKeyFailed(AutogramException e) { - System.err.println(e); + showError(e); } @Override public void onUpdateAvailable() { - System.out.println("Newer version of Autogram exists. Visit "); + System.out.println("Nová verzia"); + System.out.println(String.format( + "Je dostupná nová verzia a odporúčame stiahnuť aktualizáciu. Najnovšiu verziu si možete vždy stiahnuť na %s.", + Updater.LATEST_RELEASE_URL)); } @Override public void onAboutInfo() { - System.out.println("About autograms"); + System.out.println( + """ + O projekte Autogram + Autogram je jednoduchý nástroj na podpisovanie podľa európskej regulácie eIDAS, slovenských zákonov a štandardov. Môžete ho používať komerčne aj nekomerčne a úplne zadarmo. + Autori a sponzori + Autormi tohto projektu sú Jakub Ďuraš, Slovensko.Digital, CRYSTAL CONSULTING, s.r.o, Solver IT s.r.o. a ďalší spoluautori. + Licencia a zdrojové kódy + Tento softvér pôvodne vychádza projektu z Octosign White Label od Jakuba Ďuraša, ktorý je licencovaný pod MIT licenciou. So súhlasom autora je táto verzia distribuovaná pod licenciou EUPL v1.2. + Zdrojové kódy sú dostupné na https://github.com/slovensko-digital/autogram."""); + System.out.println(String.format("Verzia: %s", Main.getVersion())); } @Override public void onPDFAComplianceCheckFailed(SigningJob job) { + throw new PDFAComplianceException(); } @Override @@ -123,6 +165,51 @@ public void showVisualization(Visualization visualization, Autogram autogram) { @Override public void showIgnorableExceptionDialog(IgnorableException exception) { + throw exception; + } + public void showError(AutogramException e) { + String errMessage = ""; + if (e instanceof FunctionCanceledException) { + errMessage = "No security code entered"; + } else if (e instanceof InitializationFailedException) { + errMessage = "Unable to read card"; + } else if (e instanceof NoDriversDetectedException) { + errMessage = "No available drivers found"; + } else if (e instanceof NoKeysDetectedException) { + errMessage = "No signing keys found"; + } else if (e instanceof PDFAComplianceException) { + errMessage = "Document is not PDF/A compliant"; + } else if (e instanceof PINIncorrectException) { + errMessage = "Incorrect security code"; + } else if (e instanceof PINLockedException) { + errMessage = "PIN is blocked"; + } else if (e instanceof SigningCanceledByUserException) { + errMessage = "Signing canceled by user"; + } else if (e instanceof SigningWithExpiredCertificateException) { + errMessage = "Signing with expired certificate"; + } else if (e instanceof TokenNotRecognizedException) { + errMessage = "Token not recognized"; + } else if (e instanceof TokenRemovedException) { + errMessage = "Token removed"; + } else if (e instanceof TargetAlreadyExistsException) { + errMessage = "Target already exists"; + } else if (e instanceof SourceAndTargetTypeMismatchException) { + errMessage = "Source and target type mismatch (file / directory)"; + } else if (e instanceof SourceDoesNotExistException) { + errMessage = "Source does not exist"; + } else if (e instanceof SourceNotDefindedException) { + errMessage = "Source not defined"; + } else if (e instanceof UnableToCreateDirectoryException) { + errMessage = "Unable to create directory"; + } else if (e instanceof TokenDriverDoesNotExistException) { + errMessage = "Token driver does not exist"; + } else if (e instanceof TargetDirectoryDoesNotExistException) { + errMessage = "Target directory does not exist"; + } else { + errMessage = "Unknown error occurred"; + e.printStackTrace(); + } + System.err.println(errMessage); } } diff --git a/src/main/java/digital/slovensko/autogram/ui/gui/MainMenuController.java b/src/main/java/digital/slovensko/autogram/ui/gui/MainMenuController.java index 1a419fe16..727fa7564 100644 --- a/src/main/java/digital/slovensko/autogram/ui/gui/MainMenuController.java +++ b/src/main/java/digital/slovensko/autogram/ui/gui/MainMenuController.java @@ -2,6 +2,7 @@ import digital.slovensko.autogram.core.Autogram; import digital.slovensko.autogram.core.SigningJob; +import digital.slovensko.autogram.ui.SaveFileResponder; import javafx.fxml.FXML; import javafx.scene.Node; import javafx.scene.input.TransferMode; @@ -38,7 +39,8 @@ public void initialize() { dropZone.setOnDragDropped(event -> { for (File file : event.getDragboard().getFiles()) { - autogram.sign(SigningJob.buildFromFile(file, autogram)); + SigningJob job = SigningJob.buildFromFile(file, new SaveFileResponder(file, autogram), false); + autogram.sign(job); } }); } @@ -49,7 +51,8 @@ public void onUploadButtonAction() { if (list != null) { for (File file : list) { - autogram.sign(SigningJob.buildFromFile(file, autogram)); + SigningJob job = SigningJob.buildFromFile(file, new SaveFileResponder(file, autogram), false); + autogram.sign(job); } } } diff --git a/src/main/java/digital/slovensko/autogram/ui/gui/PasswordController.java b/src/main/java/digital/slovensko/autogram/ui/gui/PasswordController.java index f5a244bb6..f6317d1a7 100644 --- a/src/main/java/digital/slovensko/autogram/ui/gui/PasswordController.java +++ b/src/main/java/digital/slovensko/autogram/ui/gui/PasswordController.java @@ -4,7 +4,6 @@ import javafx.scene.control.PasswordField; import javafx.scene.layout.VBox; import javafx.scene.text.Text; -import javafx.stage.Stage; import java.util.function.Consumer; diff --git a/src/main/java/digital/slovensko/autogram/ui/gui/PickDriverDialogController.java b/src/main/java/digital/slovensko/autogram/ui/gui/PickDriverDialogController.java index be2441b6a..48dfc8a9a 100644 --- a/src/main/java/digital/slovensko/autogram/ui/gui/PickDriverDialogController.java +++ b/src/main/java/digital/slovensko/autogram/ui/gui/PickDriverDialogController.java @@ -6,7 +6,6 @@ import javafx.scene.control.ToggleGroup; import javafx.scene.layout.VBox; import javafx.scene.text.Text; -import javafx.stage.Stage; import java.util.List; import java.util.function.Consumer; diff --git a/src/main/java/digital/slovensko/autogram/ui/gui/PickKeyDialogController.java b/src/main/java/digital/slovensko/autogram/ui/gui/PickKeyDialogController.java index fb42b41d6..79b181611 100644 --- a/src/main/java/digital/slovensko/autogram/ui/gui/PickKeyDialogController.java +++ b/src/main/java/digital/slovensko/autogram/ui/gui/PickKeyDialogController.java @@ -8,7 +8,6 @@ import javafx.scene.control.Tooltip; import javafx.scene.layout.VBox; import javafx.scene.text.Text; -import javafx.stage.Stage; import javafx.util.Duration; import java.util.List; diff --git a/src/main/java/digital/slovensko/autogram/ui/gui/SigningDialogController.java b/src/main/java/digital/slovensko/autogram/ui/gui/SigningDialogController.java index 13e42ff55..a6c742566 100644 --- a/src/main/java/digital/slovensko/autogram/ui/gui/SigningDialogController.java +++ b/src/main/java/digital/slovensko/autogram/ui/gui/SigningDialogController.java @@ -1,15 +1,11 @@ package digital.slovensko.autogram.ui.gui; import digital.slovensko.autogram.core.Autogram; -import digital.slovensko.autogram.core.SigningJob; import digital.slovensko.autogram.core.SigningKey; import digital.slovensko.autogram.core.visualization.Visualization; import digital.slovensko.autogram.ui.Visualizer; import digital.slovensko.autogram.util.DSSUtils; import eu.europa.esig.dss.model.CommonDocument; -import javafx.animation.Interpolator; -import javafx.animation.RotateTransition; -import javafx.animation.Timeline; import javafx.concurrent.Worker; import javafx.event.ActionEvent; import javafx.event.Event; @@ -23,7 +19,6 @@ import javafx.scene.image.ImageView; import javafx.scene.input.ContextMenuEvent; import javafx.scene.layout.VBox; -import javafx.scene.transform.Rotate; import javafx.scene.web.WebView; import javafx.stage.Stage; diff --git a/src/main/java/digital/slovensko/autogram/util/OperatingSystem.java b/src/main/java/digital/slovensko/autogram/util/OperatingSystem.java index 3c63a8fe3..b4541ab36 100644 --- a/src/main/java/digital/slovensko/autogram/util/OperatingSystem.java +++ b/src/main/java/digital/slovensko/autogram/util/OperatingSystem.java @@ -17,7 +17,7 @@ public static OperatingSystem current() { } else if (osName.contains("nux")) { return LINUX; } else { - return null; + throw new RuntimeException("Unsupported OS"); } } } diff --git a/src/test/java/digital/slovensko/autogram/AutogramTests.java b/src/test/java/digital/slovensko/autogram/AutogramTests.java index 9d83b6def..4ae981f2a 100644 --- a/src/test/java/digital/slovensko/autogram/AutogramTests.java +++ b/src/test/java/digital/slovensko/autogram/AutogramTests.java @@ -75,7 +75,7 @@ public List getAvailableDrivers() { private static class FakeTokenDriver extends TokenDriver { public FakeTokenDriver(String name) { - super(name, Path.of(""), true); + super(name, Path.of(""), true, "fake"); } @Override @@ -92,7 +92,7 @@ public AbstractKeyStoreTokenConnection createTokenWithPassword(char[] password) private static class FakeTokenDriverWithExpiredCertificate extends TokenDriver { public FakeTokenDriverWithExpiredCertificate() { - super("fake-token-driver-with-expired-certificate", Path.of(""), true); + super("fake-token-driver-with-expired-certificate", Path.of(""), true, "fake"); } @Override diff --git a/src/test/java/digital/slovensko/autogram/XDCTransformerTests.java b/src/test/java/digital/slovensko/autogram/XDCTransformerTests.java index 03eef0b1c..d16d1ee46 100644 --- a/src/test/java/digital/slovensko/autogram/XDCTransformerTests.java +++ b/src/test/java/digital/slovensko/autogram/XDCTransformerTests.java @@ -4,7 +4,6 @@ import digital.slovensko.autogram.core.XDCTransformer; import eu.europa.esig.dss.enumerations.ASiCContainerType; import eu.europa.esig.dss.enumerations.DigestAlgorithm; -import eu.europa.esig.dss.enumerations.MimeType; import eu.europa.esig.dss.enumerations.MimeTypeEnum; import eu.europa.esig.dss.enumerations.SignatureLevel; import eu.europa.esig.dss.enumerations.SignaturePackaging; diff --git a/src/test/java/digital/slovensko/autogram/core/TargetPathTest.java b/src/test/java/digital/slovensko/autogram/core/TargetPathTest.java new file mode 100644 index 000000000..2a834cb7e --- /dev/null +++ b/src/test/java/digital/slovensko/autogram/core/TargetPathTest.java @@ -0,0 +1,268 @@ +package digital.slovensko.autogram.core; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.IOException; +import java.nio.file.FileSystem; +import java.nio.file.Files; +import java.nio.file.Path; + +import org.junit.jupiter.api.Test; + +import com.google.common.jimfs.Jimfs; + +public class TargetPathTest { + // @Rule + // public MockitoRule rule = + // MockitoJUnit.rule().strictness(Strictness.STRICT_STUBS); + + /** + * Used in GUI mode with single file + * or used in CLI mode without target eg. `--cli -s /test/virtual/source.pdf` + * + * @throws IOException + */ + @Test + public void testSingleFileNoTarget() throws IOException { + FileSystem fs = Jimfs.newFileSystem(com.google.common.jimfs.Configuration.unix()); + var sourceFile = fs.getPath("/test/virtual/source.pdf"); + Files.createDirectories(sourceFile.getParent()); + Files.createFile(sourceFile); + + var targetPath = new TargetPath(null, sourceFile, false, false, fs); + var target = targetPath.getSaveFilePath(sourceFile); + + assertEqualPath(fs, "/test/virtual/source_signed.pdf", target); + } + + /** + * Used in GUI mode with single file + * or used in CLI mode without target eg. `--cli -s source.pdf` on path `/test/virtual/` + * + * @throws IOException + */ + @Test + public void testSingleFileNoTargetNoParent() throws IOException { + var config = com.google.common.jimfs.Configuration.unix().toBuilder().setWorkingDirectory("/test/virtual/").build(); + FileSystem fs = Jimfs.newFileSystem(config); + Files.createDirectories(fs.getPath("/test/virtual/")); + var sourceFile = fs.getPath("source.pdf"); + Files.createFile(sourceFile); + + var targetPath = new TargetPath(null, sourceFile, false, false, fs); + var target = targetPath.getSaveFilePath(sourceFile); + + assertEqualPath(fs, "/test/virtual/source_signed.pdf", target); + } + + /** + * Used in GUI mode with single file and no target when generated target file + * exits + * or used in CLI mode without target eg. `--cli -s /test/virtual/source.pdf` + * + * @throws IOException + */ + @Test + public void testSingleFileNoTargetFileExists() throws IOException { + FileSystem fs = Jimfs.newFileSystem(com.google.common.jimfs.Configuration.unix()); + var sourceFile = fs.getPath("/test/virtual/source.pdf"); + Files.createDirectories(sourceFile.getParent()); + Files.createFile(sourceFile); + Files.createFile(fs.getPath("/test/virtual/source_signed.pdf")); + + var targetPath = new TargetPath(null, sourceFile, false, false, fs); + var target = targetPath.getSaveFilePath(sourceFile); + + assertEqualPath(fs, "/test/virtual/source_signed (1).pdf", target); + } + + /** + * Used in GUI mode with single file + * or used in CLI mode without target eg. `--cli -s /test/virtual/source.pdf` + * + * @throws IOException + */ + @Test + public void testSingleFileNoTargetAsice() throws IOException { + FileSystem fs = Jimfs.newFileSystem(com.google.common.jimfs.Configuration.unix()); + var sourceFile = fs.getPath("/test/virtual/source.xml"); + Files.createDirectories(sourceFile.getParent()); + Files.createFile(sourceFile); + + var targetPath = new TargetPath(null, sourceFile, false, false, fs); + var target = targetPath.getSaveFilePath(sourceFile); + + assertEqualPath(fs, "/test/virtual/source_signed.asice", target); + } + + /** + * Used in CLI mode eg. `--cli -s /test/virtual/source.pdf -t + * /test/virtual/target.pdf` + * + * @throws IOException + */ + @Test + public void testSingleFileWithTarget() throws IOException { + FileSystem fs = Jimfs.newFileSystem(com.google.common.jimfs.Configuration.unix()); + var sourceFile = fs.getPath("/test/virtual/source.pdf"); + Files.createDirectories(sourceFile.getParent()); + Files.createFile(sourceFile); + + var targetPath = new TargetPath("/test/virtual/other/target.pdf", sourceFile, false, false, fs); + var target = targetPath.getSaveFilePath(sourceFile); + + assertEqualPath(fs, "/test/virtual/other/target.pdf", target); + } + + + + /** + * `--cli -s /test/virtual/ -t /test/virtual/target/` + */ + @Test + public void testDirectoryWithTarget() throws IOException { + + FileSystem fs = Jimfs.newFileSystem(com.google.common.jimfs.Configuration.unix()); + var sourceDirectory = fs.getPath("/test/virtual/"); + Files.createDirectories(sourceDirectory); + + var source1 = fs.getPath("/test/virtual/source", "source1.pdf"); + var source2 = fs.getPath("/test/virtual/source", "source2.pdf"); + + var targetPath = new TargetPath("/test/virtual/target/", sourceDirectory, false, false, fs); + var target1 = targetPath.getSaveFilePath(source1); + var target2 = targetPath.getSaveFilePath(source2); + + assertEqualPath(fs, "/test/virtual/target/source1_signed.pdf", target1); + assertEqualPath(fs, "/test/virtual/target/source2_signed.pdf", target2); + } + + /** + * `--cli -s /test/virtual/` + */ + @Test + public void testDirectoryNoTarget() throws IOException { + + FileSystem fs = Jimfs.newFileSystem(com.google.common.jimfs.Configuration.unix()); + var sourceDirectory = fs.getPath("/test/virtual/source/"); + Files.createDirectories(sourceDirectory); + + var source1 = fs.getPath("/test/virtual/source", "source1.pdf"); + var source2 = fs.getPath("/test/virtual/source", "source2.pdf"); + + var targetPath = new TargetPath(null, sourceDirectory, false, false, fs); + var target1 = targetPath.getSaveFilePath(source1); + var target2 = targetPath.getSaveFilePath(source2); + + assertEqualPath(fs, "/test/virtual/source_signed/source1_signed.pdf", target1); + assertEqualPath(fs, "/test/virtual/source_signed/source2_signed.pdf", target2); + } + + + /** + * `--cli -s /test/virtual/` + */ + @Test + public void testDirectoryNoTargetNoParent() throws IOException { + + var config = com.google.common.jimfs.Configuration.unix().toBuilder().setWorkingDirectory("/test/virtual/").build(); + FileSystem fs = Jimfs.newFileSystem(config); + var sourceDirectory = fs.getPath("source/"); + Files.createDirectories(sourceDirectory); + + var source1 = fs.getPath("source", "source1.pdf"); + var source2 = fs.getPath("source", "source2.pdf"); + + var targetPath = new TargetPath(null, sourceDirectory, false, false, fs); + var target1 = targetPath.getSaveFilePath(source1); + var target2 = targetPath.getSaveFilePath(source2); + + assertEqualPath(fs, "/test/virtual/source_signed/source1_signed.pdf", target1); + assertEqualPath(fs, "/test/virtual/source_signed/source2_signed.pdf", target2); + } + + @Test + public void testMkdirIfDirNotExists() throws IllegalArgumentException, + IllegalAccessException, IOException { + + FileSystem fs = Jimfs.newFileSystem(com.google.common.jimfs.Configuration.unix()); + var sourceDirectory = fs.getPath("/test/virtual/source"); + Files.createDirectories(sourceDirectory); + + var targetPath = new TargetPath(null, sourceDirectory, false, false, fs); + targetPath.mkdirIfDir(); + + assertTrue(Files.exists(fs.getPath("/test/virtual/source_signed"))); + } + + @Test() + public void testTargetDirExits() throws IllegalArgumentException, + IllegalAccessException, IOException { + + FileSystem fs = Jimfs.newFileSystem(com.google.common.jimfs.Configuration.unix()); + var sourceDirectory = fs.getPath("/test/virtual/"); + Files.createDirectories(sourceDirectory); + Files.createDirectories(fs.getPath("/test/output")); + + assertThrows(digital.slovensko.autogram.core.errors.TargetAlreadyExistsException.class, () -> { + var targetPath = new TargetPath("/test/output", sourceDirectory, false, false, fs); + }); + } + + @Test() + public void testTargetFileExits() throws IllegalArgumentException, + IllegalAccessException, IOException { + + FileSystem fs = Jimfs.newFileSystem(com.google.common.jimfs.Configuration.unix()); + var sourceFile = fs.getPath("/test/virtual/source.pdf"); + Files.createDirectories(sourceFile.getParent()); + Files.createFile(sourceFile); + Files.createFile(fs.getPath("/test/output.pdf")); + + assertThrows(digital.slovensko.autogram.core.errors.TargetAlreadyExistsException.class, () -> { + var targetPath = new TargetPath("/test/output.pdf", sourceFile, false, false, fs); + }); + } + + @Test() + public void testMkdirIfDirExistsForce() throws IllegalArgumentException, + IllegalAccessException, IOException { + + FileSystem fs = Jimfs.newFileSystem(com.google.common.jimfs.Configuration.unix()); + var sourceDirectory = fs.getPath("/test/virtual/"); + Files.createDirectories(sourceDirectory); + Files.createDirectories(fs.getPath("/test/output")); + + var targetPath = new TargetPath("/test/output", sourceDirectory, true, false, fs); + targetPath.mkdirIfDir(); + } + + @Test() + public void testMkdirIfDirExistsParents() throws IllegalArgumentException, + IllegalAccessException, IOException { + + FileSystem fs = Jimfs.newFileSystem(com.google.common.jimfs.Configuration.unix()); + var sourceDirectory = fs.getPath("/test/virtual/"); + Files.createDirectories(sourceDirectory); + var targetPath = new TargetPath("/test/output/parent/directories", sourceDirectory, false, true, fs); + targetPath.mkdirIfDir(); + + } + + /* Assert helpers */ + + private void assertEqualPath(FileSystem fs, String expected, String actual) { + assertEqualPath(fs.getPath(expected), fs.getPath(actual)); + } + + private void assertEqualPath(FileSystem fs, String expected, Path actual) { + assertEqualPath(fs.getPath(expected), actual); + } + + private void assertEqualPath(Path expected, Path actual) { + assertEquals(expected.normalize().toString(), actual.normalize().toString()); + } + +}