Implement unlock private squeak (#1910)

* Add method stub for unlock private squeak in controller class

* Add methods to unlock private squeak in controller

* Remove todo comments

* Add test for create and decrypt private squeak in core module

* Update handler method for make squeak to support recipient profile id

* Add itest for make private squeak and unlock content as author
This commit is contained in:
Jonathan Zernik 2021-12-28 01:07:59 -08:00 committed by GitHub
parent 75d0b0634d
commit 20d651df8e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 198 additions and 21 deletions

View file

@ -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)

View file

@ -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"

View file

@ -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

View file

@ -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 {

View file

@ -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

View file

@ -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

View file

@ -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,
)

View file

@ -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.

View file

@ -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:

View file

@ -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)

View file

@ -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(

View file

@ -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,

View file

@ -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,