From ba34a5614dd344fc93a416239635e43a6c80a568 Mon Sep 17 00:00:00 2001 From: stefan6419846 <96178532+stefan6419846@users.noreply.github.com> Date: Mon, 22 Jan 2024 17:21:07 +0100 Subject: [PATCH] BUG: Fail on wrong state of reserved permission bits during encryption --- pypdf/_writer.py | 2 ++ pypdf/constants.py | 18 ++++++++++++++++++ tests/test_constants.py | 28 ++++++++++++++++++++++++++++ tests/test_reader.py | 4 ++-- tests/test_writer.py | 37 +++++++++++++++++++++++++++++++++++++ 5 files changed, 87 insertions(+), 2 deletions(-) diff --git a/pypdf/_writer.py b/pypdf/_writer.py index fb3049ef0..fe8a7f2c4 100644 --- a/pypdf/_writer.py +++ b/pypdf/_writer.py @@ -1092,6 +1092,8 @@ def encrypt( "AES-128", "AES-256-R5", "AES-256". If it's valid, `use_128bit` will be ignored. """ + permissions_flag.check() + if owner_password is None: owner_password = user_password diff --git a/pypdf/constants.py b/pypdf/constants.py index 5f06a0b2f..19ed2a2b7 100644 --- a/pypdf/constants.py +++ b/pypdf/constants.py @@ -124,8 +124,26 @@ def from_dict(cls, value: Dict[str, bool]) -> "UserAccessPermissions": @classmethod def all(cls) -> "UserAccessPermissions": + """Get full permissions value.""" return cls((2**32 - 1) - cls.R1 - cls.R2) + @classmethod + def minimal(cls) -> "UserAccessPermissions": + """Get the minimal permissions value.""" + result = cls(0) + for name, flag in cls.__members__.items(): + if cls._is_reserved(name) and cls._is_active(name): + result |= flag + return result + + def check(self) -> None: + """Check if the flags are valid.""" + for name, flag in UserAccessPermissions.__members__.items(): + if self._is_reserved(name): + expected = flag if self._is_active(name) else 0 + if self & flag != expected: + raise ValueError(f"Invalid value for reserved bit {name}.") + class Ressources: """TABLE 3.30 Entries in a resource dictionary.""" diff --git a/tests/test_constants.py b/tests/test_constants.py index d53ebed33..6410805aa 100644 --- a/tests/test_constants.py +++ b/tests/test_constants.py @@ -114,3 +114,31 @@ def test_user_access_permissions__all(): assert all_int & UserAccessPermissions.PRINT == UserAccessPermissions.PRINT assert all_int & UserAccessPermissions.R7 == UserAccessPermissions.R7 assert all_int & UserAccessPermissions.R31 == UserAccessPermissions.R31 + + +def test_user_access_permissions__minimal(): + minimal_permissions = UserAccessPermissions.minimal() + + assert int(minimal_permissions) == 4294963392 # All reserved bits which should be one. + + assert minimal_permissions & UserAccessPermissions.R1 == 0 + assert minimal_permissions & UserAccessPermissions.R2 == 0 + assert minimal_permissions & UserAccessPermissions.PRINT == 0 + assert minimal_permissions & UserAccessPermissions.R7 == UserAccessPermissions.R7 + assert minimal_permissions & UserAccessPermissions.R31 == UserAccessPermissions.R31 + + +def test_user_access_permissions__check(): + all_permissions = UserAccessPermissions.all() + all_permissions.check() + + r1_set = all_permissions | UserAccessPermissions.R1 + with pytest.raises(ValueError, match="Invalid value for reserved bit R1."): + r1_set.check() + r2_set = all_permissions | UserAccessPermissions.R2 + with pytest.raises(ValueError, match="Invalid value for reserved bit R2."): + r2_set.check() + + r8_unset = UserAccessPermissions(all_permissions - UserAccessPermissions.R8) + with pytest.raises(ValueError, match="Invalid value for reserved bit R8."): + r8_unset.check() diff --git a/tests/test_reader.py b/tests/test_reader.py index 98f73a01c..7c779d17b 100644 --- a/tests/test_reader.py +++ b/tests/test_reader.py @@ -761,12 +761,12 @@ def test_user_access_permissions(): writer.encrypt( user_password="", owner_password="abc", - permissions_flag=UAP.PRINT | UAP.FILL_FORM_FIELDS, + permissions_flag=UAP.minimal() | UAP.PRINT | UAP.FILL_FORM_FIELDS, ) output = BytesIO() writer.write(output) reader = PdfReader(output) - assert reader.user_access_permissions == (UAP.PRINT | UAP.FILL_FORM_FIELDS) + assert reader.user_access_permissions == (UAP.minimal() | UAP.PRINT | UAP.FILL_FORM_FIELDS) # All writer permissions. writer = PdfWriter(clone_from=RESOURCE_ROOT / "crazyones.pdf") diff --git a/tests/test_writer.py b/tests/test_writer.py index 494fa08cd..6fe0be37a 100644 --- a/tests/test_writer.py +++ b/tests/test_writer.py @@ -19,6 +19,7 @@ Transformation, ) from pypdf.annotations import Link +from pypdf.constants import UserAccessPermissions as UAP from pypdf.errors import PageSizeNotDefinedError, PyPdfError from pypdf.generic import ( ArrayObject, @@ -575,6 +576,42 @@ def test_encrypt(use_128bit, user_password, owner_password, pdf_file_path): assert new_text == orig_text +def test_user_access_permissions(): + # All writer permissions. + writer = PdfWriter(clone_from=RESOURCE_ROOT / "crazyones.pdf") + writer.encrypt( + user_password="", + owner_password="abc", + permissions_flag=UAP.all(), + ) + output = BytesIO() + writer.write(output) + reader = PdfReader(output) + assert reader.user_access_permissions == UAP.all() + + # Minimal permissions. + writer = PdfWriter(clone_from=RESOURCE_ROOT / "crazyones.pdf") + writer.encrypt( + user_password="", + owner_password="abc", + permissions_flag=UAP.minimal(), + ) + output = BytesIO() + writer.write(output) + reader = PdfReader(output) + assert reader.user_access_permissions == UAP.minimal() + + # Wrong permissions. + writer = PdfWriter(clone_from=RESOURCE_ROOT / "crazyones.pdf") + with pytest.raises(ValueError, match="Invalid value for reserved bit R2."): + writer.encrypt( + user_password="", + owner_password="abc", + permissions_flag=UAP.minimal() | UAP.R2, + ) + + + def test_add_outline_item(pdf_file_path): reader = PdfReader(RESOURCE_ROOT / "pdflatex-outline.pdf") writer = PdfWriter()