diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/UIDataController.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/UIDataController.java index 18fb5c11a8..f2766d279a 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/UIDataController.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/UIDataController.java @@ -10,7 +10,6 @@ import java.security.KeyStore; import java.security.KeyStoreException; import java.security.NoSuchAlgorithmException; import java.security.Provider; -import java.security.Security; import java.security.cert.Certificate; import java.security.cert.CertificateException; import java.security.cert.X509Certificate; @@ -20,7 +19,6 @@ import java.time.format.DateTimeFormatter; import java.util.*; import java.util.stream.Stream; -import org.apache.commons.io.FileUtils; import org.bouncycastle.asn1.x500.RDN; import org.bouncycastle.asn1.x500.X500Name; import org.bouncycastle.asn1.x500.style.BCStyle; @@ -32,6 +30,7 @@ import org.springframework.core.io.ResourceLoader; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.multipart.MultipartFile; @@ -47,6 +46,8 @@ import stirling.software.SPDF.model.Dependency; import stirling.software.SPDF.model.SignatureFile; import stirling.software.SPDF.model.api.security.CertStoreEntriesRequest; import stirling.software.SPDF.service.SharedSignatureService; +import stirling.software.SPDF.util.DesktopModeUtils; +import stirling.software.SPDF.util.Pkcs11ProviderLoader; import stirling.software.common.annotations.api.UiDataApi; import stirling.software.common.configuration.InstallationPathConfig; import stirling.software.common.configuration.RuntimePathConfig; @@ -218,8 +219,7 @@ public class UIDataController { }) @Operation(summary = "Get available certificates from OS-backed stores") public ResponseEntity getCertificateStoreEntries( - @org.springframework.web.bind.annotation.ModelAttribute CertStoreEntriesRequest request) - throws Exception { + @ModelAttribute CertStoreEntriesRequest request) throws Exception { String certType = request.getCertType(); MultipartFile pkcs11ConfigFile = request.getPkcs11ConfigFile(); String password = request.getPassword(); @@ -270,9 +270,7 @@ public class UIDataController { } private void ensureDesktopMode(String certType) { - boolean isDesktopMode = - Boolean.parseBoolean(System.getProperty("STIRLING_PDF_TAURI_MODE", "false")); - if (!isDesktopMode) { + if (!DesktopModeUtils.isDesktopMode()) { throw ExceptionUtils.createIllegalArgumentException( "error.invalidArgument", "Invalid argument: {0}", @@ -299,7 +297,7 @@ public class UIDataController { "Invalid argument: {0}", "PKCS11 configuration file is required"); } - Provider pkcs11Provider = loadPkcs11Provider(pkcs11ConfigFile); + Provider pkcs11Provider = Pkcs11ProviderLoader.loadProvider(pkcs11ConfigFile); KeyStore pkcs11Store = KeyStore.getInstance("PKCS11", pkcs11Provider); pkcs11Store.load(null, password != null ? password.toCharArray() : null); return pkcs11Store; @@ -311,21 +309,6 @@ public class UIDataController { } } - private Provider loadPkcs11Provider(MultipartFile configFile) throws IOException { - Provider baseProvider = Security.getProvider("SunPKCS11"); - if (baseProvider == null) { - throw ExceptionUtils.createIllegalArgumentException( - "error.invalidArgument", - "Invalid argument: {0}", - "SunPKCS11 provider is not available in this JVM"); - } - java.io.File tempFile = java.io.File.createTempFile("spdf-pkcs11", ".cfg"); - FileUtils.copyInputStreamToFile(configFile.getInputStream(), tempFile); - Provider provider = baseProvider.configure(tempFile.getAbsolutePath()); - Security.addProvider(provider); - return provider; - } - private CertificateStoreEntry buildCertificateEntry(String alias, X509Certificate certificate) { String displayName = getCertificateDisplayName(certificate); return new CertificateStoreEntry( diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/security/CertSignController.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/security/CertSignController.java index bfe456a734..7204d0bf1a 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/security/CertSignController.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/security/CertSignController.java @@ -73,6 +73,8 @@ import lombok.extern.slf4j.Slf4j; import stirling.software.SPDF.config.swagger.StandardPdfResponse; import stirling.software.SPDF.model.api.security.SignPDFWithCertRequest; +import stirling.software.SPDF.util.DesktopModeUtils; +import stirling.software.SPDF.util.Pkcs11ProviderLoader; import stirling.software.common.annotations.AutoJobPostMapping; import stirling.software.common.service.CustomPDFDocumentFactory; import stirling.software.common.service.ServerCertificateServiceInterface; @@ -260,7 +262,7 @@ public class CertSignController { pkcs11ConfigFile, "PKCS11 configuration", "PKCS11 configuration file is required"); - Provider pkcs11Provider = loadPkcs11Provider(pkcs11ConfigFile); + Provider pkcs11Provider = Pkcs11ProviderLoader.loadProvider(pkcs11ConfigFile); ks = KeyStore.getInstance("PKCS11", pkcs11Provider); ks.load(null, password != null ? password.toCharArray() : null); aliasRequired = true; @@ -312,9 +314,7 @@ public class CertSignController { } private void ensureDesktopMode(String certType) { - boolean isDesktopMode = - Boolean.parseBoolean(System.getProperty("STIRLING_PDF_TAURI_MODE", "false")); - if (!isDesktopMode) { + if (!DesktopModeUtils.isDesktopMode()) { throw ExceptionUtils.createIllegalArgumentException( "error.invalidArgument", "Invalid argument: {0}", @@ -367,21 +367,6 @@ public class CertSignController { return new KeyStoreSelection(filtered, entryPassword); } - private Provider loadPkcs11Provider(MultipartFile configFile) throws IOException { - Provider baseProvider = Security.getProvider("SunPKCS11"); - if (baseProvider == null) { - throw ExceptionUtils.createIllegalArgumentException( - "error.invalidArgument", - "Invalid argument: {0}", - "SunPKCS11 provider is not available in this JVM"); - } - File tempFile = File.createTempFile("spdf-pkcs11", ".cfg"); - FileUtils.copyInputStreamToFile(configFile.getInputStream(), tempFile); - Provider provider = baseProvider.configure(tempFile.getAbsolutePath()); - Security.addProvider(provider); - return provider; - } - private PrivateKey getPrivateKeyFromPEM(byte[] pemBytes, String password) throws IOException, OperatorCreationException, PKCSException { try (PEMParser pemParser = diff --git a/app/core/src/main/java/stirling/software/SPDF/util/DesktopModeUtils.java b/app/core/src/main/java/stirling/software/SPDF/util/DesktopModeUtils.java new file mode 100644 index 0000000000..819b0aa391 --- /dev/null +++ b/app/core/src/main/java/stirling/software/SPDF/util/DesktopModeUtils.java @@ -0,0 +1,14 @@ +package stirling.software.SPDF.util; + +public final class DesktopModeUtils { + + private DesktopModeUtils() {} + + public static boolean isDesktopMode() { + return Boolean.parseBoolean(System.getProperty("STIRLING_PDF_TAURI_MODE", "false")) + || Boolean.parseBoolean( + System.getenv().getOrDefault("STIRLING_PDF_TAURI_MODE", "false")) + || Boolean.parseBoolean(System.getenv().getOrDefault("STIRLING_DESKTOP", "false")) + || Boolean.parseBoolean(System.getenv().getOrDefault("VITE_DESKTOP", "false")); + } +} diff --git a/app/core/src/main/java/stirling/software/SPDF/util/Pkcs11ProviderLoader.java b/app/core/src/main/java/stirling/software/SPDF/util/Pkcs11ProviderLoader.java new file mode 100644 index 0000000000..365253c75c --- /dev/null +++ b/app/core/src/main/java/stirling/software/SPDF/util/Pkcs11ProviderLoader.java @@ -0,0 +1,44 @@ +package stirling.software.SPDF.util; + +import java.io.File; +import java.io.IOException; +import java.security.Provider; +import java.security.Security; + +import org.apache.commons.io.FileUtils; +import org.springframework.web.multipart.MultipartFile; + +import stirling.software.common.util.ExceptionUtils; + +public final class Pkcs11ProviderLoader { + + private Pkcs11ProviderLoader() {} + + public static Provider loadProvider(MultipartFile configFile) throws IOException { + Provider baseProvider = Security.getProvider("SunPKCS11"); + if (baseProvider == null) { + throw ExceptionUtils.createIllegalArgumentException( + "error.invalidArgument", + "Invalid argument: {0}", + "SunPKCS11 provider is not available in this JVM"); + } + + File tempFile = File.createTempFile("spdf-pkcs11", ".cfg"); + tempFile.deleteOnExit(); + try { + FileUtils.copyInputStreamToFile(configFile.getInputStream(), tempFile); + Provider provider = baseProvider.configure(tempFile.getAbsolutePath()); + Provider existingProvider = Security.getProvider(provider.getName()); + if (existingProvider != null) { + return existingProvider; + } + Security.addProvider(provider); + return provider; + } catch (IOException e) { + if (tempFile.exists()) { + tempFile.delete(); + } + throw e; + } + } +} diff --git a/frontend/public/locales/en-GB/translation.toml b/frontend/public/locales/en-GB/translation.toml index 4080330a80..ba1c82740a 100644 --- a/frontend/public/locales/en-GB/translation.toml +++ b/frontend/public/locales/en-GB/translation.toml @@ -2655,6 +2655,7 @@ chooseJksFile = "Choose JKS File" chooseP12File = "Choose PKCS12 File" choosePfxFile = "Choose PFX File" choosePrivateKey = "Choose Private Key File" +choosePkcs11Config = "Choose PKCS#11 Config File" location = "Location" logoTitle = "Logo" name = "Name" @@ -2663,8 +2664,18 @@ pageNumber = "Page Number" password = "Certificate Password" passwordOptional = "Leave empty if no password" reason = "Reason" +refreshStoreCertificates = "Refresh" +selectStoreCertificate = "Select a certificate" serverCertMessage = "Using server certificate - no files or password required" showLogo = "Show Logo" +storeCertificateExpires = "Expires" +storeCertificateIssuer = "Issuer" +storeCertificateSubject = "Subject" +storeCertificates = "Available Certificates" +storeCertificatesError = "Failed to load certificates" +loadingStoreCertificates = "Loading certificates..." +pin = "Token PIN / Password" +pinOptional = "Leave empty if not required" [certSign.signMode] stepTitle = "Sign Mode" @@ -2693,6 +2704,11 @@ text = "Need recipient Trusted status? Manual. Need a fast, tamper [certSign.certTypeStep] stepTitle = "Certificate Format" +[certSign.certType] +windowsStore = "Windows Store" +macKeychain = "macOS Keychain" +pkcs11 = "PKCS#11" + [certSign.certFiles] stepTitle = "Certificate Files" diff --git a/frontend/src/core/utils/isDesktopMode.ts b/frontend/src/core/utils/isDesktopMode.ts index bc826e9c54..7bf28a1f9d 100644 --- a/frontend/src/core/utils/isDesktopMode.ts +++ b/frontend/src/core/utils/isDesktopMode.ts @@ -1,4 +1,5 @@ export const isDesktopMode = (): boolean => import.meta.env.MODE === 'desktop' || import.meta.env.VITE_DESKTOP === 'true' - || import.meta.env.STIRLING_DESKTOP === 'true'; + || import.meta.env.STIRLING_DESKTOP === 'true' + || import.meta.env.STIRLING_PDF_TAURI_MODE === 'true';