8000 Implement Type-1 decryption · matplotlib/matplotlib@604c8a4 · GitHub
[go: up one dir, main page]

Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit 604c8a4

Browse files
committed
Implement Type-1 decryption
This is a prerequisite of subsetting.
1 parent 6b58ae3 commit 604c8a4

File tree

3 files changed

+73
-1
lines changed

3 files changed

+73
-1
lines changed
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
Type1Font objects now decrypt the encrypted part
2+
------------------------------------------------
3+
4+
Type 1 fonts have a large part of their code encrypted as an obsolete
5+
copy-protection measure. This part is now available decrypted as the
6+
``decrypted`` attribute of :class:`~matplotlib.type1font.Type1Font`.
7+
This decrypted data is not yet parsed, but this is a prerequisite for
8+
implementing subsetting.

lib/matplotlib/tests/test_type1font.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ def test_Type1Font():
1515
assert font.parts[2] == rawdata[0x8985:0x8ba6]
1616
assert font.parts[1:] == slanted.parts[1:]
1717
assert font.parts[1:] == condensed.parts[1:]
18+
assert font.decrypted.startswith(b'dup\n/Private 18 dict dup begin')
19+
assert font.decrypted.endswith(b'mark currentfile closefile\n')
1820

1921
differ = difflib.Differ()
2022
diff = list(differ.compare(
@@ -67,3 +69,11 @@ def test_overprecision():
6769
assert matrix == '0.001 0 0.000167 0.001 0 0'
6870
# and here we had -9.48090361795083
6971
assert angle == '-9.4809'
72+
73+
74+
def test_encrypt_decrypt_roundtrip():
75+
data = b'this is my plaintext \0\1\2\3'
76+
encrypted = t1f.Type1Font._encrypt(data, 'eexec')
77+
decrypted = t1f.Type1Font._decrypt(encrypted, 'eexec')
78+
assert encrypted != decrypted
79+
assert data == decrypted

lib/matplotlib/type1font.py

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,10 +46,12 @@ class Type1Font:
4646
parts : tuple
4747
A 3-tuple of the cleartext part, the encrypted part, and the finale of
4848
zeros.
49+
decrypted : bytes
50+
The decrypted form of parts[1].
4951
prop : dict[str, Any]
5052
A dictionary of font properties.
5153
"""
52-
__slots__ = ('parts', 'prop')
54+
__slots__ = ('parts', 'decrypted', 'prop')
5355

5456
def __init__(self, input):
5557
"""
@@ -68,6 +70,7 @@ def __init__(self, input):
6870
data = self._read(file)
6971
self.parts = self._split(data)
7072

73+
self.decrypted = self._decrypt(self.parts[1], 'eexec')
7174
self._parse()
7275

7376
def _read(self, file):
@@ -139,6 +142,57 @@ def _split(self, data):
139142
_token_re = re.compile(br'/{0,2}[^]\0\t\r\v\n ()<>{}/%[]+')
140143
_instring_re = re.compile(br'[()\\]')
141144

145+
@staticmethod
146+
def _decrypt(ciphertext, key, ndiscard=4):
147+
"""
148+
Decrypt ciphertext using the Type-1 font algorithm
149+
150+
The key argument can be an integer, or one of the strings
151+
'eexec' and 'charstring', which map to the key specified for the
152+
corresponding part of Type-1 fonts.
153+
154+
The ndiscard argument should be an integer, usually 4.
155+
That number of bytes is discarded from the beginning of plaintext.
156+
"""
157+
158+
key = {'eexec': 55665, 'charstring': 4330}.get(key, key)
159+
if not isinstance(key, int):
160+
raise ValueError(f'Invalid decryption key {key!r}')
161+
162+
plaintext = bytearray(len(ciphertext))
163+
for i, byte in enumerate(ciphertext):
164+
plaintext[i] = byte ^ (key >> 8)
165+
key = ((key+byte) * 52845 + 22719) & 0xffff
166+
167+
return bytes(plaintext[ndiscard:])
168+
169+
@staticmethod
170+
def _encrypt(plaintext, key, ndiscard=4):
171+
"""
172+
Encrypt plaintext using the Type-1 font algorithm
173+
174+
The key argument can be an integer, or one of the strings
175+
'eexec' and 'charstring', which map to the key specified for the
176+
corresponding part of Type-1 fonts.
177+
178+
The ndiscard argument should be an integer, usually 4. That
179+
number of bytes is prepended to the plaintext before encryption.
180+
This function prepends NUL bytes for reproducibility, even though
181+
the original algorithm uses random bytes, presumably to avoid
182+
cryptanalysis.
183+
"""
184+
185+
key = {'eexec': 55665, 'charstring': 4330}.get(key, key)
186+
if not isinstance(key, int):
187+
raise ValueError(f'Invalid encryption key {key!r}')
188+
189+
ciphertext = bytearray(len(plaintext) + ndiscard)
190+
for i, byte in enumerate(b'\0' * ndiscard + plaintext):
191+
ciphertext[i] = byte ^ (key >> 8)
192+
key = ((key + ciphertext[i]) * 52845 + 22719) & 0xffff
193+
194+
return bytes(ciphertext)
195+
142196
@classmethod
143197
def _tokens(cls, text):
144198
"""

0 commit comments

Comments
 (0)
0