diff --git a/CHANGELOG.md b/CHANGELOG.md index bff1003..400ee5a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -48,7 +48,7 @@ * The high level `fetchers.retrieve_remote_profile` signature has changed. It now expects as first parameter an `id` which for ActivityPub objects is the URL ID and for Diaspora objects is the handle. Additionally a `sender_key_fetcher` can be passed in as before to optimize public key fetching using a callable. * The generator class `RFC7033Webfinger` now expects instead of an `id` the `handle` and `guid` of the profile. * NodeInfo2 parser now returns the admin user in `handle` format instead of a Diaspora format URL. - * The high level inbound and outbound functions `inbound.handle_receive`, `outbound.handle_send` parameter `user` must now receive a `UserType` compatible object. This must have the attributes `id` and `private_key`. If Diaspora support is required then also `handle` and `guid` should exist. The type can be found as a class in `types.UserType`. + * The high level inbound and outbound functions `inbound.handle_receive`, `outbound.handle_send` parameter `user` must now receive a `UserType` compatible object. This must have the attribute `id`, and for `handle_send` also `private_key`. If Diaspora support is required then also `handle` and `guid` should exist. The type can be found as a class in `types.UserType`. * The high level inbound function `inbound.handle_receive` first parameter has been changed to `request` which must be a `RequestType` compatible object. This must have the attribute `body` which corrresponds to the old `payload` parameter. For ActivityPub inbound requests the object must also contain `headers`, `method` and `url`. * The outbound function `outbound.handle_send` parameter `recipients` structure has changed. It must now be a list of dictionaries, containing at minimum the following: `endpoint` for the recipient endpoint, `fid` for the recipient federation ID (ActivityPub only), `protocol` for the protocol to use and `public` as a boolean whether the payload should be treated as visible to anyone. diff --git a/federation/outbound.py b/federation/outbound.py index ab46301..cd0fa43 100644 --- a/federation/outbound.py +++ b/federation/outbound.py @@ -39,9 +39,9 @@ def handle_create_payload( mappers = importlib.import_module(f"federation.entities.{protocol_name}.mappers") protocol = importlib.import_module(f"federation.protocols.{protocol_name}.protocol") protocol = protocol.Protocol() - outbound_entity = mappers.get_outbound_entity(entity, author_user.private_key) + outbound_entity = mappers.get_outbound_entity(entity, author_user.rsa_private_key) if parent_user: - outbound_entity.sign_with_parent(parent_user.private_key) + outbound_entity.sign_with_parent(parent_user.rsa_private_key) send_as_user = parent_user if parent_user else author_user data = protocol.build_send(entity=outbound_entity, from_user=send_as_user, to_user_key=to_user_key) return data @@ -156,7 +156,7 @@ def handle_send( logger.error("handle_send - failed to generate payload for %s, %s: %s", fid, endpoint, ex) continue payloads.append({ - "auth": get_http_authentication(author_user.private_key, f"{author_user.id}#main-key"), + "auth": get_http_authentication(author_user.rsa_private_key, f"{author_user.id}#main-key"), "payload": payload, "content_type": 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"', "urls": {endpoint}, diff --git a/federation/protocols/diaspora/protocol.py b/federation/protocols/diaspora/protocol.py index 4b9250f..7871e79 100644 --- a/federation/protocols/diaspora/protocol.py +++ b/federation/protocols/diaspora/protocol.py @@ -106,7 +106,7 @@ class Protocol: def _get_user_key(self): if not getattr(self.user, "private_key", None): raise EncryptedMessageError("Cannot decrypt private message without user key") - return self.user.private_key + return self.user.rsa_private_key def get_sender(self): return MagicEnvelope.get_sender(self.doc) @@ -150,7 +150,7 @@ class Protocol: xml = entity.outbound_doc else: xml = entity.to_xml() - me = MagicEnvelope(etree.tostring(xml), private_key=from_user.private_key, author_handle=from_user.handle) + me = MagicEnvelope(etree.tostring(xml), private_key=from_user.rsa_private_key, author_handle=from_user.handle) rendered = me.render() if to_user_key: return EncryptedPayload.encrypt(rendered, to_user_key) diff --git a/federation/tests/protocols/diaspora/test_protocol.py b/federation/tests/protocols/diaspora/test_protocol.py index 75069f6..214490e 100644 --- a/federation/tests/protocols/diaspora/test_protocol.py +++ b/federation/tests/protocols/diaspora/test_protocol.py @@ -10,15 +10,15 @@ from federation.protocols.diaspora.protocol import Protocol, identify_request from federation.tests.fixtures.keys import PUBKEY, get_dummy_private_key from federation.tests.fixtures.payloads import DIASPORA_PUBLIC_PAYLOAD, DIASPORA_ENCRYPTED_PAYLOAD, \ DIASPORA_RESHARE_PAYLOAD -from federation.types import RequestType +from federation.types import RequestType, UserType class MockUser: - private_key = "foobar" - def __init__(self, nokey=False): if nokey: self.private_key = None + else: + self.private_key = get_dummy_private_key() def mock_get_contact_key(contact): @@ -113,7 +113,7 @@ class TestDiasporaProtocol(DiasporaTestBase): entity = DiasporaPost() private_key = get_dummy_private_key() outbound_entity = get_outbound_entity(entity, private_key) - data = protocol.build_send(outbound_entity, from_user=Mock( + data = protocol.build_send(outbound_entity, from_user=UserType( private_key=private_key, id="johnny@localhost", handle="johnny@localhost", )) @@ -133,7 +133,7 @@ class TestDiasporaProtocol(DiasporaTestBase): entity = DiasporaPost() private_key = get_dummy_private_key() outbound_entity = get_outbound_entity(entity, private_key) - data = protocol.build_send(outbound_entity, to_user_key="public key", from_user=Mock( + data = protocol.build_send(outbound_entity, to_user_key="public key", from_user=UserType( private_key=private_key, id="johnny@localhost", handle="johnny@localhost", )) @@ -151,8 +151,8 @@ class TestDiasporaProtocol(DiasporaTestBase): protocol = self.init_protocol() outbound_doc = etree.fromstring("foo") entity = Mock(outbound_doc=outbound_doc) - from_user = Mock( - id="foobar@domain.tld", private_key="barfoo", handle="foobar@domain.tld", + from_user = UserType( + id="foobar@domain.tld", private_key=get_dummy_private_key(), handle="foobar@domain.tld", ) protocol.build_send(entity, from_user) mock_me.assert_called_once_with( @@ -162,9 +162,9 @@ class TestDiasporaProtocol(DiasporaTestBase): @patch("federation.protocols.diaspora.protocol.EncryptedPayload.decrypt") def test_get_json_payload_magic_envelope(self, mock_decrypt): protocol = Protocol() - protocol.user = MockUser() + protocol.user = UserType(id="foobar", private_key=get_dummy_private_key()) protocol.get_json_payload_magic_envelope("payload") - mock_decrypt.assert_called_once_with(payload="payload", private_key="foobar") + mock_decrypt.assert_called_once_with(payload="payload", private_key=get_dummy_private_key()) @patch.object(Protocol, "get_json_payload_magic_envelope", return_value=etree.fromstring("bar")) def test_store_magic_envelope_doc_json_payload(self, mock_store): diff --git a/federation/tests/test_outbound.py b/federation/tests/test_outbound.py index 8a54cab..687f66e 100644 --- a/federation/tests/test_outbound.py +++ b/federation/tests/test_outbound.py @@ -5,6 +5,7 @@ import pytest from federation.entities.diaspora.entities import DiasporaPost from federation.outbound import handle_create_payload, handle_send from federation.tests.fixtures.keys import get_dummy_private_key +from federation.types import UserType from federation.utils.text import encode_if_text @@ -50,10 +51,10 @@ class TestHandleSend: "protocol": "activitypub", } ] - mock_author = Mock( + author = UserType( private_key=key, id="foo@example.com", handle="foo@example.com", ) - handle_send(profile, mock_author, recipients) + handle_send(profile, author, recipients) # Ensure first call is a private diaspora payload args, kwargs = mock_send.call_args_list[0] diff --git a/federation/types.py b/federation/types.py index 655592d..dbefde2 100644 --- a/federation/types.py +++ b/federation/types.py @@ -2,6 +2,7 @@ from enum import Enum from typing import Optional, Dict, Union import attr +from Crypto.PublicKey import RSA from Crypto.PublicKey.RSA import RsaKey @@ -28,9 +29,15 @@ class ReceiverVariant(Enum): @attr.s(frozen=True) class UserType: id: str = attr.ib() - private_key: Optional[RsaKey] = attr.ib(default=None) + private_key: Optional[Union[RsaKey, str]] = attr.ib(default=None) receiver_variant: Optional[ReceiverVariant] = attr.ib(default=None) # Required only if sending to Diaspora protocol platforms handle: Optional[str] = attr.ib(default=None) guid: Optional[str] = attr.ib(default=None) + + @property + def rsa_private_key(self) -> RsaKey: + if isinstance(self.private_key, str): + return RSA.importKey(self.private_key) + return self.private_key