2121import logging
2222import time
2323import random
24- from typing import Optional , List , Any
24+ from typing import Optional , List , Any , cast
2525
2626import aiohttp
2727from argon2 .low_level import hash_secret_raw , Type
2828from cryptography .hazmat .primitives import serialization , hashes
2929from cryptography .hazmat .primitives .asymmetric import padding
30+ from cryptography .hazmat .primitives .asymmetric .rsa import RSAPrivateKey
3031from cryptography .hazmat .primitives .ciphers .aead import AESGCM
3132
3233from .helpers import _pad_base64
@@ -81,7 +82,7 @@ def __init__(
8182 self ._fmd_id : Optional [str ] = None
8283 self ._password : Optional [str ] = None
8384 self .access_token : Optional [str ] = None
84- self .private_key = None # cryptography private key object
85+ self .private_key : Optional [ RSAPrivateKey ] = None # cryptography private key object
8586
8687 self ._session : Optional [aiohttp .ClientSession ] = None
8788
@@ -209,11 +210,11 @@ def _decrypt_private_key_blob(self, key_b64: str, password: str) -> bytes:
209210 aesgcm = AESGCM (aes_key )
210211 return aesgcm .decrypt (iv , ciphertext , None )
211212
212- def _load_private_key_from_bytes (self , privkey_bytes : bytes ):
213+ def _load_private_key_from_bytes (self , privkey_bytes : bytes ) -> RSAPrivateKey :
213214 try :
214- return serialization .load_pem_private_key (privkey_bytes , password = None )
215+ return cast ( RSAPrivateKey , serialization .load_pem_private_key (privkey_bytes , password = None ) )
215216 except ValueError :
216- return serialization .load_der_private_key (privkey_bytes , password = None )
217+ return cast ( RSAPrivateKey , serialization .load_der_private_key (privkey_bytes , password = None ) )
217218
218219 # -------------------------
219220 # Decryption
@@ -237,7 +238,10 @@ def decrypt_data_blob(self, data_b64: str) -> bytes:
237238 session_key_packet = blob [:RSA_KEY_SIZE_BYTES ]
238239 iv = blob [RSA_KEY_SIZE_BYTES : RSA_KEY_SIZE_BYTES + AES_GCM_IV_SIZE_BYTES ]
239240 ciphertext = blob [RSA_KEY_SIZE_BYTES + AES_GCM_IV_SIZE_BYTES :]
240- session_key = self .private_key .decrypt (
241+ key = self .private_key
242+ if key is None :
243+ raise FmdApiException ("Private key not loaded. Call authenticate() first." )
244+ session_key = key .decrypt (
241245 session_key_packet ,
242246 padding .OAEP (mgf = padding .MGF1 (algorithm = hashes .SHA256 ()), algorithm = hashes .SHA256 (), label = None ),
243247 )
@@ -264,6 +268,8 @@ async def _make_api_request(
264268 """
265269 url = self .base_url + endpoint
266270 await self ._ensure_session ()
271+ session = self ._session
272+ assert session is not None # for type checker; ensured by _ensure_session
267273 req_timeout = aiohttp .ClientTimeout (total = timeout if timeout is not None else self .timeout )
268274
269275 # Determine retry policy
@@ -275,7 +281,7 @@ async def _make_api_request(
275281 backoff_attempt = 0
276282 while True :
277283 try :
278- async with self . _session .request (method , url , json = payload , timeout = req_timeout ) as resp :
284+ async with session .request (method , url , json = payload , timeout = req_timeout ) as resp :
279285 # Handle 401 -> re-authenticate once
280286 if resp .status == 401 and retry_auth and self ._fmd_id and self ._password :
281287 log .info ("Received 401 Unauthorized, re-authenticating..." )
@@ -453,7 +459,9 @@ async def get_pictures(self, num_to_get: int = -1, timeout: Optional[float] = No
453459 req_timeout = aiohttp .ClientTimeout (total = timeout if timeout is not None else self .timeout )
454460 try :
455461 await self ._ensure_session ()
456- async with self ._session .put (
462+ session = self ._session
463+ assert session is not None
464+ async with session .put (
457465 f"{ self .base_url } /api/v1/pictures" , json = {"IDT" : self .access_token , "Data" : "" }, timeout = req_timeout
458466 ) as resp :
459467 resp .raise_for_status ()
@@ -601,7 +609,10 @@ async def send_command(self, command: str) -> bool:
601609 unix_time_ms = int (time .time () * 1000 )
602610 message_to_sign = f"{ unix_time_ms } :{ command } "
603611 message_bytes = message_to_sign .encode ("utf-8" )
604- signature = self .private_key .sign (
612+ key = self .private_key
613+ if key is None :
614+ raise FmdApiException ("Private key not loaded. Call authenticate() first." )
615+ signature = key .sign (
605616 message_bytes , padding .PSS (mgf = padding .MGF1 (hashes .SHA256 ()), salt_length = 32 ), hashes .SHA256 ()
606617 )
607618 signature_b64 = base64 .b64encode (signature ).decode ("utf-8" ).rstrip ("=" )
0 commit comments