]> source.dussan.org Git - xmlgraphics-fop.git/commitdiff
FOP-2248: add support for AES 256 PDF encryption
authorLuis Bernardo <lbernardo@apache.org>
Sat, 18 May 2013 22:25:52 +0000 (22:25 +0000)
committerLuis Bernardo <lbernardo@apache.org>
Sat, 18 May 2013 22:25:52 +0000 (22:25 +0000)
git-svn-id: https://svn.apache.org/repos/asf/xmlgraphics/fop/trunk@1484190 13f79535-47bb-0310-9956-ffa450edef68

12 files changed:
findbugs-exclude.xml
src/java/org/apache/fop/pdf/PDFCIDFont.java
src/java/org/apache/fop/pdf/PDFCIDSystemInfo.java
src/java/org/apache/fop/pdf/PDFDocument.java
src/java/org/apache/fop/pdf/PDFEncryption.java
src/java/org/apache/fop/pdf/PDFEncryptionJCE.java
src/java/org/apache/fop/pdf/PDFEncryptionParams.java
src/java/org/apache/fop/pdf/PDFFactory.java
src/java/org/apache/fop/render/pdf/PDFEncryptionOption.java
src/java/org/apache/fop/render/pdf/PDFRendererConfig.java
test/java/org/apache/fop/pdf/PDFEncryptionJCETestCase.java
test/java/org/apache/fop/render/pdf/PDFRendererConfigParserTestCase.java

index 3d62cd81a770c1099a6fe0d0c1792f27661fbaf2..31edec2684a37b76071844dc29f20fa49d8c2372 100644 (file)
       <!--Neither method nor field-->
       <Bug pattern="SIC_INNER_SHOULD_BE_STATIC"/>
    </Match>
+   <Match>
+      <Class name="org.apache.fop.pdf.PDFEncryptionJCE$InitializationEngine"/>
+      <Bug pattern="UWF_FIELD_NOT_INITIALIZED_IN_CONSTRUCTOR"/>
+   </Match>
    <Match>
       <Class name="org.apache.fop.render.awt.viewer.PreviewPanel$ViewportScroller"/>
       <!--Neither method nor field-->
index e240718462b3d772b891e9c30c7efb1e84d4f822..907cab0dfe580407f6f7dd61722ee7f3a13c20f9 100644 (file)
@@ -19,6 +19,9 @@
 
 package org.apache.fop.pdf;
 
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+
 import org.apache.fop.fonts.CIDFontType;
 
 // based on work by Takayuki Takeuchi
@@ -106,6 +109,7 @@ public class PDFCIDFont extends PDFObject {
         this.w = w;
         this.dw2 = null;
         this.w2 = null;
+        systemInfo.setParent(this);
         this.systemInfo = systemInfo;
         this.descriptor = descriptor;
         this.cidMap = null;
@@ -242,5 +246,60 @@ public class PDFCIDFont extends PDFObject {
         return p.toString();
     }
 
+    /**
+     * {@inheritDoc}
+     */
+    public byte[] toPDF() {
+        ByteArrayOutputStream bout = new ByteArrayOutputStream(128);
+        try {
+            bout.write(encode("<< /Type /Font\n"));
+            bout.write(encode("/BaseFont /"));
+            bout.write(encode(this.basefont));
+            bout.write(encode(" \n"));
+            bout.write(encode("/CIDToGIDMap "));
+            bout.write(encode(cidMap != null ? cidMap.referencePDF() : "/Identity"));
+            bout.write(encode(" \n"));
+            bout.write(encode("/Subtype /"));
+            bout.write(encode(getPDFNameForCIDFontType(this.cidtype)));
+            bout.write(encode("\n"));
+            bout.write(encode("/CIDSystemInfo "));
+            bout.write(systemInfo.toPDF());
+            bout.write(encode("\n"));
+            bout.write(encode("/FontDescriptor "));
+            bout.write(encode(this.descriptor.referencePDF()));
+            bout.write(encode("\n"));
+            if (cmap != null) {
+                bout.write(encode("/ToUnicode "));
+                bout.write(encode(cmap.referencePDF()));
+                bout.write(encode("\n"));
+            }
+            if (dw != null) {
+                bout.write(encode("/DW "));
+                bout.write(encode(this.dw.toString()));
+                bout.write(encode("\n"));
+            }
+            if (w != null) {
+                bout.write(encode("/W "));
+                bout.write(encode(w.toPDFString()));
+                bout.write(encode("\n"));
+            }
+            if (dw2 != null) {
+                bout.write(encode("/DW2 [")); // always two values, see p 211
+                bout.write(encode(Integer.toString(this.dw2[0])));
+                bout.write(encode(Integer.toString(this.dw2[1])));
+                bout.write(encode("]\n"));
+            }
+            if (w2 != null) {
+                bout.write(encode("/W2 "));
+                bout.write(encode(w2.toPDFString()));
+                bout.write(encode("\n"));
+            }
+            bout.write(encode(">>"));
+        } catch (IOException ioe) {
+            log.error("Ignored I/O exception", ioe);
+        }
+        return bout.toByteArray();
+    }
+
 }
 
index 0228addb65a2b20d82bd8954d9c8a96c3d4bc7ae..7a96930aa4d19402ad8432c76a8692c664271f30 100644 (file)
@@ -19,6 +19,9 @@
 
 package org.apache.fop.pdf;
 
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+
 // based on work by Takayuki Takeuchi
 
 /**
@@ -64,5 +67,24 @@ public class PDFCIDSystemInfo extends PDFObject {
         return p.toString();
     }
 
+    /**
+     * {@inheritDoc}
+     */
+    public byte[] toPDF() {
+        ByteArrayOutputStream bout = new ByteArrayOutputStream(128);
+        try {
+            bout.write(encode("<< /Registry "));
+            bout.write(encodeText(registry));
+            bout.write(encode(" /Ordering "));
+            bout.write(encodeText(ordering));
+            bout.write(encode(" /Supplement "));
+            bout.write(encode(Integer.toString(supplement)));
+            bout.write(encode(" >>"));
+        } catch (IOException ioe) {
+            log.error("Ignored I/O exception", ioe);
+        }
+        return bout.toByteArray();
+    }
+
 }
 
index 0b412842f99b829942a074526e8d4d3e2261571f..ff9f61201a4496bcac4ff52b8c21f50342105d8c 100644 (file)
@@ -516,10 +516,19 @@ public class PDFDocument {
         if (this.encryption != null) {
             PDFObject pdfObject = (PDFObject)this.encryption;
             addTrailerObject(pdfObject);
+            try {
+                versionController.setPDFVersion(encryption.getPDFVersion());
+            } catch (IllegalStateException ise) {
+                log.warn("Configured encryption requires PDF version " + encryption.getPDFVersion()
+                        + " but version has been set to " + versionController.getPDFVersion() + ".");
+                throw ise;
+            }
         } else {
-            log.warn(
-                "PDF encryption is unavailable. PDF will be "
-                    + "generated without encryption.");
+            log.warn("PDF encryption is unavailable. PDF will be generated without encryption.");
+            if (params.getEncryptionLengthInBits() == 256) {
+                log.warn("Make sure the JCE Unlimited Strength Jurisdiction Policy files are available."
+                        + "AES 256 encryption cannot be performed without them.");
+            }
         }
     }
 
index fcb56e50b73f0af3b21114fd44e58e35f4977435..87b9d2522c0c0c525fcf82730dbf916a9ff6ec5a 100644 (file)
@@ -46,4 +46,10 @@ public interface PDFEncryption {
      * of the document's encryption dictionary
      */
     String getTrailerEntry();
+
+    /**
+     * Returns the PDF version required by the current encryption algorithm.
+     * @return the PDF Version
+     */
+    Version getPDFVersion();
 }
index 129bab183a03f72c19e635b3c0dd33cbb49dc09c..2fe9f29e03f3318b5ba85bb5cb3595fe4c195d2f 100644 (file)
@@ -21,9 +21,12 @@ package org.apache.fop.pdf;
 
 import java.io.IOException;
 import java.io.OutputStream;
+import java.io.UnsupportedEncodingException;
+import java.security.InvalidAlgorithmParameterException;
 import java.security.InvalidKeyException;
 import java.security.MessageDigest;
 import java.security.NoSuchAlgorithmException;
+import java.security.SecureRandom;
 import java.util.Arrays;
 
 import javax.crypto.BadPaddingException;
@@ -31,6 +34,7 @@ import javax.crypto.Cipher;
 import javax.crypto.CipherOutputStream;
 import javax.crypto.IllegalBlockSizeException;
 import javax.crypto.NoSuchPaddingException;
+import javax.crypto.spec.IvParameterSpec;
 import javax.crypto.spec.SecretKeySpec;
 
 /**
@@ -40,10 +44,20 @@ public final class PDFEncryptionJCE extends PDFObject implements PDFEncryption {
 
     private final MessageDigest digest;
 
+    private SecureRandom random;
+
     private byte[] encryptionKey;
 
     private String encryptionDictionary;
 
+    private boolean useAlgorithm31a;
+
+    private boolean encryptMetadata = true;
+
+    private Version pdfVersion = Version.V1_4;
+
+    private static byte[] ivZero = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0};
+
     private class EncryptionInitializer {
 
         private final PDFEncryptionParams encryptionParams;
@@ -64,18 +78,30 @@ public final class PDFEncryptionJCE extends PDFObject implements PDFEncryption {
             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);
+                    encryptionParams.getUserPassword(), encryptionParams.getOwnerPassword(),
+                    encryptionParams.encryptMetadata());
+            InitializationEngine initializationEngine = createEngine(encryptionSettings);
             initializationEngine.run();
-            encryptionDictionary = createEncryptionDictionary(permissions,
-                    initializationEngine.oValue,
-                    initializationEngine.uValue);
+            encryptionDictionary = createEncryptionDictionary(permissions, initializationEngine);
+            encryptMetadata = encryptionParams.encryptMetadata();
+        }
+
+        private InitializationEngine createEngine(EncryptionSettings encryptionSettings) {
+            if (revision == 5) {
+                return new Rev5Engine(encryptionSettings);
+            } else if (revision == 2) {
+                return new Rev2Engine(encryptionSettings);
+            } else {
+                return new Rev3Engine(encryptionSettings);
+            }
         }
 
         private void determineEncryptionAlgorithm() {
-            if (isVersion1Revision2Algorithm()) {
+            if (isVersion5Revision5Algorithm()) {
+                version = 5;
+                revision = 5;
+                pdfVersion = Version.V1_7;
+            } else if (isVersion1Revision2Algorithm()) {
                 version = 1;
                 revision = 2;
             } else {
@@ -92,16 +118,20 @@ public final class PDFEncryptionJCE extends PDFObject implements PDFEncryption {
                     && encryptionParams.isAllowPrintHq();
         }
 
-        private String createEncryptionDictionary(final int permissions, final byte[] oValue,
-                final byte[] uValue) {
-            return "<< /Filter /Standard\n"
+        private boolean isVersion5Revision5Algorithm() {
+            return encryptionLength == 256;
+        }
+
+        private String createEncryptionDictionary(final int permissions, InitializationEngine engine) {
+            String encryptionDict = "<<\n"
+                    + "/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"
+                    + "/P " + permissions + "\n"
+                    + engine.getEncryptionDictionaryPart()
                     + ">>";
+            return encryptionDict;
         }
 
     }
@@ -173,62 +203,88 @@ public final class PDFEncryptionJCE extends PDFObject implements PDFEncryption {
 
         final String ownerPassword;
 
+        final boolean encryptMetadata;
+
         EncryptionSettings(int encryptionLength, int permissions,
-                String userPassword, String ownerPassword) {
+                String userPassword, String ownerPassword, boolean encryptMetadata) {
             this.encryptionLength = encryptionLength;
             this.permissions = permissions;
             this.userPassword = userPassword;
             this.ownerPassword = ownerPassword;
+            this.encryptMetadata = encryptMetadata;
         }
 
     }
 
     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;
+        protected final int permissions;
+
+        private final String userPassword;
 
-        private byte[] oValue;
+        private final String ownerPassword;
 
-        private byte[] uValue;
+        protected byte[] oValue;
 
-        private final byte[] preparedUserPassword;
+        protected byte[] uValue;
 
-        protected final String ownerPassword;
+        protected byte[] preparedUserPassword;
+
+        protected byte[] preparedOwnerPassword;
 
         InitializationEngine(EncryptionSettings encryptionSettings) {
             this.encryptionLengthInBytes = encryptionSettings.encryptionLength / 8;
             this.permissions = encryptionSettings.permissions;
-            this.preparedUserPassword = preparePassword(encryptionSettings.userPassword);
+            this.userPassword = encryptionSettings.userPassword;
             this.ownerPassword = encryptionSettings.ownerPassword;
         }
 
         void run() {
-            oValue = computeOValue();
-            createEncryptionKey();
-            uValue = computeUValue();
+            preparedUserPassword = preparePassword(userPassword);
+            if (ownerPassword == null || ownerPassword.length() == 0) {
+                preparedOwnerPassword = preparedUserPassword;
+            } else {
+                preparedOwnerPassword = preparePassword(ownerPassword);
+            }
+        }
+
+        protected String getEncryptionDictionaryPart() {
+            String encryptionDictionaryPart = "/O " + PDFText.toHex(oValue) + "\n"
+                    + "/U " + PDFText.toHex(uValue) + "\n";
+            return encryptionDictionaryPart;
+        }
+
+        protected abstract void computeOValue();
+
+        protected abstract void computeUValue();
+
+        protected abstract void createEncryptionKey();
+
+        protected abstract byte[] preparePassword(String password);
+    }
+
+    private abstract class RevBefore5Engine extends 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};
+
+        RevBefore5Engine(EncryptionSettings encryptionSettings) {
+            super(encryptionSettings);
         }
 
         /**
          * Applies Algorithm 3.3 Page 79 of the PDF 1.4 Reference.
          *
-         * @return the O value
          */
-        private byte[] computeOValue() {
+        protected void computeOValue() {
             // Step 1
-            byte[] md5Input = prepareMD5Input();
+            byte[] md5Input = preparedOwnerPassword;
             // Step 2
             digest.reset();
             byte[] hash = digest.digest(md5Input);
@@ -240,15 +296,13 @@ public final class PDFEncryptionJCE extends PDFObject implements PDFEncryption {
             // Steps 5, 6
             byte[] encryptionResult = encryptWithKey(key, preparedUserPassword);
             // Step 7
-            encryptionResult = computeOValueStep7(key, encryptionResult);
-            // Step 8
-            return encryptionResult;
+            oValue = computeOValueStep7(key, encryptionResult);
         }
 
         /**
          * Applies Algorithm 3.2 Page 78 of the PDF 1.4 Reference.
          */
-        private void createEncryptionKey() {
+        protected void createEncryptionKey() {
             // Steps 1, 2
             digest.reset();
             digest.update(preparedUserPassword);
@@ -269,30 +323,31 @@ public final class PDFEncryptionJCE extends PDFObject implements PDFEncryption {
             System.arraycopy(hash, 0, encryptionKey, 0, encryptionLengthInBytes);
         }
 
-        protected abstract byte[] computeUValue();
-
         /**
          * 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) {
+        protected 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;
+            try {
+                byte[] passwordBytes = password.getBytes("UTF-8");
+                System.arraycopy(passwordBytes, 0, preparedPassword, 0, passwordBytes.length);
+                System.arraycopy(padding, 0, preparedPassword, passwordBytes.length, finalLength
+                        - passwordBytes.length);
+                return preparedPassword;
+            } catch (UnsupportedEncodingException e) {
+                throw new UnsupportedOperationException(e);
+            }
         }
 
-        private byte[] prepareMD5Input() {
-            if (ownerPassword.length() != 0) {
-                return preparePassword(ownerPassword);
-            } else {
-                return preparedUserPassword;
-            }
+        void run() {
+            super.run();
+            computeOValue();
+            createEncryptionKey();
+            computeUValue();
         }
 
         protected abstract byte[] computeOValueStep3(byte[] hash);
@@ -303,7 +358,7 @@ public final class PDFEncryptionJCE extends PDFObject implements PDFEncryption {
 
     }
 
-    private class Rev2Engine extends InitializationEngine {
+    private class Rev2Engine extends RevBefore5Engine {
 
         Rev2Engine(EncryptionSettings encryptionSettings) {
             super(encryptionSettings);
@@ -325,13 +380,13 @@ public final class PDFEncryptionJCE extends PDFObject implements PDFEncryption {
         }
 
         @Override
-        protected byte[] computeUValue() {
-            return encryptWithKey(encryptionKey, padding);
+        protected void computeUValue() {
+            uValue = encryptWithKey(encryptionKey, padding);
         }
 
     }
 
-    private class Rev3Engine extends InitializationEngine {
+    private class Rev3Engine extends RevBefore5Engine {
 
         Rev3Engine(EncryptionSettings encryptionSettings) {
             super(encryptionSettings);
@@ -360,7 +415,7 @@ public final class PDFEncryptionJCE extends PDFObject implements PDFEncryption {
         }
 
         @Override
-        protected byte[] computeUValue() {
+        protected void computeUValue() {
             // Step 1 is encryptionKey
             // Step 2
             digest.reset();
@@ -372,11 +427,10 @@ public final class PDFEncryptionJCE extends PDFObject implements PDFEncryption {
             // Step 5
             encryptionResult = xorKeyAndEncrypt19Times(encryptionKey, encryptionResult);
             // Step 6
-            byte[] uValue = new byte[32];
+            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 byte[] xorKeyAndEncrypt19Times(byte[] key, byte[] input) {
@@ -393,6 +447,174 @@ public final class PDFEncryptionJCE extends PDFObject implements PDFEncryption {
 
     }
 
+    private class Rev5Engine extends InitializationEngine {
+
+        // private SecureRandom random = new SecureRandom();
+        private byte[] userValidationSalt = new byte[8];
+        private byte[] userKeySalt = new byte[8];
+        private byte[] ownerValidationSalt = new byte[8];
+        private byte[] ownerKeySalt = new byte[8];
+        private byte[] ueValue;
+        private byte[] oeValue;
+        private final boolean encryptMetadata;
+
+        Rev5Engine(EncryptionSettings encryptionSettings) {
+            super(encryptionSettings);
+            this.encryptMetadata = encryptionSettings.encryptMetadata;
+        }
+
+        void run() {
+            super.run();
+            random = new SecureRandom();
+            createEncryptionKey();
+            computeUValue();
+            computeOValue();
+            computeUEValue();
+            computeOEValue();
+        }
+
+        protected String getEncryptionDictionaryPart() {
+            String encryptionDictionaryPart = super.getEncryptionDictionaryPart();
+            encryptionDictionaryPart += "/OE " + PDFText.toHex(oeValue) + "\n"
+                    + "/UE " + PDFText.toHex(ueValue) + "\n"
+                    + "/Perms " + PDFText.toHex(computePermsValue(permissions)) + "\n"
+                    + "/EncryptMetadata " + encryptMetadata + "\n"
+                    // note: I think Length below should be 256 but Acrobat 9 uses 32...
+                    + "/CF <</StdCF <</AuthEvent /DocOpen /CFM /AESV3 /Length 32>>>>\n"
+                    + "/StmF /StdCF /StrF /StdCF\n";
+            return encryptionDictionaryPart;
+        }
+
+        /**
+         * Algorithm 3.8-1 (page 20, Adobe Supplement to the ISO 32000, BaseVersion: 1.7, ExtensionLevel: 3)
+         */
+        @Override
+        protected void computeUValue() {
+            byte[] userBytes = new byte[16];
+            random.nextBytes(userBytes);
+            System.arraycopy(userBytes, 0, userValidationSalt, 0, 8);
+            System.arraycopy(userBytes, 8, userKeySalt, 0, 8);
+            digest.reset();
+            byte[] prepared = preparedUserPassword;
+            byte[] concatenated = new byte[prepared.length + 8];
+            System.arraycopy(prepared, 0, concatenated, 0, prepared.length);
+            System.arraycopy(userValidationSalt, 0, concatenated, prepared.length, 8);
+            digest.update(concatenated);
+            byte[] sha256 = digest.digest();
+            uValue = new byte[48];
+            System.arraycopy(sha256, 0, uValue, 0, 32);
+            System.arraycopy(userValidationSalt, 0, uValue, 32, 8);
+            System.arraycopy(userKeySalt, 0, uValue, 40, 8);
+        }
+
+        /**
+         * Algorithm 3.9-1 (page 20, Adobe Supplement to the ISO 32000, BaseVersion: 1.7, ExtensionLevel: 3)
+         */
+        @Override
+        protected void computeOValue() {
+            byte[] ownerBytes = new byte[16];
+            random.nextBytes(ownerBytes);
+            System.arraycopy(ownerBytes, 0, ownerValidationSalt, 0, 8);
+            System.arraycopy(ownerBytes, 8, ownerKeySalt, 0, 8);
+            digest.reset();
+            byte[] prepared = preparedOwnerPassword;
+            byte[] concatenated = new byte[prepared.length + 56];
+            System.arraycopy(prepared, 0, concatenated, 0, prepared.length);
+            System.arraycopy(ownerValidationSalt, 0, concatenated, prepared.length, 8);
+            System.arraycopy(uValue, 0, concatenated, prepared.length + 8, 48);
+            digest.update(concatenated);
+            byte[] sha256 = digest.digest();
+            oValue = new byte[48];
+            System.arraycopy(sha256, 0, oValue, 0, 32);
+            System.arraycopy(ownerValidationSalt, 0, oValue, 32, 8);
+            System.arraycopy(ownerKeySalt, 0, oValue, 40, 8);
+        }
+
+        /**
+         * See Adobe Supplement to the ISO 32000, BaseVersion: 1.7, ExtensionLevel: 3, page 20, paragraph 5.
+         */
+        protected void createEncryptionKey() {
+            encryptionKey = new byte[encryptionLengthInBytes];
+            random.nextBytes(encryptionKey);
+        }
+
+        /**
+         * Algorithm 3.2a-1 (page 19, Adobe Supplement to the ISO 32000, BaseVersion: 1.7, ExtensionLevel: 3)
+         */
+        protected byte[] preparePassword(String password) {
+            byte[] passwordBytes;
+            byte[] preparedPassword;
+            try {
+                // the password needs to be normalized first but we are bypassing that step for now
+                passwordBytes = password.getBytes("UTF-8");
+                if (passwordBytes.length > 127) {
+                    preparedPassword = new byte[127];
+                    System.arraycopy(passwordBytes, 0, preparedPassword, 0, 127);
+                } else {
+                    preparedPassword = new byte[passwordBytes.length];
+                    System.arraycopy(passwordBytes, 0, preparedPassword, 0, passwordBytes.length);
+                }
+                return preparedPassword;
+            } catch (UnsupportedEncodingException e) {
+                throw new UnsupportedOperationException(e.getMessage());
+            }
+        }
+
+        /**
+         * Algorithm 3.8-2 (page 20, Adobe Supplement to the ISO 32000, BaseVersion: 1.7, ExtensionLevel: 3)
+         */
+        private void computeUEValue() {
+            digest.reset();
+            byte[] prepared = preparedUserPassword;
+            byte[] concatenated = new byte[prepared.length + 8];
+            System.arraycopy(prepared, 0, concatenated, 0, prepared.length);
+            System.arraycopy(userKeySalt, 0, concatenated, prepared.length, 8);
+            digest.update(concatenated);
+            byte[] ueEncryptionKey = digest.digest();
+            ueValue = encryptWithKey(ueEncryptionKey, encryptionKey, true, ivZero);
+        }
+
+        /**
+         * Algorithm 3.9-2 (page 20, Adobe Supplement to the ISO 32000, BaseVersion: 1.7, ExtensionLevel: 3)
+         */
+        private void computeOEValue() {
+            digest.reset();
+            byte[] prepared = preparedOwnerPassword;
+            byte[] concatenated = new byte[prepared.length + 56];
+            System.arraycopy(prepared, 0, concatenated, 0, prepared.length);
+            System.arraycopy(ownerKeySalt, 0, concatenated, prepared.length, 8);
+            System.arraycopy(uValue, 0, concatenated, prepared.length + 8, 48);
+            digest.update(concatenated);
+            byte[] oeEncryptionKey = digest.digest();
+            oeValue = encryptWithKey(oeEncryptionKey, encryptionKey, true, ivZero);
+        }
+
+        /**
+         * Algorithm 3.10 (page 20, Adobe Supplement to the ISO 32000, BaseVersion: 1.7, ExtensionLevel: 3)
+         */
+        public byte[] computePermsValue(int permissions) {
+            byte[] perms = new byte[16];
+            long extendedPermissions = 0xffffffff00000000L | permissions;
+            for (int k = 0; k < 8; k++) {
+                perms[k] = (byte) (extendedPermissions & 0xff);
+                extendedPermissions >>= 8;
+            }
+            if (encryptMetadata) {
+                perms[8] = 'T';
+            } else {
+                perms[8] = 'F';
+            }
+            perms[9] = 'a';
+            perms[10] = 'd';
+            perms[11] = 'b';
+            byte[] randomBytes = new byte[4];
+            random.nextBytes(randomBytes);
+            System.arraycopy(randomBytes, 0, perms, 12, 4);
+            byte[] encryptedPerms = encryptWithKey(encryptionKey, perms, true, ivZero);
+            return encryptedPerms;
+        }
+    }
+
     private class EncryptionFilter extends PDFFilter {
 
         private int streamNumber;
@@ -424,9 +646,18 @@ public final class PDFEncryptionJCE extends PDFObject implements PDFEncryption {
 
         /** {@inheritDoc} */
         public OutputStream applyFilter(OutputStream out) throws IOException {
-            byte[] key = createEncryptionKey(streamNumber, streamGeneration);
-            Cipher cipher = initCipher(key);
-            return new CipherOutputStream(out, cipher);
+            if (useAlgorithm31a) {
+                byte[] iv = new byte[16];
+                random.nextBytes(iv);
+                Cipher cipher = initCipher(encryptionKey, false, iv);
+                out.write(iv);
+                out.flush();
+                return new CipherOutputStream(out, cipher);
+            } else {
+                byte[] key = createEncryptionKey(streamNumber, streamGeneration);
+                Cipher cipher = initCipher(key);
+                return new CipherOutputStream(out, cipher);
+            }
         }
 
     }
@@ -434,13 +665,18 @@ public final class PDFEncryptionJCE extends PDFObject implements PDFEncryption {
     private PDFEncryptionJCE(int objectNumber, PDFEncryptionParams params, PDFDocument pdf) {
         setObjectNumber(objectNumber);
         try {
-            digest = MessageDigest.getInstance("MD5");
+            if (params.getEncryptionLengthInBits() == 256) {
+                digest = MessageDigest.getInstance("SHA-256");
+            } else {
+                digest = MessageDigest.getInstance("MD5");
+            }
         } catch (NoSuchAlgorithmException e) {
             throw new UnsupportedOperationException(e.getMessage());
         }
         setDocument(pdf);
         EncryptionInitializer encryptionInitializer = new EncryptionInitializer(params);
         encryptionInitializer.init();
+        useAlgorithm31a = encryptionInitializer.isVersion5Revision5Algorithm();
     }
 
     /**
@@ -462,15 +698,28 @@ public final class PDFEncryptionJCE extends PDFObject implements PDFEncryption {
         while (o != null && !o.hasObjectNumber()) {
             o = o.getParent();
         }
-        if (o == null) {
+        if (o == null && !useAlgorithm31a) {
             throw new IllegalStateException("No object number could be obtained for a PDF object");
         }
-        byte[] key = createEncryptionKey(o.getObjectNumber(), o.getGeneration());
-        return encryptWithKey(key, data);
+        if (useAlgorithm31a) {
+            byte[] iv = new byte[16];
+            random.nextBytes(iv);
+            byte[] encryptedData = encryptWithKey(encryptionKey, data, false, iv);
+            byte[] storedData = new byte[encryptedData.length + 16];
+            System.arraycopy(iv, 0, storedData, 0, 16);
+            System.arraycopy(encryptedData, 0, storedData, 16, encryptedData.length);
+            return storedData;
+        } else {
+            byte[] key = createEncryptionKey(o.getObjectNumber(), o.getGeneration());
+            return encryptWithKey(key, data);
+        }
     }
 
     /** {@inheritDoc} */
     public void applyFilter(AbstractPDFStream stream) {
+        if (!encryptMetadata && stream instanceof PDFMetadata) {
+            return;
+        }
         stream.getFilterList().addFilter(
                 new EncryptionFilter(stream.getObjectNumber(), stream.getGeneration()));
     }
@@ -501,12 +750,23 @@ public final class PDFEncryptionJCE extends PDFObject implements PDFEncryption {
         }
     }
 
+    private static byte[] encryptWithKey(byte[] key, byte[] data, boolean noPadding, byte[] iv) {
+        try {
+            final Cipher c = initCipher(key, noPadding, iv);
+            return c.doFinal(data);
+        } catch (IllegalBlockSizeException e) {
+            throw new IllegalStateException(e.getMessage());
+        } catch (BadPaddingException e) {
+            throw new IllegalStateException(e.getMessage());
+        }
+    }
+
     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;
+            Cipher cipher = Cipher.getInstance("RC4");
+            cipher.init(Cipher.ENCRYPT_MODE, keyspec);
+            return cipher;
         } catch (InvalidKeyException e) {
             throw new IllegalStateException(e);
         } catch (NoSuchAlgorithmException e) {
@@ -516,6 +776,25 @@ public final class PDFEncryptionJCE extends PDFObject implements PDFEncryption {
         }
     }
 
+    private static Cipher initCipher(byte[] key, boolean noPadding, byte[] iv) {
+        try {
+            SecretKeySpec skeySpec = new SecretKeySpec(key, "AES");
+            IvParameterSpec ivspec = new IvParameterSpec(iv);
+            Cipher cipher = noPadding ? Cipher.getInstance("AES/CBC/NoPadding") : Cipher
+                    .getInstance("AES/CBC/PKCS5Padding");
+            cipher.init(Cipher.ENCRYPT_MODE, skeySpec, ivspec);
+            return cipher;
+        } catch (InvalidKeyException e) {
+            throw new IllegalStateException(e);
+        } catch (NoSuchAlgorithmException e) {
+            throw new UnsupportedOperationException(e);
+        } catch (NoSuchPaddingException e) {
+            throw new UnsupportedOperationException(e);
+        } catch (InvalidAlgorithmParameterException e) {
+            throw new UnsupportedOperationException(e);
+        }
+    }
+
     /**
      * Applies Algorithm 3.1 from the PDF 1.4 Reference.
      *
@@ -549,4 +828,9 @@ public final class PDFEncryptionJCE extends PDFObject implements PDFEncryption {
         return md5Input;
     }
 
+    /** {@inheritDoc} */
+    public Version getPDFVersion() {
+        return pdfVersion;
+    }
+
 }
index 563c052334c0c6c2a6c7587f9c3638393309319e..a1925b0f49e254a24d92bec98cba053a697cc830 100644 (file)
@@ -35,8 +35,9 @@ public class PDFEncryptionParams {
     private boolean allowAccessContent = true;
     private boolean allowAssembleDocument = true;
     private boolean allowPrintHq = true;
+    private boolean encryptMetadata = true;
 
-    private int encryptionLengthInBits = 40;
+    private int encryptionLengthInBits = 128;
 
     /**
      * Creates a new instance.
@@ -51,13 +52,15 @@ public class PDFEncryptionParams {
                                boolean allowPrint,
                                boolean allowCopyContent,
                                boolean allowEditContent,
-                               boolean allowEditAnnotations) {
+                               boolean allowEditAnnotations,
+                               boolean encryptMetadata) {
         setUserPassword(userPassword);
         setOwnerPassword(ownerPassword);
         setAllowPrint(allowPrint);
         setAllowCopyContent(allowCopyContent);
         setAllowEditContent(allowEditContent);
         setAllowEditAnnotations(allowEditAnnotations);
+        this.encryptMetadata = encryptMetadata;
     }
 
     /**
@@ -84,6 +87,7 @@ public class PDFEncryptionParams {
         setAllowFillInForms(source.isAllowFillInForms());
         setAllowPrintHq(source.isAllowPrintHq());
         setEncryptionLengthInBits(source.getEncryptionLengthInBits());
+        encryptMetadata = source.encryptMetadata();
     }
 
     /**
@@ -150,6 +154,14 @@ public class PDFEncryptionParams {
         return allowPrintHq;
     }
 
+    /**
+     * Indicates whether Metadata should be encrypted.
+     * @return true or false
+     */
+    public boolean encryptMetadata() {
+        return encryptMetadata;
+    }
+
     /**
      * Returns the owner password.
      * @return the owner password, an empty string if no password applies
@@ -230,6 +242,14 @@ public class PDFEncryptionParams {
         this.allowPrintHq = allowPrintHq;
     }
 
+    /**
+     * Whether the Metadata should be encrypted or not; default is true;
+     * @param encryptMetadata true or false
+     */
+    public void setEncryptMetadata(boolean encryptMetadata) {
+        this.encryptMetadata = encryptMetadata;
+    }
+
     /**
      * Sets the owner password.
      * @param ownerPassword The owner password to set, null or an empty String
@@ -283,7 +303,8 @@ public class PDFEncryptionParams {
                 + "allowFillInForms  = " + allowFillInForms + "\n"
                 + "allowAccessContent = " + allowAccessContent + "\n"
                 + "allowAssembleDocument = " + allowAssembleDocument + "\n"
-                + "allowPrintHq = " + allowPrintHq;
+                + "allowPrintHq = " + allowPrintHq + "\n"
+                + "encryptMetadata = " + encryptMetadata;
     }
 
 }
index 0f8d360c8926dceeb12fd815048b2c0aa0bfb096..1756f1d56e3c8945547c7dd405397420384a6455 100644 (file)
@@ -1364,6 +1364,7 @@ public class PDFFactory {
                 }
                 PDFCIDSystemInfo sysInfo = new PDFCIDSystemInfo(cidMetrics.getRegistry(),
                         cidMetrics.getOrdering(), cidMetrics.getSupplement());
+                sysInfo.setDocument(document);
                 PDFCIDFont cidFont = new PDFCIDFont(subsetFontName, cidMetrics.getCIDType(),
                         cidMetrics.getDefaultWidth(), getFontWidths(cidMetrics), sysInfo,
                         (PDFCIDFontDescriptor) pdfdesc);
index f3e51e34a495ef7b66e1329151dcfdb0a96e931e..c14be719a4c2b00410dcefd4e3cf9538ed79dcdc 100644 (file)
@@ -25,9 +25,9 @@ public enum PDFEncryptionOption implements RendererConfigOption {
 
     /**
      * PDF encryption length parameter: must be a multiple of 8 between 40 and 128,
-     * default value 40, datatype: int, default: 40
+     * datatype: int, default: 128
      */
-    ENCRYPTION_LENGTH("encryption-length", 40),
+    ENCRYPTION_LENGTH("encryption-length", 128),
     /**
      * PDF encryption parameter: Forbids printing to high quality, datatype: Boolean or
      * "true"/"false", default: false
@@ -71,7 +71,11 @@ public enum PDFEncryptionOption implements RendererConfigOption {
     /** PDF encryption parameter: user password, datatype: String, default: "" */
     USER_PASSWORD("user-password", ""),
     /** PDF encryption parameter: owner password, datatype: String, default: "" */
-    OWNER_PASSWORD("owner-password", "");
+    OWNER_PASSWORD("owner-password", ""),
+    /**
+     * PDF encryption parameter: encrypts Metadata, datatype: Boolean or "true"/"false", default: true
+     */
+    ENCRYPT_METADATA("encrypt-metadata", true);
 
     public static final String ENCRYPTION_PARAMS = "encryption-params";
 
index 0de7443bfb8b2d32d2781488683508a59afea97b..5e87deb6babb4ce006e4b87fcb3344f983964927 100644 (file)
@@ -43,6 +43,7 @@ import org.apache.fop.util.LogUtil;
 
 import static org.apache.fop.render.pdf.PDFEncryptionOption.ENCRYPTION_LENGTH;
 import static org.apache.fop.render.pdf.PDFEncryptionOption.ENCRYPTION_PARAMS;
+import static org.apache.fop.render.pdf.PDFEncryptionOption.ENCRYPT_METADATA;
 import static org.apache.fop.render.pdf.PDFEncryptionOption.NO_ACCESSCONTENT;
 import static org.apache.fop.render.pdf.PDFEncryptionOption.NO_ANNOTATIONS;
 import static org.apache.fop.render.pdf.PDFEncryptionOption.NO_ASSEMBLEDOC;
@@ -155,6 +156,7 @@ public final class PDFRendererConfig implements RendererConfig {
                 encryptionConfig.setAllowAccessContent(!doesValueExist(encryptCfg, NO_ACCESSCONTENT));
                 encryptionConfig.setAllowAssembleDocument(!doesValueExist(encryptCfg, NO_ASSEMBLEDOC));
                 encryptionConfig.setAllowPrintHq(!doesValueExist(encryptCfg, NO_PRINTHQ));
+                encryptionConfig.setEncryptMetadata(getConfigValue(encryptCfg, ENCRYPT_METADATA, true));
                 String encryptionLength = parseConfig(encryptCfg, ENCRYPTION_LENGTH);
                 if (encryptionLength != null) {
                     int validatedLength = checkEncryptionLength(Integer.parseInt(encryptionLength), userAgent);
@@ -206,11 +208,26 @@ public final class PDFRendererConfig implements RendererConfig {
             return cfg.getChild(option.getName(), false) != null;
         }
 
+        private boolean getConfigValue(Configuration cfg, RendererConfigOption option, boolean defaultTo) {
+            if (cfg.getChild(option.getName(), false) != null) {
+                Configuration child = cfg.getChild(option.getName());
+                try {
+                    return child.getValueAsBoolean();
+                } catch (ConfigurationException e) {
+                    return defaultTo;
+                }
+            } else {
+                return defaultTo;
+            }
+        }
+
         private int checkEncryptionLength(int encryptionLength, FOUserAgent userAgent) {
             int correctEncryptionLength = encryptionLength;
             if (encryptionLength < 40) {
                 correctEncryptionLength = 40;
-            } else if (encryptionLength > 128) {
+            } else if (encryptionLength > 256) {
+                correctEncryptionLength = 256;
+            } else if (encryptionLength > 128 && encryptionLength < 256) {
                 correctEncryptionLength = 128;
             } else if (encryptionLength % 8 != 0) {
                 correctEncryptionLength = Math.round(encryptionLength / 8.0f) * 8;
index db10e656eacd5354343a38b998d1faeb7b92ea66..ea3b011c7f90ba1f213f921d8894f3d647b0ac12 100644 (file)
 
 package org.apache.fop.pdf;
 
+import static org.junit.Assert.assertArrayEquals;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertTrue;
 
 import java.io.ByteArrayOutputStream;
 import java.io.IOException;
 import java.io.UnsupportedEncodingException;
+import java.security.InvalidAlgorithmParameterException;
+import java.security.InvalidKeyException;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
 import java.util.Arrays;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 
+import javax.crypto.BadPaddingException;
+import javax.crypto.Cipher;
+import javax.crypto.IllegalBlockSizeException;
+import javax.crypto.NoSuchPaddingException;
+import javax.crypto.spec.IvParameterSpec;
+import javax.crypto.spec.SecretKeySpec;
+
 import org.junit.Test;
 
 /**
@@ -171,19 +183,17 @@ public class PDFEncryptionJCETestCase {
 
     private static final class EncryptionDictionaryTester {
 
-        private int version = 1;
+        private int version = 2;
 
-        private int revision = 2;
+        private int revision = 3;
 
-        private int length = 40;
+        private int length = 128;
 
         private int permissions = -4;
 
-        private String ownerEntry
-                = "3EE8C4000CA44B2645EED029C9EA7D4FC63C6D9B89349E8FA5A40C7691AB96B5";
+        private String ownerEntry = "D9A98017F0500EF9B69738641C9B4CBA1229EDC3F2151BC6C9C4FB07B1CB315E";
 
-        private String userEntry
-                = "D1810D9E6E488BA5D2DDCBB3F974F7472D0D5389F554DB55574A787DC5C59884";
+        private String userEntry = "D3EF424BFEA2E434000E1A74941CC87300000000000000000000000000000000";
 
         EncryptionDictionaryTester setVersion(int version) {
             this.version = version;
@@ -282,16 +292,16 @@ public class PDFEncryptionJCETestCase {
     @Test
     public void testBasic() throws IOException {
         test = new EncryptionTest();
-        test.setData(0x00).setEncryptedData(0x56);
+        test.setData(0x00).setEncryptedData(0x24);
         runEncryptionTests();
 
-        test.setData(0xAA).setEncryptedData(0xFC);
+        test.setData(0xAA).setEncryptedData(0x8E);
         runEncryptionTests();
 
-        test.setData(0xFF).setEncryptedData(0xA9);
+        test.setData(0xFF).setEncryptedData(0xDB);
         runEncryptionTests();
 
-        test = new EncryptionTest().setEncryptedData(0x56, 0x0C, 0xFC, 0xA5, 0xAB, 0x61, 0x73);
+        test = new EncryptionTest().setEncryptedData(0x24, 0x07, 0x85, 0xF7, 0x87, 0x31, 0x90);
         runEncryptionTests();
     }
 
@@ -313,14 +323,14 @@ public class PDFEncryptionJCETestCase {
     @Test
     public void testDisableRev2Permissions() throws IOException {
         EncryptionDictionaryTester encryptionDictionaryTester = new EncryptionDictionaryTester()
+                .setVersion(1)
+                .setRevision(2)
+                .setLength(40)
                 .setPermissions(-64)
+                .setOwnerEntry("3EE8C4000CA44B2645EED029C9EA7D4FC63C6D9B89349E8FA5A40C7691AB96B5")
                 .setUserEntry("3E65D0090746C4C37C5EF23C1BDB6323E00C24C4B2D744DD3BFB654CD58591A1");
-        test = new EncryptionTest(encryptionDictionaryTester)
-                .setObjectNumber(3)
-                .disablePrint()
-                .disableEditContent()
-                .disableCopyContent()
-                .disableEditAnnotations()
+        test = new EncryptionTest(encryptionDictionaryTester).setObjectNumber(3).setEncryptionLength(40)
+                .disablePrint().disableEditContent().disableCopyContent().disableEditAnnotations()
                 .setEncryptedData(0x66, 0xEE, 0xA7, 0x93, 0xC4, 0xB1, 0xB4);
         runEncryptionTests();
     }
@@ -330,11 +340,13 @@ public class PDFEncryptionJCETestCase {
         EncryptionDictionaryTester encryptionDictionaryTester = new EncryptionDictionaryTester()
                 .setVersion(2)
                 .setRevision(3)
+                .setLength(40)
                 .setPermissions(-3844)
                 .setOwnerEntry("8D4BCA4F4AB2BAB4E38F161D61F937EC50BE5EB30C2DC05EA409D252CD695E55")
                 .setUserEntry("0F01171E22C7FB27B079C132BA4277DE00000000000000000000000000000000");
         test = new EncryptionTest(encryptionDictionaryTester)
                 .setObjectNumber(4)
+                .setEncryptionLength(40)
                 .disableFillInForms()
                 .disableAccessContent()
                 .disableAssembleDocument()
@@ -365,10 +377,14 @@ public class PDFEncryptionJCETestCase {
     @Test
     public void testDifferentPasswords() throws IOException {
         EncryptionDictionaryTester encryptionDictionaryTester = new EncryptionDictionaryTester()
+                .setRevision(2)
+                .setVersion(1)
+                .setLength(40)
                 .setOwnerEntry("D11C233C65E9DC872E858ABBD8B62198771167ADCE7AB8DC7AE0A1A7E21A1E25")
                 .setUserEntry("6F449167DB8DDF0D2DF4602DDBBA97ABF9A9101F632CC16AB0BE74EB9500B469");
         test = new EncryptionTest(encryptionDictionaryTester)
                 .setObjectNumber(6)
+                .setEncryptionLength(40)
                 .setUserPassword("ADifferentUserPassword")
                 .setOwnerPassword("ADifferentOwnerPassword")
                 .setEncryptedData(0x27, 0xAC, 0xB1, 0x6C, 0x42, 0xE0, 0xA8);
@@ -378,10 +394,14 @@ public class PDFEncryptionJCETestCase {
     @Test
     public void testNoOwnerPassword() throws IOException {
         EncryptionDictionaryTester encryptionDictionaryTester = new EncryptionDictionaryTester()
+                .setRevision(2)
+                .setVersion(1)
+                .setLength(40)
                 .setOwnerEntry("5163AAF3EE74C76D7C223593A84C8702FEA8AA4493E4933FF5B5A5BBB20AE4BB")
                 .setUserEntry("42DDF1C1BF3AB04786D5038E7B0A723AE614D944E1DE91A922FC54F5F2345E00");
         test = new EncryptionTest(encryptionDictionaryTester)
                 .setObjectNumber(7)
+                .setEncryptionLength(40)
                 .setUserPassword("ADifferentUserPassword")
                 .setOwnerPassword("")
                 .setEncryptedData(0xEC, 0x2E, 0x5D, 0xC2, 0x7F, 0xAD, 0x58);
@@ -434,6 +454,130 @@ public class PDFEncryptionJCETestCase {
         runEncryptionTests();
     }
 
+    @Test
+    public void testAES256() throws UnsupportedEncodingException, NoSuchAlgorithmException,
+            NoSuchPaddingException, InvalidKeyException, InvalidAlgorithmParameterException,
+            IllegalBlockSizeException, BadPaddingException {
+        MessageDigest sha256 = MessageDigest.getInstance("SHA-256");
+        String dataText = "Test data to encrypt.";
+        byte[] data = dataText.getBytes("UTF-8");
+        PDFEncryptionParams params = new PDFEncryptionParams();
+        params.setEncryptionLengthInBits(256);
+        params.setUserPassword("userpassword");
+        params.setOwnerPassword("ownerpassword");
+        PDFEncryptionJCE encryption = createEncryptionObject(params);
+        PDFText text = new PDFText();
+        text.setObjectNumber(1); // obj number not used with AES 256, can be anything
+        String dictionary = new String(encryption.toPDF());
+        byte[] encrypted = encryption.encrypt(data, text);
+        byte[] u = parseHexStringEntries(dictionary, "U");
+        byte[] o = parseHexStringEntries(dictionary, "O");
+        byte[] ue = parseHexStringEntries(dictionary, "UE");
+        byte[] oe = parseHexStringEntries(dictionary, "OE");
+        byte[] perms = parseHexStringEntries(dictionary, "Perms");
+        // check byte arrays lengths
+        assertEquals(48, u.length);
+        assertEquals(48, o.length);
+        assertEquals(32, ue.length);
+        assertEquals(32, oe.length);
+        assertEquals(16, perms.length);
+        // check user password is valid
+        byte[] userValSalt = new byte[8];
+        byte[] userKeySalt = new byte[8];
+        System.arraycopy(u, 32, userValSalt, 0, 8);
+        System.arraycopy(u, 40, userKeySalt, 0, 8);
+        byte[] uPassBytes = params.getUserPassword().getBytes("UTF-8");
+        byte[] testUPass = new byte[uPassBytes.length + 8];
+        System.arraycopy(uPassBytes, 0, testUPass, 0, uPassBytes.length);
+        System.arraycopy(userValSalt, 0, testUPass, uPassBytes.length, 8);
+        sha256.reset();
+        sha256.update(testUPass);
+        byte[] actualUPass = sha256.digest();
+        byte[] expectedUPass = new byte[32];
+        System.arraycopy(u, 0, expectedUPass, 0, 32);
+        assertArrayEquals(expectedUPass, actualUPass);
+        // check owner password is valid
+        byte[] ownerValSalt = new byte[8];
+        byte[] ownerKeySalt = new byte[8];
+        System.arraycopy(o, 32, ownerValSalt, 0, 8);
+        System.arraycopy(o, 40, ownerKeySalt, 0, 8);
+        byte[] oPassBytes = params.getOwnerPassword().getBytes("UTF-8");
+        byte[] testOPass = new byte[oPassBytes.length + 8 + 48];
+        System.arraycopy(oPassBytes, 0, testOPass, 0, oPassBytes.length);
+        System.arraycopy(ownerValSalt, 0, testOPass, oPassBytes.length, 8);
+        System.arraycopy(u, 0, testOPass, oPassBytes.length + 8, 48);
+        sha256.reset();
+        sha256.update(testOPass);
+        byte[] actualOPass = sha256.digest();
+        byte[] expectedOPass = new byte[32];
+        System.arraycopy(o, 0, expectedOPass, 0, 32);
+        assertArrayEquals(expectedOPass, actualOPass);
+        // compute encryption key from ue
+        byte[] ivZero = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0};
+        IvParameterSpec ivspecZero = new IvParameterSpec(ivZero);
+        Cipher cipherNoPadding = Cipher.getInstance("AES/CBC/NoPadding");
+        byte[] tmpUKey = new byte[uPassBytes.length + 8];
+        System.arraycopy(uPassBytes, 0, tmpUKey, 0, uPassBytes.length);
+        System.arraycopy(userKeySalt, 0, tmpUKey, uPassBytes.length, 8);
+        sha256.reset();
+        sha256.update(tmpUKey);
+        byte[] intUKey = sha256.digest();
+        SecretKeySpec uSKeySpec = new SecretKeySpec(intUKey, "AES");
+        cipherNoPadding.init(Cipher.DECRYPT_MODE, uSKeySpec, ivspecZero);
+        byte[] uFileEncryptionKey = cipherNoPadding.doFinal(ue);
+        // compute encryption key from oe
+        byte[] tmpOKey = new byte[oPassBytes.length + 8 + 48];
+        System.arraycopy(oPassBytes, 0, tmpOKey, 0, oPassBytes.length);
+        System.arraycopy(ownerKeySalt, 0, tmpOKey, oPassBytes.length, 8);
+        System.arraycopy(u, 0, tmpOKey, oPassBytes.length + 8, 48);
+        sha256.reset();
+        sha256.update(tmpOKey);
+        byte[] intOKey = sha256.digest();
+        SecretKeySpec oSKeySpec = new SecretKeySpec(intOKey, "AES");
+        cipherNoPadding.init(Cipher.DECRYPT_MODE, oSKeySpec, ivspecZero);
+        byte[] oFileEncryptionKey = cipherNoPadding.doFinal(oe);
+        // check both keys are the same
+        assertArrayEquals(uFileEncryptionKey, oFileEncryptionKey);
+        byte[] fileEncryptionKey = new byte[uFileEncryptionKey.length];
+        System.arraycopy(uFileEncryptionKey, 0, fileEncryptionKey, 0, uFileEncryptionKey.length);
+        // decrypt perms
+        SecretKeySpec sKeySpec = new SecretKeySpec(fileEncryptionKey, "AES");
+        cipherNoPadding.init(Cipher.DECRYPT_MODE, sKeySpec, ivspecZero);
+        byte[] decryptedPerms = cipherNoPadding.doFinal(perms);
+        assertEquals('T', decryptedPerms[8]); // metadata encrypted by default
+        assertEquals('a', decryptedPerms[9]);
+        assertEquals('d', decryptedPerms[10]);
+        assertEquals('b', decryptedPerms[11]);
+        int expectedPermissions = -4; // default if nothing set
+        int actualPermissions = decryptedPerms[3] << 24 | (decryptedPerms[2] & 0xFF) << 16
+                | (decryptedPerms[1] & 0xFF) << 8 | (decryptedPerms[0] & 0xFF);
+        assertEquals(expectedPermissions, actualPermissions);
+        // decrypt data
+        byte[] iv = new byte[16];
+        System.arraycopy(encrypted, 0, iv, 0, 16);
+        byte[] encryptedData = new byte[encrypted.length - 16];
+        System.arraycopy(encrypted, 16, encryptedData, 0, encrypted.length - 16);
+        IvParameterSpec ivspec = new IvParameterSpec(iv);
+        Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
+        cipher.init(Cipher.DECRYPT_MODE, sKeySpec, ivspec);
+        byte[] decryptedData = cipher.doFinal(encryptedData);
+        assertArrayEquals(data, decryptedData);
+    }
+
+    private byte[] parseHexStringEntries(String dictionary, String entry) throws UnsupportedEncodingException {
+        String token = "/" + entry + " <";
+        int start = dictionary.indexOf(token) + token.length();
+        int end = dictionary.indexOf(">", start);
+        String parsedEntry = dictionary.substring(start, end);
+        int length = parsedEntry.length();
+        byte[] data = new byte[length / 2];
+        for (int i = 0; i < length; i += 2) {
+            data[i / 2] = (byte) ((Character.digit(parsedEntry.charAt(i), 16) << 4) + Character.digit(
+                    parsedEntry.charAt(i + 1), 16));
+        }
+        return data;
+    }
+
     /**
      * Creates an encryption object using a fixed file ID generator for test reproducibility.
      *
index 2d21b399c831cfe2faafdc3db8cc9e1871549f66..2d3dfb760426e39f0f76d2ae4e41d91a279c0ebc 100644 (file)
@@ -165,13 +165,21 @@ public class PDFRendererConfigParserTestCase
                                                  .getEncryptionLengthInBits());
         }
 
-        for (int i = 128; i < 1000; i += 50) {
+        for (int i = 128; i < 256; i += 10) {
             parseConfig(createRenderer()
                     .startEncryptionParams()
                         .setEncryptionLength(i)
                     .endEncryptionParams());
             assertEquals(128, conf.getConfigOptions().getEncryptionParameters().getEncryptionLengthInBits());
         }
+
+        for (int i = 256; i < 1000; i += 50) {
+            parseConfig(createRenderer()
+                    .startEncryptionParams()
+                        .setEncryptionLength(i)
+                    .endEncryptionParams());
+            assertEquals(256, conf.getConfigOptions().getEncryptionParameters().getEncryptionLengthInBits());
+        }
     }
 
     @Test