diff --git a/itests/tests/conftest.py b/itests/tests/conftest.py index 98a737d3..5248fd7b 100644 --- a/itests/tests/conftest.py +++ b/itests/tests/conftest.py @@ -95,6 +95,29 @@ def contact_profile_id(admin_stub, random_name, public_key): ) +@pytest.fixture +def recipient_signing_profile_id(admin_stub, random_recipient_name): + # Create a new signing profile + profile_id = create_signing_profile(admin_stub, random_recipient_name) + yield profile_id + # Delete the profile + delete_profile(admin_stub, profile_id) + + +@pytest.fixture +def recipient_contact_profile_id(admin_stub, random_recipient_name, public_key): + # Create a new contact profile + contact_profile_id = create_contact_profile( + admin_stub, random_recipient_name, public_key) + yield contact_profile_id + # Delete the profile + admin_stub.DeleteSqueakProfile( + squeak_admin_pb2.DeleteSqueakProfileRequest( + profile_id=contact_profile_id, + ) + ) + + @pytest.fixture def saved_squeak_hash(admin_stub, signing_profile_id): # Create a new squeak using the new profile @@ -138,6 +161,11 @@ def random_name(): yield "random_name_{}".format(uuid.uuid1()) +@pytest.fixture +def random_recipient_name(): + yield "random_recipient_name_{}".format(uuid.uuid1()) + + @pytest.fixture def random_image(): yield os.urandom(567) diff --git a/itests/tests/test_squeak_node.py b/itests/tests/test_squeak_node.py index 5ec990f1..5bee69c9 100644 --- a/itests/tests/test_squeak_node.py +++ b/itests/tests/test_squeak_node.py @@ -267,6 +267,32 @@ def test_make_reply_squeak( ] +def test_make_private_squeak(admin_stub, signing_profile_id, recipient_contact_profile_id): + make_squeak_content = "This is a private squeak!" + make_squeak_hash = make_squeak( + admin_stub, + signing_profile_id, + make_squeak_content, + recipient_profile_id=recipient_contact_profile_id, + ) + assert len(make_squeak_hash) == 32 * 2 + + # Get the squeak display item + get_squeak_display_entry = get_squeak_display( + admin_stub, make_squeak_hash) + assert get_squeak_display_entry.squeak_hash == make_squeak_hash + assert ( + get_squeak_display_entry.content_str == "This is a private squeak!" + ) + assert get_squeak_display_entry.is_author_known + assert get_squeak_display_entry.HasField("author") + assert len( + get_squeak_display_entry.author.profile_image) > 0 + assert not get_squeak_display_entry.is_reply + assert not bool(get_squeak_display_entry.reply_to) + assert len(get_squeak_display_entry.secret_key_hex) == 32 * 2 + + def test_make_signing_profile(admin_stub): # Create a new signing profile profile_name = "test_signing_profile_name" diff --git a/itests/tests/util.py b/itests/tests/util.py index b899fef2..67a35839 100644 --- a/itests/tests/util.py +++ b/itests/tests/util.py @@ -356,12 +356,14 @@ def get_default_peer_port(node_stub): return get_default_peer_port_response.port -def make_squeak(node_stub, profile_id, squeak_content, reply_to_hash=None): +def make_squeak(node_stub, profile_id, squeak_content, reply_to_hash=None, recipient_profile_id=None): make_squeak_response = node_stub.MakeSqueak( squeak_admin_pb2.MakeSqueakRequest( profile_id=profile_id, content=squeak_content, replyto=reply_to_hash, + has_recipient=(recipient_profile_id is not None), + recipient_profile_id=recipient_profile_id, ) ) return make_squeak_response.squeak_hash diff --git a/proto/squeak_admin.proto b/proto/squeak_admin.proto index 3f6afb26..23d761f3 100644 --- a/proto/squeak_admin.proto +++ b/proto/squeak_admin.proto @@ -539,6 +539,12 @@ message MakeSqueakRequest { /// The replyto hash string replyto = 3; + + /// Has a recipient + bool has_recipient = 4; + + /// The profile id of the recipient if squeak is private + int32 recipient_profile_id = 5; } message MakeSqueakReply { diff --git a/requirements-itest.txt b/requirements-itest.txt index 64baa555..4dab6ef4 100644 --- a/requirements-itest.txt +++ b/requirements-itest.txt @@ -3,4 +3,4 @@ grpcio==1.39.0 grpcio-tools==1.39.0 importlib_resources==1.4.0 pytest==6.2.5 -squeaklib==0.10.2 +squeaklib==0.10.3 diff --git a/requirements.txt b/requirements.txt index 58dd95dc..9f3c6d74 100644 --- a/requirements.txt +++ b/requirements.txt @@ -14,5 +14,5 @@ python-bitcoinlib==0.11.0 pyzmq==22.3.0 requests==2.26.0 SQLAlchemy==1.4.25 -squeaklib==0.10.2 +squeaklib==0.10.3 typed-config==0.2.5 diff --git a/squeaknode/admin/squeak_admin_server_handler.py b/squeaknode/admin/squeak_admin_server_handler.py index d33d1aea..ebec25fd 100644 --- a/squeaknode/admin/squeak_admin_server_handler.py +++ b/squeaknode/admin/squeak_admin_server_handler.py @@ -292,12 +292,18 @@ class SqueakAdminServerHandler(object): replyto_hash_str = request.replyto replyto_hash = bytes.fromhex( replyto_hash_str) if replyto_hash_str else None + has_recipient = request.has_recipient + recipient_profile_id = request.recipient_profile_id if has_recipient else None logger.info("Handle make squeak profile with id: {}".format(profile_id)) inserted_squeak_hash = self.squeak_controller.make_squeak( - profile_id, content_str, replyto_hash + profile_id, + content_str, + replyto_hash, + recipient_profile_id, ) inserted_squeak_hash_str = optional_squeak_hash_to_hex( - inserted_squeak_hash) + inserted_squeak_hash, + ) return squeak_admin_pb2.MakeSqueakReply( squeak_hash=inserted_squeak_hash_str, ) diff --git a/squeaknode/core/squeak_core.py b/squeaknode/core/squeak_core.py index 9849c496..5f6a12fa 100644 --- a/squeaknode/core/squeak_core.py +++ b/squeaknode/core/squeak_core.py @@ -67,6 +67,7 @@ class SqueakCore: signing_profile: SqueakProfile, content_str: str, replyto_hash: Optional[bytes] = None, + recipient_profile: Optional[SqueakProfile] = None, ) -> Tuple[CSqueak, bytes]: """Create a new squeak. @@ -94,7 +95,8 @@ class SqueakCore: content_str, block_height, block_hash, - replyto_hash, + replyto_hash=replyto_hash, + recipient_public_key=recipient_profile.public_key if recipient_profile else None, ) return squeak, secret_key @@ -131,21 +133,38 @@ class SqueakCore: raise Exception("Block hash incorrect.") return block_info.block_header - def get_decrypted_content(self, squeak: CSqueak, secret_key: bytes) -> str: + def get_decrypted_content( + self, + squeak: CSqueak, + secret_key: bytes, + author_profile: Optional[SqueakProfile] = None, + recipient_profile: Optional[SqueakProfile] = None, + ) -> str: """Checks if the secret key is valid for the given squeak and returns the decrypted content. Args: squeak: The squeak to be validated. secret_key: The secret key. + author_profile: The profile of the author. + recipient_profile: The profile of the recipient. Returns: - bytes: the decrypted content + str: the decrypted content as a string. Raises: Exception: If the secret key is not valid. """ - return get_decrypted_content(squeak, secret_key) + if author_profile and author_profile.private_key is None: + raise Exception("Author profile does not have private key.") + if recipient_profile and recipient_profile.private_key is None: + raise Exception("Recipient profile does not have private key.") + return get_decrypted_content( + squeak, + secret_key, + authorPrivKey=author_profile.private_key if author_profile else None, + recipientPrivKey=recipient_profile.private_key if recipient_profile else None, + ) def get_best_block_height(self) -> int: """Get the current height of the latest block in the blockchain. diff --git a/squeaknode/core/squeaks.py b/squeaknode/core/squeaks.py index 9767a3f1..59b82e21 100644 --- a/squeaknode/core/squeaks.py +++ b/squeaknode/core/squeaks.py @@ -69,6 +69,7 @@ def make_squeak_with_block( block_height: The height of the latest block in the bitcoin blockchain. block_hahs: The hash of the latest block in the bitcoin blockchain. replyto_hash: The hash of the squeak to which this one is replying. + recipient_public_key: The public key of the recipient of a private squeak. Returns: Tuple[CSqueak, bytes]: the squeak that was created together @@ -102,7 +103,12 @@ def check_squeak(squeak: CSqueak) -> None: # TODO: return bytes (encoded utf-8 content) -def get_decrypted_content(squeak: CSqueak, secret_key: bytes) -> str: +def get_decrypted_content( + squeak: CSqueak, + secret_key: bytes, + authorPrivKey: Optional[SqueakPrivateKey] = None, + recipientPrivKey: Optional[SqueakPrivateKey] = None, +) -> str: """Checks if the secret key is valid for the given squeak and returns the decrypted content. @@ -116,7 +122,11 @@ def get_decrypted_content(squeak: CSqueak, secret_key: bytes) -> str: Raises: Exception: If the secret key is not valid. """ - return squeak.GetDecryptedContentStr(secret_key) + return squeak.GetDecryptedContentStr( + secret_key, + authorPrivKey=authorPrivKey, + recipientPrivKey=recipientPrivKey, + ) def get_payment_point_of_secret_key(secret_key: bytes) -> bytes: diff --git a/squeaknode/node/squeak_controller.py b/squeaknode/node/squeak_controller.py index d8350b9c..c2abb8ee 100644 --- a/squeaknode/node/squeak_controller.py +++ b/squeaknode/node/squeak_controller.py @@ -80,21 +80,40 @@ class SqueakController: self.node_settings = node_settings self.config = config - def make_squeak(self, profile_id: int, content_str: str, replyto_hash: Optional[bytes]) -> Optional[bytes]: + def make_squeak( + self, + profile_id: int, + content_str: str, + replyto_hash: Optional[bytes], + recipient_profile_id: Optional[int], + ) -> Optional[bytes]: squeak_profile = self.squeak_store.get_squeak_profile(profile_id) if squeak_profile is None: raise Exception("Profile with id {} not found.".format( profile_id, )) + if recipient_profile_id: + recipient_profile = self.squeak_store.get_squeak_profile( + recipient_profile_id) + if recipient_profile is None: + raise Exception("Recipient profile with id {} not found.".format( + recipient_profile_id, + )) squeak, secret_key = self.squeak_core.make_squeak( squeak_profile, content_str, replyto_hash, + recipient_profile=recipient_profile if recipient_profile_id else None, ) inserted_squeak_hash = self.squeak_store.save_squeak(squeak) if inserted_squeak_hash is None: raise Exception("Failed to save squeak.") self.squeak_store.save_secret_key(inserted_squeak_hash, secret_key) + if squeak.is_private_message: + self.squeak_store.unlock_squeak( + inserted_squeak_hash, + author_profile_id=profile_id, + ) return inserted_squeak_hash def pay_offer(self, received_offer_id: int) -> int: @@ -117,6 +136,18 @@ class SqueakController: ) return sent_payment_id + def unlock_private_squeak_as_recipient(self, squeak_hash: bytes, recipient_profile_id: int): + self.squeak_store.unlock_squeak( + squeak_hash, + recipient_profile_id=recipient_profile_id, + ) + + def unlock_private_squeak_as_author(self, squeak_hash: bytes, author_profile_id: int): + self.squeak_store.unlock_squeak( + squeak_hash, + author_profile_id=author_profile_id, + ) + def get_squeak(self, squeak_hash: bytes) -> Optional[CSqueak]: return self.squeak_store.get_squeak(squeak_hash) diff --git a/squeaknode/node/squeak_store.py b/squeaknode/node/squeak_store.py index 21b914e4..3b103fbb 100644 --- a/squeaknode/node/squeak_store.py +++ b/squeaknode/node/squeak_store.py @@ -126,7 +126,12 @@ class SqueakStore: if not squeak.is_private_message: self.unlock_squeak(squeak_hash) - def unlock_squeak(self, squeak_hash: bytes, recipient_profile_id: Optional[int] = None): + def unlock_squeak( + self, + squeak_hash: bytes, + author_profile_id: Optional[int] = None, + recipient_profile_id: Optional[int] = None, + ): squeak = self.squeak_db.get_squeak(squeak_hash) secret_key = self.squeak_db.get_squeak_secret_key(squeak_hash) if squeak is None: @@ -138,17 +143,20 @@ class SqueakStore: recipient_profile_id) if recipient_profile is None: raise Exception("Recipient profile does not exist.") - recipient_private_key = recipient_profile.private_key - if recipient_private_key is None: - raise Exception("Recipient profile must own the private key.") - # TODO: remove this log line later. - logger.info("Use private key here: {}".format( - recipient_private_key, - )) decrypted_content = self.squeak_core.get_decrypted_content( squeak, secret_key, - # TODO: use recipient private key here. + recipient_profile=recipient_profile, + ) + elif author_profile_id: + author_profile = self.squeak_db.get_profile( + author_profile_id) + if author_profile is None: + raise Exception("Author profile does not exist.") + decrypted_content = self.squeak_core.get_decrypted_content( + squeak, + secret_key, + author_profile=author_profile, ) else: decrypted_content = self.squeak_core.get_decrypted_content( diff --git a/tests/conftest.py b/tests/conftest.py index 943d261a..ae269309 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -241,6 +241,11 @@ def recipient_contact_profile_name(): yield "recipient_contact_profile_name" +@pytest.fixture +def recipient_signing_profile_name(): + yield "recipient_signing_profile_name" + + @pytest.fixture def signing_profile(signing_profile_name, private_key): yield gen_signing_profile( @@ -265,6 +270,14 @@ def recipient_contact_profile(recipient_contact_profile_name, recipient_public_k ) +@pytest.fixture +def recipient_signing_profile(recipient_signing_profile_name, recipient_private_key): + yield gen_signing_profile( + recipient_signing_profile_name, + recipient_private_key, + ) + + @pytest.fixture def squeak_entry_locked( squeak, diff --git a/tests/core/test_squeak_core.py b/tests/core/test_squeak_core.py index 2c4146fd..c361f34c 100644 --- a/tests/core/test_squeak_core.py +++ b/tests/core/test_squeak_core.py @@ -316,6 +316,34 @@ def test_make_squeak_with_contact_profile( assert "Can't make squeak with a contact profile." in str(excinfo.value) +def test_make_private_squeak( + squeak_core, + signing_profile, + squeak_content, + block_header, + recipient_signing_profile, + recipient_contact_profile, +): + created_squeak, created_secret_key = squeak_core.make_squeak( + signing_profile, + squeak_content, + recipient_profile=recipient_contact_profile, + ) + recipient_decrypted_content = squeak_core.get_decrypted_content( + created_squeak, + created_secret_key, + recipient_profile=recipient_signing_profile, + ) + author_decrypted_content = squeak_core.get_decrypted_content( + created_squeak, + created_secret_key, + author_profile=signing_profile, + ) + + assert recipient_decrypted_content == squeak_content + assert author_decrypted_content == squeak_content + + def test_get_block_header( squeak_core, squeak,