aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--findbugs-exclude.xml4
-rw-r--r--src/java/org/apache/fop/pdf/PDFCIDFont.java59
-rw-r--r--src/java/org/apache/fop/pdf/PDFCIDSystemInfo.java22
-rw-r--r--src/java/org/apache/fop/pdf/PDFDocument.java15
-rw-r--r--src/java/org/apache/fop/pdf/PDFEncryption.java6
-rw-r--r--src/java/org/apache/fop/pdf/PDFEncryptionJCE.java430
-rw-r--r--src/java/org/apache/fop/pdf/PDFEncryptionParams.java27
-rw-r--r--src/java/org/apache/fop/pdf/PDFFactory.java1
-rw-r--r--src/java/org/apache/fop/render/pdf/PDFEncryptionOption.java10
-rw-r--r--src/java/org/apache/fop/render/pdf/PDFRendererConfig.java19
-rw-r--r--test/java/org/apache/fop/pdf/PDFEncryptionJCETestCase.java178
-rw-r--r--test/java/org/apache/fop/render/pdf/PDFRendererConfigParserTestCase.java10
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
@@ -2723,6 +2723,10 @@
<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-->
<Bug pattern="SIC_INNER_SHOULD_BE_STATIC"/>
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 <</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;
+ }
+
}
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();
}
/**
@@ -151,6 +155,14 @@ public class PDFEncryptionParams {
}
/**
+ * 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
*/
@@ -231,6 +243,14 @@ public class PDFEncryptionParams {
}
/**
+ * 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
* if no password is applicable
@@ -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