From 016cb3199865f798f401c452554eea6e74055950 Mon Sep 17 00:00:00 2001 From: Luis Bernardo Date: Sat, 18 May 2013 22:25:52 +0000 Subject: [PATCH] FOP-2248: add support for AES 256 PDF encryption git-svn-id: https://svn.apache.org/repos/asf/xmlgraphics/fop/trunk@1484190 13f79535-47bb-0310-9956-ffa450edef68 --- findbugs-exclude.xml | 4 + src/java/org/apache/fop/pdf/PDFCIDFont.java | 59 +++ .../org/apache/fop/pdf/PDFCIDSystemInfo.java | 22 + src/java/org/apache/fop/pdf/PDFDocument.java | 15 +- .../org/apache/fop/pdf/PDFEncryption.java | 6 + .../org/apache/fop/pdf/PDFEncryptionJCE.java | 430 +++++++++++++++--- .../apache/fop/pdf/PDFEncryptionParams.java | 27 +- src/java/org/apache/fop/pdf/PDFFactory.java | 1 + .../fop/render/pdf/PDFEncryptionOption.java | 10 +- .../fop/render/pdf/PDFRendererConfig.java | 19 +- .../fop/pdf/PDFEncryptionJCETestCase.java | 178 +++++++- .../pdf/PDFRendererConfigParserTestCase.java | 10 +- 12 files changed, 680 insertions(+), 101 deletions(-) diff --git a/findbugs-exclude.xml b/findbugs-exclude.xml index 3d62cd81a..31edec268 100644 --- a/findbugs-exclude.xml +++ b/findbugs-exclude.xml @@ -2722,6 +2722,10 @@ + + + + diff --git a/src/java/org/apache/fop/pdf/PDFCIDFont.java b/src/java/org/apache/fop/pdf/PDFCIDFont.java index e24071846..907cab0df 100644 --- a/src/java/org/apache/fop/pdf/PDFCIDFont.java +++ b/src/java/org/apache/fop/pdf/PDFCIDFont.java @@ -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(); + } + } diff --git a/src/java/org/apache/fop/pdf/PDFCIDSystemInfo.java b/src/java/org/apache/fop/pdf/PDFCIDSystemInfo.java index 0228addb6..7a96930aa 100644 --- a/src/java/org/apache/fop/pdf/PDFCIDSystemInfo.java +++ b/src/java/org/apache/fop/pdf/PDFCIDSystemInfo.java @@ -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(); + } + } diff --git a/src/java/org/apache/fop/pdf/PDFDocument.java b/src/java/org/apache/fop/pdf/PDFDocument.java index 0b412842f..ff9f61201 100644 --- a/src/java/org/apache/fop/pdf/PDFDocument.java +++ b/src/java/org/apache/fop/pdf/PDFDocument.java @@ -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."); + } } } diff --git a/src/java/org/apache/fop/pdf/PDFEncryption.java b/src/java/org/apache/fop/pdf/PDFEncryption.java index fcb56e50b..87b9d2522 100644 --- a/src/java/org/apache/fop/pdf/PDFEncryption.java +++ b/src/java/org/apache/fop/pdf/PDFEncryption.java @@ -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(); } diff --git a/src/java/org/apache/fop/pdf/PDFEncryptionJCE.java b/src/java/org/apache/fop/pdf/PDFEncryptionJCE.java index 129bab183..2fe9f29e0 100644 --- a/src/java/org/apache/fop/pdf/PDFEncryptionJCE.java +++ b/src/java/org/apache/fop/pdf/PDFEncryptionJCE.java @@ -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 <>>>\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; + } + } diff --git a/src/java/org/apache/fop/pdf/PDFEncryptionParams.java b/src/java/org/apache/fop/pdf/PDFEncryptionParams.java index 563c05233..a1925b0f4 100644 --- a/src/java/org/apache/fop/pdf/PDFEncryptionParams.java +++ b/src/java/org/apache/fop/pdf/PDFEncryptionParams.java @@ -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; } } diff --git a/src/java/org/apache/fop/pdf/PDFFactory.java b/src/java/org/apache/fop/pdf/PDFFactory.java index 0f8d360c8..1756f1d56 100644 --- a/src/java/org/apache/fop/pdf/PDFFactory.java +++ b/src/java/org/apache/fop/pdf/PDFFactory.java @@ -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); diff --git a/src/java/org/apache/fop/render/pdf/PDFEncryptionOption.java b/src/java/org/apache/fop/render/pdf/PDFEncryptionOption.java index f3e51e34a..c14be719a 100644 --- a/src/java/org/apache/fop/render/pdf/PDFEncryptionOption.java +++ b/src/java/org/apache/fop/render/pdf/PDFEncryptionOption.java @@ -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"; diff --git a/src/java/org/apache/fop/render/pdf/PDFRendererConfig.java b/src/java/org/apache/fop/render/pdf/PDFRendererConfig.java index 0de7443bf..5e87deb6b 100644 --- a/src/java/org/apache/fop/render/pdf/PDFRendererConfig.java +++ b/src/java/org/apache/fop/render/pdf/PDFRendererConfig.java @@ -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; diff --git a/test/java/org/apache/fop/pdf/PDFEncryptionJCETestCase.java b/test/java/org/apache/fop/pdf/PDFEncryptionJCETestCase.java index db10e656e..ea3b011c7 100644 --- a/test/java/org/apache/fop/pdf/PDFEncryptionJCETestCase.java +++ b/test/java/org/apache/fop/pdf/PDFEncryptionJCETestCase.java @@ -19,16 +19,28 @@ 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. * diff --git a/test/java/org/apache/fop/render/pdf/PDFRendererConfigParserTestCase.java b/test/java/org/apache/fop/render/pdf/PDFRendererConfigParserTestCase.java index 2d21b399c..2d3dfb760 100644 --- a/test/java/org/apache/fop/render/pdf/PDFRendererConfigParserTestCase.java +++ b/test/java/org/apache/fop/render/pdf/PDFRendererConfigParserTestCase.java @@ -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 -- 2.39.5