Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

BUG: Fail on wrong state of reserved permission bits during encryption #2421

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 2 additions & 0 deletions pypdf/_writer.py
Expand Up @@ -1144,6 +1144,8 @@ def encrypt(
"AES-128", "AES-256-R5", "AES-256". If it is valid,
`use_128bit` will be ignored.
"""
permissions_flag.check()

if owner_password is None:
owner_password = user_password

Expand Down
18 changes: 18 additions & 0 deletions pypdf/constants.py
Expand Up @@ -126,8 +126,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 Resources:
"""
Expand Down
28 changes: 28 additions & 0 deletions tests/test_constants.py
Expand Up @@ -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()
4 changes: 2 additions & 2 deletions tests/test_reader.py
Expand Up @@ -764,12 +764,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")
Expand Down
37 changes: 37 additions & 0 deletions tests/test_writer.py
Expand Up @@ -20,6 +20,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,
Expand Down Expand Up @@ -576,6 +577,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()
Expand Down