Coverage for confy_addons/encryption/rsa.py: 100%

115 statements  

« prev     ^ index     » next       coverage.py v7.10.7, created at 2026-02-20 18:19 +0000

1"""RSA encryption handler with automatic key pair generation. 

2 

3This module provides classes for RSA encryption and decryption using 

4the `cryptography` library. It includes functionality for generating RSA 

5key pairs, serializing public keys, and performing encryption and decryption 

6operations using RSA with OAEP padding. 

7""" 

8 

9import base64 

10import binascii 

11import logging 

12 

13from cryptography.hazmat.primitives import hashes, serialization 

14from cryptography.hazmat.primitives.asymmetric import padding, rsa 

15from cryptography.hazmat.primitives.asymmetric.rsa import RSAPrivateKey, RSAPublicKey 

16from cryptography.hazmat.primitives.asymmetric.types import PublicKeyTypes 

17 

18from confy_addons.core.constants import DEFAULT_RSA_KEY_SIZE, LOGGER_LEVEL, RSA_PUBLIC_EXPONENT 

19from confy_addons.core.exceptions import ( 

20 DecryptionError, 

21 EncryptionError, 

22) 

23from confy_addons.core.mixins import EncryptionMixin 

24 

25logging.basicConfig(level=LOGGER_LEVEL) 

26logger = logging.getLogger(__name__) 

27 

28 

29class RSAEncryption(EncryptionMixin): 

30 """RSA encryption handler with automatic key pair generation. 

31 

32 This class generates and manages an RSA key pair for asymmetric encryption 

33 and decryption operations. It provides convenient access to both private 

34 and public keys, as well as serialized forms of the public key. 

35 

36 Attributes: 

37 key_size: The size of the RSA key in bits. 

38 public_key: The RSA public key. 

39 private_key: The RSA private key. 

40 serialized_public_key: The public key in PEM format. 

41 base64_public_key: The public key in base64-encoded PEM format. 

42 

43 """ 

44 

45 def __init__(self, key_size: int = DEFAULT_RSA_KEY_SIZE): 

46 """Initialize RSAEncryption with a new key pair. 

47 

48 Generates a new RSA key pair with the specified key size and public 

49 exponent. The private key is stored internally for decryption operations. 

50 

51 Args: 

52 key_size: The size of the RSA key in bits. Defaults to 4096. 

53 

54 Raises: 

55 TypeError: If key_size is not an integer. 

56 ValueError: If key_size is less than the recommended minimum for security. 

57 RuntimeError: If key pair generation fails. 

58 

59 """ 

60 if not isinstance(key_size, int): 

61 logger.error(f'Invalid key_size type: {type(key_size)}') 

62 raise TypeError('key_size must be an integer') 

63 if key_size < DEFAULT_RSA_KEY_SIZE: 

64 logger.error(f'Invalid key_size value: {key_size}') 

65 raise ValueError(f'key_size must be at least {DEFAULT_RSA_KEY_SIZE} bits for security') 

66 

67 self._key_size = key_size 

68 self._public_exponent = RSA_PUBLIC_EXPONENT 

69 

70 try: 

71 self._private_key = rsa.generate_private_key( 

72 public_exponent=self._public_exponent, key_size=self._key_size 

73 ) 

74 except Exception as e: 

75 logger.error('Error occurred while generating RSA key pair: %s', e) 

76 raise RuntimeError('Failed to generate RSA key pair') from e 

77 

78 logger.debug(f'RSA key pair generated with key size: {self._key_size} bits') 

79 

80 def __repr__(self): 

81 """Return a string representation of the RSAEncryption instance. 

82 

83 Returns: 

84 str: A detailed string representation including module, class name, 

85 parameters, and memory address. 

86 

87 """ 

88 class_name = type(self).__name__ 

89 return f"""{self.__module__}.{class_name}(key_size={self._key_size!r}) 

90 object at {hex(id(self))}""" 

91 

92 def decrypt(self, encrypted_data: bytes) -> bytes: 

93 """Decrypts data using the private key. 

94 

95 Decrypts the provided encrypted data using RSA with OAEP padding 

96 and SHA256 hashing algorithm. 

97 

98 Args: 

99 encrypted_data: The encrypted bytes to decrypt. 

100 

101 Returns: 

102 bytes: The decrypted data. 

103 

104 Raises: 

105 TypeError: If encrypted_data is not bytes. 

106 ValueError: If encrypted_data is empty. 

107 DecryptionError: If decryption fails. 

108 

109 """ 

110 if not isinstance(encrypted_data, bytes): 

111 logger.error(f'Invalid encrypted_data type: {type(encrypted_data)}') 

112 raise TypeError('encrypted_data must be bytes') 

113 if len(encrypted_data) == 0: 

114 logger.error('Invalid encrypted_data value: %s', encrypted_data) 

115 raise ValueError('encrypted_data is empty') 

116 

117 try: 

118 decrypted_data = self._private_key.decrypt( 

119 encrypted_data, 

120 padding.OAEP( 

121 mgf=padding.MGF1(algorithm=hashes.SHA256()), 

122 algorithm=hashes.SHA256(), 

123 label=None, 

124 ), 

125 ) 

126 return decrypted_data 

127 except Exception as e: 

128 logger.error('Decryption failed: %s', e) 

129 raise DecryptionError('RSA decryption failed') from e 

130 

131 def sign(self, data: bytes) -> bytes: 

132 """Signs the data using the private key with PSS padding. 

133 

134 Args: 

135 data: The bytes to be signed. 

136 

137 Returns: 

138 bytes: The signature. 

139 

140 Raises: 

141 TypeError: If the data is not a byte. 

142 ValueError: If the data is empty. 

143 RuntimeError: If the signature fails. 

144 

145 """ 

146 if not isinstance(data, bytes): 

147 logger.error(f'Invalid data type for signing: {type(data)}') 

148 raise TypeError('data must be bytes') 

149 if len(data) == 0: 

150 logger.error('Invalid data value for signing: empty') 

151 raise ValueError('data is empty') 

152 

153 try: 

154 return self._private_key.sign( 

155 data, 

156 padding.PSS( 

157 mgf=padding.MGF1(algorithm=hashes.SHA256()), 

158 salt_length=padding.PSS.MAX_LENGTH, 

159 ), 

160 hashes.SHA256(), 

161 ) 

162 except Exception as e: 

163 logger.exception('Signing failed: %s', e) 

164 raise RuntimeError('RSA signing failed') from e 

165 

166 @property 

167 def key_size(self) -> int: 

168 """Returns the size of the RSA key in bits. 

169 

170 Returns: 

171 int: The key size in bits. 

172 

173 """ 

174 return self._key_size 

175 

176 @property 

177 def public_key(self) -> RSAPublicKey: 

178 """Returns the RSA public key. 

179 

180 Returns: 

181 RSAPublicKey: The public key object that can be shared with others 

182 for encryption operations. 

183 

184 """ 

185 return self._private_key.public_key() 

186 

187 @property 

188 def private_key(self) -> RSAPrivateKey: 

189 """Returns the RSA private key. 

190 

191 Returns: 

192 RSAPrivateKey: The private key object that should be kept secure 

193 and used for decryption operations. 

194 

195 """ 

196 return self._private_key 

197 

198 @property 

199 def serialized_public_key(self) -> bytes: 

200 """Returns the public key in PEM format. 

201 

202 Serializes the public key to PEM format with SubjectPublicKeyInfo 

203 structure, suitable for transmission or storage. 

204 

205 Returns: 

206 bytes: The public key in PEM-encoded bytes. 

207 

208 """ 

209 return self._private_key.public_key().public_bytes( 

210 encoding=serialization.Encoding.PEM, 

211 format=serialization.PublicFormat.SubjectPublicKeyInfo, 

212 ) 

213 

214 @property 

215 def base64_public_key(self) -> str: 

216 """Returns the public key in base64-encoded PEM format. 

217 

218 Provides the public key as a base64-encoded string, which is convenient 

219 for transmission over text-based protocols. 

220 

221 Returns: 

222 str: The base64-encoded PEM representation of the public key. 

223 

224 """ 

225 serialized_public_key = self._private_key.public_key().public_bytes( 

226 encoding=serialization.Encoding.PEM, 

227 format=serialization.PublicFormat.SubjectPublicKeyInfo, 

228 ) 

229 

230 return base64.b64encode(serialized_public_key).decode() 

231 

232 

233class RSAPublicEncryption(EncryptionMixin): 

234 """RSA encryption handler using a public key only. 

235 

236 This class provides encryption operations using an existing RSA public key. 

237 It is intended for encrypting data that will be decrypted by the holder 

238 of the corresponding private key. 

239 

240 Attributes: 

241 key: The RSA public key used for encryption. 

242 

243 """ 

244 

245 def __init__(self, key: RSAPublicKey): 

246 """Initialize RSAPublicEncryption with a public key. 

247 

248 Args: 

249 key: An RSA public key object to use for encryption operations. 

250 

251 Raises: 

252 TypeError: If the provided key is not an instance of RSAPublicKey. 

253 

254 """ 

255 if not isinstance(key, RSAPublicKey): 

256 raise TypeError('key must be an instance of RSAPublicKey') 

257 

258 self._key = key 

259 

260 def __repr__(self): 

261 """Return a string representation of the RSAPublicEncryption instance. 

262 

263 Returns: 

264 str: A detailed string representation including module, class name, 

265 key, and memory address. 

266 

267 """ 

268 class_name = type(self).__name__ 

269 return f'{self.__module__}.{class_name}(key={self._key!r}) object at {hex(id(self))}' 

270 

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

272 """Encrypts data using the public key. 

273 

274 Encrypts the provided data using RSA with OAEP padding and SHA256 

275 hashing algorithm for secure encryption. 

276 

277 Args: 

278 data: The bytes to encrypt. 

279 

280 Returns: 

281 bytes: The encrypted data. 

282 

283 Raises: 

284 TypeError: If the provided data is not bytes. 

285 EncryptionError: If encryption fails. 

286 

287 """ 

288 if not isinstance(data, bytes): 

289 logger.error(f'Invalid data type: {type(data)}') 

290 raise TypeError('data must be bytes') 

291 try: 

292 return self._key.encrypt( 

293 data, 

294 padding.OAEP( 

295 mgf=padding.MGF1(algorithm=hashes.SHA256()), 

296 algorithm=hashes.SHA256(), 

297 label=None, 

298 ), 

299 ) 

300 except Exception as e: 

301 logger.exception('Encryption failed: %s', e) 

302 raise EncryptionError('RSA encryption failed') from e 

303 

304 def verify(self, data: bytes, signature: bytes): 

305 """Verifica a assinatura dos dados usando a chave pública. 

306 

307 Args: 

308 data: Os dados originais que foram assinados. 

309 signature: A assinatura recebida. 

310 

311 Raises: 

312 TypeError: Se os dados ou assinatura não forem bytes. 

313 SignatureVerificationError: Se a assinatura for inválida. 

314 

315 """ 

316 if not isinstance(data, bytes) or not isinstance(signature, bytes): 

317 raise TypeError('data and signature must be bytes') 

318 

319 try: 

320 self._key.verify( 

321 signature, 

322 data, 

323 padding.PSS( 

324 mgf=padding.MGF1(algorithm=hashes.SHA256()), salt_length=padding.PSS.MAX_LENGTH 

325 ), 

326 hashes.SHA256(), 

327 ) 

328 # Se chegar aqui, a verificação foi bem-sucedida 

329 logger.debug('Signature verification successful') 

330 

331 except Exception as e: 

332 logger.error(f'An unexpected error occurred during verification: {e}') 

333 raise Exception(f'Verification error: {e}') from e 

334 

335 @property 

336 def key(self) -> RSAPublicKey: 

337 """Returns the RSA public key. 

338 

339 Returns: 

340 RSAPublicKey: The public key object used for encryption. 

341 

342 """ 

343 return self._key 

344 

345 

346def deserialize_public_key(b64_key: str) -> PublicKeyTypes: 

347 """Deserializes a base64-encoded PEM string back to an RSA public key object. 

348 

349 Decodes a base64-encoded PEM string and loads it as an RSA public key object 

350 that can be used for encryption operations. 

351 

352 Args: 

353 b64_key (str): The base64-encoded PEM representation of the public key. 

354 

355 Returns: 

356 PublicKeyTypes: The deserialized RSA public key object. 

357 

358 Raises: 

359 TypeError: If b64_key is not a string. 

360 ValueError: If the base64 decoding fails or the key cannot be loaded. 

361 

362 """ 

363 if not isinstance(b64_key, str): 

364 logger.error(f'Invalid b64_key type: {type(b64_key)}') 

365 raise TypeError('b64_key must be a base64-encoded string') 

366 try: 

367 key_bytes = base64.b64decode(b64_key.encode('ascii'), validate=True) 

368 except (binascii.Error, ValueError) as e: 

369 logger.error(f'Error occurred while decoding base64 key: {e}') 

370 raise ValueError('Invalid base64 public key') from e 

371 

372 try: 

373 return serialization.load_pem_public_key(key_bytes) 

374 except Exception as e: 

375 logger.error(f'Error occurred while loading public key from PEM: {e}') 

376 raise ValueError('Failed to load public key from PEM') from e