Skip to content

Commit f4e3b5c

Browse files
fixes
1 parent 9f6ebc7 commit f4e3b5c

8 files changed

Lines changed: 543 additions & 19 deletions

File tree

openpdf-core/src/main/java/org/openpdf/text/pdf/PdfEncryption.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@
5959
import java.math.BigInteger;
6060
import java.security.GeneralSecurityException;
6161
import java.security.MessageDigest;
62+
import java.security.PublicKey;
6263
import java.security.cert.Certificate;
6364
import java.util.Arrays;
6465
import javax.crypto.Cipher;
@@ -686,7 +687,7 @@ public PdfDictionary getEncryptionDictionary() {
686687

687688
if (revision == AES_256_V3) {
688689
// For V=5 the entire 32-byte SHA-256 digest is the file encryption key.
689-
key = mdResult;
690+
key = mdResult.clone();
690691
keySize = 32;
691692
} else {
692693
setupByEncryptionKey(mdResult, keyLength);
@@ -833,7 +834,7 @@ public void addRecipient(Certificate cert, int permission) {
833834
* @param permission the PDF permission bitmask
834835
* @since 3.0.5
835836
*/
836-
public void addRecipient(Certificate cert, java.security.PublicKey pqcPublicKey, int permission) {
837+
public void addRecipient(Certificate cert, PublicKey pqcPublicKey, int permission) {
837838
documentID = createDocumentId();
838839
PdfPublicKeyRecipient recipient = new PdfPublicKeyRecipient(cert, permission);
839840
recipient.setPqcPublicKey(pqcPublicKey);

openpdf-core/src/main/java/org/openpdf/text/pdf/PdfName.java

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -142,18 +142,6 @@ public class PdfName extends PdfObject implements Comparable<PdfName> {
142142
* A name
143143
*/
144144
public static final PdfName ADBE_PKCS7_SHA1 = new PdfName("adbe.pkcs7.sha1");
145-
/**
146-
* (PDF 2.0) SubFilter for CAdES detached signatures (ISO 32000-2 §12.8.3.4, ETSI EN 319 142-1).
147-
*/
148-
public static final PdfName ETSI_CADES_DETACHED = new PdfName("ETSI.CAdES.detached");
149-
/**
150-
* (PDF 2.0) SubFilter for document timestamp signatures (ISO 32000-2 §12.8.5, RFC 3161).
151-
*/
152-
public static final PdfName ETSI_RFC3161 = new PdfName("ETSI.RFC3161");
153-
/**
154-
* (PDF 2.0) Document timestamp signature type (ISO 32000-2 §12.8.5).
155-
*/
156-
public static final PdfName DOC_TIME_STAMP = new PdfName("DocTimeStamp");
157145
/**
158146
* A name
159147
*/
@@ -856,6 +844,10 @@ public class PdfName extends PdfObject implements Comparable<PdfName> {
856844
* A name
857845
*/
858846
public static final PdfName DOCOPEN = new PdfName("DocOpen");
847+
/**
848+
* (PDF 2.0) Document timestamp signature type (ISO 32000-2 §12.8.5).
849+
*/
850+
public static final PdfName DOC_TIME_STAMP = new PdfName("DocTimeStamp");
859851
/**
860852
* A name.
861853
*
@@ -988,6 +980,14 @@ public class PdfName extends PdfObject implements Comparable<PdfName> {
988980
* Extension supplied by ETSI TS 102 778-4 V1.1.2 (2009-12)
989981
*/
990982
public static final PdfName ESIC = new PdfName("ESIC");
983+
/**
984+
* (PDF 2.0) SubFilter for CAdES detached signatures (ISO 32000-2 §12.8.3.4, ETSI EN 319 142-1).
985+
*/
986+
public static final PdfName ETSI_CADES_DETACHED = new PdfName("ETSI.CAdES.detached");
987+
/**
988+
* (PDF 2.0) SubFilter for document timestamp signatures (ISO 32000-2 §12.8.5, RFC 3161).
989+
*/
990+
public static final PdfName ETSI_RFC3161 = new PdfName("ETSI.RFC3161");
991991

992992
/**
993993
* Stands for "Exclude all fields except those specified in Fields array" which is one possible value of the Action

openpdf-core/src/main/java/org/openpdf/text/pdf/security/DocumentTimestamp.java

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
*/
88
package org.openpdf.text.pdf.security;
99

10+
import java.security.GeneralSecurityException;
1011
import java.security.MessageDigest;
1112
import org.openpdf.text.pdf.PdfDate;
1213
import org.openpdf.text.pdf.PdfDictionary;
@@ -76,15 +77,22 @@ public static PdfSignature newDictionary(java.util.Calendar signDate) {
7677
* @param tsa the time-stamp authority client
7778
* @param data the data to be timestamped (typically the digest of the byte range)
7879
* @return the encoded time-stamp token, or {@code null} if the TSA refused to issue one
79-
* @throws Exception if the TSA request fails for any reason
80+
* @throws GeneralSecurityException if the message digest cannot be computed
81+
* @throws java.io.IOException if the TSA request fails
8082
*/
81-
public static byte[] timestamp(TSAClient tsa, byte[] data) throws Exception {
83+
public static byte[] timestamp(TSAClient tsa, byte[] data) throws GeneralSecurityException, java.io.IOException {
8284
if (tsa == null || data == null) {
8385
return null;
8486
}
85-
MessageDigest md = tsa.getMessageDigest();
86-
byte[] imprint = md.digest(data);
87-
return tsa.getTimeStampToken(null, imprint);
87+
try {
88+
MessageDigest md = tsa.getMessageDigest();
89+
byte[] imprint = md.digest(data);
90+
return tsa.getTimeStampToken(null, imprint);
91+
} catch (GeneralSecurityException | java.io.IOException e) {
92+
throw e;
93+
} catch (Exception e) {
94+
throw new java.io.IOException("TSA request failed: " + e.getMessage(), e);
95+
}
8896
}
8997
}
9098

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
package org.openpdf.text.pdf.encryption;
2+
3+
import static org.junit.jupiter.api.Assertions.assertEquals;
4+
import static org.junit.jupiter.api.Assertions.assertTrue;
5+
6+
import org.openpdf.text.Document;
7+
import org.openpdf.text.Paragraph;
8+
import org.openpdf.text.pdf.PdfReader;
9+
import org.openpdf.text.pdf.PdfWriter;
10+
import org.openpdf.text.pdf.parser.PdfTextExtractor;
11+
import java.io.ByteArrayOutputStream;
12+
import java.io.File;
13+
import java.io.IOException;
14+
import java.math.BigInteger;
15+
import java.nio.file.Files;
16+
import java.security.KeyPair;
17+
import java.security.KeyPairGenerator;
18+
import java.security.NoSuchProviderException;
19+
import java.security.Security;
20+
import java.security.cert.X509Certificate;
21+
import java.util.Date;
22+
import org.bouncycastle.asn1.x500.X500Name;
23+
import org.bouncycastle.cert.X509v3CertificateBuilder;
24+
import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter;
25+
import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder;
26+
import org.bouncycastle.jce.provider.BouncyCastleProvider;
27+
import org.bouncycastle.operator.ContentSigner;
28+
import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
29+
import org.junit.jupiter.api.BeforeAll;
30+
import org.junit.jupiter.api.Test;
31+
import org.junit.jupiter.api.io.TempDir;
32+
33+
/**
34+
* Round-trip tests for PDF 2.0 public-key encryption (V=5 / R=6, AES-256-CBC)
35+
* as specified in ISO 32000-2 §7.6.5.
36+
*/
37+
class PubSecV5Test {
38+
39+
@TempDir
40+
File tempDir;
41+
42+
@BeforeAll
43+
static void installBc() {
44+
if (Security.getProvider("BC") == null) {
45+
Security.addProvider(new BouncyCastleProvider());
46+
}
47+
}
48+
49+
@Test
50+
void pubSecV5EncryptAndDecryptRoundTrip() throws Exception {
51+
KeyPair kp = generateRsaKeyPair();
52+
X509Certificate cert = selfSignedCert(kp);
53+
54+
byte[] pdfBytes = createEncryptedPdf(cert);
55+
56+
File tmp = new File(tempDir, "pubsec_v5.pdf");
57+
Files.write(tmp.toPath(), pdfBytes);
58+
59+
try (PdfReader reader = new PdfReader(tmp.getAbsolutePath(), cert, kp.getPrivate(), "BC")) {
60+
assertTrue(reader.isEncrypted(), "Document should be marked encrypted");
61+
assertEquals(
62+
"PDF 2.0 PUBSEC test",
63+
new PdfTextExtractor(reader).getTextFromPage(1),
64+
"Decrypted text must match what was written");
65+
}
66+
}
67+
68+
@Test
69+
void pubSecV5EncryptionDictionaryHasCorrectVersion() throws Exception {
70+
KeyPair kp = generateRsaKeyPair();
71+
X509Certificate cert = selfSignedCert(kp);
72+
byte[] pdfBytes = createEncryptedPdf(cert);
73+
74+
// Open without decrypting to inspect the encryption dictionary
75+
File tmp = new File(tempDir, "pubsec_v5_dict.pdf");
76+
Files.write(tmp.toPath(), pdfBytes);
77+
78+
try (PdfReader reader = new PdfReader(tmp.getAbsolutePath(), cert, kp.getPrivate(), "BC")) {
79+
assertTrue(reader.isEncrypted());
80+
// V=5 / R=6 maps to crypto mode 4 (ENCRYPTION_AES_256_V3)
81+
int mode = reader.getCryptoMode() & 0x0F;
82+
assertEquals(PdfWriter.ENCRYPTION_AES_256_V3, mode,
83+
"Crypto mode should be AES_256_V3 (V=5)");
84+
}
85+
}
86+
87+
// ---- helpers ----
88+
89+
private static byte[] createEncryptedPdf(X509Certificate cert) throws IOException {
90+
ByteArrayOutputStream baos = new ByteArrayOutputStream();
91+
Document doc = new Document();
92+
PdfWriter writer = PdfWriter.getInstance(doc, baos);
93+
writer.setEncryption(
94+
new java.security.cert.Certificate[]{cert},
95+
new int[]{PdfWriter.ALLOW_PRINTING},
96+
PdfWriter.ENCRYPTION_AES_256_V3);
97+
doc.open();
98+
doc.add(new Paragraph("PDF 2.0 PUBSEC test"));
99+
doc.close();
100+
return baos.toByteArray();
101+
}
102+
103+
private static KeyPair generateRsaKeyPair() throws Exception {
104+
KeyPairGenerator gen = KeyPairGenerator.getInstance("RSA");
105+
gen.initialize(2048);
106+
return gen.generateKeyPair();
107+
}
108+
109+
private static X509Certificate selfSignedCert(KeyPair kp)
110+
throws Exception {
111+
X500Name subject = new X500Name("CN=OpenPDF Test, O=Test, C=NO");
112+
BigInteger serial = BigInteger.valueOf(System.currentTimeMillis());
113+
Date notBefore = new Date();
114+
Date notAfter = new Date(notBefore.getTime() + 365L * 24 * 60 * 60 * 1000);
115+
116+
X509v3CertificateBuilder builder = new JcaX509v3CertificateBuilder(
117+
subject, serial, notBefore, notAfter, subject, kp.getPublic());
118+
119+
ContentSigner signer = new JcaContentSignerBuilder("SHA256withRSA")
120+
.setProvider("BC")
121+
.build(kp.getPrivate());
122+
123+
return new JcaX509CertificateConverter()
124+
.setProvider("BC")
125+
.getCertificate(builder.build(signer));
126+
}
127+
}
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
package org.openpdf.text.pdf.security;
2+
3+
import static org.junit.jupiter.api.Assertions.assertNotNull;
4+
import static org.junit.jupiter.api.Assertions.assertNull;
5+
import static org.junit.jupiter.api.Assertions.assertTrue;
6+
7+
import org.openpdf.text.Document;
8+
import org.openpdf.text.Paragraph;
9+
import org.openpdf.text.pdf.PdfArray;
10+
import org.openpdf.text.pdf.PdfDictionary;
11+
import org.openpdf.text.pdf.PdfName;
12+
import org.openpdf.text.pdf.PdfWriter;
13+
import java.io.ByteArrayOutputStream;
14+
import java.math.BigInteger;
15+
import java.security.KeyPair;
16+
import java.security.KeyPairGenerator;
17+
import java.security.Security;
18+
import java.security.cert.X509Certificate;
19+
import java.util.Date;
20+
import org.bouncycastle.asn1.x500.X500Name;
21+
import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter;
22+
import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder;
23+
import org.bouncycastle.jce.provider.BouncyCastleProvider;
24+
import org.bouncycastle.operator.ContentSigner;
25+
import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
26+
import org.junit.jupiter.api.BeforeAll;
27+
import org.junit.jupiter.api.Test;
28+
29+
class DocumentSecurityStoreTest {
30+
31+
@BeforeAll
32+
static void installBc() {
33+
if (Security.getProvider("BC") == null) {
34+
Security.addProvider(new BouncyCastleProvider());
35+
}
36+
}
37+
38+
@Test
39+
void buildEmptyDssHasNoArrays() throws Exception {
40+
PdfWriter writer = createWriter();
41+
PdfDictionary dss = new DocumentSecurityStore().build(writer);
42+
assertNotNull(dss);
43+
// empty store should produce a Type=DSS dict with no Certs/CRLs/OCSPs entries
44+
assertNull(dss.get(PdfName.CERTS), "Empty DSS should have no /Certs");
45+
assertNull(dss.get(PdfName.CRLS), "Empty DSS should have no /CRLs");
46+
assertNull(dss.get(PdfName.OCSPS), "Empty DSS should have no /OCSPs");
47+
}
48+
49+
@Test
50+
void buildDssWithCertHasCertsArray() throws Exception {
51+
X509Certificate cert = selfSignedCert();
52+
DocumentSecurityStore dss = new DocumentSecurityStore();
53+
dss.addCertificate(cert);
54+
55+
PdfWriter writer = createWriter();
56+
PdfDictionary result = dss.build(writer);
57+
58+
PdfArray certs = result.getAsArray(PdfName.CERTS);
59+
assertNotNull(certs, "DSS with a certificate must have /Certs");
60+
assertTrue(certs.size() > 0);
61+
}
62+
63+
@Test
64+
void buildDssWithOcspHasOcspsArray() throws Exception {
65+
DocumentSecurityStore dss = new DocumentSecurityStore();
66+
dss.addOcsp(new byte[]{0x30, 0x00}); // minimal placeholder DER
67+
68+
PdfWriter writer = createWriter();
69+
PdfDictionary result = dss.build(writer);
70+
71+
PdfArray ocsps = result.getAsArray(PdfName.OCSPS);
72+
assertNotNull(ocsps, "DSS with an OCSP response must have /OCSPs");
73+
assertTrue(ocsps.size() > 0);
74+
}
75+
76+
@Test
77+
void buildDssWithCrlBytesHasCrlsArray() throws Exception {
78+
DocumentSecurityStore dss = new DocumentSecurityStore();
79+
dss.addCrl(new byte[]{0x30, 0x00}); // minimal placeholder DER
80+
81+
PdfWriter writer = createWriter();
82+
PdfDictionary result = dss.build(writer);
83+
84+
PdfArray crls = result.getAsArray(PdfName.CRLS);
85+
assertNotNull(crls, "DSS with a CRL must have /CRLs");
86+
assertTrue(crls.size() > 0);
87+
}
88+
89+
@Test
90+
void addNullCertIsIgnored() throws Exception {
91+
DocumentSecurityStore dss = new DocumentSecurityStore();
92+
dss.addCertificate(null); // must not throw
93+
94+
PdfWriter writer = createWriter();
95+
PdfDictionary result = dss.build(writer);
96+
assertNull(result.get(PdfName.CERTS));
97+
}
98+
99+
// ---- helpers ----
100+
101+
private static PdfWriter createWriter() throws Exception {
102+
Document doc = new Document();
103+
ByteArrayOutputStream baos = new ByteArrayOutputStream();
104+
return PdfWriter.getInstance(doc, baos);
105+
}
106+
107+
private static X509Certificate selfSignedCert() throws Exception {
108+
KeyPairGenerator gen = KeyPairGenerator.getInstance("RSA");
109+
gen.initialize(1024);
110+
KeyPair kp = gen.generateKeyPair();
111+
112+
X500Name subject = new X500Name("CN=DSS Test");
113+
BigInteger serial = BigInteger.ONE;
114+
Date now = new Date();
115+
Date later = new Date(now.getTime() + 86400_000L);
116+
117+
ContentSigner signer = new JcaContentSignerBuilder("SHA256withRSA")
118+
.setProvider("BC").build(kp.getPrivate());
119+
120+
return new JcaX509CertificateConverter().setProvider("BC")
121+
.getCertificate(new JcaX509v3CertificateBuilder(
122+
subject, serial, now, later, subject, kp.getPublic()).build(signer));
123+
}
124+
}

0 commit comments

Comments
 (0)