Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 13 additions & 1 deletion at_client/atclient.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from queue import Empty, Queue
import time
import traceback
import uuid

from at_client.connections.notification.atevents import AtEvent, AtEventType

Expand Down Expand Up @@ -420,8 +421,19 @@ def handle_event(self, queue, at_event):
else:
raise Exception("You must assign a Queue object to the queue paremeter of AtClient class")

def notify(self, at_key : AtKey, value, operation = OperationEnum.UPDATE, session_id = str(uuid.uuid4())):
def notify(self, at_key : AtKey, value, operation = OperationEnum.UPDATE, session_id = None):
# Generate a fresh session id per call. A default of str(uuid.uuid4()) in the
# signature is evaluated once at import, so every notify() without an explicit
# id would reuse the same one and the server would dedup/drop the duplicates.
if session_id is None:
session_id = str(uuid.uuid4())
# Ensure an AES nonce exists. AES-CTR requires one; without it aes_encrypt
# raises "nonce must be bytes-like". Generate it here and set it on the key so
# it travels in the notification metadata for the receiver to decrypt with.
iv = at_key.metadata.iv_nonce
if iv is None:
iv = EncryptionUtil.generate_iv_nonce()
at_key.metadata.iv_nonce = iv
Comment on lines 433 to +436
shared_key = self.get_encryption_key_shared_by_me(at_key)
encrypted_value = EncryptionUtil.aes_encrypt_from_base64(value, shared_key, iv)
command = NotifyVerbBuilder().with_at_key(at_key, encrypted_value, operation, session_id).build()
Expand Down
46 changes: 46 additions & 0 deletions test/notify_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import inspect
import unittest
from unittest.mock import MagicMock

from at_client import AtClient
from at_client.common import AtSign
from at_client.common.keys import SharedKey
from at_client.util.encryptionutil import EncryptionUtil


class NotifyTest(unittest.TestCase):
"""Network-free regression tests for AtClient.notify()."""

def test_session_id_default_is_none(self):
"""The session_id default must not bake in a single UUID at import time.

A signature default of str(uuid.uuid4()) is evaluated once, so every notify()
without an explicit session_id reuses it and the server dedups/drops repeats.
"""
default = inspect.signature(AtClient.notify).parameters["session_id"].default
self.assertIsNone(default)

def test_notify_generates_iv_nonce_when_unset(self):
"""notify() must generate an AES nonce when the key has none (else it crashes)."""
client = AtClient.__new__(AtClient) # bypass the network-connecting __init__
client.queue = None
client.get_encryption_key_shared_by_me = MagicMock(
return_value=EncryptionUtil.generate_aes_key_base64()
)
resp = MagicMock()
resp.get_raw_data_response.return_value = "data:ok"
client.secondary_connection = MagicMock()
client.secondary_connection.execute_command.return_value = resp

key = SharedKey("demo", AtSign("@alice"), AtSign("@bob"))
key.set_namespace("test")
self.assertIsNone(key.metadata.iv_nonce)

result = client.notify(key, "hello") # must not raise

self.assertIsNotNone(key.metadata.iv_nonce) # generated and set on the key
self.assertEqual(result, "data:ok")


if __name__ == "__main__":
unittest.main()