1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206
|
from pathlib import Path
import pytest
import pypdf
from pypdf import PasswordType, PdfReader
from pypdf._encryption import AlgV5, CryptRC4
from pypdf.errors import DependencyError, PdfReadError
try:
from Cryptodome.Cipher import AES # noqa: F401
HAS_PYCRYPTODOME = True
except ImportError:
HAS_PYCRYPTODOME = False
TESTS_ROOT = Path(__file__).parent.resolve()
PROJECT_ROOT = TESTS_ROOT.parent
RESOURCE_ROOT = PROJECT_ROOT / "resources"
@pytest.mark.parametrize(
("name", "requires_pycryptodome"),
[
# unencrypted pdf
("unencrypted.pdf", False),
# created by:
# qpdf --encrypt "" "" 40 -- unencrypted.pdf r2-empty-password.pdf
("r2-empty-password.pdf", False),
# created by:
# qpdf --encrypt "" "" 128 -- unencrypted.pdf r3-empty-password.pdf
("r3-empty-password.pdf", False),
# created by:
# qpdf --encrypt "asdfzxcv" "" 40 -- unencrypted.pdf r2-user-password.pdf
("r2-user-password.pdf", False),
# created by:
# qpdf --encrypt "" "asdfzxcv" 40 -- unencrypted.pdf r2-user-password.pdf
("r2-owner-password.pdf", False),
# created by:
# qpdf --encrypt "asdfzxcv" "" 128 -- unencrypted.pdf r3-user-password.pdf
("r3-user-password.pdf", False),
# created by:
# qpdf --encrypt "asdfzxcv" "" 128 --force-V4 -- unencrypted.pdf r4-user-password.pdf
("r4-user-password.pdf", False),
# created by:
# qpdf --encrypt "" "asdfzxcv" 128 --force-V4 -- unencrypted.pdf r4-owner-password.pdf
("r4-owner-password.pdf", False),
# created by:
# qpdf --encrypt "asdfzxcv" "" 128 --use-aes=y -- unencrypted.pdf r4-aes-user-password.pdf
("r4-aes-user-password.pdf", True),
# created by:
# qpdf --encrypt "" "" 256 --force-R5 -- unencrypted.pdf r5-empty-password.pdf
("r5-empty-password.pdf", True),
# created by:
# qpdf --encrypt "asdfzxcv" "" 256 --force-R5 -- unencrypted.pdf r5-user-password.pdf
("r5-user-password.pdf", True),
# created by:
# qpdf --encrypt "" "asdfzxcv" 256 --force-R5 -- unencrypted.pdf r5-owner-password.pdf
("r5-owner-password.pdf", True),
# created by:
# qpdf --encrypt "" "" 256 -- unencrypted.pdf r6-empty-password.pdf
("r6-empty-password.pdf", True),
# created by:
# qpdf --encrypt "asdfzxcv" "" 256 -- unencrypted.pdf r6-user-password.pdf
("r6-user-password.pdf", True),
# created by:
# qpdf --encrypt "" "asdfzxcv" 256 -- unencrypted.pdf r6-owner-password.pdf
("r6-owner-password.pdf", True),
],
)
def test_encryption(name, requires_pycryptodome):
inputfile = RESOURCE_ROOT / "encryption" / name
if requires_pycryptodome and not HAS_PYCRYPTODOME:
with pytest.raises(DependencyError) as exc:
ipdf = pypdf.PdfReader(inputfile)
ipdf.decrypt("asdfzxcv")
dd = dict(ipdf.metadata)
assert exc.value.args[0] == "PyCryptodome is required for AES algorithm"
return
else:
ipdf = pypdf.PdfReader(inputfile)
if str(inputfile).endswith("unencrypted.pdf"):
assert not ipdf.is_encrypted
else:
assert ipdf.is_encrypted
ipdf.decrypt("asdfzxcv")
assert len(ipdf.pages) == 1
dd = dict(ipdf.metadata)
# remove empty value entry
dd = {x[0]: x[1] for x in dd.items() if x[1]}
assert dd == {
"/Author": "cheng",
"/CreationDate": "D:20220414132421+05'24'",
"/Creator": "WPS Writer",
"/ModDate": "D:20220414132421+05'24'",
"/SourceModified": "D:20220414132421+05'24'",
"/Trapped": "/False",
}
@pytest.mark.parametrize(
("name", "user_passwd", "owner_passwd"),
[
# created by
# qpdf --encrypt "foo" "bar" 256 -- unencrypted.pdf r6-both-passwords.pdf
("r6-both-passwords.pdf", "foo", "bar"),
],
)
@pytest.mark.skipif(not HAS_PYCRYPTODOME, reason="No pycryptodome")
def test_both_password(name, user_passwd, owner_passwd):
inputfile = RESOURCE_ROOT / "encryption" / name
ipdf = pypdf.PdfReader(inputfile)
assert ipdf.is_encrypted
assert ipdf.decrypt(user_passwd) == PasswordType.USER_PASSWORD
assert ipdf.decrypt(owner_passwd) == PasswordType.OWNER_PASSWORD
assert len(ipdf.pages) == 1
@pytest.mark.parametrize(
("pdffile", "password"),
[
("crazyones-encrypted-256.pdf", "password"),
("crazyones-encrypted-256.pdf", b"password"),
],
)
@pytest.mark.skipif(not HAS_PYCRYPTODOME, reason="No pycryptodome")
def test_get_page_of_encrypted_file_new_algorithm(pdffile, password):
"""
Check if we can read a page of an encrypted file.
This is a regression test for issue 327:
IndexError for get_page() of decrypted file
"""
path = RESOURCE_ROOT / pdffile
pypdf.PdfReader(path, password=password).pages[0]
@pytest.mark.parametrize(
"names",
[
(
[
"unencrypted.pdf",
"r3-user-password.pdf",
"r4-aes-user-password.pdf",
"r5-user-password.pdf",
]
),
],
)
@pytest.mark.skipif(not HAS_PYCRYPTODOME, reason="No pycryptodome")
def test_encryption_merge(names):
merger = pypdf.PdfMerger()
files = [RESOURCE_ROOT / "encryption" / x for x in names]
pdfs = [pypdf.PdfReader(x) for x in files]
for pdf in pdfs:
if pdf.is_encrypted:
pdf.decrypt("asdfzxcv")
merger.append(pdf)
# no need to write to file
merger.close()
@pytest.mark.parametrize(
"cryptcls",
[
CryptRC4,
],
)
def test_encrypt_decrypt_class(cryptcls):
message = b"Hello World"
key = bytes(0 for _ in range(128)) # b"secret key"
crypt = cryptcls(key)
assert crypt.decrypt(crypt.encrypt(message)) == message
def test_decrypt_not_decrypted_pdf():
path = RESOURCE_ROOT / "crazyones.pdf"
with pytest.raises(PdfReadError) as exc:
PdfReader(path, password="nonexistant")
assert exc.value.args[0] == "Not encrypted file"
def test_generate_values():
"""
This test only checks if there is an exception.
It does not verify that the content is correct.
"""
if not HAS_PYCRYPTODOME:
return
key = b"0123456789123451"
values = AlgV5.generate_values(
user_password=b"foo",
owner_password=b"bar",
key=key,
p=0,
metadata_encrypted=True,
)
assert values == {
"/U": values["/U"],
"/UE": values["/UE"],
"/O": values["/O"],
"/OE": values["/OE"],
"/Perms": values["/Perms"],
}
|