Skip to content

Commit

Permalink
Implement Type-1 decryption
Browse files Browse the repository at this point in the history
This is a prerequisite of subsetting.
  • Loading branch information
jkseppan committed Jul 12, 2021
1 parent 6b58ae3 commit dec2657
Show file tree
Hide file tree
Showing 3 changed files with 73 additions and 1 deletion.
8 changes: 8 additions & 0 deletions doc/users/next_whats_new/type1decrypt.rst
@@ -0,0 +1,8 @@
Type1Font objects now decrypt the encrypted part
------------------------------------------------

Type 1 fonts have a large part of their code encrypted as an obsolete
copy-protection measure. This part is now available decrypted as the
``decrypted`` attribute of :class:`~type1font.Type1Font`. This decrypted
data is not yet parsed, but this is a prerequisite for implementing
subsetting.
10 changes: 10 additions & 0 deletions lib/matplotlib/tests/test_type1font.py
Expand Up @@ -15,6 +15,8 @@ def test_Type1Font():
assert font.parts[2] == rawdata[0x8985:0x8ba6]
assert font.parts[1:] == slanted.parts[1:]
assert font.parts[1:] == condensed.parts[1:]
assert font.decrypted.startswith(b'dup\n/Private 18 dict dup begin')
assert font.decrypted.endswith(b'mark currentfile closefile\n')

differ = difflib.Differ()
diff = list(differ.compare(
Expand Down Expand Up @@ -67,3 +69,11 @@ def test_overprecision():
assert matrix == '0.001 0 0.000167 0.001 0 0'
# and here we had -9.48090361795083
assert angle == '-9.4809'


def test_encrypt_decrypt_roundtrip():
data = b'this is my plaintext \0\1\2\3'
encrypted = t1f.Type1Font._encrypt(data, 'eexec')
decrypted = t1f.Type1Font._decrypt(encrypted, 'eexec')
assert encrypted != decrypted
assert data == decrypted
56 changes: 55 additions & 1 deletion lib/matplotlib/type1font.py
Expand Up @@ -46,10 +46,12 @@ class Type1Font:
parts : tuple
A 3-tuple of the cleartext part, the encrypted part, and the finale of
zeros.
decrypted : bytes
The decrypted form of parts[1].
prop : dict[str, Any]
A dictionary of font properties.
"""
__slots__ = ('parts', 'prop')
__slots__ = ('parts', 'decrypted', 'prop')

def __init__(self, input):
"""
Expand All @@ -68,6 +70,7 @@ def __init__(self, input):
data = self._read(file)
self.parts = self._split(data)

self.decrypted = self._decrypt(self.parts[1], 'eexec')
self._parse()

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

@staticmethod
def _decrypt(ciphertext, key, ndiscard=4):
"""
Decrypt ciphertext using the Type-1 font algorithm
The key argument can be an integer, or one of the strings
'eexec' and 'charstring', which map to the key specified for the
corresponding part of Type-1 fonts.
The ndiscard argument should be an integer, usually 4.
That number of bytes is discarded from the beginning of plaintext.
"""

key = {'eexec': 55665, 'charstring': 4330}.get(key, key)
if not isinstance(key, int):
raise ValueError(f'Invalid decryption key {key!r}')

plaintext = bytearray(len(ciphertext))
for i, byte in enumerate(ciphertext):
plaintext[i] = byte ^ (key >> 8)
key = ((key+byte) * 52845 + 22719) & 0xffff

return bytes(plaintext[ndiscard:])

@staticmethod
def _encrypt(plaintext, key, ndiscard=4):
"""
Encrypt plaintext using the Type-1 font algorithm
The key argument can be an integer, or one of the strings
'eexec' and 'charstring', which map to the key specified for the
corresponding part of Type-1 fonts.
The ndiscard argument should be an integer, usually 4. That
number of bytes is prepended to the plaintext before encryption.
This function prepends NUL bytes for reproducibility, even though
the original algorithm uses random bytes, presumably to avoid
cryptanalysis.
"""

key = {'eexec': 55665, 'charstring': 4330}.get(key, key)
if not isinstance(key, int):
raise ValueError(f'Invalid encryption key {key!r}')

ciphertext = bytearray(len(plaintext) + ndiscard)
for i, byte in enumerate(b'\0' * ndiscard + plaintext):
ciphertext[i] = byte ^ (key >> 8)
key = ((key + ciphertext[i]) * 52845 + 22719) & 0xffff

return bytes(ciphertext)

@classmethod
def _tokens(cls, text):
"""
Expand Down

0 comments on commit dec2657

Please sign in to comment.