| /* |
| * Licensed to the Apache Software Foundation (ASF) under one or more |
| * contributor license agreements. See the NOTICE file distributed with |
| * this work for additional information regarding copyright ownership. |
| * The ASF licenses this file to You under the Apache License, Version 2.0 |
| * (the "License"); you may not use this file except in compliance with |
| * the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| package org.apache.pdfbox.encryption; |
| |
| import static org.junit.jupiter.api.Assertions.assertEquals; |
| import static org.junit.jupiter.api.Assertions.assertFalse; |
| import static org.junit.jupiter.api.Assertions.assertTrue; |
| import static org.junit.jupiter.api.Assertions.fail; |
| |
| import java.io.File; |
| import java.io.IOException; |
| import java.io.InputStream; |
| import java.net.URISyntaxException; |
| import java.security.NoSuchAlgorithmException; |
| import java.security.cert.CertificateException; |
| import java.security.cert.CertificateFactory; |
| import java.security.cert.X509Certificate; |
| import java.util.Arrays; |
| import java.util.Collection; |
| |
| import javax.crypto.Cipher; |
| |
| import org.apache.pdfbox.Loader; |
| import org.apache.pdfbox.io.MemoryUsageSetting; |
| import org.apache.pdfbox.pdmodel.PDDocument; |
| import org.apache.pdfbox.pdmodel.encryption.AccessPermission; |
| import org.apache.pdfbox.pdmodel.encryption.PublicKeyProtectionPolicy; |
| import org.apache.pdfbox.pdmodel.encryption.PublicKeyRecipient; |
| import org.apache.pdfbox.text.PDFTextStripper; |
| import org.junit.jupiter.api.AfterEach; |
| import org.junit.jupiter.api.BeforeAll; |
| import org.junit.jupiter.api.BeforeEach; |
| import org.junit.jupiter.params.ParameterizedTest; |
| import org.junit.jupiter.params.provider.MethodSource; |
| |
| /** |
| * Tests for public key encryption. These tests are not perfect - to be sure, encrypt a file by |
| * using a certificate exported from your digital id in Adobe Reader, and then open that file with |
| * Adobe Reader. Do this with every key length. |
| * |
| * @author Ben Litchfield |
| */ |
| class TestPublicKeyEncryption |
| { |
| private static final File TESTRESULTSDIR = new File("target/test-output/crypto"); |
| |
| private AccessPermission permission1; |
| private AccessPermission permission2; |
| |
| private PublicKeyRecipient recipient1; |
| private PublicKeyRecipient recipient2; |
| |
| private String keyStore1; |
| private String keyStore2; |
| |
| private String password1; |
| private String password2; |
| |
| /** |
| * Simple test document that gets encrypted by the test cases. |
| */ |
| private PDDocument document; |
| |
| private String text; |
| private String producer; |
| |
| public int keyLength; |
| |
| /** |
| * Values for keyLength test parameter. |
| * |
| * @return |
| */ |
| public static Collection<Integer> keyLengths() |
| { |
| return Arrays.asList(40, 128, 256); |
| } |
| |
| @BeforeAll |
| static void init() throws NoSuchAlgorithmException |
| { |
| if (Cipher.getMaxAllowedKeyLength("AES") != Integer.MAX_VALUE) |
| { |
| // we need strong encryption for these tests |
| fail("JCE unlimited strength jurisdiction policy files are not installed"); |
| } |
| TESTRESULTSDIR.mkdirs(); |
| } |
| |
| /** |
| * {@inheritDoc} |
| */ |
| @BeforeEach |
| void setUp() throws IOException, CertificateException, URISyntaxException |
| { |
| permission1 = new AccessPermission(); |
| permission1.setCanAssembleDocument(false); |
| permission1.setCanExtractContent(false); |
| permission1.setCanExtractForAccessibility(true); |
| permission1.setCanFillInForm(false); |
| permission1.setCanModify(false); |
| permission1.setCanModifyAnnotations(false); |
| permission1.setCanPrint(false); |
| permission1.setCanPrintDegraded(false); |
| |
| permission2 = new AccessPermission(); |
| permission2.setCanAssembleDocument(false); |
| permission2.setCanExtractContent(false); |
| permission2.setCanExtractForAccessibility(true); |
| permission2.setCanFillInForm(false); |
| permission2.setCanModify(false); |
| permission2.setCanModifyAnnotations(false); |
| permission2.setCanPrint(true); // it is true now ! |
| permission2.setCanPrintDegraded(false); |
| |
| recipient1 = getRecipient("test1.der", permission1); |
| recipient2 = getRecipient("test2.der", permission2); |
| |
| password1 = "test1"; |
| password2 = "test2"; |
| |
| keyStore1 = "test1.pfx"; |
| keyStore2 = "test2.pfx"; |
| |
| document = Loader.loadPDF(new File(this.getClass().getResource("test.pdf").toURI())); |
| text = new PDFTextStripper().getText(document); |
| producer = document.getDocumentInformation().getProducer(); |
| document.setVersion(1.7f); |
| } |
| |
| /** |
| * {@inheritDoc} |
| */ |
| @AfterEach |
| public void tearDown() throws Exception |
| { |
| document.close(); |
| } |
| |
| /** |
| * Protect a document with certificate 1 and try to open it with |
| * certificate 2 and catch the exception. |
| * |
| * @throws Exception If there is an unexpected error during the test. |
| */ |
| @ParameterizedTest |
| @MethodSource("keyLengths") |
| void testProtectionError(int keyLength) throws Exception |
| { |
| PublicKeyProtectionPolicy policy = new PublicKeyProtectionPolicy(); |
| policy.addRecipient(recipient1); |
| policy.setEncryptionKeyLength(keyLength); |
| document.protect(policy); |
| |
| PDDocument encryptedDoc = null; |
| try |
| { |
| File file = save("testProtectionError"); |
| encryptedDoc = reload(file, password2, getKeyStore(keyStore2)); |
| assertTrue(encryptedDoc.isEncrypted()); |
| fail("No exception when using an incorrect decryption key"); |
| } |
| catch (IOException ex) |
| { |
| String msg = ex.getMessage(); |
| assertTrue(msg.contains("serial-#: rid 2 vs. cert 3"), "not the expected exception: " + msg); |
| } |
| finally |
| { |
| if (encryptedDoc != null) |
| { |
| encryptedDoc.close(); |
| } |
| } |
| } |
| |
| |
| /** |
| * Protect a document with a public certificate and try to open it |
| * with the corresponding private certificate. |
| * |
| * @throws Exception If there is an unexpected error during the test. |
| */ |
| @ParameterizedTest |
| @MethodSource("keyLengths") |
| void testProtection(int keyLength) throws Exception |
| { |
| PublicKeyProtectionPolicy policy = new PublicKeyProtectionPolicy(); |
| policy.addRecipient(recipient1); |
| policy.setEncryptionKeyLength(keyLength); |
| document.protect(policy); |
| |
| File file = save("testProtection"); |
| try (PDDocument encryptedDoc = reload(file, password1, getKeyStore(keyStore1))) |
| { |
| assertTrue(encryptedDoc.isEncrypted()); |
| |
| AccessPermission permission = encryptedDoc.getCurrentAccessPermission(); |
| assertFalse(permission.canAssembleDocument()); |
| assertFalse(permission.canExtractContent()); |
| assertTrue(permission.canExtractForAccessibility()); |
| assertFalse(permission.canFillInForm()); |
| assertFalse(permission.canModify()); |
| assertFalse(permission.canModifyAnnotations()); |
| assertFalse(permission.canPrint()); |
| assertFalse(permission.canPrintDegraded()); |
| } |
| } |
| |
| |
| /** |
| * Protect the document for 2 recipients and try to open it. |
| * |
| * @throws Exception If there is an error during the test. |
| */ |
| @ParameterizedTest |
| @MethodSource("keyLengths") |
| void testMultipleRecipients(int keyLength) throws Exception |
| { |
| PublicKeyProtectionPolicy policy = new PublicKeyProtectionPolicy(); |
| policy.addRecipient(recipient1); |
| policy.addRecipient(recipient2); |
| policy.setEncryptionKeyLength(keyLength); |
| document.protect(policy); |
| |
| // open first time |
| File file = save("testMultipleRecipients"); |
| try (PDDocument encryptedDoc1 = reload(file, password1, getKeyStore(keyStore1))) |
| { |
| AccessPermission permission = encryptedDoc1.getCurrentAccessPermission(); |
| assertFalse(permission.canAssembleDocument()); |
| assertFalse(permission.canExtractContent()); |
| assertTrue(permission.canExtractForAccessibility()); |
| assertFalse(permission.canFillInForm()); |
| assertFalse(permission.canModify()); |
| assertFalse(permission.canModifyAnnotations()); |
| assertFalse(permission.canPrint()); |
| assertFalse(permission.canPrintDegraded()); |
| } |
| |
| // open second time |
| try (PDDocument encryptedDoc2 = reload(file, password2, getKeyStore(keyStore2))) |
| { |
| AccessPermission permission = encryptedDoc2.getCurrentAccessPermission(); |
| assertFalse(permission.canAssembleDocument()); |
| assertFalse(permission.canExtractContent()); |
| assertTrue(permission.canExtractForAccessibility()); |
| assertFalse(permission.canFillInForm()); |
| assertFalse(permission.canModify()); |
| assertFalse(permission.canModifyAnnotations()); |
| assertTrue(permission.canPrint()); |
| assertFalse(permission.canPrintDegraded()); |
| } |
| } |
| |
| /** |
| * Reloads the given document from a file and check some contents. |
| * |
| * @param file input file |
| * @param decryptionPassword password to be used to decrypt the doc |
| * @param keyStore password to be used to decrypt the doc |
| * @return reloaded document |
| * @throws Exception if |
| */ |
| private PDDocument reload(File file, String decryptionPassword, InputStream keyStore) |
| throws IOException, NoSuchAlgorithmException |
| { |
| PDDocument doc2 = Loader.loadPDF(file, decryptionPassword, |
| keyStore, null, MemoryUsageSetting.setupMainMemoryOnly()); |
| assertEquals(text, new PDFTextStripper().getText(doc2), |
| "Extracted text is different"); |
| assertEquals(producer, doc2.getDocumentInformation().getProducer(), |
| "Producer is different"); |
| return doc2; |
| } |
| |
| /** |
| * Returns a recipient specification with the given access permissions |
| * and an X.509 certificate read from the given classpath resource. |
| * |
| * @param certificate X.509 certificate resource, relative to this class |
| * @param permission access permissions |
| * @return recipient specification |
| * @throws CertificateException if the certificate could not be read |
| * @throws IOException |
| */ |
| private static PublicKeyRecipient getRecipient(String certificate, AccessPermission permission) |
| throws IOException, CertificateException |
| { |
| try (InputStream input = TestPublicKeyEncryption.class.getResourceAsStream(certificate)) |
| { |
| CertificateFactory factory = CertificateFactory.getInstance("X.509"); |
| PublicKeyRecipient recipient = new PublicKeyRecipient(); |
| recipient.setPermission(permission); |
| recipient.setX509((X509Certificate) factory.generateCertificate(input)); |
| return recipient; |
| } |
| } |
| |
| private InputStream getKeyStore(String name) |
| { |
| return TestPublicKeyEncryption.class.getResourceAsStream(name); |
| } |
| |
| private File save(String name) throws IOException |
| { |
| File file = new File(TESTRESULTSDIR, name + "-" + keyLength + "bit.pdf"); |
| document.save(file); |
| return file; |
| } |
| |
| /** |
| * PDFBOX-4421: Read a file encrypted with AES128 but not with PDFBox, and with missing /Length |
| * entry. Test to be changed so it's done locally with small file. |
| * |
| * @throws IOException |
| */ |
| @ParameterizedTest |
| @MethodSource("keyLengths") |
| void testReadPubkeyEncryptedAES128(int keyLength) throws IOException |
| { |
| try (InputStream is = TestPublicKeyEncryption.class.getResourceAsStream("AESkeylength128.pdf"); |
| PDDocument doc = Loader.loadPDF(is, |
| "w!z%C*F-JaNdRgUk", |
| TestPublicKeyEncryption.class.getResourceAsStream("PDFBOX-4421-keystore.pfx"), |
| "testnutzer")) |
| { |
| assertEquals("PublicKeySecurityHandler", |
| doc.getEncryption().getSecurityHandler().getClass().getSimpleName()); |
| assertEquals(128, doc.getEncryption().getSecurityHandler().getKeyLength()); |
| PDFTextStripper stripper = new PDFTextStripper(); |
| assertEquals("Key length: 128", stripper.getText(doc).trim()); |
| } |
| } |
| |
| /** |
| * PDFBOX-4421: Read a file encrypted with AES128 but not with PDFBox, and with missing /Length |
| * entry. Test to be changed so it's done locally with small file. |
| * |
| * @throws IOException |
| */ |
| @ParameterizedTest |
| @MethodSource("keyLengths") |
| void testReadPubkeyEncryptedAES256(int keyLength) throws IOException |
| { |
| try (InputStream is = TestPublicKeyEncryption.class.getResourceAsStream("AESkeylength256.pdf"); |
| PDDocument doc = Loader.loadPDF(is, |
| "w!z%C*F-JaNdRgUk", |
| TestPublicKeyEncryption.class.getResourceAsStream("PDFBOX-4421-keystore.pfx"), |
| "testnutzer")) |
| { |
| assertEquals("PublicKeySecurityHandler", |
| doc.getEncryption().getSecurityHandler().getClass().getSimpleName()); |
| assertEquals(256, doc.getEncryption().getSecurityHandler().getKeyLength()); |
| PDFTextStripper stripper = new PDFTextStripper(); |
| assertEquals("Key length: 256", stripper.getText(doc).trim()); |
| } |
| } |
| } |