Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion openpdf-core/src/main/java/org/openpdf/text/pdf/PdfNumber.java
Original file line number Diff line number Diff line change
Expand Up @@ -135,11 +135,20 @@ public PdfNumber(float value) {

/**
* Returns the primitive <CODE>int</CODE> value of this object.
* <p>
* Some PDF producers write 32-bit values that are meant to be interpreted as a two's-complement
* signed integer (for example the {@code /P} entry of an encryption dictionary) using their
* equivalent unsigned decimal representation instead of the negative signed value, e.g.
* {@code 4294965956} instead of {@code -1340}. A plain {@code (int) value} narrowing cast on the
* underlying <CODE>double</CODE> would clamp such an out-of-range value to
* {@link Integer#MAX_VALUE} rather than wrapping it, silently corrupting values like that. Going
* through <CODE>long</CODE> first reproduces the C-style truncation to the low-order 32 bits that
* PDF producers/consumers expect.
*
* @return The value as <CODE>int</CODE>
*/
public int intValue() {
return (int) value;
return (int) (long) value;
}

/**
Expand Down
31 changes: 31 additions & 0 deletions openpdf-core/src/test/java/org/openpdf/text/pdf/PdfNumberTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package org.openpdf.text.pdf;

import static org.junit.jupiter.api.Assertions.assertEquals;

import org.junit.jupiter.api.Test;

class PdfNumberTest {

/**
* Some PDF producers write the encryption dictionary's {@code /P} (permissions) entry using its
* equivalent unsigned 32-bit decimal representation instead of the negative signed value, e.g.
* {@code 4294965956} instead of {@code -1340}. {@link PdfNumber#intValue()} must truncate such a
* value to its low-order 32 bits rather than clamping it to {@link Integer#MAX_VALUE}, otherwise
* the permission bits used to derive the decryption key no longer match the value the producer
* used, and a document with an empty user password is wrongly reported as requiring a password.
*/
@Test
void intValueTruncatesOutOfRangeUnsignedRepresentationInsteadOfClamping() {
assertEquals(-1340, new PdfNumber("4294965956").intValue());
assertEquals(-1, new PdfNumber("4294967295").intValue());
assertEquals(Integer.MIN_VALUE, new PdfNumber(String.valueOf(1L << 31)).intValue());
}

@Test
void intValueRoundTripsValuesWithinIntRange() {
assertEquals(-1340, new PdfNumber("-1340").intValue());
assertEquals(0, new PdfNumber("0").intValue());
assertEquals(Integer.MAX_VALUE, new PdfNumber(String.valueOf(Integer.MAX_VALUE)).intValue());
assertEquals(Integer.MIN_VALUE, new PdfNumber(String.valueOf(Integer.MIN_VALUE)).intValue());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package org.openpdf.text.pdf.encryption;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;

import java.io.IOException;
import java.io.InputStream;
import org.junit.jupiter.api.Test;
import org.openpdf.text.pdf.PdfReader;

/**
* Regression test for a document that has no user password but is nevertheless reported by
* {@link PdfReader} as requiring one, because its {@code /Encrypt} dictionary writes the {@code /P}
* (permissions) entry using the unsigned 32-bit decimal representation of a negative value (e.g.
* {@code 4294965956} instead of {@code -1340}) - a convention some PDF producers use.
* <p>
* {@link org.openpdf.text.pdf.PdfNumber#intValue()} used a plain {@code (int)} narrowing cast on the
* underlying <code>double</code>, which clamps such an out-of-range value to
* {@link Integer#MAX_VALUE} instead of truncating it to its low-order 32 bits. The corrupted
* permission value then fed into the standard security handler's key derivation, producing a user key
* that did not match {@code /U}, so {@link PdfReader} threw a {@link org.openpdf.text.exceptions.BadPasswordException}
* even though the correct (empty) password was supplied.
*/
class DecryptUnsignedPermissionsTest {

@Test
void opensWithEmptyPasswordDespiteUnsignedPermissionsValue() throws IOException {
try (InputStream resource = getClass()
.getResourceAsStream("/permissions/empty-user-password-unsigned-p-value.pdf")) {
try (PdfReader pdfReader = new PdfReader(resource)) {
assertTrue(pdfReader.isEncrypted(), "PdfReader fails to report test file to be encrypted.");
assertFalse(pdfReader.isOwnerPasswordUsed(), "PdfReader fails to report limited permissions.");
assertEquals(1, pdfReader.getNumberOfPages(),
"PdfReader fails to report the correct number of pages");
}
}
}
}
Binary file not shown.