Skip to content

BadPasswordException for RC4 40-bit encrypted PDFs with /P stored as unsigned int32 #1585

Description

@colinagel

PDFs encrypted with RC4 40-bit (R=2, V=1) — generated by mPDF 7.1.9 — throw BadPasswordException: Bad user password even though no password is set and the file opens normally in every other PDF reader (Acrobat, qpdf, PDFBox, Evince).

mPDF writes the /P permissions field as an unsigned 32-bit integer (e.g. 4294963412) instead of the signed value required by the PDF spec (e.g. -3884). The bit pattern is identical — it is purely an interpretation issue.

PdfNumber stores all numbers internally as double. intValue() does:

public int intValue() {
return (int) value; // value = 4294963412.0
}

Per JLS §5.1.3, casting a double to int does not use modular arithmetic. When the value exceeds Integer.MAX_VALUE, the result is clamped to 2147483647 instead of the correct -3884. The wrong pValue is fed into the MD5 that derives the RC4 key, which no longer matches the /U entry in the PDF.

The fix is a one-liner in PdfNumber.intValue():

// before
return (int) value;

// after — goes through long to preserve two's complement semantics
return (int)(long) value;

To Reproduce
// PDF generated by mPDF 7.1.9 with RC4 encryption (PDF 1.4 default)
// Encryption dict contains: /P 4294963412 (should be /P -3884)
new PdfReader("mpdf-rc4-encrypted.pdf", new byte[0]); // throws BadPasswordException

Replacing /P 4294963412 with /P -3884 (same byte length, padded with spaces) in the raw file bytes makes PdfReader open the document successfully, confirming the bug is isolated to the intValue() conversion.

Expected behavior:
PdfReader should open the document without a password, as the user password is empty and all other PDF readers handle it correctly.

System

  • OS: Linux
  • OpenPDF version: 3.0.3

Additional context:
The /P field in the PDF encryption dictionary is defined as a signed 32-bit integer by the PDF spec (ISO 32000). Some producers (including mPDF) write it as an unsigned value. Other readers handle this by converting through long before casting to int: (int)(long) value.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions