kopia lustrzana https://gitlab.com/jaywink/federation
Start implementing send parts for diaspora protocol
rodzic
394dbab0e1
commit
b143a1765b
|
@ -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
|
||||||
|
|
|
@ -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
|
|
@ -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):
|
||||||
|
|
|
@ -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)
|
||||||
|
|
Ładowanie…
Reference in New Issue