kopia lustrzana https://gitlab.com/jaywink/federation
Send outbound Diaspora payloads in new format
Remove possibility to generate legacy MagicEnvelope payloads. Refs: #82merge-requests/130/head
rodzic
f6091d270a
commit
c6bbd3ac4b
|
@ -1,5 +1,11 @@
|
|||
# Changelog
|
||||
|
||||
## [unreleased]
|
||||
|
||||
### Changed
|
||||
|
||||
* Send outbound Diaspora payloads in new format. Remove possibility to generate legacy MagicEnvelope payloads. ([related issue](https://github.com/jaywink/federation/issues/82))
|
||||
|
||||
## [0.15.0] - 2018-02-12
|
||||
|
||||
### Added
|
||||
|
|
|
@ -1,12 +1,9 @@
|
|||
import json
|
||||
import logging
|
||||
from base64 import b64decode, urlsafe_b64decode, b64encode, urlsafe_b64encode
|
||||
from base64 import b64decode, urlsafe_b64decode
|
||||
from urllib.parse import unquote_plus
|
||||
|
||||
from Crypto.Cipher import AES, PKCS1_v1_5
|
||||
from Crypto.Hash import SHA256
|
||||
from Crypto.Random import get_random_bytes
|
||||
from Crypto.Signature import PKCS1_v1_5 as PKCSSign
|
||||
from lxml import etree
|
||||
|
||||
from federation.exceptions import EncryptedMessageError, NoSenderKeyFoundError
|
||||
|
@ -234,200 +231,11 @@ class Protocol(BaseProtocol):
|
|||
|
||||
def build_send(self, entity, from_user, to_user=None, *args, **kwargs):
|
||||
"""Build POST data for sending out to remotes."""
|
||||
if entity.outbound_doc:
|
||||
if entity.outbound_doc is not None:
|
||||
# Use pregenerated outbound document
|
||||
xml = entity.outbound_doc
|
||||
else:
|
||||
xml = entity.to_xml()
|
||||
self.init_message(xml, from_user.handle, from_user.private_key)
|
||||
xml = self.create_salmon_envelope(to_user)
|
||||
return {'xml': xml}
|
||||
|
||||
def init_message(self, message, author_username, private_key):
|
||||
"""
|
||||
Build a Diaspora message and prepare to send the payload <message>,
|
||||
authored by Contact <author>. The receipient is specified later, so
|
||||
that the same message can be sent to several people without needing to
|
||||
keep re-encrypting the inner.
|
||||
"""
|
||||
|
||||
# We need an AES key for the envelope
|
||||
self.inner_iv = get_random_bytes(AES.block_size)
|
||||
self.inner_key = get_random_bytes(32)
|
||||
self.inner_encrypter = AES.new(
|
||||
self.inner_key, AES.MODE_CBC, self.inner_iv)
|
||||
|
||||
# ...and one for the payload message
|
||||
self.outer_iv = get_random_bytes(AES.block_size)
|
||||
self.outer_key = get_random_bytes(32)
|
||||
self.outer_encrypter = AES.new(
|
||||
self.outer_key, AES.MODE_CBC, self.outer_iv)
|
||||
self.message = message
|
||||
self.author_username = author_username
|
||||
self.private_key = private_key
|
||||
|
||||
def xml_to_string(self, doc, xml_declaration=False):
|
||||
"""
|
||||
Utility function to turn an XML document to a string. This is
|
||||
abstracted out so that pretty-printing can be turned on and off in one
|
||||
place.
|
||||
"""
|
||||
return etree.tostring(
|
||||
doc,
|
||||
xml_declaration=xml_declaration,
|
||||
pretty_print=True,
|
||||
encoding="UTF-8"
|
||||
)
|
||||
|
||||
def create_decrypted_header(self):
|
||||
"""
|
||||
Build the XML document for the header. The header contains the key
|
||||
used to encrypt the message body.
|
||||
"""
|
||||
decrypted_header = etree.Element('decrypted_header')
|
||||
etree.SubElement(decrypted_header, "iv").text = \
|
||||
b64encode(self.inner_iv)
|
||||
etree.SubElement(decrypted_header, "aes_key").text = \
|
||||
b64encode(self.inner_key)
|
||||
etree.SubElement(decrypted_header, "author_id").text = \
|
||||
self.author_username
|
||||
return self.xml_to_string(decrypted_header)
|
||||
|
||||
def create_public_header(self):
|
||||
decrypted_header = etree.Element('header')
|
||||
etree.SubElement(decrypted_header, "author_id").text = \
|
||||
self.author_username
|
||||
return decrypted_header
|
||||
|
||||
def create_ciphertext(self):
|
||||
"""
|
||||
Encrypt the header.
|
||||
"""
|
||||
to_encrypt = self.pkcs7_pad(
|
||||
self.create_decrypted_header(),
|
||||
AES.block_size
|
||||
)
|
||||
out = self.outer_encrypter.encrypt(to_encrypt)
|
||||
return out
|
||||
|
||||
def create_outer_aes_key_bundle(self):
|
||||
"""
|
||||
Record the information on the key used to encrypt the header.
|
||||
"""
|
||||
d = json.dumps({
|
||||
"iv": b64encode(self.outer_iv).decode("ascii"),
|
||||
"key": b64encode(self.outer_key).decode("ascii")
|
||||
})
|
||||
return d
|
||||
|
||||
def create_encrypted_outer_aes_key_bundle(self, recipient_rsa):
|
||||
"""
|
||||
The Outer AES Key Bundle is encrypted with the receipient's public
|
||||
key, so only the receipient can decrypt the header.
|
||||
"""
|
||||
cipher = PKCS1_v1_5.new(recipient_rsa)
|
||||
return cipher.encrypt(
|
||||
self.create_outer_aes_key_bundle().encode("utf-8"))
|
||||
|
||||
def create_encrypted_header_json_object(self, public_key):
|
||||
"""
|
||||
The actual header and the encrypted outer (header) key are put into a
|
||||
document together.
|
||||
"""
|
||||
aes_key = b64encode(self.create_encrypted_outer_aes_key_bundle(
|
||||
public_key)).decode("ascii")
|
||||
ciphertext = b64encode(self.create_ciphertext()).decode("ascii")
|
||||
|
||||
d = json.dumps({
|
||||
"aes_key": aes_key,
|
||||
"ciphertext": ciphertext
|
||||
})
|
||||
return d
|
||||
|
||||
def create_encrypted_header(self, public_key):
|
||||
"""
|
||||
The "encrypted header JSON object" is dropped into some XML. I am not
|
||||
sure what this is for, but is required to interact.
|
||||
"""
|
||||
doc = etree.Element("encrypted_header")
|
||||
doc.text = b64encode(self.create_encrypted_header_json_object(
|
||||
public_key).encode("ascii"))
|
||||
return doc
|
||||
|
||||
def create_payload(self):
|
||||
"""
|
||||
Wrap the actual payload message in the standard XML wrapping.
|
||||
"""
|
||||
doc = etree.Element("XML")
|
||||
inner = etree.SubElement(doc, "post")
|
||||
if isinstance(self.message, str):
|
||||
inner.text = self.message
|
||||
else:
|
||||
inner.append(self.message)
|
||||
return self.xml_to_string(doc)
|
||||
|
||||
def create_encrypted_payload(self):
|
||||
"""
|
||||
Encrypt the payload XML with the inner (body) key.
|
||||
"""
|
||||
to_encrypt = self.pkcs7_pad(self.create_payload(), AES.block_size)
|
||||
return self.inner_encrypter.encrypt(to_encrypt)
|
||||
|
||||
def create_salmon_envelope(self, recipient):
|
||||
"""
|
||||
Build the whole message, pulling together the encrypted payload and the
|
||||
encrypted header. Selected elements are signed by the author so that
|
||||
tampering can be detected.
|
||||
|
||||
Note, this corresponds to the old Diaspora protocol which will slowly be replaced by the
|
||||
new version. See PR https://github.com/diaspora/diaspora_federation/issues/30
|
||||
|
||||
Args:
|
||||
recipient - Recipient object which must have public key as `key` (private messages only)
|
||||
|
||||
Returns:
|
||||
XML document as string
|
||||
"""
|
||||
nsmap = {
|
||||
None: PROTOCOL_NS,
|
||||
'me': 'http://salmon-protocol.org/ns/magic-env'
|
||||
}
|
||||
doc = etree.Element("{%s}diaspora" % nsmap[None], nsmap=nsmap)
|
||||
if recipient:
|
||||
doc.append(self.create_encrypted_header(recipient.key))
|
||||
else:
|
||||
doc.append(self.create_public_header())
|
||||
env = etree.SubElement(doc, "{%s}env" % nsmap["me"])
|
||||
etree.SubElement(env, "{%s}encoding" % nsmap["me"]).text = 'base64url'
|
||||
etree.SubElement(env, "{%s}alg" % nsmap["me"]).text = 'RSA-SHA256'
|
||||
if recipient:
|
||||
payload = urlsafe_b64encode(b64encode(
|
||||
self.create_encrypted_payload())).decode("ascii")
|
||||
else:
|
||||
payload = urlsafe_b64encode(self.create_payload()).decode("ascii")
|
||||
etree.SubElement(env, "{%s}data" % nsmap["me"],
|
||||
{"type": "application/xml"}).text = payload
|
||||
sig_contents = payload + "." + \
|
||||
b64encode(b"application/xml").decode("ascii") + "." + \
|
||||
b64encode(b"base64url").decode("ascii") + "." + \
|
||||
b64encode(b"RSA-SHA256").decode("ascii")
|
||||
sig_hash = SHA256.new(sig_contents.encode("ascii"))
|
||||
cipher = PKCSSign.new(self.private_key)
|
||||
sig = urlsafe_b64encode(cipher.sign(sig_hash))
|
||||
etree.SubElement(env, "{%s}sig" % nsmap["me"]).text = sig
|
||||
return self.xml_to_string(doc)
|
||||
|
||||
def pkcs7_pad(self, inp, block_size):
|
||||
"""
|
||||
Using the PKCS#7 padding scheme, pad <inp> to be a multiple of
|
||||
<block_size> bytes. Ruby's AES encryption pads with this scheme, but
|
||||
pycrypto doesn't support it.
|
||||
"""
|
||||
val = block_size - len(inp) % block_size
|
||||
if val == 0:
|
||||
return inp + (self.array_to_bytes([block_size]) * block_size)
|
||||
else:
|
||||
return inp + (self.array_to_bytes([val]) * val)
|
||||
|
||||
def array_to_bytes(self, vals):
|
||||
return bytes(vals)
|
||||
me = MagicEnvelope(etree.tostring(xml), private_key=from_user.private_key, author_handle=from_user.handle)
|
||||
# TODO wrap if doing encrypted delivery
|
||||
return me.render()
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import datetime
|
||||
from base64 import urlsafe_b64decode
|
||||
from unittest.mock import Mock, patch
|
||||
from xml.etree.ElementTree import ElementTree
|
||||
|
@ -5,9 +6,12 @@ from xml.etree.ElementTree import ElementTree
|
|||
from lxml import etree
|
||||
import pytest
|
||||
|
||||
from federation.entities.base import Post
|
||||
from federation.entities.diaspora.entities import DiasporaPost
|
||||
from federation.entities.diaspora.mappers import get_outbound_entity
|
||||
from federation.exceptions import EncryptedMessageError, NoSenderKeyFoundError, SignatureVerificationError
|
||||
from federation.protocols.diaspora.protocol import Protocol, identify_payload
|
||||
from federation.tests.fixtures.keys import PUBKEY
|
||||
from federation.tests.fixtures.keys import PUBKEY, get_dummy_private_key
|
||||
from federation.tests.fixtures.payloads import (
|
||||
ENCRYPTED_LEGACY_DIASPORA_PAYLOAD, UNENCRYPTED_LEGACY_DIASPORA_PAYLOAD, DIASPORA_PUBLIC_PAYLOAD,
|
||||
DIASPORA_ENCRYPTED_PAYLOAD,
|
||||
|
@ -171,26 +175,32 @@ class TestDiasporaProtocol(DiasporaTestBase):
|
|||
protocol.content = "<content><handle>bob@example.com</handle></content>"
|
||||
assert protocol.get_sender_legacy() is None
|
||||
|
||||
@patch.object(Protocol, "init_message")
|
||||
@patch.object(Protocol, "create_salmon_envelope")
|
||||
def test_build_send(self, mock_create_salmon, mock_init_message):
|
||||
mock_create_salmon.return_value = "xmldata"
|
||||
protocol = self.init_protocol()
|
||||
mock_entity_xml = Mock()
|
||||
entity = Mock(to_xml=Mock(return_value=mock_entity_xml), outbound_doc=None)
|
||||
from_user = Mock(handle="foobar", private_key="barfoo")
|
||||
data = protocol.build_send(entity, from_user)
|
||||
mock_init_message.assert_called_once_with(mock_entity_xml, from_user.handle, from_user.private_key)
|
||||
assert data == {"xml": "xmldata"}
|
||||
@patch("federation.protocols.diaspora.protocol.MagicEnvelope")
|
||||
def test_build_send_does_right_calls(self, mock_me):
|
||||
mock_render = Mock(return_value="rendered")
|
||||
mock_me_instance = Mock(render=mock_render)
|
||||
mock_me.return_value = mock_me_instance
|
||||
protocol = Protocol()
|
||||
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(
|
||||
private_key=private_key, handle="johnny@localhost",
|
||||
))
|
||||
mock_me.assert_called_once_with(
|
||||
etree.tostring(entity.to_xml()), private_key=private_key, author_handle="johnny@localhost",
|
||||
)
|
||||
mock_render.assert_called_once_with()
|
||||
assert data == "rendered"
|
||||
|
||||
@patch.object(Protocol, "init_message")
|
||||
@patch.object(Protocol, "create_salmon_envelope")
|
||||
def test_build_send_uses_outbound_doc(self, mock_create_salmon, mock_init_message):
|
||||
@patch("federation.protocols.diaspora.protocol.MagicEnvelope")
|
||||
def test_build_send_uses_outbound_doc(self, mock_me):
|
||||
protocol = self.init_protocol()
|
||||
entity = Mock(to_xml=Mock(return_value=Mock()), outbound_doc="outbound_doc")
|
||||
outbound_doc = etree.fromstring("<xml>foo</xml>")
|
||||
entity = Mock(outbound_doc=outbound_doc)
|
||||
from_user = Mock(handle="foobar", private_key="barfoo")
|
||||
protocol.build_send(entity, from_user)
|
||||
mock_init_message.assert_called_once_with("outbound_doc", from_user.handle, from_user.private_key)
|
||||
mock_me.assert_called_once_with(b"<xml>foo</xml>", private_key=from_user.private_key, author_handle="foobar")
|
||||
|
||||
@patch("federation.protocols.diaspora.protocol.EncryptedPayload.decrypt")
|
||||
def test_get_json_payload_magic_envelope(self, mock_decrypt):
|
||||
|
|
Ładowanie…
Reference in New Issue