diff --git a/CHANGELOG.md b/CHANGELOG.md
index 63c2f0f..3ba3370 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,10 @@
 
 ## [unreleased]
 
+### Added
+* New style Diaspora private encrypted JSON payloads are now supported in the receiving side. Outbound private Diaspora payloads are still sent as legacy encrypted payloads. ([issue](https://github.com/jaywink/federation/issues/83))
+    * No additional changes need to be made when calling `handle_receive` from your task processing. Just pass in the full received XML or JSON payload as a string with recipient user object as before.
+
 ### Fixed
 * Fix getting sender from a combination of legacy Diaspora encrypted payload and new entity names (for example `author`). This combination probably only existed in this library.
 * Correctly extend entity `_children`. Certain Diaspora payloads caused `_children` for an entity to be written over by an empty list, causing for example status message photos to not be saved. Correctly do an extend on it. ([issue](https://github.com/jaywink/federation/issues/89))
@@ -9,6 +13,7 @@
 
 ### Removed
 * `Post.photos` entity attribute was never used by any code and has been removed. Child entities of type `Image` are stored in the `Post._children` as before.
+* Removed deprecated user private key lookup using `user.key` in Diaspora receive processing. Passed in `user` objects must now have a `private_key` attribute. 
 
 ## [0.12.0] - 2017-05-22
 
diff --git a/federation/protocols/base.py b/federation/protocols/base.py
index 0f48de3..e298ce0 100644
--- a/federation/protocols/base.py
+++ b/federation/protocols/base.py
@@ -37,7 +37,7 @@ class BaseProtocol(object):
         """
         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):
         """Receive a payload.
 
         Args:
diff --git a/federation/protocols/diaspora/encrypted.py b/federation/protocols/diaspora/encrypted.py
new file mode 100644
index 0000000..ccf23a4
--- /dev/null
+++ b/federation/protocols/diaspora/encrypted.py
@@ -0,0 +1,30 @@
+import json
+from base64 import b64decode
+
+from Crypto.Cipher import PKCS1_v1_5, AES
+from lxml import etree
+
+
+def pkcs7_unpad(data):
+    """Remove the padding bytes that were added at point of encryption."""
+    if isinstance(data, str):
+        return data[0:-ord(data[-1])]
+    else:
+        return data[0:-data[-1]]
+
+
+class EncryptedPayload:
+    """Diaspora encrypted JSON payloads."""
+
+    @staticmethod
+    def decrypt(payload, private_key):
+        """Decrypt an encrypted JSON payload and return the Magic Envelope document inside."""
+        cipher = PKCS1_v1_5.new(private_key)
+        aes_key_str = cipher.decrypt(b64decode(payload.get("aes_key")), sentinel=None)
+        aes_key = json.loads(aes_key_str.decode("utf-8"))
+        key = b64decode(aes_key.get("key"))
+        iv = b64decode(aes_key.get("iv"))
+        encrypted_magic_envelope = b64decode(payload.get("encrypted_magic_envelope"))
+        encrypter = AES.new(key, AES.MODE_CBC, iv)
+        content = encrypter.decrypt(encrypted_magic_envelope)
+        return etree.fromstring(pkcs7_unpad(content))
diff --git a/federation/protocols/diaspora/magic_envelope.py b/federation/protocols/diaspora/magic_envelope.py
index ff9cc13..46a9a08 100644
--- a/federation/protocols/diaspora/magic_envelope.py
+++ b/federation/protocols/diaspora/magic_envelope.py
@@ -8,7 +8,7 @@ from lxml import etree
 NAMESPACE = "http://salmon-protocol.org/ns/magic-env"
 
 
-class MagicEnvelope():
+class MagicEnvelope:
     """Diaspora protocol magic envelope.
 
     See: http://diaspora.github.io/diaspora_federation/federation/magicsig.html
diff --git a/federation/protocols/diaspora/protocol.py b/federation/protocols/diaspora/protocol.py
index e65d3ef..73c417e 100644
--- a/federation/protocols/diaspora/protocol.py
+++ b/federation/protocols/diaspora/protocol.py
@@ -1,6 +1,5 @@
 import json
 import logging
-import warnings
 from base64 import b64decode, urlsafe_b64decode, b64encode, urlsafe_b64encode
 from urllib.parse import unquote_plus
 
@@ -13,7 +12,9 @@ from lxml import etree
 
 from federation.exceptions import EncryptedMessageError, NoSenderKeyFoundError, SignatureVerificationError
 from federation.protocols.base import BaseProtocol
+from federation.protocols.diaspora.encrypted import EncryptedPayload
 from federation.protocols.diaspora.magic_envelope import MagicEnvelope
+from federation.utils.text import decode_if_bytes
 
 logger = logging.getLogger("federation")
 
@@ -29,7 +30,7 @@ def identify_payload(payload):
     """
     # Private encrypted JSON payload
     try:
-        data = json.loads(payload)
+        data = json.loads(decode_if_bytes(payload))
         if "encrypted_magic_envelope" in data:
             return True
     except Exception:
@@ -55,17 +56,36 @@ class Protocol(BaseProtocol):
 
     Mostly taken from Pyaspora (https://github.com/lukeross/pyaspora).
     """
-    def receive(self, payload, user=None, sender_key_fetcher=None, skip_author_verification=False, *args, **kwargs):
+    def __init__(self):
+        super().__init__()
+        self.encrypted = self.legacy = False
+
+    def get_json_payload_magic_envelope(self, payload):
+        """Encrypted JSON payload"""
+        private_key = self._get_user_key(self.user)
+        return EncryptedPayload.decrypt(payload=payload, private_key=private_key)
+
+    def store_magic_envelope_doc(self, payload):
+        """Get the Magic Envelope, trying JSON first."""
+        try:
+            json_payload = json.loads(decode_if_bytes(payload))
+        except ValueError:
+            # XML payload
+            xml = unquote_plus(payload)
+            xml = xml.lstrip().encode("utf-8")
+            logger.debug("diaspora.protocol.store_magic_envelope_doc: xml payload: %s", xml)
+            self.doc = etree.fromstring(xml)
+        else:
+            logger.debug("diaspora.protocol.store_magic_envelope_doc: json payload: %s", json_payload)
+            self.doc = self.get_json_payload_magic_envelope(json_payload)
+
+    def receive(self, payload, user=None, sender_key_fetcher=None, skip_author_verification=False):
         """Receive a payload.
 
         For testing purposes, `skip_author_verification` can be passed. Authorship will not be verified."""
         self.user = user
         self.get_contact_key = sender_key_fetcher
-        # Prepare payload
-        xml = unquote_plus(payload)
-        xml = xml.lstrip().encode("utf-8")
-        logger.debug("diaspora.protocol.receive: xml content: %s", xml)
-        self.doc = etree.fromstring(xml)
+        self.store_magic_envelope_doc(payload)
         # Check for a legacy header
         self.find_header()
         # Open payload and get actual message
@@ -78,17 +98,11 @@ class Protocol(BaseProtocol):
         return self.sender_handle, self.content
 
     def _get_user_key(self, user):
-        if not hasattr(self.user, "private_key") or not self.user.private_key:
-            if hasattr(self.user, "key") and self.user.key:
-                warnings.warn("Using `key` in user object for private key has been deprecated. Please "
-                              "have available `private_key` instead. Usage of `key` will be removed after 0.8.0.",
-                              DeprecationWarning)
-                return self.user.key
+        if not getattr(self.user, "private_key", None):
             raise EncryptedMessageError("Cannot decrypt private message without user key")
         return self.user.private_key
 
     def find_header(self):
-        self.encrypted = self.legacy = False
         self.header = self.doc.find(".//{"+PROTOCOL_NS+"}header")
         if self.header != None:
             # Legacy public header found
diff --git a/federation/utils/text.py b/federation/utils/text.py
new file mode 100644
index 0000000..310a5e1
--- /dev/null
+++ b/federation/utils/text.py
@@ -0,0 +1,5 @@
+def decode_if_bytes(text):
+    try:
+        return text.decode("utf-8")
+    except AttributeError:
+        return text

Add support for receiving new style Diaspora private message payloads
See https://github.com/diaspora/diaspora_federation/issues/30
diff --git a/federation/tests/protocols/diaspora/test_encrypted.py b/federation/tests/protocols/diaspora/test_encrypted.py
new file mode 100644
index 0000000..5d10988
--- /dev/null
+++ b/federation/tests/protocols/diaspora/test_encrypted.py
@@ -0,0 +1,33 @@
+from unittest.mock import patch, Mock
+
+from Crypto.Cipher import AES
+
+from federation.protocols.diaspora.encrypted import pkcs7_unpad, EncryptedPayload
+
+
+def test_pkcs7_unpad():
+    assert pkcs7_unpad(b"foobar\x02\x02") == b"foobar"
+    assert pkcs7_unpad("foobar\x02\x02") == "foobar"
+
+
+class TestEncryptedPayload:
+    @patch("federation.protocols.diaspora.encrypted.PKCS1_v1_5.new")
+    @patch("federation.protocols.diaspora.encrypted.AES.new")
+    @patch("federation.protocols.diaspora.encrypted.pkcs7_unpad", side_effect=lambda x: x)
+    @patch("federation.protocols.diaspora.encrypted.b64decode", side_effect=lambda x: x)
+    def test_decrypt(self, mock_decode, mock_unpad, mock_aes, mock_pkcs1):
+        mock_decrypt = Mock(return_value=b'{"iv": "foo", "key": "bar"}')
+        mock_pkcs1.return_value = Mock(decrypt=mock_decrypt)
+        mock_encrypter = Mock(return_value="<foo>bar</foo>")
+        mock_aes.return_value = Mock(decrypt=mock_encrypter)
+        doc = EncryptedPayload.decrypt(
+            {"aes_key": '{"iv": "foo", "key": "bar"}', "encrypted_magic_envelope": "magically encrypted"},
+            "private_key",
+        )
+        mock_pkcs1.assert_called_once_with("private_key")
+        mock_decrypt.assert_called_once_with('{"iv": "foo", "key": "bar"}', sentinel=None)
+        assert mock_decode.call_count == 4
+        mock_aes.assert_called_once_with("bar", AES.MODE_CBC, "foo")
+        mock_encrypter.assert_called_once_with("magically encrypted")
+        assert doc.tag == "foo"
+        assert doc.text == "bar"
diff --git a/federation/tests/protocols/diaspora/test_protocol.py b/federation/tests/protocols/diaspora/test_protocol.py
index 3fd5a37..f33a31a 100644
--- a/federation/tests/protocols/diaspora/test_protocol.py
+++ b/federation/tests/protocols/diaspora/test_protocol.py
@@ -155,3 +155,24 @@ class TestDiasporaProtocol(DiasporaTestBase):
         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.EncryptedPayload.decrypt")
+    def test_get_json_payload_magic_envelope(self, mock_decrypt):
+        protocol = Protocol()
+        protocol.user = MockUser()
+        protocol.get_json_payload_magic_envelope("payload")
+        mock_decrypt.assert_called_once_with(payload="payload", private_key="foobar")
+
+    @patch.object(Protocol, "get_json_payload_magic_envelope", return_value=etree.fromstring("<foo>bar</foo>"))
+    def test_store_magic_envelope_doc_json_payload(self, mock_store):
+        protocol = Protocol()
+        protocol.store_magic_envelope_doc('{"foo": "bar"}')
+        mock_store.assert_called_once_with({"foo": "bar"})
+        assert protocol.doc.tag == "foo"
+        assert protocol.doc.text == "bar"
+
+    def test_store_magic_envelope_doc_xml_payload(self):
+        protocol = Protocol()
+        protocol.store_magic_envelope_doc("<foo>bar</foo>")
+        assert protocol.doc.tag == "foo"
+        assert protocol.doc.text == "bar"
diff --git a/federation/tests/utils/test_text.py b/federation/tests/utils/test_text.py
new file mode 100644
index 0000000..7f629d3
--- /dev/null
+++ b/federation/tests/utils/test_text.py
@@ -0,0 +1,6 @@
+from federation.utils.text import decode_if_bytes
+
+
+def test_decode_if_bytes():
+    assert decode_if_bytes(b"foobar") == "foobar"
+    assert decode_if_bytes("foobar") == "foobar"

pip install -e .[dev]
parse_log_pytest
requirements.txt
pytest pytest-cov pytest-warnings
apt-get update apt-get install -y gcc libxml2-dev libxslt1-dev
3.6
dev-requirements.txt
pytest --no-header -rA --tb=line --color=no -p no:cacheprovider -W ignore::DeprecationWarning
alabaster==0.7.13
arrow==1.2.3
attrs==22.2.0
Babel==2.11.0
certifi==2021.5.30
charset-normalizer==2.0.12
codecov==2.1.13
colorama==0.4.5
commonmark==0.9.1
coverage==6.2
cssselect==1.1.0
dirty-validators==0.5.4
docutils==0.18.1
factory-boy==3.2.1
Faker==14.2.1
-e git+https://github.com/jaywink/federation.git@5580b5143f3fb536d791ec74c32a92fb03d5c7be#egg=federation
idna==3.10
imagesize==1.4.1
importlib-metadata==4.8.3
iniconfig==1.1.1
isodate==0.6.1
Jinja2==3.0.3
jsonschema==3.2.0
livereload==2.6.3
lxml==5.3.1
MarkupSafe==2.0.1
packaging==21.3
pluggy==1.0.0
py==1.11.0
pycrypto==2.6.1
Pygments==2.14.0
pyparsing==3.1.4
pyrsistent==0.18.0
pytest==7.0.1
pytest-cov==4.0.0
pytest-warnings==0.3.1
python-dateutil==2.9.0.post0
python-xrd==0.1
pytz==2025.2
recommonmark==0.7.1
requests==2.27.1
six==1.17.0
snowballstemmer==2.2.0
Sphinx==5.3.0
sphinx-autobuild==2021.3.14
sphinxcontrib-applehelp==1.0.2
sphinxcontrib-devhelp==1.0.2
sphinxcontrib-htmlhelp==2.0.0
sphinxcontrib-jsmath==1.0.1
sphinxcontrib-qthelp==1.0.3
sphinxcontrib-serializinghtml==1.1.5
tomli==1.2.3
tornado==6.1
typing_extensions==4.1.1
urllib3==1.26.20
zipp==3.6.0

name: federation
channels:
  - defaults
  - https://repo.anaconda.com/pkgs/main
  - https://repo.anaconda.com/pkgs/r
  - conda-forge
dependencies:
  - _libgcc_mutex=0.1=main
  - _openmp_mutex=5.1=1_gnu
  - ca-certificates=2025.2.25=h06a4308_0
  - certifi=2021.5.30=py36h06a4308_0
  - ld_impl_linux-64=2.40=h12ee557_0
  - libffi=3.3=he6710b0_2
  - libgcc-ng=11.2.0=h1234567_1
  - libgomp=11.2.0=h1234567_1
  - libstdcxx-ng=11.2.0=h1234567_1
  - ncurses=6.4=h6a678d5_0
  - openssl=1.1.1w=h7f8727e_0
  - pip=21.2.2=py36h06a4308_0
  - python=3.6.13=h12debd9_1
  - readline=8.2=h5eee18b_0
  - setuptools=58.0.4=py36h06a4308_0
  - sqlite=3.45.3=h5eee18b_0
  - tk=8.6.14=h39e8969_0
  - wheel=0.37.1=pyhd3eb1b0_0
  - xz=5.6.4=h5eee18b_1
  - zlib=1.2.13=h5eee18b_1
  - pip:
      - alabaster==0.7.13
      - arrow==1.2.3
      - attrs==22.2.0
      - babel==2.11.0
      - charset-normalizer==2.0.12
      - codecov==2.1.13
      - colorama==0.4.5
      - commonmark==0.9.1
      - coverage==6.2
      - cssselect==1.1.0
      - dirty-validators==0.5.4
      - docutils==0.18.1
      - factory-boy==3.2.1
      - faker==14.2.1
      - idna==3.10
      - imagesize==1.4.1
      - importlib-metadata==4.8.3
      - iniconfig==1.1.1
      - isodate==0.6.1
      - jinja2==3.0.3
      - jsonschema==3.2.0
      - livereload==2.6.3
      - lxml==5.3.1
      - markupsafe==2.0.1
      - packaging==21.3
      - pluggy==1.0.0
      - py==1.11.0
      - pycrypto==2.6.1
      - pygments==2.14.0
      - pyparsing==3.1.4
      - pyrsistent==0.18.0
      - pytest==7.0.1
      - pytest-cov==4.0.0
      - pytest-warnings==0.3.1
      - python-dateutil==2.9.0.post0
      - python-xrd==0.1
      - pytz==2025.2
      - recommonmark==0.7.1
      - requests==2.27.1
      - six==1.17.0
      - snowballstemmer==2.2.0
      - sphinx==5.3.0
      - sphinx-autobuild==2021.3.14
      - sphinxcontrib-applehelp==1.0.2
      - sphinxcontrib-devhelp==1.0.2
      - sphinxcontrib-htmlhelp==2.0.0
      - sphinxcontrib-jsmath==1.0.1
      - sphinxcontrib-qthelp==1.0.3
      - sphinxcontrib-serializinghtml==1.1.5
      - tomli==1.2.3
      - tornado==6.1
      - typing-extensions==4.1.1
      - urllib3==1.26.20
      - zipp==3.6.0
prefix: /opt/conda/envs/federation

federation/tests/protocols/diaspora/test_encrypted.py::test_pkcs7_unpad federation/tests/protocols/diaspora/test_encrypted.py::TestEncryptedPayload::test_decrypt federation/tests/protocols/diaspora/test_protocol.py::TestDiasporaProtocol::test_find_unencrypted_header federation/tests/protocols/diaspora/test_protocol.py::TestDiasporaProtocol::test_find_encrypted_header federation/tests/protocols/diaspora/test_protocol.py::TestDiasporaProtocol::test_receive_unencrypted_returns_sender_and_content federation/tests/protocols/diaspora/test_protocol.py::TestDiasporaProtocol::test_receive_encrypted_returns_sender_and_content federation/tests/protocols/diaspora/test_protocol.py::TestDiasporaProtocol::test_receive_raises_on_encrypted_message_and_no_user federation/tests/protocols/diaspora/test_protocol.py::TestDiasporaProtocol::test_receive_raises_on_encrypted_message_and_no_user_key federation/tests/protocols/diaspora/test_protocol.py::TestDiasporaProtocol::test_receive_raises_if_sender_key_cannot_be_found federation/tests/protocols/diaspora/test_protocol.py::TestDiasporaProtocol::test_get_message_content federation/tests/protocols/diaspora/test_protocol.py::TestDiasporaProtocol::test_identify_payload_with_legacy_diaspora_payload federation/tests/protocols/diaspora/test_protocol.py::TestDiasporaProtocol::test_identify_payload_with_diaspora_public_payload federation/tests/protocols/diaspora/test_protocol.py::TestDiasporaProtocol::test_identify_payload_with_diaspora_encrypted_payload federation/tests/protocols/diaspora/test_protocol.py::TestDiasporaProtocol::test_identify_payload_with_other_payload federation/tests/protocols/diaspora/test_protocol.py::TestDiasporaProtocol::test_get_sender_legacy_returns_sender_in_header federation/tests/protocols/diaspora/test_protocol.py::TestDiasporaProtocol::test_get_sender_legacy_returns_sender_in_content federation/tests/protocols/diaspora/test_protocol.py::TestDiasporaProtocol::test_get_sender_legacy_returns_none_if_no_sender_found federation/tests/protocols/diaspora/test_protocol.py::TestDiasporaProtocol::test_build_send federation/tests/protocols/diaspora/test_protocol.py::TestDiasporaProtocol::test_get_json_payload_magic_envelope federation/tests/protocols/diaspora/test_protocol.py::TestDiasporaProtocol::test_store_magic_envelope_doc_json_payload federation/tests/protocols/diaspora/test_protocol.py::TestDiasporaProtocol::test_store_magic_envelope_doc_xml_payload federation/tests/utils/test_text.py::test_decode_if_bytes
federation/protocols/diaspora/protocol.py federation/protocols/diaspora/encrypted.py federation/utils/text.py federation/protocols/diaspora/magic_envelope.py CHANGELOG.md federation/protocols/base.py
federation/protocols/diaspora/protocol.py federation/protocols/diaspora/encrypted.py federation/utils/text.py federation/protocols/diaspora/magic_envelope.py CHANGELOG.md federation/protocols/base.py