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

Skip to content

Commit b81313f

Browse files
committed
Implement Type-1 decryption
This is a prerequisite of subsetting.
1 parent 73b7abf commit b81313f

File tree

3 files changed

+75
-1
lines changed

3 files changed

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

0 commit comments

Comments
 (0)