Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 41bb30c

Browse files
committed
Implement Type-1 decryption
This is a prerequisite of subsetting.
1 parent 14b34fd commit 41bb30c

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)