mirror of
https://github.com/Frooodle/Stirling-PDF.git
synced 2025-01-19 00:07:17 +01:00
parent
88f3594d80
commit
a7ed99084f
@ -1,8 +1,11 @@
|
||||
package stirling.software.SPDF.controller.api.security;
|
||||
|
||||
import java.awt.Color;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.OutputStream;
|
||||
import java.security.KeyStore;
|
||||
@ -14,12 +17,38 @@ import java.security.UnrecoverableKeyException;
|
||||
import java.security.cert.Certificate;
|
||||
import java.security.cert.CertificateException;
|
||||
import java.security.cert.CertificateFactory;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.util.Calendar;
|
||||
import java.util.List;
|
||||
|
||||
import org.apache.pdfbox.examples.signature.CreateSignatureBase;
|
||||
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||
import org.apache.pdfbox.pdmodel.PDPage;
|
||||
import org.apache.pdfbox.pdmodel.PDPageContentStream;
|
||||
import org.apache.pdfbox.pdmodel.PDResources;
|
||||
import org.apache.pdfbox.pdmodel.common.PDRectangle;
|
||||
import org.apache.pdfbox.pdmodel.common.PDStream;
|
||||
import org.apache.pdfbox.pdmodel.font.PDFont;
|
||||
import org.apache.pdfbox.pdmodel.font.PDType1Font;
|
||||
import org.apache.pdfbox.pdmodel.font.Standard14Fonts.FontName;
|
||||
import org.apache.pdfbox.pdmodel.graphics.blend.BlendMode;
|
||||
import org.apache.pdfbox.pdmodel.graphics.form.PDFormXObject;
|
||||
import org.apache.pdfbox.pdmodel.graphics.image.PDImageXObject;
|
||||
import org.apache.pdfbox.pdmodel.graphics.state.PDExtendedGraphicsState;
|
||||
import org.apache.pdfbox.pdmodel.interactive.annotation.PDAnnotationWidget;
|
||||
import org.apache.pdfbox.pdmodel.interactive.annotation.PDAppearanceDictionary;
|
||||
import org.apache.pdfbox.pdmodel.interactive.annotation.PDAppearanceStream;
|
||||
import org.apache.pdfbox.pdmodel.interactive.digitalsignature.PDSignature;
|
||||
import org.apache.pdfbox.pdmodel.interactive.digitalsignature.SignatureOptions;
|
||||
import org.apache.pdfbox.pdmodel.interactive.form.PDAcroForm;
|
||||
import org.apache.pdfbox.pdmodel.interactive.form.PDField;
|
||||
import org.apache.pdfbox.pdmodel.interactive.form.PDSignatureField;
|
||||
import org.apache.pdfbox.util.Matrix;
|
||||
import org.bouncycastle.asn1.pkcs.PrivateKeyInfo;
|
||||
import org.bouncycastle.asn1.x500.RDN;
|
||||
import org.bouncycastle.asn1.x500.X500Name;
|
||||
import org.bouncycastle.asn1.x500.style.BCStyle;
|
||||
import org.bouncycastle.asn1.x500.style.IETFUtils;
|
||||
import org.bouncycastle.jce.provider.BouncyCastleProvider;
|
||||
import org.bouncycastle.openssl.PEMDecryptorProvider;
|
||||
import org.bouncycastle.openssl.PEMEncryptedKeyPair;
|
||||
@ -35,6 +64,7 @@ import org.bouncycastle.pkcs.PKCSException;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.core.io.ClassPathResource;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
@ -62,13 +92,103 @@ public class CertSignController {
|
||||
}
|
||||
|
||||
class CreateSignature extends CreateSignatureBase {
|
||||
File imageFile;
|
||||
|
||||
public CreateSignature(KeyStore keystore, char[] pin)
|
||||
throws KeyStoreException,
|
||||
UnrecoverableKeyException,
|
||||
NoSuchAlgorithmException,
|
||||
IOException,
|
||||
CertificateException {
|
||||
UnrecoverableKeyException,
|
||||
NoSuchAlgorithmException,
|
||||
IOException,
|
||||
CertificateException {
|
||||
super(keystore, pin);
|
||||
ClassPathResource resource = new ClassPathResource("static/images/signature.png");
|
||||
imageFile = resource.getFile();
|
||||
}
|
||||
|
||||
public InputStream createVisibleSignature(PDDocument srcDoc, PDSignature signature, Integer pageNumber,
|
||||
Boolean showImage) throws IOException {
|
||||
// modified from org.apache.pdfbox.examples.signature.CreateVisibleSignature2
|
||||
try (PDDocument doc = new PDDocument()) {
|
||||
PDPage page = new PDPage(srcDoc.getPage(pageNumber).getMediaBox());
|
||||
doc.addPage(page);
|
||||
PDAcroForm acroForm = new PDAcroForm(doc);
|
||||
doc.getDocumentCatalog().setAcroForm(acroForm);
|
||||
PDSignatureField signatureField = new PDSignatureField(acroForm);
|
||||
PDAnnotationWidget widget = signatureField.getWidgets().get(0);
|
||||
List<PDField> acroFormFields = acroForm.getFields();
|
||||
acroForm.setSignaturesExist(true);
|
||||
acroForm.setAppendOnly(true);
|
||||
acroForm.getCOSObject().setDirect(true);
|
||||
acroFormFields.add(signatureField);
|
||||
|
||||
PDRectangle rect = new PDRectangle(0, 0, 200, 50);
|
||||
|
||||
widget.setRectangle(rect);
|
||||
|
||||
// from PDVisualSigBuilder.createHolderForm()
|
||||
PDStream stream = new PDStream(doc);
|
||||
PDFormXObject form = new PDFormXObject(stream);
|
||||
PDResources res = new PDResources();
|
||||
form.setResources(res);
|
||||
form.setFormType(1);
|
||||
PDRectangle bbox = new PDRectangle(rect.getWidth(), rect.getHeight());
|
||||
float height = bbox.getHeight();
|
||||
form.setBBox(bbox);
|
||||
PDFont font = new PDType1Font(FontName.TIMES_BOLD);
|
||||
|
||||
// from PDVisualSigBuilder.createAppearanceDictionary()
|
||||
PDAppearanceDictionary appearance = new PDAppearanceDictionary();
|
||||
appearance.getCOSObject().setDirect(true);
|
||||
PDAppearanceStream appearanceStream = new PDAppearanceStream(form.getCOSObject());
|
||||
appearance.setNormalAppearance(appearanceStream);
|
||||
widget.setAppearance(appearance);
|
||||
|
||||
try (PDPageContentStream cs = new PDPageContentStream(doc, appearanceStream)) {
|
||||
if (showImage) {
|
||||
cs.saveGraphicsState();
|
||||
PDExtendedGraphicsState extState = new PDExtendedGraphicsState();
|
||||
extState.setBlendMode(BlendMode.MULTIPLY);
|
||||
extState.setNonStrokingAlphaConstant(0.5f);
|
||||
cs.setGraphicsStateParameters(extState);
|
||||
cs.transform(Matrix.getScaleInstance(0.08f, 0.08f));
|
||||
PDImageXObject img = PDImageXObject.createFromFileByExtension(imageFile,
|
||||
doc);
|
||||
cs.drawImage(img, 100, 0);
|
||||
cs.restoreGraphicsState();
|
||||
}
|
||||
|
||||
// show text
|
||||
float fontSize = 10;
|
||||
float leading = fontSize * 1.5f;
|
||||
cs.beginText();
|
||||
cs.setFont(font, fontSize);
|
||||
cs.setNonStrokingColor(Color.black);
|
||||
cs.newLineAtOffset(fontSize, height - leading);
|
||||
cs.setLeading(leading);
|
||||
|
||||
X509Certificate cert = (X509Certificate) getCertificateChain()[0];
|
||||
|
||||
// https://stackoverflow.com/questions/2914521/
|
||||
X500Name x500Name = new X500Name(cert.getSubjectX500Principal().getName());
|
||||
RDN cn = x500Name.getRDNs(BCStyle.CN)[0];
|
||||
String name = IETFUtils.valueToString(cn.getFirst().getValue());
|
||||
|
||||
String date = signature.getSignDate().getTime().toString();
|
||||
String reason = signature.getReason();
|
||||
|
||||
cs.showText("Signed by " + name);
|
||||
cs.newLine();
|
||||
cs.showText(date);
|
||||
cs.newLine();
|
||||
cs.showText(reason);
|
||||
|
||||
cs.endText();
|
||||
}
|
||||
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
doc.save(baos);
|
||||
return new ByteArrayInputStream(baos.toByteArray());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -80,10 +200,7 @@ public class CertSignController {
|
||||
}
|
||||
|
||||
@PostMapping(consumes = "multipart/form-data", value = "/cert-sign")
|
||||
@Operation(
|
||||
summary = "Sign PDF with a Digital Certificate",
|
||||
description =
|
||||
"This endpoint accepts a PDF file, a digital certificate and related information to sign the PDF. It then returns the digitally signed PDF file. Input:PDF Output:PDF Type:SISO")
|
||||
@Operation(summary = "Sign PDF with a Digital Certificate", description = "This endpoint accepts a PDF file, a digital certificate and related information to sign the PDF. It then returns the digitally signed PDF file. Input:PDF Output:PDF Type:SISO")
|
||||
public ResponseEntity<byte[]> signPDFWithCert(@ModelAttribute SignPDFWithCertRequest request)
|
||||
throws Exception {
|
||||
MultipartFile pdf = request.getFileInput();
|
||||
@ -97,7 +214,7 @@ public class CertSignController {
|
||||
String reason = request.getReason();
|
||||
String location = request.getLocation();
|
||||
String name = request.getName();
|
||||
Integer pageNumber = request.getPageNumber();
|
||||
Integer pageNumber = request.getPageNumber() - 1;
|
||||
|
||||
if (certType == null) {
|
||||
throw new IllegalArgumentException("Cert type must be provided");
|
||||
@ -112,7 +229,7 @@ public class CertSignController {
|
||||
PrivateKey privateKey = getPrivateKeyFromPEM(privateKeyFile.getBytes(), password);
|
||||
Certificate cert = (Certificate) getCertificateFromPEM(certFile.getBytes());
|
||||
ks.setKeyEntry(
|
||||
"alias", privateKey, password.toCharArray(), new Certificate[] {cert});
|
||||
"alias", privateKey, password.toCharArray(), new Certificate[] { cert });
|
||||
break;
|
||||
case "PKCS12":
|
||||
ks = KeyStore.getInstance("PKCS12");
|
||||
@ -126,11 +243,10 @@ public class CertSignController {
|
||||
throw new IllegalArgumentException("Invalid cert type: " + certType);
|
||||
}
|
||||
|
||||
// TODO: page number
|
||||
|
||||
CreateSignature createSignature = new CreateSignature(ks, password.toCharArray());
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
sign(pdfDocumentFactory, pdf.getBytes(), baos, createSignature, name, location, reason);
|
||||
sign(pdfDocumentFactory, pdf.getBytes(), baos, createSignature, showSignature, pageNumber, name, location,
|
||||
reason);
|
||||
return WebResponseUtils.boasToWebResponse(
|
||||
baos,
|
||||
Filenames.toSimpleFileName(pdf.getOriginalFilename()).replaceFirst("[.][^.]+$", "")
|
||||
@ -142,6 +258,8 @@ public class CertSignController {
|
||||
byte[] input,
|
||||
OutputStream output,
|
||||
CreateSignature instance,
|
||||
Boolean showSignature,
|
||||
Integer pageNumber,
|
||||
String name,
|
||||
String location,
|
||||
String reason) {
|
||||
@ -154,7 +272,17 @@ public class CertSignController {
|
||||
signature.setReason(reason);
|
||||
signature.setSignDate(Calendar.getInstance());
|
||||
|
||||
doc.addSignature(signature, instance);
|
||||
if (showSignature) {
|
||||
SignatureOptions signatureOptions = new SignatureOptions();
|
||||
signatureOptions
|
||||
.setVisualSignature(instance.createVisibleSignature(doc, signature, pageNumber, true));
|
||||
signatureOptions.setPage(pageNumber);
|
||||
|
||||
doc.addSignature(signature, instance, signatureOptions);
|
||||
|
||||
} else {
|
||||
doc.addSignature(signature, instance);
|
||||
}
|
||||
doc.saveIncremental(output);
|
||||
} catch (Exception e) {
|
||||
logger.error("exception", e);
|
||||
@ -163,22 +291,19 @@ public class CertSignController {
|
||||
|
||||
private PrivateKey getPrivateKeyFromPEM(byte[] pemBytes, String password)
|
||||
throws IOException, OperatorCreationException, PKCSException {
|
||||
try (PEMParser pemParser =
|
||||
new PEMParser(new InputStreamReader(new ByteArrayInputStream(pemBytes)))) {
|
||||
try (PEMParser pemParser = new PEMParser(new InputStreamReader(new ByteArrayInputStream(pemBytes)))) {
|
||||
Object pemObject = pemParser.readObject();
|
||||
JcaPEMKeyConverter converter = new JcaPEMKeyConverter().setProvider("BC");
|
||||
PrivateKeyInfo pkInfo;
|
||||
if (pemObject instanceof PKCS8EncryptedPrivateKeyInfo) {
|
||||
InputDecryptorProvider decProv =
|
||||
new JceOpenSSLPKCS8DecryptorProviderBuilder().build(password.toCharArray());
|
||||
InputDecryptorProvider decProv = new JceOpenSSLPKCS8DecryptorProviderBuilder()
|
||||
.build(password.toCharArray());
|
||||
pkInfo = ((PKCS8EncryptedPrivateKeyInfo) pemObject).decryptPrivateKeyInfo(decProv);
|
||||
} else if (pemObject instanceof PEMEncryptedKeyPair) {
|
||||
PEMDecryptorProvider decProv =
|
||||
new JcePEMDecryptorProviderBuilder().build(password.toCharArray());
|
||||
pkInfo =
|
||||
((PEMEncryptedKeyPair) pemObject)
|
||||
.decryptKeyPair(decProv)
|
||||
.getPrivateKeyInfo();
|
||||
PEMDecryptorProvider decProv = new JcePEMDecryptorProviderBuilder().build(password.toCharArray());
|
||||
pkInfo = ((PEMEncryptedKeyPair) pemObject)
|
||||
.decryptKeyPair(decProv)
|
||||
.getPrivateKeyInfo();
|
||||
} else {
|
||||
pkInfo = ((PEMKeyPair) pemObject).getPrivateKeyInfo();
|
||||
}
|
||||
|
BIN
src/main/resources/static/images/signature.png
Normal file
BIN
src/main/resources/static/images/signature.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 19 KiB |
@ -71,7 +71,7 @@
|
||||
<label for="name" th:text="#{certSign.name}"></label> <input type="text" class="form-control" id="name" name="name">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="pageNumber" th:text="#{pageNum}"></label> <input type="number" class="form-control" id="pageNumber" name="pageNumber" min="1" disabled>
|
||||
<label for="pageNumber" th:text="#{pageNum}"></label> <input type="number" class="form-control" id="pageNumber" name="pageNumber" min="1">
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-3 text-left">
|
||||
|
Loading…
Reference in New Issue
Block a user