Start implementing send parts for diaspora protocol

merge-requests/130/head
Jason Robinson 2015-07-06 01:08:27 +03:00
rodzic 394dbab0e1
commit b143a1765b
4 zmienionych plików z 271 dodań i 8 usunięć

Wyświetl plik

@ -33,3 +33,8 @@ def handle_receive(payload, user=None, sender_key_fetcher=None):
entities = mappers.message_to_objects(message) entities = mappers.message_to_objects(message)
return sender, found_protocol.PROTOCOL_NAME, entities return sender, found_protocol.PROTOCOL_NAME, entities
def handle_send():
"""Send."""
pass

Wyświetl plik

@ -0,0 +1,54 @@
from dateutil.tz import tzlocal, tzutc
from lxml import etree
def ensure_timezone(dt, tz=None):
"""
Make sure the datetime <dt> has a timezone set, using timezone <tz> if it
doesn't. <tz> defaults to the local timezone.
"""
if dt.tzinfo is None:
return dt.replace(tzinfo=tz or tzlocal())
else:
return dt
class EntityConverter(object):
def __init__(self, entity):
self.entity = entity
self.entity_type = entity.__class__.__name__.lower()
def struct_to_xml(self, node, struct):
"""
Turn a list of dicts into XML nodes with tag names taken from the dict
keys and element text taken from dict values. This is a list of dicts
so that the XML nodes can be ordered in the XML output.
"""
for obj in struct:
for k, v in obj.items():
etree.SubElement(node, k).text = v
def convert_to_xml(self):
if hasattr(self, "%s_to_xml" % self.entity_type):
method_name = "%s_to_xml" % self.entity_type
return method_name()
def format_dt(cls, dt):
"""
Format a datetime in the way that D* nodes expect.
"""
return ensure_timezone(dt).astimezone(tzutc()).strftime(
'%Y-%m-%d %H:%M:%S %Z'
)
def post_to_xml(self):
req = etree.Element("status_message")
self.struct_to_xml(req, [
{'raw_message': self.entity.raw_content},
{'guid': self.entity.guid},
{'diaspora_handle': self.entity.handle},
{'public': 'true' if self.entity.public else 'false'},
{'created_at': self.format_dt(self.entity.created_at)}
])
return req

Wyświetl plik

@ -23,12 +23,18 @@ class BaseProtocol(object):
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
def _build(self, *args, **kwargs): def build_send(self, *args, **kwargs):
"""Build a payload.""" """Build a payload for sending.
raise NotImplementedError("Implement in subclass")
def send(self, *args, **kwargs): Args:
"""Send a payload.""" from_user (obj) - The user object who is sending
Must contain attributes `handle` and `private_key`
to_user (obj) - The user object we are sending to
Must contain attribute `key` (public key)
generator (function) - Generator function to generate object for sending
"""
raise NotImplementedError("Implement in subclass") raise NotImplementedError("Implement in subclass")
def receive(self, payload, user=None, sender_key_fetcher=None, *args, **kwargs): def receive(self, payload, user=None, sender_key_fetcher=None, *args, **kwargs):

Wyświetl plik

@ -1,13 +1,15 @@
from base64 import b64decode, urlsafe_b64decode, b64encode from base64 import b64decode, urlsafe_b64decode, b64encode, urlsafe_b64encode
from json import loads from json import loads, dumps
from urllib.parse import unquote_plus from urllib.parse import unquote_plus, quote_plus, urlencode
from Crypto.Cipher import AES, PKCS1_v1_5 from Crypto.Cipher import AES, PKCS1_v1_5
from Crypto.Hash import SHA256 from Crypto.Hash import SHA256
from Crypto.PublicKey import RSA from Crypto.PublicKey import RSA
from Crypto.Random import get_random_bytes
from Crypto.Signature import PKCS1_v1_5 as PKCSSign from Crypto.Signature import PKCS1_v1_5 as PKCSSign
from lxml import etree from lxml import etree
from federation.entities.diaspora.generators import EntityConverter
from federation.exceptions import EncryptedMessageError, NoHeaderInMessageError, NoSenderKeyFoundError from federation.exceptions import EncryptedMessageError, NoHeaderInMessageError, NoSenderKeyFoundError
from federation.protocols.base import BaseProtocol from federation.protocols.base import BaseProtocol
@ -152,3 +154,199 @@ class Protocol(BaseProtocol):
return data[0:-ord(data[-1])] return data[0:-ord(data[-1])]
else: else:
return data[0:-data[-1]] return data[0:-data[-1]]
def build_send(self, from_user, to_user, entity, *args, **kwargs):
"""Build POST data for sending out to remotes."""
converter = EntityConverter(entity)
xml = converter.convert_to_xml()
self.init_message(xml, from_user.handle, from_user.private_key)
xml = quote_plus(
self.create_salmon_envelope(to_user.key))
data = urlencode({
'xml': xml
})
return data
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 = 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 = 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_public_key):
"""
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.
"""
nsmap = {
None: PROTOCOL_NS,
'me': 'http://salmon-protocol.org/ns/magic-env'
}
doc = etree.Element("{%s}diaspora" % nsmap[None], nsmap=nsmap)
if recipient_public_key:
doc.append(self.create_encrypted_header(recipient_public_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_public_key:
payload = urlsafe_b64encode(b64encode(
self.create_encrypted_payload())).decode("ascii")
else:
payload = urlsafe_b64encode(self.create_payload()).decode("ascii")
# Split every 60 chars
payload = '\n'.join([payload[start:start+60]
for start in range(0, len(payload), 60)])
payload = payload + "\n"
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)