Skip to content

Commit e9a3593

Browse files
committed
Fixed optional location payload and private key usage
1 parent df8be3e commit e9a3593

File tree

3 files changed

+24
-13
lines changed

3 files changed

+24
-13
lines changed

fmd_api/client.py

Lines changed: 20 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,13 @@
2121
import logging
2222
import time
2323
import random
24-
from typing import Optional, List, Any
24+
from typing import Optional, List, Any, cast
2525

2626
import aiohttp
2727
from argon2.low_level import hash_secret_raw, Type
2828
from cryptography.hazmat.primitives import serialization, hashes
2929
from cryptography.hazmat.primitives.asymmetric import padding
30+
from cryptography.hazmat.primitives.asymmetric.rsa import RSAPrivateKey
3031
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
3132

3233
from .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("=")

fmd_api/device.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88

99
import json
1010
from datetime import datetime, timezone
11-
from typing import Optional, AsyncIterator, List
11+
from typing import Optional, AsyncIterator, List, Dict, Any
1212

1313
from .models import Location, PhotoResult
1414
from .exceptions import OperationError
@@ -24,10 +24,10 @@ def _parse_location_blob(blob_b64: str) -> Location:
2424

2525

2626
class Device:
27-
def __init__(self, client: FmdClient, fmd_id: str, raw: dict = None):
27+
def __init__(self, client: FmdClient, fmd_id: str, raw: Optional[Dict[str, Any]] = None):
2828
self.client = client
2929
self.id = fmd_id
30-
self.raw = raw or {}
30+
self.raw: Dict[str, Any] = raw or {}
3131
self.name = self.raw.get("name")
3232
self.cached_location: Optional[Location] = None
3333
self._last_refresh = None

fmd_api/models.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
class Location:
88
lat: float
99
lon: float
10-
timestamp: datetime
10+
timestamp: Optional[datetime]
1111
accuracy_m: Optional[float] = None
1212
altitude_m: Optional[float] = None
1313
speed_m_s: Optional[float] = None

0 commit comments

Comments
 (0)