From: Vincent Hennebert Date: Mon, 8 Aug 2011 15:51:43 +0000 (+0000) Subject: Added support for 128bit encryption in PDF output. Based on work by Michael Rubin. X-Git-Tag: fop-1_1rc1old~189 X-Git-Url: https://source.dussan.org/?a=commitdiff_plain;h=8e894b822fd834209663744877fb6d648630abfc;p=xmlgraphics-fop.git Added support for 128bit encryption in PDF output. Based on work by Michael Rubin. git-svn-id: https://svn.apache.org/repos/asf/xmlgraphics/fop/trunk@1154998 13f79535-47bb-0310-9956-ffa450edef68 --- diff --git a/build.xml b/build.xml index cfad23a74..1dd2ca689 100644 --- a/build.xml +++ b/build.xml @@ -962,7 +962,13 @@ list of possible build targets. - + + + + + diff --git a/src/documentation/content/xdocs/trunk/configuration.xml b/src/documentation/content/xdocs/trunk/configuration.xml index 26707edbf..e5469d037 100644 --- a/src/documentation/content/xdocs/trunk/configuration.xml +++ b/src/documentation/content/xdocs/trunk/configuration.xml @@ -367,12 +367,17 @@ + 128 testuserpass testownerpass + + + + ]]> diff --git a/src/documentation/content/xdocs/trunk/pdfencryption.xml b/src/documentation/content/xdocs/trunk/pdfencryption.xml index 22d965057..3562bb591 100644 --- a/src/documentation/content/xdocs/trunk/pdfencryption.xml +++ b/src/documentation/content/xdocs/trunk/pdfencryption.xml @@ -69,10 +69,45 @@ supplied, viewing the content is not restricted.

- Further restrictions can be imposed by using the -noprint, - -nocopy, -noedit and - -noannotations options, which disable printing, copying - text, editing in Adobe Acrobat and making annotations, respectively. + Further restrictions can be imposed by using the following command-line options: + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
OptionDescription
-noprintdisable printing
-nocopydisable copy/paste of content
-noeditdisable editing in Adobe Acrobat
-noannotationsdisable editing of annotations
-nofillinformsdisable filling in forms
-noaccesscontentdisable text and graphics extraction for accessibility purposes
-noassembledocdisable assembling documents
-noprinthqdisable high quality printing

@@ -88,6 +123,12 @@ Values Default + + encryption-length + The encryption length in bit + Any multiple of 8 between 40 and 128 + 40 + ownerPassword The owner password @@ -114,7 +155,7 @@ allowEditContent - Allows/disallows editing of content + Allows/disallows editing in Adobe Acrobat "TRUE" or "FALSE" "TRUE" @@ -124,6 +165,30 @@ "TRUE" or "FALSE" "TRUE" + + allowFillInForms + Allows/disallows filling in forms + "TRUE" or "FALSE" + "TRUE" + + + allowAccessContent + Allows/disallows text and graphics extraction for accessibility purposes + "TRUE" or "FALSE" + "TRUE" + + + allowAssembleDocument + Allows/disallows assembling document + "TRUE" or "FALSE" + "TRUE" + + + allowPrintHq + Allows/disallows high quality printing + "TRUE" or "FALSE" + "TRUE" + Encryption is enabled as soon as one of these options is set. @@ -151,6 +216,10 @@ Fop fop = fopFactory.newFop(MimeConstants.MIME_PDF, userAgent);
  • allowCopyContent: true if copying content is allowed
  • allowEditContent: true if editing content is allowed
  • allowEditAnnotations: true if editing annotations is allowed
  • +
  • allowFillInForms: true if filling in forms is allowed.
  • +
  • allowAccessContent: true if extracting text and graphics is allowed
  • +
  • allowAssembleDocument: true if assembling document is allowed
  • +
  • allowPrintHq: true if printing to high quality is allowed
  • Alternatively, you can set each value separately in the Map provided by @@ -163,6 +232,10 @@ Fop fop = fopFactory.newFop(MimeConstants.MIME_PDF, userAgent);

  • nocopy: Boolean or "true"/"false"
  • noedit: Boolean or "true"/"false"
  • noannotations: Boolean or "true"/"false"
  • +
  • nofillinforms: Boolean or "true"/"false"
  • +
  • noaccesscontent: Boolean or "true"/"false"
  • +
  • noassembledoc: Boolean or "true"/"false"
  • +
  • noprinthq: Boolean or "true"/"false"
  • diff --git a/src/documentation/content/xdocs/trunk/running.xml b/src/documentation/content/xdocs/trunk/running.xml index 11dc2848a..49ba7efd8 100644 --- a/src/documentation/content/xdocs/trunk/running.xml +++ b/src/documentation/content/xdocs/trunk/running.xml @@ -128,6 +128,10 @@ Fop [options] [-fo|-xml] infile [-xsl file] [-awt|-pdf|-mif|-rtf|-tiff|-png|-pcl -nocopy PDF file will be encrypted without copy content permission -noedit PDF file will be encrypted without edit content permission -noannotations PDF file will be encrypted without edit annotation permission + -nofillinforms PDF file will be encrypted without fill in forms permission + -noaccesscontent PDF file will be encrypted without extract text and graphics permission + -noassembledoc PDF file will be encrypted without assemble the document permission + -noprinthq PDF file will be encrypted without print high quality permission -a enables accessibility features (Tagged PDF etc., default off) -pdfprofile prof PDF file will be generated with the specified profile (Examples for prof: PDF/A-1b or PDF/X-3:2003) diff --git a/src/java/org/apache/fop/cli/CommandLineOptions.java b/src/java/org/apache/fop/cli/CommandLineOptions.java index 2a5c0d272..0d4c3790c 100644 --- a/src/java/org/apache/fop/cli/CommandLineOptions.java +++ b/src/java/org/apache/fop/cli/CommandLineOptions.java @@ -381,6 +381,14 @@ public class CommandLineOptions { getPDFEncryptionParams().setAllowEditContent(false); } else if (args[i].equals("-noannotations")) { getPDFEncryptionParams().setAllowEditAnnotations(false); + } else if (args[i].equals("-nofillinforms")) { + getPDFEncryptionParams().setAllowFillInForms(false); + } else if (args[i].equals("-noaccesscontent")) { + getPDFEncryptionParams().setAllowAccessContent(false); + } else if (args[i].equals("-noassembledoc")) { + getPDFEncryptionParams().setAllowAssembleDocument(false); + } else if (args[i].equals("-noprinthq")) { + getPDFEncryptionParams().setAllowPrintHq(false); } else if (args[i].equals("-version")) { printVersion(); return false; @@ -1181,6 +1189,14 @@ public class CommandLineOptions { + " -nocopy PDF file will be encrypted without copy content permission\n" + " -noedit PDF file will be encrypted without edit content permission\n" + " -noannotations PDF file will be encrypted without edit annotation permission\n" + + " -nofillinforms PDF file will be encrypted without" + + " fill in interactive form fields permission\n" + + " -noaccesscontent PDF file will be encrypted without" + + " extract text and graphics permission\n" + + " -noassembledoc PDF file will be encrypted without" + + " assemble the document permission\n" + + " -noprinthq PDF file will be encrypted without" + + " print high quality permission\n" + " -a enables accessibility features (Tagged PDF etc., default off)\n" + " -pdfprofile prof PDF file will be generated with the specified profile\n" + " (Examples for prof: PDF/A-1b or PDF/X-3:2003)\n\n" diff --git a/src/java/org/apache/fop/pdf/FileIDGenerator.java b/src/java/org/apache/fop/pdf/FileIDGenerator.java new file mode 100644 index 000000000..00aad4426 --- /dev/null +++ b/src/java/org/apache/fop/pdf/FileIDGenerator.java @@ -0,0 +1,124 @@ +/* + * 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. + */ + +/* $Id$ */ + +package org.apache.fop.pdf; + +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.text.DateFormat; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.Random; + +/** + * A class to generate the File Identifier of a PDF document (the ID entry of the file + * trailer dictionary). + */ +abstract class FileIDGenerator { + + abstract byte[] getOriginalFileID(); + + abstract byte[] getUpdatedFileID(); + + private static final class RandomFileIDGenerator extends FileIDGenerator { + + private byte[] fileID; + + private RandomFileIDGenerator() { + Random random = new Random(); + fileID = new byte[16]; + random.nextBytes(fileID); + } + + @Override + byte[] getOriginalFileID() { + return fileID; + } + + @Override + byte[] getUpdatedFileID() { + return fileID; + } + + } + + private static final class DigestFileIDGenerator extends FileIDGenerator { + + private byte[] fileID; + + private final PDFDocument document; + + private final MessageDigest digest; + + DigestFileIDGenerator(PDFDocument document) throws NoSuchAlgorithmException { + this.document = document; + this.digest = MessageDigest.getInstance("MD5"); + } + + @Override + byte[] getOriginalFileID() { + if (fileID == null) { + generateFileID(); + } + return fileID; + } + + @Override + byte[] getUpdatedFileID() { + return getOriginalFileID(); + } + + private void generateFileID() { + DateFormat df = new SimpleDateFormat("yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'SSS"); + digest.update(PDFDocument.encode(df.format(new Date()))); + // Ignoring the filename here for simplicity even though it's recommended + // by the PDF spec + digest.update(PDFDocument.encode(String.valueOf(document.getCurrentFileSize()))); + digest.update(document.getInfo().toPDF()); + fileID = digest.digest(); + } + + } + + /** + * Use this method when the file ID is needed before the document is finalized. The + * digest method recommended by the PDF Reference is based, among other things, on the + * file size. + * + * @return an instance that generates a random sequence of bytes for the File + * Identifier + */ + static FileIDGenerator getRandomFileIDGenerator() { + return new RandomFileIDGenerator(); + } + + /** + * Returns an instance that generates a file ID using the digest method recommended by + * the PDF Reference. To properly follow the Reference, the size of the document must + * no longer change after this method is called. + * + * @param document the document whose File Identifier must be generated + * @return the generator + * @throws NoSuchAlgorithmException if the MD5 Digest algorithm is not available + */ + static FileIDGenerator getDigestFileIDGenerator(PDFDocument document) + throws NoSuchAlgorithmException { + return new DigestFileIDGenerator(document); + } +} diff --git a/src/java/org/apache/fop/pdf/PDFDocument.java b/src/java/org/apache/fop/pdf/PDFDocument.java index 9268ae921..cbca3ea8f 100644 --- a/src/java/org/apache/fop/pdf/PDFDocument.java +++ b/src/java/org/apache/fop/pdf/PDFDocument.java @@ -25,10 +25,7 @@ import java.io.InputStream; import java.io.OutputStream; import java.io.UnsupportedEncodingException; import java.io.Writer; -import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; -import java.text.DateFormat; -import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Collections; import java.util.Date; @@ -186,6 +183,8 @@ public class PDFDocument { private boolean encodingOnTheFly = true; + private FileIDGenerator fileIDGenerator; + /** * Creates an empty PDF document. * @@ -513,10 +512,10 @@ public class PDFDocument { */ public void setEncryption(PDFEncryptionParams params) { getProfile().verifyEncryptionAllowed(); - this.encryption = PDFEncryptionManager.newInstance(++this.objectcount, params); + fileIDGenerator = FileIDGenerator.getRandomFileIDGenerator(); + this.encryption = PDFEncryptionManager.newInstance(++this.objectcount, params, this); if (this.encryption != null) { PDFObject pdfObject = (PDFObject)this.encryption; - pdfObject.setDocument(this); addTrailerObject(pdfObject); } else { log.warn( @@ -979,27 +978,6 @@ public class PDFDocument { this.position += bin.length; } - /** @return the "ID" entry for the file trailer */ - protected String getIDEntry() { - try { - MessageDigest digest = MessageDigest.getInstance("MD5"); - DateFormat df = new SimpleDateFormat("yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'SSS"); - digest.update(encode(df.format(new Date()))); - //Ignoring the filename here for simplicity even though it's recommended by the PDF spec - digest.update(encode(String.valueOf(this.position))); - digest.update(getInfo().toPDF()); - byte[] res = digest.digest(); - String s = PDFText.toHex(res); - return "/ID [" + s + " " + s + "]"; - } catch (NoSuchAlgorithmException e) { - if (getProfile().isIDEntryRequired()) { - throw new UnsupportedOperationException("MD5 not available: " + e.getMessage()); - } else { - return ""; //Entry is optional if PDF/A or PDF/X are not active - } - } - } - /** * Write the trailer * @@ -1038,7 +1016,9 @@ public class PDFDocument { if (this.isEncryptionActive()) { pdf.append(this.encryption.getTrailerEntry()); } else { - pdf.append(this.getIDEntry()); + byte[] fileID = getFileIDGenerator().getOriginalFileID(); + String fileIDAsString = PDFText.toHex(fileID); + pdf.append("/ID [" + fileIDAsString + " " + fileIDAsString + "]"); } pdf.append("\n>>\nstartxref\n") @@ -1089,4 +1069,18 @@ public class PDFDocument { return pdfBytes.length; } + long getCurrentFileSize() { + return position; + } + + FileIDGenerator getFileIDGenerator() { + if (fileIDGenerator == null) { + try { + fileIDGenerator = FileIDGenerator.getDigestFileIDGenerator(this); + } catch (NoSuchAlgorithmException e) { + fileIDGenerator = FileIDGenerator.getRandomFileIDGenerator(); + } + } + return fileIDGenerator; + } } diff --git a/src/java/org/apache/fop/pdf/PDFEncryption.java b/src/java/org/apache/fop/pdf/PDFEncryption.java index 5852df157..277cf0a94 100644 --- a/src/java/org/apache/fop/pdf/PDFEncryption.java +++ b/src/java/org/apache/fop/pdf/PDFEncryption.java @@ -24,18 +24,6 @@ package org.apache.fop.pdf; */ public interface PDFEncryption { - /** - * Returns the encryption parameters. - * @return the encryption parameters - */ - PDFEncryptionParams getParams(); - - /** - * Sets the encryption parameters. - * @param params The parameterss to set - */ - void setParams(PDFEncryptionParams params); - /** * Adds a PDFFilter to the PDFStream object * @param stream the stream to add an encryption filter to diff --git a/src/java/org/apache/fop/pdf/PDFEncryptionJCE.java b/src/java/org/apache/fop/pdf/PDFEncryptionJCE.java index 269f0639d..c9b9c58ba 100644 --- a/src/java/org/apache/fop/pdf/PDFEncryptionJCE.java +++ b/src/java/org/apache/fop/pdf/PDFEncryptionJCE.java @@ -19,13 +19,12 @@ package org.apache.fop.pdf; -// Java import java.io.IOException; import java.io.OutputStream; import java.security.InvalidKeyException; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; -import java.util.Random; +import java.util.Arrays; import javax.crypto.BadPaddingException; import javax.crypto.Cipher; @@ -35,310 +34,427 @@ import javax.crypto.NoSuchPaddingException; import javax.crypto.spec.SecretKeySpec; /** - * class representing a /Filter /Standard object. - * + * An implementation of the Standard Security Handler. */ -public class PDFEncryptionJCE extends PDFObject implements PDFEncryption { +public final class PDFEncryptionJCE extends PDFObject implements PDFEncryption { - private class EncryptionFilter extends PDFFilter { - private PDFEncryptionJCE encryption; - private int number; - private int generation; + private final MessageDigest digest; - /** - * The constructor for the internal PDFEncryptionJCE filter - * @param encryption The encryption object to use - * @param number The number of the object to be encrypted - * @param generation The generation of the object to be encrypted - */ - public EncryptionFilter(PDFEncryptionJCE encryption, - int number, int generation) { - super(); - this.encryption = encryption; - this.number = number; - this.generation = generation; - log.debug("new encryption filter for number " - + number + " and generation " + generation); + private byte[] encryptionKey; + + private String encryptionDictionary; + + private class EncryptionInitializer { + + private final PDFEncryptionParams encryptionParams; + + private int encryptionLength; + + private int version; + + private int revision; + + EncryptionInitializer(PDFEncryptionParams params) { + this.encryptionParams = new PDFEncryptionParams(params); } + void init() { + encryptionLength = encryptionParams.getEncryptionLengthInBits(); + determineEncryptionAlgorithm(); + int permissions = Permission.computePermissions(encryptionParams); + EncryptionSettings encryptionSettings = new EncryptionSettings( + encryptionLength, permissions, + encryptionParams.getUserPassword(), encryptionParams.getOwnerPassword()); + InitializationEngine initializationEngine = (revision == 2) + ? new Rev2Engine(encryptionSettings) + : new Rev3Engine(encryptionSettings); + initializationEngine.run(); + encryptionDictionary = createEncryptionDictionary(getObjectID(), permissions, + initializationEngine.oValue, initializationEngine.uValue); + } + + private void determineEncryptionAlgorithm() { + if (isVersion1Revision2Algorithm()) { + version = 1; + revision = 2; + } else { + version = 2; + revision = 3; + } + } + + private boolean isVersion1Revision2Algorithm() { + return encryptionLength == 40 + && encryptionParams.isAllowFillInForms() + && encryptionParams.isAllowAccessContent() + && encryptionParams.isAllowAssembleDocument() + && encryptionParams.isAllowPrintHq(); + } + + private String createEncryptionDictionary(final String objectId, final int permissions, + final byte[] oValue, final byte[] uValue) { + return objectId + + "<< /Filter /Standard\n" + + "/V " + version + "\n" + + "/R " + revision + "\n" + + "/Length " + encryptionLength + "\n" + + "/P " + permissions + "\n" + + "/O " + PDFText.toHex(oValue) + "\n" + + "/U " + PDFText.toHex(uValue) + "\n" + + ">>\n" + + "endobj\n"; + } + + } + + private static enum Permission { + + PRINT(3), + EDIT_CONTENT(4), + COPY_CONTENT(5), + EDIT_ANNOTATIONS(6), + FILL_IN_FORMS(9), + ACCESS_CONTENT(10), + ASSEMBLE_DOCUMENT(11), + PRINT_HQ(12); + + private final int mask; + /** - * Return a PDF string representation of the filter. In this - * case no filter name is passed. - * @return The filter name, blank in this case + * Creates a new permission. + * + * @param bit bit position for this permission, 1-based to match the PDF Reference */ - public String getName() { - return ""; + private Permission(int bit) { + mask = 1 << (bit - 1); + } + + private int removeFrom(int permissions) { + return permissions - mask; + } + + static int computePermissions(PDFEncryptionParams encryptionParams) { + int permissions = -4; + + if (!encryptionParams.isAllowPrint()) { + permissions = PRINT.removeFrom(permissions); + } + if (!encryptionParams.isAllowCopyContent()) { + permissions = COPY_CONTENT.removeFrom(permissions); + } + if (!encryptionParams.isAllowEditContent()) { + permissions = EDIT_CONTENT.removeFrom(permissions); + } + if (!encryptionParams.isAllowEditAnnotations()) { + permissions = EDIT_ANNOTATIONS.removeFrom(permissions); + } + if (!encryptionParams.isAllowFillInForms()) { + permissions = FILL_IN_FORMS.removeFrom(permissions); + } + if (!encryptionParams.isAllowAccessContent()) { + permissions = ACCESS_CONTENT.removeFrom(permissions); + } + if (!encryptionParams.isAllowAssembleDocument()) { + permissions = ASSEMBLE_DOCUMENT.removeFrom(permissions); + } + if (!encryptionParams.isAllowPrintHq()) { + permissions = PRINT_HQ.removeFrom(permissions); + } + return permissions; + } + } + + private static final class EncryptionSettings { + + final int encryptionLength; // CSOK: VisibilityModifier + + final int permissions; // CSOK: VisibilityModifier + + final String userPassword; // CSOK: VisibilityModifier + + final String ownerPassword; // CSOK: VisibilityModifier + + EncryptionSettings(int encryptionLength, int permissions, + String userPassword, String ownerPassword) { + this.encryptionLength = encryptionLength; + this.permissions = permissions; + this.userPassword = userPassword; + this.ownerPassword = ownerPassword; + } + + } + + private abstract class InitializationEngine { + + /** Padding for passwords. */ + protected final byte[] padding = new byte[] { + (byte) 0x28, (byte) 0xBF, (byte) 0x4E, (byte) 0x5E, + (byte) 0x4E, (byte) 0x75, (byte) 0x8A, (byte) 0x41, + (byte) 0x64, (byte) 0x00, (byte) 0x4E, (byte) 0x56, + (byte) 0xFF, (byte) 0xFA, (byte) 0x01, (byte) 0x08, + (byte) 0x2E, (byte) 0x2E, (byte) 0x00, (byte) 0xB6, + (byte) 0xD0, (byte) 0x68, (byte) 0x3E, (byte) 0x80, + (byte) 0x2F, (byte) 0x0C, (byte) 0xA9, (byte) 0xFE, + (byte) 0x64, (byte) 0x53, (byte) 0x69, (byte) 0x7A}; + + protected final int encryptionLengthInBytes; + + private final int permissions; + + private byte[] oValue; + + private byte[] uValue; + + private final byte[] preparedUserPassword; + + protected final String ownerPassword; + + InitializationEngine(EncryptionSettings encryptionSettings) { + this.encryptionLengthInBytes = encryptionSettings.encryptionLength / 8; + this.permissions = encryptionSettings.permissions; + this.preparedUserPassword = preparePassword(encryptionSettings.userPassword); + this.ownerPassword = encryptionSettings.ownerPassword; + } + + void run() { + oValue = computeOValue(); + createEncryptionKey(); + uValue = computeUValue(); } /** - * Return a parameter dictionary for this filter, or null - * @return The parameter dictionary. In this case, null. + * Applies Algorithm 3.3 Page 79 of the PDF 1.4 Reference. + * + * @return the O value */ - public PDFObject getDecodeParms() { - return null; + private byte[] computeOValue() { + // Step 1 + byte[] md5Input = prepareMD5Input(); + // Step 2 + digest.reset(); + byte[] hash = digest.digest(md5Input); + // Step 3 + hash = computeOValueStep3(hash); + // Step 4 + byte[] key = new byte[encryptionLengthInBytes]; + System.arraycopy(hash, 0, key, 0, encryptionLengthInBytes); + // Steps 5, 6 + byte[] encryptionResult = encryptWithKey(key, preparedUserPassword); + // Step 7 + encryptionResult = computeOValueStep7(key, encryptionResult); + // Step 8 + return encryptionResult; } /** - * {@inheritDoc} + * Applies Algorithm 3.2 Page 78 of the PDF 1.4 Reference. */ - public OutputStream applyFilter(OutputStream out) throws IOException { - return new CipherOutputStream(out, - encryption.initCipher(number, generation)); + private void createEncryptionKey() { + // Steps 1, 2 + digest.reset(); + digest.update(preparedUserPassword); + // Step 3 + digest.update(oValue); + // Step 4 + digest.update((byte) (permissions >>> 0)); + digest.update((byte) (permissions >>> 8)); + digest.update((byte) (permissions >>> 16)); + digest.update((byte) (permissions >>> 24)); + // Step 5 + digest.update(getDocumentSafely().getFileIDGenerator().getOriginalFileID()); + byte[] hash = digest.digest(); + // Step 6 + hash = createEncryptionKeyStep6(hash); + // Step 7 + encryptionKey = new byte[encryptionLengthInBytes]; + System.arraycopy(hash, 0, encryptionKey, 0, encryptionLengthInBytes); } - } + protected abstract byte[] computeUValue(); - private static final char [] PAD - = {0x28, 0xBF, 0x4E, 0x5E, 0x4E, 0x75, 0x8A, 0x41, - 0x64, 0x00, 0x4E, 0x56, 0xFF, 0xFA, 0x01, 0x08, - 0x2E, 0x2E, 0x00, 0xB6, 0xD0, 0x68, 0x3E, 0x80, - 0x2F, 0x0C, 0xA9, 0xFE, 0x64, 0x53, 0x69, 0x7A}; - - /** Value of PRINT permission */ - public static final int PERMISSION_PRINT = 4; - /** Value of content editting permission */ - public static final int PERMISSION_EDIT_CONTENT = 8; - /** Value of content extraction permission */ - public static final int PERMISSION_COPY_CONTENT = 16; - /** Value of annotation editting permission */ - public static final int PERMISSION_EDIT_ANNOTATIONS = 32; - - // Encryption tools - private MessageDigest digest = null; - //private Cipher cipher = null; - private Random random = new Random(); - // Control attributes - private PDFEncryptionParams params; - // Output attributes - private byte[] fileID = null; - private byte[] encryptionKey = null; - private String dictionary = null; + /** + * Adds padding to the password as directed in page 78 of the PDF 1.4 Reference. + * + * @param password the password + * @return the password with additional padding if necessary + */ + private byte[] preparePassword(String password) { + int finalLength = 32; + byte[] preparedPassword = new byte[finalLength]; + byte[] passwordBytes = password.getBytes(); + System.arraycopy(passwordBytes, 0, preparedPassword, 0, passwordBytes.length); + System.arraycopy(padding, 0, preparedPassword, passwordBytes.length, + finalLength - passwordBytes.length); + return preparedPassword; + } - /** - * Create a /Filter /Standard object. - * - * @param objnum the object's number - */ - public PDFEncryptionJCE(int objnum) { - /* generic creation of object */ - super(); - setObjectNumber(objnum); - try { - digest = MessageDigest.getInstance("MD5"); - //cipher = Cipher.getInstance("RC4"); - } catch (NoSuchAlgorithmException e) { - throw new UnsupportedOperationException(e.getMessage()); - /*} catch (NoSuchPaddingException e) { - throw new UnsupportedOperationException(e.getMessage());*/ + private byte[] prepareMD5Input() { + if (ownerPassword.length() != 0) { + return preparePassword(ownerPassword); + } else { + return preparedUserPassword; + } } - } - /** - * Local factory method. - * @param objnum PDF object number for the encryption object - * @param params PDF encryption parameters - * @return PDFEncryption the newly created PDFEncryption object - */ - public static PDFEncryption make(int objnum, PDFEncryptionParams params) { - PDFEncryptionJCE impl = new PDFEncryptionJCE(objnum); - impl.setParams(params); - impl.init(); - return impl; - } + protected abstract byte[] computeOValueStep3(byte[] hash); + protected abstract byte[] computeOValueStep7(byte[] key, byte[] encryptionResult); - /** - * Returns the encryption parameters. - * @return the encryption parameters - */ - public PDFEncryptionParams getParams() { - return this.params; - } + protected abstract byte[] createEncryptionKeyStep6(byte[] hash); - /** - * Sets the encryption parameters. - * @param params The parameterss to set - */ - public void setParams(PDFEncryptionParams params) { - this.params = params; } - // Internal procedures + private class Rev2Engine extends InitializationEngine { - private byte[] prepPassword(String password) { - byte[] obuffer = new byte[32]; - byte[] pbuffer = password.getBytes(); + Rev2Engine(EncryptionSettings encryptionSettings) { + super(encryptionSettings); + } - int i = 0; - int j = 0; + @Override + protected byte[] computeOValueStep3(byte[] hash) { + return hash; + } - while (i < obuffer.length && i < pbuffer.length) { - obuffer[i] = pbuffer[i]; - i++; + @Override + protected byte[] computeOValueStep7(byte[] key, byte[] encryptionResult) { + return encryptionResult; } - while (i < obuffer.length) { - obuffer[i++] = (byte) PAD[j++]; + + @Override + protected byte[] createEncryptionKeyStep6(byte[] hash) { + return hash; + } + + @Override + protected byte[] computeUValue() { + return encryptWithKey(encryptionKey, padding); } - return obuffer; } - /** - * Returns the document file ID - * @return The file ID - */ - public byte[] getFileID() { - if (fileID == null) { - fileID = new byte[16]; - random.nextBytes(fileID); + private class Rev3Engine extends InitializationEngine { + + Rev3Engine(EncryptionSettings encryptionSettings) { + super(encryptionSettings); } - return fileID; - } + @Override + protected byte[] computeOValueStep3(byte[] hash) { + for (int i = 0; i < 50; i++) { + hash = digest.digest(hash); + } + return hash; + } - /** - * This method returns the indexed file ID - * @param index The index to access the file ID - * @return The file ID - */ - public String getFileID(int index) { - if (index == 1) { - return PDFText.toHex(getFileID()); + @Override + protected byte[] computeOValueStep7(byte[] key, byte[] encryptionResult) { + return xorKeyAndEncrypt19Times(key, encryptionResult); } - byte[] id = new byte[16]; - random.nextBytes(id); - return PDFText.toHex(id); - } + @Override + protected byte[] createEncryptionKeyStep6(byte[] hash) { + for (int i = 0; i < 50; i++) { + digest.update(hash, 0, encryptionLengthInBytes); + hash = digest.digest(); + } + return hash; + } - private byte[] encryptWithKey(byte[] data, byte[] key) { - try { - final Cipher c = initCipher(key); - return c.doFinal(data); - } catch (IllegalBlockSizeException e) { - throw new IllegalStateException(e.getMessage()); - } catch (BadPaddingException e) { - throw new IllegalStateException(e.getMessage()); + @Override + protected byte[] computeUValue() { + // Step 1 is encryptionKey + // Step 2 + digest.reset(); + digest.update(padding); + // Step 3 + digest.update(getDocumentSafely().getFileIDGenerator().getOriginalFileID()); + // Step 4 + byte[] encryptionResult = encryptWithKey(encryptionKey, digest.digest()); + // Step 5 + encryptionResult = xorKeyAndEncrypt19Times(encryptionKey, encryptionResult); + // Step 6 + byte[] uValue = new byte[32]; + System.arraycopy(encryptionResult, 0, uValue, 0, 16); + // Add the arbitrary padding + Arrays.fill(uValue, 16, 32, (byte) 0); + return uValue; } - } - private Cipher initCipher(byte[] key) { - try { - Cipher c = Cipher.getInstance("RC4"); - SecretKeySpec keyspec = new SecretKeySpec(key, "RC4"); - c.init(Cipher.ENCRYPT_MODE, keyspec); - return c; - } catch (InvalidKeyException e) { - throw new IllegalStateException(e.getMessage()); - } catch (NoSuchAlgorithmException e) { - throw new UnsupportedOperationException(e.getMessage()); - } catch (NoSuchPaddingException e) { - throw new UnsupportedOperationException(e.getMessage()); + private byte[] xorKeyAndEncrypt19Times(byte[] key, byte[] input) { + byte[] result = input; + byte[] encryptionKey = new byte[key.length]; + for (int i = 1; i <= 19; i++) { + for (int j = 0; j < key.length; j++) { + encryptionKey[j] = (byte) (key[j] ^ i); + } + result = encryptWithKey(encryptionKey, result); + } + return result; } - } - private Cipher initCipher(int number, int generation) { - byte[] hash = calcHash(number, generation); - int size = hash.length; - hash = digest.digest(hash); - byte[] key = calcKey(hash, size); - return initCipher(key); } - private byte[] encryptWithHash(byte[] data, byte[] hash, int size) { - hash = digest.digest(hash); + private class EncryptionFilter extends PDFFilter { - byte[] key = calcKey(hash, size); + private int streamNumber; - return encryptWithKey(data, key); - } + private int streamGeneration; - private byte[] calcKey(byte[] hash, int size) { - byte[] key = new byte[size]; + EncryptionFilter(int streamNumber, int streamGeneration) { + this.streamNumber = streamNumber; + this.streamGeneration = streamGeneration; + } - for (int i = 0; i < size; i++) { - key[i] = hash[i]; + /** + * Returns a PDF string representation of this filter. + * + * @return the empty string + */ + public String getName() { + return ""; } - return key; + + /** + * Returns a parameter dictionary for this filter. + * + * @return null, this filter has no parameters + */ + public PDFObject getDecodeParms() { + return null; + } + + /** {@inheritDoc} */ + public OutputStream applyFilter(OutputStream out) throws IOException { + byte[] key = createEncryptionKey(streamNumber, streamGeneration); + Cipher cipher = initCipher(key); + return new CipherOutputStream(out, cipher); + } + } - /** - * This method initializes the encryption algorithms and values - */ - public void init() { - // Generate the owner value - byte[] oValue; - if (params.getOwnerPassword().length() > 0) { - oValue = encryptWithHash( - prepPassword(params.getUserPassword()), - prepPassword(params.getOwnerPassword()), 5); - } else { - oValue = encryptWithHash( - prepPassword(params.getUserPassword()), - prepPassword(params.getUserPassword()), 5); - } - - // Generate permissions value - int permissions = -4; - - if (!params.isAllowPrint()) { - permissions -= PERMISSION_PRINT; - } - if (!params.isAllowCopyContent()) { - permissions -= PERMISSION_COPY_CONTENT; - } - if (!params.isAllowEditContent()) { - permissions -= PERMISSION_EDIT_CONTENT; - } - if (!params.isAllowEditAnnotations()) { - permissions -= PERMISSION_EDIT_ANNOTATIONS; - } - - // Create the encrption key - digest.update(prepPassword(params.getUserPassword())); - digest.update(oValue); - digest.update((byte) (permissions >>> 0)); - digest.update((byte) (permissions >>> 8)); - digest.update((byte) (permissions >>> 16)); - digest.update((byte) (permissions >>> 24)); - digest.update(getFileID()); - - byte [] hash = digest.digest(); - this.encryptionKey = new byte[5]; - - for (int i = 0; i < 5; i++) { - this.encryptionKey[i] = hash[i]; - } - - // Create the user value - byte[] uValue = encryptWithKey(prepPassword(""), this.encryptionKey); - - // Create the dictionary - this.dictionary = getObjectID() - + "<< /Filter /Standard\n" - + "/V 1\n" - + "/R 2\n" - + "/Length 40\n" - + "/P " + permissions + "\n" - + "/O " + PDFText.toHex(oValue) + "\n" - + "/U " + PDFText.toHex(uValue) + "\n" - + ">>\n" - + "endobj\n"; + private PDFEncryptionJCE(int objectNumber, PDFEncryptionParams params, PDFDocument pdf) { + setObjectNumber(objectNumber); + try { + digest = MessageDigest.getInstance("MD5"); + } catch (NoSuchAlgorithmException e) { + throw new UnsupportedOperationException(e.getMessage()); + } + setDocument(pdf); + EncryptionInitializer encryptionInitializer = new EncryptionInitializer(params); + encryptionInitializer.init(); } /** - * This method encrypts the passed data using the generated keys. - * @param data The data to be encrypted - * @param number The block number - * @param generation The block generation - * @return The encrypted data + * Creates and returns an encryption object. + * + * @param objectNumber the object number for the encryption dictionary + * @param params the encryption parameters + * @param pdf the PDF document to be encrypted + * @return the newly created encryption object */ - public byte[] encryptData(byte[] data, int number, int generation) { - if (this.encryptionKey == null) { - throw new IllegalStateException("PDF Encryption has not been initialized"); - } - byte[] hash = calcHash(number, generation); - return encryptWithHash(data, hash, hash.length); + public static PDFEncryption make( + int objectNumber, PDFEncryptionParams params, PDFDocument pdf) { + return new PDFEncryptionJCE(objectNumber, params, pdf); } /** {@inheritDoc} */ @@ -350,63 +466,95 @@ public class PDFEncryptionJCE extends PDFObject implements PDFEncryption { if (o == null) { throw new IllegalStateException("No object number could be obtained for a PDF object"); } - return encryptData(data, o.getObjectNumber(), o.getGeneration()); + byte[] key = createEncryptionKey(o.getObjectNumber(), o.getGeneration()); + return encryptWithKey(key, data); } - private byte[] calcHash(int number, int generation) { - byte[] hash = new byte[this.encryptionKey.length + 5]; - - int i = 0; - while (i < this.encryptionKey.length) { - hash[i] = this.encryptionKey[i]; i++; - } - - hash[i++] = (byte) (number >>> 0); - hash[i++] = (byte) (number >>> 8); - hash[i++] = (byte) (number >>> 16); - hash[i++] = (byte) (generation >>> 0); - hash[i++] = (byte) (generation >>> 8); - return hash; - } - - /** - * Creates PDFFilter for the encryption object - * @param number The object number - * @param generation The objects generation - * @return The resulting filter - */ - public PDFFilter makeFilter(int number, int generation) { - return new EncryptionFilter(this, number, generation); - } - - /** - * Adds a PDFFilter to the PDFStream object - * @param stream the stream to add an encryption filter to - */ + /** {@inheritDoc} */ public void applyFilter(AbstractPDFStream stream) { stream.getFilterList().addFilter( - this.makeFilter(stream.getObjectNumber(), stream.getGeneration())); + new EncryptionFilter(stream.getObjectNumber(), stream.getGeneration())); } /** - * Represent the object in PDF + * Prepares the encryption dictionary for output to a PDF file. * - * @return the PDF + * @return the encryption dictionary as a byte array */ public byte[] toPDF() { - if (this.dictionary == null) { - throw new IllegalStateException("PDF Encryption has not been initialized"); + assert encryptionDictionary != null; + return encode(this.encryptionDictionary); + } + + /** {@inheritDoc} */ + public String getTrailerEntry() { + PDFDocument doc = getDocumentSafely(); + FileIDGenerator gen = doc.getFileIDGenerator(); + return "/Encrypt " + getObjectNumber() + " " + + getGeneration() + " R\n" + + "/ID[" + + PDFText.toHex(gen.getOriginalFileID()) + + PDFText.toHex(gen.getUpdatedFileID()) + + "]\n"; + } + + private static byte[] encryptWithKey(byte[] key, byte[] data) { + try { + final Cipher c = initCipher(key); + return c.doFinal(data); + } catch (IllegalBlockSizeException e) { + throw new IllegalStateException(e.getMessage()); + } catch (BadPaddingException e) { + throw new IllegalStateException(e.getMessage()); } + } - return encode(this.dictionary); + private static Cipher initCipher(byte[] key) { + try { + Cipher c = Cipher.getInstance("RC4"); + SecretKeySpec keyspec = new SecretKeySpec(key, "RC4"); + c.init(Cipher.ENCRYPT_MODE, keyspec); + return c; + } catch (InvalidKeyException e) { + throw new IllegalStateException(e); + } catch (NoSuchAlgorithmException e) { + throw new UnsupportedOperationException(e); + } catch (NoSuchPaddingException e) { + throw new UnsupportedOperationException(e); + } } /** - * {@inheritDoc} + * Applies Algorithm 3.1 from the PDF 1.4 Reference. + * + * @param objectNumber the object number + * @param generationNumber the generation number + * @return the key to use for encryption */ - public String getTrailerEntry() { - return "/Encrypt " + getObjectNumber() + " " - + getGeneration() + " R\n" - + "/ID[" + getFileID(1) + getFileID(2) + "]\n"; + private byte[] createEncryptionKey(int objectNumber, int generationNumber) { + // Step 1 passed in + // Step 2 + byte[] md5Input = prepareMD5Input(objectNumber, generationNumber); + // Step 3 + digest.reset(); + byte[] hash = digest.digest(md5Input); + // Step 4 + int keyLength = Math.min(16, md5Input.length); + byte[] key = new byte[keyLength]; + System.arraycopy(hash, 0, key, 0, keyLength); + return key; } + + private byte[] prepareMD5Input(int objectNumber, int generationNumber) { + byte[] md5Input = new byte[encryptionKey.length + 5]; + System.arraycopy(encryptionKey, 0, md5Input, 0, encryptionKey.length); + int i = encryptionKey.length; + md5Input[i++] = (byte) (objectNumber >>> 0); + md5Input[i++] = (byte) (objectNumber >>> 8); + md5Input[i++] = (byte) (objectNumber >>> 16); + md5Input[i++] = (byte) (generationNumber >>> 0); + md5Input[i++] = (byte) (generationNumber >>> 8); + return md5Input; + } + } diff --git a/src/java/org/apache/fop/pdf/PDFEncryptionManager.java b/src/java/org/apache/fop/pdf/PDFEncryptionManager.java index 3f5ae2a3d..6e57b1518 100644 --- a/src/java/org/apache/fop/pdf/PDFEncryptionManager.java +++ b/src/java/org/apache/fop/pdf/PDFEncryptionManager.java @@ -109,16 +109,18 @@ public final class PDFEncryptionManager { * Creates a new PDFEncryption instance if PDF encryption is available. * @param objnum PDF object number * @param params PDF encryption parameters + * @param pdf the PDF document to encrypt * @return PDFEncryption the newly created instance, null if PDF encryption * is unavailable. */ - public static PDFEncryption newInstance(int objnum, PDFEncryptionParams params) { + public static PDFEncryption newInstance(int objnum, PDFEncryptionParams params, + PDFDocument pdf) { try { - Class clazz = Class.forName("org.apache.fop.pdf.PDFEncryptionJCE"); + Class clazz = Class.forName("org.apache.fop.pdf.PDFEncryptionJCE"); Method makeMethod = clazz.getMethod("make", - new Class[] {int.class, PDFEncryptionParams.class}); + new Class[] {int.class, PDFEncryptionParams.class, PDFDocument.class}); Object obj = makeMethod.invoke(null, - new Object[] {new Integer(objnum), params}); + new Object[] {new Integer(objnum), params, pdf}); return (PDFEncryption)obj; } catch (ClassNotFoundException e) { if (checkAvailableAlgorithms()) { diff --git a/src/java/org/apache/fop/pdf/PDFEncryptionParams.java b/src/java/org/apache/fop/pdf/PDFEncryptionParams.java index 9cc502c42..71dccd867 100644 --- a/src/java/org/apache/fop/pdf/PDFEncryptionParams.java +++ b/src/java/org/apache/fop/pdf/PDFEncryptionParams.java @@ -26,10 +26,17 @@ public class PDFEncryptionParams { private String userPassword = ""; //May not be null private String ownerPassword = ""; //May not be null + private boolean allowPrint = true; private boolean allowCopyContent = true; private boolean allowEditContent = true; private boolean allowEditAnnotations = true; + private boolean allowFillInForms = true; + private boolean allowAccessContent = true; + private boolean allowAssembleDocument = true; + private boolean allowPrintHq = true; + + private int encryptionLengthInBits = 40; /** * Creates a new instance. @@ -60,6 +67,25 @@ public class PDFEncryptionParams { //nop } + /** + * Creates a copy of the given encryption parameters. + * + * @param source source encryption parameters + */ + public PDFEncryptionParams(PDFEncryptionParams source) { + setUserPassword(source.getUserPassword()); + setOwnerPassword(source.getOwnerPassword()); + setAllowPrint(source.isAllowPrint()); + setAllowCopyContent(source.isAllowCopyContent()); + setAllowEditContent(source.isAllowEditContent()); + setAllowEditAnnotations(source.isAllowEditAnnotations()); + setAllowAssembleDocument(source.isAllowAssembleDocument()); + setAllowAccessContent(source.isAllowAccessContent()); + setAllowFillInForms(source.isAllowFillInForms()); + setAllowPrintHq(source.isAllowPrintHq()); + setEncryptionLengthInBits(source.getEncryptionLengthInBits()); + } + /** * Indicates whether copying content is allowed. * @return true if copying is allowed @@ -92,6 +118,38 @@ public class PDFEncryptionParams { return allowPrint; } + /** + * Indicates whether revision 3 filling in forms is allowed. + * @return true if revision 3 filling in forms is allowed + */ + public boolean isAllowFillInForms() { + return allowFillInForms; + } + + /** + * Indicates whether revision 3 extracting text and graphics is allowed. + * @return true if revision 3 extracting text and graphics is allowed + */ + public boolean isAllowAccessContent() { + return allowAccessContent; + } + + /** + * Indicates whether revision 3 assembling document is allowed. + * @return true if revision 3 assembling document is allowed + */ + public boolean isAllowAssembleDocument() { + return allowAssembleDocument; + } + + /** + * Indicates whether revision 3 printing to high quality is allowed. + * @return true if revision 3 printing to high quality is allowed + */ + public boolean isAllowPrintHq() { + return allowPrintHq; + } + /** * Returns the owner password. * @return the owner password, an empty string if no password applies @@ -133,13 +191,45 @@ public class PDFEncryptionParams { } /** - * Sets the persmission for printing. + * Sets the permission for printing. * @param allowPrint true if printing is allowed */ public void setAllowPrint(boolean allowPrint) { this.allowPrint = allowPrint; } + /** + * Sets whether revision 3 filling in forms is allowed. + * @param allowFillInForms true if revision 3 filling in forms is allowed. + */ + public void setAllowFillInForms(boolean allowFillInForms) { + this.allowFillInForms = allowFillInForms; + } + + /** + * Sets whether revision 3 extracting text and graphics is allowed. + * @param allowAccessContent true if revision 3 extracting text and graphics is allowed + */ + public void setAllowAccessContent(boolean allowAccessContent) { + this.allowAccessContent = allowAccessContent; + } + + /** + * Sets whether revision 3 assembling document is allowed. + * @param allowAssembleDocument true if revision 3 assembling document is allowed + */ + public void setAllowAssembleDocument(boolean allowAssembleDocument) { + this.allowAssembleDocument = allowAssembleDocument; + } + + /** + * Sets whether revision 3 printing to high quality is allowed. + * @param allowPrintHq true if revision 3 printing to high quality is allowed + */ + public void setAllowPrintHq(boolean allowPrintHq) { + this.allowPrintHq = allowPrintHq; + } + /** * Sets the owner password. * @param ownerPassword The owner password to set, null or an empty String @@ -166,4 +256,21 @@ public class PDFEncryptionParams { } } + /** + * Returns the encryption length. + * @return the encryption length + */ + public int getEncryptionLengthInBits() { + return encryptionLengthInBits; + } + + /** + * Sets the encryption length. + * + * @param encryptionLength the encryption length + */ + public void setEncryptionLengthInBits(int encryptionLength) { + this.encryptionLengthInBits = encryptionLength; + } + } diff --git a/src/java/org/apache/fop/pdf/PDFObject.java b/src/java/org/apache/fop/pdf/PDFObject.java index 09b8673a8..6dd0c800f 100644 --- a/src/java/org/apache/fop/pdf/PDFObject.java +++ b/src/java/org/apache/fop/pdf/PDFObject.java @@ -112,7 +112,7 @@ public abstract class PDFObject implements PDFWritable { } /** - * Returns the object's generation. + * Returns this object's generation. * @return the PDF Object generation */ public int getGeneration() { diff --git a/src/java/org/apache/fop/render/pdf/PDFConfigurationConstants.java b/src/java/org/apache/fop/render/pdf/PDFConfigurationConstants.java index 841dd7e01..994cbc0de 100644 --- a/src/java/org/apache/fop/render/pdf/PDFConfigurationConstants.java +++ b/src/java/org/apache/fop/render/pdf/PDFConfigurationConstants.java @@ -38,6 +38,36 @@ public interface PDFConfigurationConstants { String NO_EDIT_CONTENT = "noedit"; /** PDF encryption parameter: Forbids annotations, datatype: Boolean or "true"/"false" */ String NO_ANNOTATIONS = "noannotations"; + + /** + * PDF encryption parameter: Forbids filling in existing interactive forms, datatype: + * Boolean or "true"/"false" + */ + String NO_FILLINFORMS = "nofillinforms"; + + /** + * PDF encryption parameter: Forbids extracting text and graphics, datatype: Boolean + * or "true"/"false" + */ + String NO_ACCESSCONTENT = "noaccesscontent"; + + /** + * PDF encryption parameter: Forbids assembling document, datatype: Boolean or + * "true"/"false" + */ + String NO_ASSEMBLEDOC = "noassembledoc"; + + /** + * PDF encryption parameter: Forbids printing to high quality, datatype: Boolean or + * "true"/"false" + */ + String NO_PRINTHQ = "noprinthq"; + + /** + * PDF encryption length parameter: must be a multiple of 8 between 40 and 128, + * default value 40, datatype: int. + */ + String ENCRYPTION_LENGTH = "encryption-length"; /** Rendering Options key for the PDF/A mode. */ String PDF_A_MODE = "pdf-a-mode"; /** Rendering Options key for the PDF/X mode. */ diff --git a/src/java/org/apache/fop/render/pdf/PDFEventProducer.java b/src/java/org/apache/fop/render/pdf/PDFEventProducer.java index 1e1ddf98a..40062f73f 100644 --- a/src/java/org/apache/fop/render/pdf/PDFEventProducer.java +++ b/src/java/org/apache/fop/render/pdf/PDFEventProducer.java @@ -65,4 +65,14 @@ public interface PDFEventProducer extends EventProducer { * @event.severity WARN */ void nonStandardStructureType(Object source, String fo, String type, String fallback); + + /** + * The encryption length must be a multiple of 8 between 40 and 128. + * + * @param source the event source + * @param originalValue requested encryption length + * @param correctedValue corrected encryption length + * @event.severity WARN + */ + void incorrectEncryptionLength(Object source, int originalValue, int correctedValue); } diff --git a/src/java/org/apache/fop/render/pdf/PDFEventProducer.xml b/src/java/org/apache/fop/render/pdf/PDFEventProducer.xml index 7f3c9d609..bf930ea34 100644 --- a/src/java/org/apache/fop/render/pdf/PDFEventProducer.xml +++ b/src/java/org/apache/fop/render/pdf/PDFEventProducer.xml @@ -2,4 +2,5 @@ {count} link target{count,equals,1,,s} could not be fully resolved and now point{count,equals,1,,s} to the top of the page or {count,equals,1,is,are} dysfunctional. ‘{type}’ is not a standard structure type defined by the PDF Reference. Falling back to ‘{fallback}’. + Encryption length must be a multiple of 8 between 40 and 128. Setting encryption length to {correctedValue} instead of {originalValue}. diff --git a/src/java/org/apache/fop/render/pdf/PDFRendererConfigurator.java b/src/java/org/apache/fop/render/pdf/PDFRendererConfigurator.java index 9ebb1d5a1..dcc7dd32e 100644 --- a/src/java/org/apache/fop/render/pdf/PDFRendererConfigurator.java +++ b/src/java/org/apache/fop/render/pdf/PDFRendererConfigurator.java @@ -81,7 +81,7 @@ public class PDFRendererConfigurator extends PrintRendererConfigurator { Configuration encryptionParamsConfig = cfg.getChild(PDFConfigurationConstants.ENCRYPTION_PARAMS, false); if (encryptionParamsConfig != null) { - PDFEncryptionParams encryptionParams = new PDFEncryptionParams(); + PDFEncryptionParams encryptionParams = pdfUtil.getEncryptionParams(); Configuration ownerPasswordConfig = encryptionParamsConfig.getChild( PDFConfigurationConstants.OWNER_PASSWORD, false); if (ownerPasswordConfig != null) { @@ -118,8 +118,35 @@ public class PDFRendererConfigurator extends PrintRendererConfigurator { if (noAnnotationsConfig != null) { encryptionParams.setAllowEditAnnotations(false); } - pdfUtil.setEncryptionParams(encryptionParams); + Configuration noFillInForms = encryptionParamsConfig.getChild( + PDFConfigurationConstants.NO_FILLINFORMS, false); + if (noFillInForms != null) { + encryptionParams.setAllowFillInForms(false); + } + Configuration noAccessContentConfig = encryptionParamsConfig.getChild( + PDFConfigurationConstants.NO_ACCESSCONTENT, false); + if (noAccessContentConfig != null) { + encryptionParams.setAllowAccessContent(false); + } + Configuration noAssembleDocConfig = encryptionParamsConfig.getChild( + PDFConfigurationConstants.NO_ASSEMBLEDOC, false); + if (noAssembleDocConfig != null) { + encryptionParams.setAllowAssembleDocument(false); + } + Configuration noPrintHqConfig = encryptionParamsConfig.getChild( + PDFConfigurationConstants.NO_PRINTHQ, false); + if (noPrintHqConfig != null) { + encryptionParams.setAllowPrintHq(false); + } + Configuration encryptionLengthConfig = encryptionParamsConfig.getChild( + PDFConfigurationConstants.ENCRYPTION_LENGTH, false); + if (encryptionLengthConfig != null) { + int encryptionLength = checkEncryptionLength( + Integer.parseInt(encryptionLengthConfig.getValue(null))); + encryptionParams.setEncryptionLengthInBits(encryptionLength); + } } + s = cfg.getChild(PDFConfigurationConstants.KEY_OUTPUT_PROFILE, true).getValue(null); if (s != null) { pdfUtil.setOutputProfileURI(s); @@ -132,6 +159,22 @@ public class PDFRendererConfigurator extends PrintRendererConfigurator { } } + private int checkEncryptionLength(int encryptionLength) { + int correctEncryptionLength = encryptionLength; + if (encryptionLength < 40) { + correctEncryptionLength = 40; + } else if (encryptionLength > 128) { + correctEncryptionLength = 128; + } else if (encryptionLength % 8 != 0) { + correctEncryptionLength = ((int) Math.round(encryptionLength / 8.0f)) * 8; + } + if (correctEncryptionLength != encryptionLength) { + PDFEventProducer.Provider.get(userAgent.getEventBroadcaster()) + .incorrectEncryptionLength(this, encryptionLength, correctEncryptionLength); + } + return correctEncryptionLength; + } + /** * Builds a filter map from an Avalon Configuration object. * diff --git a/src/java/org/apache/fop/render/pdf/PDFRenderingUtil.java b/src/java/org/apache/fop/render/pdf/PDFRenderingUtil.java index e63059472..c662b0345 100644 --- a/src/java/org/apache/fop/render/pdf/PDFRenderingUtil.java +++ b/src/java/org/apache/fop/render/pdf/PDFRenderingUtil.java @@ -124,49 +124,45 @@ class PDFRenderingUtil implements PDFConfigurationConstants { if (params != null) { this.encryptionParams = params; //overwrite if available } - String pwd; - pwd = (String)userAgent.getRendererOptions().get(USER_PASSWORD); - if (pwd != null) { - if (encryptionParams == null) { - this.encryptionParams = new PDFEncryptionParams(); - } - this.encryptionParams.setUserPassword(pwd); + String userPassword = (String)userAgent.getRendererOptions().get(USER_PASSWORD); + if (userPassword != null) { + getEncryptionParams().setUserPassword(userPassword); } - pwd = (String)userAgent.getRendererOptions().get(OWNER_PASSWORD); - if (pwd != null) { - if (encryptionParams == null) { - this.encryptionParams = new PDFEncryptionParams(); - } - this.encryptionParams.setOwnerPassword(pwd); + String ownerPassword = (String)userAgent.getRendererOptions().get(OWNER_PASSWORD); + if (ownerPassword != null) { + getEncryptionParams().setOwnerPassword(ownerPassword); } - Object setting; - setting = userAgent.getRendererOptions().get(NO_PRINT); - if (setting != null) { - if (encryptionParams == null) { - this.encryptionParams = new PDFEncryptionParams(); - } - this.encryptionParams.setAllowPrint(!booleanValueOf(setting)); + Object noPrint = userAgent.getRendererOptions().get(NO_PRINT); + if (noPrint != null) { + getEncryptionParams().setAllowPrint(!booleanValueOf(noPrint)); } - setting = userAgent.getRendererOptions().get(NO_COPY_CONTENT); - if (setting != null) { - if (encryptionParams == null) { - this.encryptionParams = new PDFEncryptionParams(); - } - this.encryptionParams.setAllowCopyContent(!booleanValueOf(setting)); + Object noCopyContent = userAgent.getRendererOptions().get(NO_COPY_CONTENT); + if (noCopyContent != null) { + getEncryptionParams().setAllowCopyContent(!booleanValueOf(noCopyContent)); } - setting = userAgent.getRendererOptions().get(NO_EDIT_CONTENT); - if (setting != null) { - if (encryptionParams == null) { - this.encryptionParams = new PDFEncryptionParams(); - } - this.encryptionParams.setAllowEditContent(!booleanValueOf(setting)); + Object noEditContent = userAgent.getRendererOptions().get(NO_EDIT_CONTENT); + if (noEditContent != null) { + getEncryptionParams().setAllowEditContent(!booleanValueOf(noEditContent)); } - setting = userAgent.getRendererOptions().get(NO_ANNOTATIONS); - if (setting != null) { - if (encryptionParams == null) { - this.encryptionParams = new PDFEncryptionParams(); - } - this.encryptionParams.setAllowEditAnnotations(!booleanValueOf(setting)); + Object noAnnotations = userAgent.getRendererOptions().get(NO_ANNOTATIONS); + if (noAnnotations != null) { + getEncryptionParams().setAllowEditAnnotations(!booleanValueOf(noAnnotations)); + } + Object noFillInForms = userAgent.getRendererOptions().get(NO_FILLINFORMS); + if (noFillInForms != null) { + getEncryptionParams().setAllowFillInForms(!booleanValueOf(noFillInForms)); + } + Object noAccessContent = userAgent.getRendererOptions().get(NO_ACCESSCONTENT); + if (noAccessContent != null) { + getEncryptionParams().setAllowAccessContent(!booleanValueOf(noAccessContent)); + } + Object noAssembleDoc = userAgent.getRendererOptions().get(NO_ASSEMBLEDOC); + if (noAssembleDoc != null) { + getEncryptionParams().setAllowAssembleDocument(!booleanValueOf(noAssembleDoc)); + } + Object noPrintHQ = userAgent.getRendererOptions().get(NO_PRINTHQ); + if (noPrintHQ != null) { + getEncryptionParams().setAllowPrintHq(!booleanValueOf(noPrintHQ)); } String s = (String)userAgent.getRendererOptions().get(PDF_A_MODE); if (s != null) { @@ -184,9 +180,10 @@ class PDFRenderingUtil implements PDFConfigurationConstants { if (s != null) { this.outputProfileURI = s; } - setting = userAgent.getRendererOptions().get(KEY_DISABLE_SRGB_COLORSPACE); - if (setting != null) { - this.disableSRGBColorSpace = booleanValueOf(setting); + Object disableSRGBColorSpace = userAgent.getRendererOptions().get( + KEY_DISABLE_SRGB_COLORSPACE); + if (disableSRGBColorSpace != null) { + this.disableSRGBColorSpace = booleanValueOf(disableSRGBColorSpace); } } @@ -236,11 +233,14 @@ class PDFRenderingUtil implements PDFConfigurationConstants { } /** - * Sets the encryption parameters used by the PDF renderer. - * @param encryptionParams the encryption parameters + * Gets the encryption parameters used by the PDF renderer. + * @return encryptionParams the encryption parameters */ - public void setEncryptionParams(PDFEncryptionParams encryptionParams) { - this.encryptionParams = encryptionParams; + PDFEncryptionParams getEncryptionParams() { + if (this.encryptionParams == null) { + this.encryptionParams = new PDFEncryptionParams(); + } + return this.encryptionParams; } private void updateInfo() { diff --git a/status.xml b/status.xml index 25f8b5e2a..37231bfc2 100644 --- a/status.xml +++ b/status.xml @@ -60,6 +60,9 @@ documents. Example: the fix of marks layering will be such a case when it's done. --> + + Added support for 128bit encryption in PDF output. Based on work by Michael Rubin. + Fixed a bug in AFP where the object area axes of an Include Object was incorrectly set when rotated by 180. diff --git a/test/java/org/apache/fop/UtilityCodeTestSuite.java b/test/java/org/apache/fop/UtilityCodeTestSuite.java index 004b8f3c3..9ef3c5510 100644 --- a/test/java/org/apache/fop/UtilityCodeTestSuite.java +++ b/test/java/org/apache/fop/UtilityCodeTestSuite.java @@ -23,6 +23,8 @@ import junit.framework.Test; import junit.framework.TestSuite; import org.apache.fop.events.BasicEventTestCase; +import org.apache.fop.pdf.FileIDGeneratorTestCase; +import org.apache.fop.pdf.PDFEncryptionJCETestCase; import org.apache.fop.pdf.PDFObjectTestCase; import org.apache.fop.traits.BorderPropsTestCase; import org.apache.fop.util.BitmapImageUtilTestCase; @@ -46,6 +48,7 @@ public class UtilityCodeTestSuite { //$JUnit-BEGIN$ suite.addTest(new TestSuite(PDFNumberTestCase.class)); suite.addTest(new TestSuite(PDFObjectTestCase.class)); + suite.addTest(FileIDGeneratorTestCase.suite()); suite.addTest(new TestSuite(ColorUtilTestCase.class)); suite.addTest(new TestSuite(BorderPropsTestCase.class)); suite.addTest(new TestSuite(ElementListUtilsTestCase.class)); @@ -53,6 +56,7 @@ public class UtilityCodeTestSuite { suite.addTest(new TestSuite(XMLResourceBundleTestCase.class)); suite.addTest(new TestSuite(URIResolutionTestCase.class)); suite.addTest(new TestSuite(BitmapImageUtilTestCase.class)); + suite.addTest(new TestSuite(PDFEncryptionJCETestCase.class)); //$JUnit-END$ return suite; } diff --git a/test/java/org/apache/fop/pdf/FileIDGeneratorTestCase.java b/test/java/org/apache/fop/pdf/FileIDGeneratorTestCase.java new file mode 100644 index 000000000..3e9617743 --- /dev/null +++ b/test/java/org/apache/fop/pdf/FileIDGeneratorTestCase.java @@ -0,0 +1,106 @@ +/* + * 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. + */ + +/* $Id$ */ + +package org.apache.fop.pdf; + +import java.util.Arrays; + +import junit.framework.Test; +import junit.framework.TestCase; +import junit.framework.TestSuite; + +/** + * Tests the {@link FileIDGenerator} class. + */ +public abstract class FileIDGeneratorTestCase extends TestCase { + + /** + * Returns a suite containing all the {@link FileIDGenerator} test cases. + * + * @return the test suite + */ + public static final Test suite() { + TestSuite suite = new TestSuite(new Class[] { + RandomFileIDGeneratorTestCase.class, + DigestFileIDGeneratorTestCase.class }, + FileIDGeneratorTestCase.class.getName()); + return suite; + } + + /** The generator under test. */ + protected FileIDGenerator fileIDGenerator; + + + /** Tests that the getOriginalFileID method generates valid output. */ + public void testOriginal() { + byte[] fileID = fileIDGenerator.getOriginalFileID(); + fileIDMustBeValid(fileID); + } + + /** Tests that the getUpdatedFileID method generates valid output. */ + public void testUpdated() { + byte[] fileID = fileIDGenerator.getUpdatedFileID(); + fileIDMustBeValid(fileID); + } + + private void fileIDMustBeValid(byte[] fileID) { + assertNotNull(fileID); + assertEquals(16, fileID.length); + } + + /** Tests that multiple calls to getOriginalFileID method always return the same value. */ + public void testOriginalMultipleCalls() { + byte[] fileID1 = fileIDGenerator.getUpdatedFileID(); + byte[] fileID2 = fileIDGenerator.getUpdatedFileID(); + assertTrue(Arrays.equals(fileID1, fileID2)); + } + + /** Tests that getUpdatedFileID returns the same value as getOriginalFileID. */ + public void testUpdateEqualsOriginal() { + byte[] originalFileID = fileIDGenerator.getOriginalFileID(); + byte[] updatedFileID = fileIDGenerator.getUpdatedFileID(); + assertTrue(Arrays.equals(originalFileID, updatedFileID)); + } + + /** + * Tests the random file ID generator. + */ + public static class RandomFileIDGeneratorTestCase extends FileIDGeneratorTestCase { + + @Override + protected void setUp() throws Exception { + fileIDGenerator = FileIDGenerator.getRandomFileIDGenerator(); + } + + } + + /** + * Tests the file ID generator based on an MD5 digest. + */ + public static class DigestFileIDGeneratorTestCase extends FileIDGeneratorTestCase { + + @Override + protected void setUp() throws Exception { + fileIDGenerator = FileIDGenerator.getDigestFileIDGenerator( + new PDFDocument("Apache FOP")); + } + + } + +} diff --git a/test/java/org/apache/fop/pdf/PDFEncryptionJCETestCase.java b/test/java/org/apache/fop/pdf/PDFEncryptionJCETestCase.java new file mode 100644 index 000000000..c85018166 --- /dev/null +++ b/test/java/org/apache/fop/pdf/PDFEncryptionJCETestCase.java @@ -0,0 +1,492 @@ +/* + * 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. + */ + +/* $Id$ */ + +package org.apache.fop.pdf; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.util.Arrays; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import junit.framework.TestCase; + +/** + * Tests the {@link PDFEncryptionJCE} class. + */ +public class PDFEncryptionJCETestCase extends TestCase { + + private EncryptionTest test; + + private PDFEncryptionJCE encryptionObject; + + private static final class EncryptionTest { + + private int objectNumber = 1; + + private final PDFEncryptionParams encryptionParameters = new PDFEncryptionParams(); + + private byte[] data; + + private byte[] encryptedData; + + private final EncryptionDictionaryTester encryptionDictionaryTester; + + EncryptionTest() { + this(new EncryptionDictionaryTester()); + } + + EncryptionTest(EncryptionDictionaryTester encryptionDictionaryTester) { + encryptionParameters.setUserPassword("TestUserPassword"); + encryptionParameters.setOwnerPassword("TestOwnerPassword"); + setData(0x00, 0xAA, 0xFF, 0x55, 0xCC, 0x33, 0xF0); + this.encryptionDictionaryTester = encryptionDictionaryTester; + this.encryptionDictionaryTester.setLength( + encryptionParameters.getEncryptionLengthInBits()); + } + + int getObjectNumber() { + return objectNumber; + } + + EncryptionTest setObjectNumber(int objectNumber) { + this.objectNumber = objectNumber; + return this; + } + + byte[] getData() { + return data; + } + + EncryptionTest setData(int... data) { + /* + * Use an array of int to avoid having to cast some elements to byte in the + * method call. + */ + this.data = convertIntArrayToByteArray(data); + return this; + } + + byte[] getEncryptedData() { + return encryptedData; + } + + EncryptionTest setEncryptedData(int... encryptedData) { + this.encryptedData = convertIntArrayToByteArray(encryptedData); + return this; + } + + private byte[] convertIntArrayToByteArray(int[] intArray) { + byte[] byteArray = new byte[intArray.length]; + for (int i = 0; i < intArray.length; i++) { + byteArray[i] = (byte) intArray[i]; + } + return byteArray; + } + + PDFEncryptionParams getEncryptionParameters() { + return encryptionParameters; + } + + EncryptionTest setUserPassword(String userPassword) { + encryptionParameters.setUserPassword(userPassword); + return this; + } + + EncryptionTest setOwnerPassword(String ownerPassword) { + encryptionParameters.setOwnerPassword(ownerPassword); + return this; + } + + EncryptionTest setEncryptionLength(int encryptionLength) { + encryptionParameters.setEncryptionLengthInBits(encryptionLength); + encryptionDictionaryTester.setLength(encryptionLength); + return this; + } + + EncryptionTest disablePrint() { + encryptionParameters.setAllowPrint(false); + return this; + } + + EncryptionTest disableEditContent() { + encryptionParameters.setAllowEditContent(false); + return this; + } + + EncryptionTest disableCopyContent() { + encryptionParameters.setAllowCopyContent(false); + return this; + } + + EncryptionTest disableEditAnnotations() { + encryptionParameters.setAllowEditAnnotations(false); + return this; + } + + EncryptionTest disableFillInForms() { + encryptionParameters.setAllowFillInForms(false); + return this; + } + + EncryptionTest disableAccessContent() { + encryptionParameters.setAllowAccessContent(false); + return this; + } + + EncryptionTest disableAssembleDocument() { + encryptionParameters.setAllowAssembleDocument(false); + return this; + } + + EncryptionTest disablePrintHq() { + encryptionParameters.setAllowPrintHq(false); + return this; + } + + void testEncryptionDictionary(PDFEncryptionJCE encryptionObject) { + encryptionDictionaryTester.test(encryptionObject); + } + } + + private static final class EncryptionDictionaryTester { + + private int version = 1; + + private int revision = 2; + + private int length = 40; + + private int permissions = -4; + + private String ownerEntry + = "3EE8C4000CA44B2645EED029C9EA7D4FC63C6D9B89349E8FA5A40C7691AB96B5"; + + private String userEntry + = "D1810D9E6E488BA5D2DDCBB3F974F7472D0D5389F554DB55574A787DC5C59884"; + + EncryptionDictionaryTester setVersion(int version) { + this.version = version; + return this; + } + + EncryptionDictionaryTester setRevision(int revision) { + this.revision = revision; + return this; + } + + EncryptionDictionaryTester setLength(int length) { + this.length = length; + return this; + } + + EncryptionDictionaryTester setPermissions(int permissions) { + this.permissions = permissions; + return this; + } + + EncryptionDictionaryTester setOwnerEntry(String ownerEntry) { + this.ownerEntry = ownerEntry; + return this; + } + + EncryptionDictionaryTester setUserEntry(String userEntry) { + this.userEntry = userEntry; + return this; + } + + void test(PDFEncryptionJCE encryptionObject) { + byte[] encryptionDictionary = encryptionObject.toPDF(); + RegexTestedCharSequence dictionary = new RegexTestedCharSequence(encryptionDictionary); + + final String whitespace = "\\s+"; + final String digits = "\\d+"; + final String hexDigits = "\\p{XDigit}+"; + + dictionary.mustContain("1" + whitespace + "0" + whitespace + "obj"); + + dictionary.mustContain("/Filter" + whitespace + "/Standard\\b"); + + dictionary.mustContain("/V" + whitespace + "(" + digits + ")") + .withGroup1EqualTo(Integer.toString(version)); + + dictionary.mustContain("/R" + whitespace + "(" + digits + ")") + .withGroup1EqualTo(Integer.toString(revision)); + + dictionary.mustContain("/Length" + whitespace + "(" + digits + ")") + .withGroup1EqualTo(Integer.toString(length)); + + dictionary.mustContain("/P" + whitespace + "(-?" + digits + ")") + .withGroup1EqualTo(Integer.toString(permissions)); + + dictionary.mustContain("/O" + whitespace + "<(" + hexDigits + ")>") + .withGroup1EqualTo(ownerEntry); + + dictionary.mustContain("/U" + whitespace + "<(" + hexDigits + ")>") + .withGroup1EqualTo(userEntry); + } + } + + private static final class RegexTestedCharSequence { + + private final String string; + + private Matcher matcher; + + RegexTestedCharSequence(byte[] bytes) { + try { + string = new String(bytes, "US-ASCII"); + } catch (UnsupportedEncodingException e) { + throw new RuntimeException(e); + } + } + + RegexTestedCharSequence mustContain(String regex) { + Pattern pattern = Pattern.compile(regex); + matcher = pattern.matcher(string); + assertTrue(matcher.find()); + return this; + } + + RegexTestedCharSequence withGroup1EqualTo(String expected) { + assertEquals(expected, matcher.group(1)); + return this; + } + } + + public final void testMake() { + PDFEncryption testEncryptionObj = createEncryptionObject(new PDFEncryptionParams()); + assertTrue(testEncryptionObj instanceof PDFEncryptionJCE); + assertEquals(1, ((PDFEncryptionJCE) testEncryptionObj).getObjectNumber()); + } + + public void testBasic() throws IOException { + test = new EncryptionTest(); + test.setData(0x00).setEncryptedData(0x56); + runEncryptionTests(); + + test.setData(0xAA).setEncryptedData(0xFC); + runEncryptionTests(); + + test.setData(0xFF).setEncryptedData(0xA9); + runEncryptionTests(); + + test = new EncryptionTest().setEncryptedData(0x56, 0x0C, 0xFC, 0xA5, 0xAB, 0x61, 0x73); + runEncryptionTests(); + } + + public void test128bit() throws IOException { + EncryptionDictionaryTester encryptionDictionaryTester = new EncryptionDictionaryTester() + .setVersion(2) + .setRevision(3) + .setPermissions(-4) + .setOwnerEntry("D9A98017F0500EF9B69738641C9B4CBA1229EDC3F2151BC6C9C4FB07B1CB315E") + .setUserEntry("D3EF424BFEA2E434000E1A74941CC87300000000000000000000000000000000"); + test = new EncryptionTest(encryptionDictionaryTester) + .setObjectNumber(2) + .setEncryptionLength(128) + .setEncryptedData(0xE3, 0xCB, 0xB2, 0x55, 0xD9, 0x26, 0x55); + runEncryptionTests(); + } + + public void testDisableRev2Permissions() throws IOException { + EncryptionDictionaryTester encryptionDictionaryTester = new EncryptionDictionaryTester() + .setPermissions(-64) + .setUserEntry("3E65D0090746C4C37C5EF23C1BDB6323E00C24C4B2D744DD3BFB654CD58591A1"); + test = new EncryptionTest(encryptionDictionaryTester) + .setObjectNumber(3) + .disablePrint() + .disableEditContent() + .disableCopyContent() + .disableEditAnnotations() + .setEncryptedData(0x66, 0xEE, 0xA7, 0x93, 0xC4, 0xB1, 0xB4); + runEncryptionTests(); + } + + public void testDisableRev3Permissions() throws IOException { + EncryptionDictionaryTester encryptionDictionaryTester = new EncryptionDictionaryTester() + .setVersion(2) + .setRevision(3) + .setPermissions(-3844) + .setOwnerEntry("8D4BCA4F4AB2BAB4E38F161D61F937EC50BE5EB30C2DC05EA409D252CD695E55") + .setUserEntry("0F01171E22C7FB27B079C132BA4277DE00000000000000000000000000000000"); + test = new EncryptionTest(encryptionDictionaryTester) + .setObjectNumber(4) + .disableFillInForms() + .disableAccessContent() + .disableAssembleDocument() + .disablePrintHq() + .setEncryptedData(0x8E, 0x3C, 0xD2, 0x05, 0x50, 0x48, 0x82); + runEncryptionTests(); + } + + public void test128bitDisableSomePermissions() throws IOException { + EncryptionDictionaryTester encryptionDictionaryTester = new EncryptionDictionaryTester() + .setVersion(2) + .setRevision(3) + .setPermissions(-1304) + .setOwnerEntry("D9A98017F0500EF9B69738641C9B4CBA1229EDC3F2151BC6C9C4FB07B1CB315E") + .setUserEntry("62F0E4D8641D482E0F8E71A89270045A00000000000000000000000000000000"); + test = new EncryptionTest(encryptionDictionaryTester) + .setObjectNumber(5) + .setEncryptionLength(128) + .disablePrint() + .disableCopyContent() + .disableFillInForms() + .disableAssembleDocument() + .setEncryptedData(0xF7, 0x85, 0x4F, 0xB0, 0x50, 0x5C, 0xDF); + runEncryptionTests(); + } + + public void testDifferentPasswords() throws IOException { + EncryptionDictionaryTester encryptionDictionaryTester = new EncryptionDictionaryTester() + .setOwnerEntry("D11C233C65E9DC872E858ABBD8B62198771167ADCE7AB8DC7AE0A1A7E21A1E25") + .setUserEntry("6F449167DB8DDF0D2DF4602DDBBA97ABF9A9101F632CC16AB0BE74EB9500B469"); + test = new EncryptionTest(encryptionDictionaryTester) + .setObjectNumber(6) + .setUserPassword("ADifferentUserPassword") + .setOwnerPassword("ADifferentOwnerPassword") + .setEncryptedData(0x27, 0xAC, 0xB1, 0x6C, 0x42, 0xE0, 0xA8); + runEncryptionTests(); + } + + public void testNoOwnerPassword() throws IOException { + EncryptionDictionaryTester encryptionDictionaryTester = new EncryptionDictionaryTester() + .setOwnerEntry("5163AAF3EE74C76D7C223593A84C8702FEA8AA4493E4933FF5B5A5BBB20AE4BB") + .setUserEntry("42DDF1C1BF3AB04786D5038E7B0A723AE614D944E1DE91A922FC54F5F2345E00"); + test = new EncryptionTest(encryptionDictionaryTester) + .setObjectNumber(7) + .setUserPassword("ADifferentUserPassword") + .setOwnerPassword("") + .setEncryptedData(0xEC, 0x2E, 0x5D, 0xC2, 0x7F, 0xAD, 0x58); + runEncryptionTests(); + } + + public void test128bitDisableSomePermissionsDifferentPasswords() throws IOException { + EncryptionDictionaryTester encryptionDictionaryTester = new EncryptionDictionaryTester() + .setVersion(2) + .setRevision(3) + .setPermissions(-2604) + .setOwnerEntry("F83CA049FAA2F774F8541F25E746A92EE2A7F060C46C91C693E673BF18FF7B36") + .setUserEntry("88A4C58F5385B5F08FACA0636D790EDF00000000000000000000000000000000"); + test = new EncryptionTest(encryptionDictionaryTester) + .setObjectNumber(8) + .setUserPassword("ADifferentUserPassword") + .setOwnerPassword("ADifferentOwnerPassword") + .setEncryptionLength(128) + .disableEditContent() + .disableEditAnnotations() + .disableAccessContent() + .disablePrintHq() + .setEncryptedData(0x77, 0x54, 0x67, 0xA5, 0xCC, 0x73, 0xDE); + runEncryptionTests(); + } + + public void test128bitNoPermissionNoOwnerPassword() throws IOException { + EncryptionDictionaryTester encryptionDictionaryTester = new EncryptionDictionaryTester() + .setVersion(2) + .setRevision(3) + .setPermissions(-3904) + .setOwnerEntry("3EEB3FA5594CBD935BFB2F83FB184DD41FBCD7C36A04F1FFD0899B0DFFCFF96B") + .setUserEntry("D972B72DD2633F613B0DDB7511C719C500000000000000000000000000000000"); + test = new EncryptionTest(encryptionDictionaryTester) + .setObjectNumber(9) + .setUserPassword("ADifferentUserPassword") + .setOwnerPassword("") + .setEncryptionLength(128) + .disablePrint() + .disableEditContent() + .disableCopyContent() + .disableEditAnnotations() + .disableFillInForms() + .disableAccessContent() + .disableAssembleDocument() + .disablePrintHq() + .setEncryptedData(0x0C, 0xAD, 0x49, 0xC7, 0xE5, 0x05, 0xB8); + runEncryptionTests(); + } + + /** + * Creates an encryption object using a fixed file ID generator for test reproducibility. + * + * @param params the encryption parameters + * @return PDFEncryptionJCE the encryption object + */ + private PDFEncryptionJCE createEncryptionObject(PDFEncryptionParams params) { + PDFDocument doc = new PDFDocument("Apache FOP") { + + @Override + FileIDGenerator getFileIDGenerator() { + return new FileIDGenerator() { + + private final byte[] fixedFileID = new byte[] { + 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, + 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F}; + + @Override + byte[] getOriginalFileID() { + return fixedFileID; + } + + @Override + byte[] getUpdatedFileID() { + return fixedFileID; + } + + }; + } + }; + return (PDFEncryptionJCE) PDFEncryptionJCE.make(1, params, doc); + } + + private void runEncryptionTests() throws IOException { + encryptionObject = createEncryptionObject(test.getEncryptionParameters()); + runEncryptTest(); + runFilterTest(); + runEncryptionDictionaryTest(); + } + + private void runEncryptTest() { + PDFText text = new PDFText(); + text.setObjectNumber(test.getObjectNumber()); + byte[] byteResult = encryptionObject.encrypt(test.getData(), text); + + assertTrue(Arrays.equals(test.getEncryptedData(), byteResult)); + } + + private void runFilterTest() throws IOException { + PDFStream stream = new PDFStream(); + stream.setDocument(encryptionObject.getDocumentSafely()); + stream.setObjectNumber(test.getObjectNumber()); + stream.setData(test.getData()); + encryptionObject.applyFilter(stream); + + StreamCache streamCache = stream.encodeStream(); + ByteArrayOutputStream testOutputStream = new ByteArrayOutputStream(); + streamCache.outputContents(testOutputStream); + + assertTrue(Arrays.equals(test.getEncryptedData(), testOutputStream.toByteArray())); + } + + private void runEncryptionDictionaryTest() { + test.testEncryptionDictionary(encryptionObject); + } + +} diff --git a/test/java/org/apache/fop/render/pdf/PDFRendererConfiguratorTestCase.java b/test/java/org/apache/fop/render/pdf/PDFRendererConfiguratorTestCase.java new file mode 100644 index 000000000..01889e437 --- /dev/null +++ b/test/java/org/apache/fop/render/pdf/PDFRendererConfiguratorTestCase.java @@ -0,0 +1,146 @@ +/* + * 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. + */ + +/* $Id$ */ + +package org.apache.fop.render.pdf; + +import java.io.File; + +import junit.framework.AssertionFailedError; +import junit.framework.TestCase; + +import org.apache.fop.apps.FOPException; +import org.apache.fop.apps.FOUserAgent; +import org.apache.fop.apps.FopFactory; +import org.apache.fop.events.Event; +import org.apache.fop.events.EventListener; +import org.apache.fop.pdf.PDFEncryptionParams; + +/** + * Tests that encryption length is properly set up. + */ +public class PDFRendererConfiguratorTestCase extends TestCase { + + private FOUserAgent foUserAgent; + + private PDFDocumentHandler documentHandler; + + private boolean eventTriggered; + + private class EncryptionEventFilter implements EventListener { + + private final int specifiedEncryptionLength; + + private final int correctedEncryptionLength; + + EncryptionEventFilter(int specifiedEncryptionLength, int correctedEncryptionLength) { + this.specifiedEncryptionLength = specifiedEncryptionLength; + this.correctedEncryptionLength = correctedEncryptionLength; + } + + public void processEvent(Event event) { + assertEquals(PDFEventProducer.class.getName() + ".incorrectEncryptionLength", + event.getEventID()); + assertEquals(specifiedEncryptionLength, event.getParam("originalValue")); + assertEquals(correctedEncryptionLength, event.getParam("correctedValue")); + eventTriggered = true; + } + } + + /** + * Non-multiple of 8 should be rounded. + * + * @throws Exception if an error occurs + */ + public void testRoundUp() throws Exception { + runTest("roundUp", 55, 56); + } + + /** + * Non-multiple of 8 should be rounded. + * + * @throws Exception if an error occurs + */ + public void testRoundDown() throws Exception { + runTest("roundDown", 67, 64); + } + + /** + * Encryption length must be at least 40. + * + * @throws Exception if an error occurs + */ + public void testBelow40() throws Exception { + runTest("below40", 32, 40); + } + + /** + * Encryption length must be at most 128. + * + * @throws Exception if an error occurs + */ + public void testAbove128() throws Exception { + runTest("above128", 233, 128); + } + + /** + * A correct value must be properly set up. + * + * @throws Exception if an error occurs + */ + public void testCorrectValue() throws Exception { + givenAConfigurationFile("correct", new EventListener() { + + public void processEvent(Event event) { + throw new AssertionFailedError("No event was expected"); + } + }); + whenCreatingAndConfiguringDocumentHandler(); + thenEncryptionLengthShouldBe(128); + + } + + private void runTest(String configFilename, + final int specifiedEncryptionLength, + final int correctedEncryptionLength) throws Exception { + givenAConfigurationFile(configFilename, + new EncryptionEventFilter(specifiedEncryptionLength, correctedEncryptionLength)); + whenCreatingAndConfiguringDocumentHandler(); + assertTrue(eventTriggered); + } + + private void givenAConfigurationFile(String filename, EventListener eventListener) + throws Exception { + FopFactory fopFactory = FopFactory.newInstance(); + fopFactory.setUserConfig(new File("test/resources/org/apache/fop/render/pdf/" + + filename + ".xconf")); + foUserAgent = fopFactory.newFOUserAgent(); + foUserAgent.getEventBroadcaster().addEventListener(eventListener); + } + + private void whenCreatingAndConfiguringDocumentHandler() throws FOPException { + PDFDocumentHandlerMaker maker = new PDFDocumentHandlerMaker(); + documentHandler = (PDFDocumentHandler) maker.makeIFDocumentHandler(foUserAgent); + new PDFRendererConfigurator(foUserAgent).configure(documentHandler); + } + + private void thenEncryptionLengthShouldBe(int expectedEncryptionLength) { + PDFEncryptionParams encryptionParams = documentHandler.getPDFUtil().getEncryptionParams(); + assertEquals(expectedEncryptionLength, encryptionParams.getEncryptionLengthInBits()); + } +} diff --git a/test/java/org/apache/fop/render/pdf/RenderPDFTestSuite.java b/test/java/org/apache/fop/render/pdf/RenderPDFTestSuite.java new file mode 100644 index 000000000..c9a17da0b --- /dev/null +++ b/test/java/org/apache/fop/render/pdf/RenderPDFTestSuite.java @@ -0,0 +1,45 @@ +/* + * 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. + */ + +/* $Id$ */ + +package org.apache.fop.render.pdf; + +import junit.framework.Test; +import junit.framework.TestSuite; + + +/** + * A test suite for org.apache.fop.render.pdf.* + */ +public final class RenderPDFTestSuite { + + private RenderPDFTestSuite() { } + + /** + * Creates the test suite. + * + * @return the test suite + */ + public static Test suite() { + TestSuite suite = new TestSuite(); + //$JUnit-BEGIN$ + suite.addTest(new TestSuite(PDFRendererConfiguratorTestCase.class)); + //$JUnit-END$ + return suite; + } +} diff --git a/test/resources/org/apache/fop/render/pdf/above128.xconf b/test/resources/org/apache/fop/render/pdf/above128.xconf new file mode 100644 index 000000000..2bdab04f4 --- /dev/null +++ b/test/resources/org/apache/fop/render/pdf/above128.xconf @@ -0,0 +1,10 @@ + + + + + + 233 + + + + diff --git a/test/resources/org/apache/fop/render/pdf/below40.xconf b/test/resources/org/apache/fop/render/pdf/below40.xconf new file mode 100644 index 000000000..19086f763 --- /dev/null +++ b/test/resources/org/apache/fop/render/pdf/below40.xconf @@ -0,0 +1,10 @@ + + + + + + 32 + + + + diff --git a/test/resources/org/apache/fop/render/pdf/correct.xconf b/test/resources/org/apache/fop/render/pdf/correct.xconf new file mode 100644 index 000000000..246c17e99 --- /dev/null +++ b/test/resources/org/apache/fop/render/pdf/correct.xconf @@ -0,0 +1,10 @@ + + + + + + 128 + + + + diff --git a/test/resources/org/apache/fop/render/pdf/roundDown.xconf b/test/resources/org/apache/fop/render/pdf/roundDown.xconf new file mode 100644 index 000000000..722808c03 --- /dev/null +++ b/test/resources/org/apache/fop/render/pdf/roundDown.xconf @@ -0,0 +1,10 @@ + + + + + + 67 + + + + diff --git a/test/resources/org/apache/fop/render/pdf/roundUp.xconf b/test/resources/org/apache/fop/render/pdf/roundUp.xconf new file mode 100644 index 000000000..ffe06905d --- /dev/null +++ b/test/resources/org/apache/fop/render/pdf/roundUp.xconf @@ -0,0 +1,10 @@ + + + + + + 55 + + + +