Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/pypdf/_crypt_providers/_cryptography.py: 2%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

113 statements  

1# Copyright (c) 2023, exiledkingcc 

2# All rights reserved. 

3# 

4# Redistribution and use in source and binary forms, with or without 

5# modification, are permitted provided that the following conditions are 

6# met: 

7# 

8# * Redistributions of source code must retain the above copyright notice, 

9# this list of conditions and the following disclaimer. 

10# * Redistributions in binary form must reproduce the above copyright notice, 

11# this list of conditions and the following disclaimer in the documentation 

12# and/or other materials provided with the distribution. 

13# * The name of the author may not be used to endorse or promote products 

14# derived from this software without specific prior written permission. 

15# 

16# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 

17# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 

18# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 

19# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE 

20# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 

21# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 

22# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 

23# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 

24# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 

25# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 

26# POSSIBILITY OF SUCH DAMAGE. 

27 

28import secrets 

29 

30from cryptography import __version__ 

31from cryptography.exceptions import UnsupportedAlgorithm 

32from cryptography.hazmat.primitives.ciphers.algorithms import AES 

33from cryptography.hazmat.primitives.ciphers.base import Cipher 

34from cryptography.hazmat.primitives.ciphers.modes import CBC, ECB 

35from cryptography.hazmat.primitives.padding import PKCS7 

36 

37from pypdf._crypt_providers._base import CryptBase 

38from pypdf._utils import logger_warning 

39from pypdf.errors import PdfStreamError 

40 

41try: 

42 # 43.0.0: https://cryptography.io/en/latest/changelog/#v43-0-0 

43 from cryptography.hazmat.decrepit.ciphers.algorithms import ARC4 

44except ImportError: 

45 from cryptography.hazmat.primitives.ciphers.algorithms import ARC4 

46 

47crypt_provider = ("cryptography", __version__) 

48 

49 

50class CryptRC4(CryptBase): 

51 # Assume OpenSSL provides RC4; flipped to False the first time it rejects 

52 # the cipher (e.g. legacy provider disabled), so the failing attempt is 

53 # paid once. 

54 _is_rc4_supported = True 

55 

56 def __init__(self, key: bytes) -> None: 

57 self._key = key 

58 self._fallback = None 

59 if CryptRC4._is_rc4_supported: 

60 self.cipher = Cipher(ARC4(key), mode=None) 

61 else: 

62 self._fallback = self._pure_python_rc4(key) 

63 

64 @staticmethod 

65 def _pure_python_rc4(key: bytes) -> CryptBase: 

66 from pypdf._crypt_providers import _fallback # noqa: PLC0415 

67 

68 return _fallback.CryptRC4(key) 

69 

70 @classmethod 

71 def _disable_rc4(cls, key: bytes) -> CryptBase: 

72 if cls._is_rc4_supported: 

73 logger_warning( 

74 ( 

75 "RC4 is not supported by the current OpenSSL build; " 

76 "falling back to the pure-Python RC4 implementation." 

77 ), 

78 source=__name__, 

79 ) 

80 cls._is_rc4_supported = False 

81 return cls._pure_python_rc4(key) 

82 

83 def encrypt(self, data: bytes) -> bytes: 

84 if self._fallback is not None: 

85 return self._fallback.encrypt(data) 

86 try: 

87 encryptor = self.cipher.encryptor() 

88 return encryptor.update(data) + encryptor.finalize() 

89 except UnsupportedAlgorithm: 

90 self._fallback = self._disable_rc4(self._key) 

91 return self._fallback.encrypt(data) 

92 

93 def decrypt(self, data: bytes, *, strict: bool = True) -> bytes: 

94 if self._fallback is not None: 

95 return self._fallback.decrypt(data, strict=strict) 

96 try: 

97 decryptor = self.cipher.decryptor() 

98 return decryptor.update(data) + decryptor.finalize() 

99 except UnsupportedAlgorithm: 

100 self._fallback = self._disable_rc4(self._key) 

101 return self._fallback.decrypt(data, strict=strict) 

102 

103 

104class CryptAES(CryptBase): 

105 def __init__(self, key: bytes) -> None: 

106 self.alg = AES(key) 

107 

108 def encrypt(self, data: bytes) -> bytes: 

109 iv = secrets.token_bytes(16) 

110 padder = PKCS7(128).padder() 

111 padded_data = padder.update(data) + padder.finalize() 

112 

113 cipher = Cipher(self.alg, CBC(iv)) 

114 encryptor = cipher.encryptor() 

115 return iv + encryptor.update(padded_data) + encryptor.finalize() 

116 

117 def decrypt(self, data: bytes, *, strict: bool = True) -> bytes: 

118 iv = data[:16] 

119 data = data[16:] 

120 # for empty encrypted data 

121 if not data: 

122 return data 

123 

124 if not strict and len(data) % 16 != 0: 

125 logger_warning("Adding missing padding.", source=__name__) 

126 padder = PKCS7(128).padder() 

127 data = padder.update(data) + padder.finalize() 

128 

129 cipher = Cipher(self.alg, CBC(iv)) 

130 decryptor = cipher.decryptor() 

131 try: 

132 padded_data = decryptor.update(data) + decryptor.finalize() 

133 except ValueError as exception: 

134 # Only raised in strict mode. Non-strict mode fixes padding. 

135 raise PdfStreamError(exception) 

136 

137 unpadder = PKCS7(128).unpadder() 

138 try: 

139 return unpadder.update(padded_data) + unpadder.finalize() 

140 except ValueError as exception: 

141 if strict: 

142 raise PdfStreamError(exception) 

143 logger_warning("Ignoring padding error: %(exception)s", source=__name__, exception=exception) 

144 return padded_data[: -padded_data[-1]] 

145 

146 

147def rc4_encrypt(key: bytes, data: bytes) -> bytes: 

148 if CryptRC4._is_rc4_supported: 

149 try: 

150 encryptor = Cipher(ARC4(key), mode=None).encryptor() 

151 return encryptor.update(data) + encryptor.finalize() 

152 except UnsupportedAlgorithm: 

153 return CryptRC4._disable_rc4(key).encrypt(data) 

154 return CryptRC4._pure_python_rc4(key).encrypt(data) 

155 

156 

157def rc4_decrypt(key: bytes, data: bytes) -> bytes: 

158 if CryptRC4._is_rc4_supported: 

159 try: 

160 decryptor = Cipher(ARC4(key), mode=None).decryptor() 

161 return decryptor.update(data) + decryptor.finalize() 

162 except UnsupportedAlgorithm: 

163 return CryptRC4._disable_rc4(key).decrypt(data) 

164 return CryptRC4._pure_python_rc4(key).decrypt(data) 

165 

166 

167def aes_ecb_encrypt(key: bytes, data: bytes) -> bytes: 

168 encryptor = Cipher(AES(key), mode=ECB()).encryptor() 

169 return encryptor.update(data) + encryptor.finalize() 

170 

171 

172def aes_ecb_decrypt(key: bytes, data: bytes) -> bytes: 

173 decryptor = Cipher(AES(key), mode=ECB()).decryptor() 

174 return decryptor.update(data) + decryptor.finalize() 

175 

176 

177def aes_cbc_encrypt(key: bytes, iv: bytes, data: bytes) -> bytes: 

178 encryptor = Cipher(AES(key), mode=CBC(iv)).encryptor() 

179 return encryptor.update(data) + encryptor.finalize() 

180 

181 

182def aes_cbc_decrypt(key: bytes, iv: bytes, data: bytes) -> bytes: 

183 decryptor = Cipher(AES(key), mode=CBC(iv)).decryptor() 

184 return decryptor.update(data) + decryptor.finalize()