diff options
author | Vincent Hennebert <vhennebert@apache.org> | 2011-08-08 15:51:43 +0000 |
---|---|---|
committer | Vincent Hennebert <vhennebert@apache.org> | 2011-08-08 15:51:43 +0000 |
commit | 8e894b822fd834209663744877fb6d648630abfc (patch) | |
tree | 6c1c05ba4c9627ed902c5f9b9d9942b7bb7fb2ef /src/java/org/apache/fop/pdf | |
parent | 3331d47de93f70b0fb6a5ac79ffb41ea8432796c (diff) | |
download | xmlgraphics-fop-8e894b822fd834209663744877fb6d648630abfc.tar.gz xmlgraphics-fop-8e894b822fd834209663744877fb6d648630abfc.zip |
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
Diffstat (limited to 'src/java/org/apache/fop/pdf')
-rw-r--r-- | src/java/org/apache/fop/pdf/FileIDGenerator.java | 124 | ||||
-rw-r--r-- | src/java/org/apache/fop/pdf/PDFDocument.java | 48 | ||||
-rw-r--r-- | src/java/org/apache/fop/pdf/PDFEncryption.java | 12 | ||||
-rw-r--r-- | src/java/org/apache/fop/pdf/PDFEncryptionJCE.java | 730 | ||||
-rw-r--r-- | src/java/org/apache/fop/pdf/PDFEncryptionManager.java | 10 | ||||
-rw-r--r-- | src/java/org/apache/fop/pdf/PDFEncryptionParams.java | 109 | ||||
-rw-r--r-- | src/java/org/apache/fop/pdf/PDFObject.java | 2 |
7 files changed, 699 insertions, 336 deletions
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 @@ -25,18 +25,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. @@ -61,6 +68,25 @@ public class PDFEncryptionParams { } /** + * 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 */ @@ -93,6 +119,38 @@ public class PDFEncryptionParams { } /** + * 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,7 +191,7 @@ 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) { @@ -141,6 +199,38 @@ public class PDFEncryptionParams { } /** + * 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 * if no password is applicable @@ -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() { |