diff --git a/Makefile b/Makefile
index 6d5bbcccd898051280a361c849c31f4c71f68ca5..082cdb3006dd598516c9536d52b057b1c61141fa 100644
--- a/Makefile
+++ b/Makefile
@@ -1,6 +1,6 @@
 FILES=`find lib include tests examples -type f -type f \( -iname "*.cpp" -o -iname "*.hpp" \)`
 
-SYNAPSE_IMAGE="avhost/docker-matrix:v0.33.3"
+SYNAPSE_IMAGE="avhost/docker-matrix:v0.33.4"
 
 DEPS_BUILD_DIR=.deps
 DEPS_SOURCE_DIR=deps
diff --git a/include/mtxclient/crypto/client.hpp b/include/mtxclient/crypto/client.hpp
index 3015d95ef4062a916df54a6c694ef618c5bfbea2..ef16c3f5fc2147bf11ce49fa66f85853bb4696fa 100644
--- a/include/mtxclient/crypto/client.hpp
+++ b/include/mtxclient/crypto/client.hpp
@@ -226,6 +226,21 @@ matches_inbound_session_from(OlmSession *session,
                              const std::string &id_key,
                              const std::string &one_time_key_message);
 
+std::string
+encrypt_exported_sessions(const mtx::crypto::ExportedSessionKeys &keys, std::string pass);
+
+mtx::crypto::ExportedSessionKeys
+decrypt_exported_sessions(const std::string &data, std::string pass);
+
+std::string
+base642bin(const std::string &b64);
+
+std::string
+bin2base64(const std::string &b64);
+
+BinaryBuf
+derive_key(const std::string &pass, const BinaryBuf &salt);
+
 //! Verify a signature object as obtained from the response of /keys/query endpoint
 bool
 verify_identity_signature(nlohmann::json obj, const DeviceId &device_id, const UserId &user_id);
diff --git a/include/mtxclient/crypto/types.hpp b/include/mtxclient/crypto/types.hpp
index e0a30cd203ebe8f2f0f4979f56a1f4d1812ce30b..287757512fbb98ca1b9ba388f6c49e500053d3f0 100644
--- a/include/mtxclient/crypto/types.hpp
+++ b/include/mtxclient/crypto/types.hpp
@@ -10,8 +10,26 @@ STRONG_TYPE(RoomId, std::string)
 namespace mtx {
 namespace crypto {
 
-static constexpr const char *ED25519    = "ed25519";
-static constexpr const char *CURVE25519 = "curve25519";
+constexpr auto ED25519     = "ed25519";
+constexpr auto CURVE25519  = "curve25519";
+constexpr auto MEGOLM_ALGO = "m.megolm.v1.aes-sha2";
+
+struct ExportedSession
+{
+        std::map<std::string, std::string> sender_claimed_keys;   // currently unused.
+        std::vector<std::string> forwarding_curve25519_key_chain; // currently unused.
+
+        std::string algorithm = MEGOLM_ALGO;
+        std::string room_id;
+        std::string sender_key;
+        std::string session_id;
+        std::string session_key;
+};
+
+struct ExportedSessionKeys
+{
+        std::vector<ExportedSession> sessions;
+};
 
 struct IdentityKeys
 {
@@ -19,6 +37,58 @@ struct IdentityKeys
         std::string ed25519;
 };
 
+struct OneTimeKeys
+{
+        using KeyId      = std::string;
+        using EncodedKey = std::string;
+
+        std::map<KeyId, EncodedKey> curve25519;
+};
+
+inline void
+to_json(nlohmann::json &obj, const ExportedSession &s)
+{
+        obj["sender_claimed_keys"]             = s.sender_claimed_keys;
+        obj["forwarding_curve25519_key_chain"] = s.forwarding_curve25519_key_chain;
+
+        obj["algorithm"]   = s.algorithm;
+        obj["room_id"]     = s.room_id;
+        obj["sender_key"]  = s.sender_key;
+        obj["session_id"]  = s.session_id;
+        obj["session_key"] = s.session_key;
+}
+
+inline void
+from_json(const nlohmann::json &obj, ExportedSession &s)
+{
+        s.room_id     = obj.at("room_id").get<std::string>();
+        s.sender_key  = obj.at("sender_key").get<std::string>();
+        s.session_id  = obj.at("session_id").get<std::string>();
+        s.session_key = obj.at("session_key").get<std::string>();
+
+        using ClaimedKeys = std::map<std::string, std::string>;
+        using KeyChain    = std::vector<std::string>;
+
+        if (obj.find("sender_claimed_keys") != obj.end())
+                s.sender_claimed_keys = obj.at("sender_claimed_keys").get<ClaimedKeys>();
+
+        if (obj.find("forwarding_curve25519_key_chain") != obj.end())
+                s.forwarding_curve25519_key_chain =
+                  obj.at("forwarding_curve25519_key_chain").get<KeyChain>();
+}
+
+inline void
+to_json(nlohmann::json &obj, const ExportedSessionKeys &keys)
+{
+        obj["sessions"] = keys.sessions;
+}
+
+inline void
+from_json(const nlohmann::json &obj, ExportedSessionKeys &keys)
+{
+        keys.sessions = obj.at("sessions").get<std::vector<ExportedSession>>();
+}
+
 inline void
 to_json(nlohmann::json &obj, const IdentityKeys &keys)
 {
@@ -33,24 +103,17 @@ from_json(const nlohmann::json &obj, IdentityKeys &keys)
         keys.curve25519 = obj.at(CURVE25519).get<std::string>();
 }
 
-struct OneTimeKeys
-{
-        using KeyId      = std::string;
-        using EncodedKey = std::string;
-
-        std::map<KeyId, EncodedKey> curve25519;
-};
-
 inline void
 to_json(nlohmann::json &obj, const OneTimeKeys &keys)
 {
-        obj["curve25519"] = keys.curve25519;
+        obj[CURVE25519] = keys.curve25519;
 }
 
 inline void
 from_json(const nlohmann::json &obj, OneTimeKeys &keys)
 {
-        keys.curve25519 = obj.at("curve25519").get<std::map<std::string, std::string>>();
-}
-}
+        keys.curve25519 = obj.at(CURVE25519).get<std::map<std::string, std::string>>();
 }
+
+} // namespace crypto
+} // namespace mtx
diff --git a/lib/crypto/client.cpp b/lib/crypto/client.cpp
index 3254556a017e817b0a38bc77da8f35b003ee15e8..22f95ecd772e77a827efed3982f1c7ef9c1fe77a 100644
--- a/lib/crypto/client.cpp
+++ b/lib/crypto/client.cpp
@@ -1,6 +1,7 @@
 #include <iostream>
 
 #include "mtxclient/crypto/client.hpp"
+#include "mtxclient/crypto/types.hpp"
 
 using json = nlohmann::json;
 using namespace mtx::crypto;
@@ -23,7 +24,7 @@ OlmClient::restore_account(const std::string &saved_data, const std::string &key
         account_ = unpickle<AccountObject>(saved_data, key);
 }
 
-IdentityKeys
+mtx::crypto::IdentityKeys
 OlmClient::identity_keys() const
 {
         auto tmp_buf = create_buffer(olm_account_identity_keys_length(account_.get()));
@@ -80,7 +81,7 @@ OlmClient::generate_one_time_keys(std::size_t number_of_keys)
         return ret;
 }
 
-OneTimeKeys
+mtx::crypto::OneTimeKeys
 OlmClient::one_time_keys()
 {
         auto buf = create_buffer(olm_account_one_time_keys_length(account_.get()));
@@ -497,3 +498,130 @@ mtx::crypto::verify_identity_signature(nlohmann::json obj,
 
         return false;
 }
+
+std::string
+mtx::crypto::encrypt_exported_sessions(const mtx::crypto::ExportedSessionKeys &keys,
+                                       std::string pass)
+{
+        const auto plaintext      = json(keys).dump();
+        const auto msg_len        = plaintext.size();
+        const auto ciphertext_len = crypto_secretbox_MACBYTES + msg_len;
+
+        auto nonce      = create_buffer(crypto_secretbox_NONCEBYTES);
+        auto ciphertext = create_buffer(ciphertext_len);
+
+        auto salt = create_buffer(crypto_pwhash_SALTBYTES);
+        auto key  = derive_key(pass, salt);
+
+        crypto_secretbox_easy(reinterpret_cast<unsigned char *>(ciphertext.data()),
+                              reinterpret_cast<const unsigned char *>(plaintext.data()),
+                              msg_len,
+                              nonce.data(),
+                              reinterpret_cast<const unsigned char *>(key.data()));
+
+        // Format of the output buffer: (nonce + salt + ciphertext)
+        BinaryBuf output{nonce};
+        output.insert(
+          output.end(), std::make_move_iterator(salt.begin()), std::make_move_iterator(salt.end()));
+        output.insert(output.end(),
+                      std::make_move_iterator(ciphertext.begin()),
+                      std::make_move_iterator(ciphertext.end()));
+
+        return std::string(output.begin(), output.end());
+}
+
+mtx::crypto::ExportedSessionKeys
+mtx::crypto::decrypt_exported_sessions(const std::string &data, std::string pass)
+{
+        if (data.size() <
+            crypto_secretbox_MACBYTES + crypto_secretbox_NONCEBYTES + crypto_pwhash_SALTBYTES)
+                throw std::runtime_error{"decrypt_exported_sessions ciphertext too small"};
+
+        const auto nonce_start = data.begin();
+        const auto nonce_end   = nonce_start + crypto_secretbox_NONCEBYTES;
+        auto nonce             = BinaryBuf(nonce_start, nonce_end);
+
+        const auto salt_end = nonce_end + crypto_pwhash_SALTBYTES;
+        auto salt           = BinaryBuf(nonce_end, salt_end);
+
+        auto ciphertext = BinaryBuf(salt_end, data.end());
+        auto decrypted  = create_buffer(ciphertext.size() - crypto_secretbox_MACBYTES);
+
+        auto key = derive_key(pass, salt);
+
+        if (crypto_secretbox_open_easy(decrypted.data(),
+                                       reinterpret_cast<const unsigned char *>(ciphertext.data()),
+                                       ciphertext.size(),
+                                       nonce.data(),
+                                       reinterpret_cast<const unsigned char *>(key.data())) != 0)
+                throw std::runtime_error{"crypto_secretbox_open_easy: failed to decrypt"};
+
+        return json::parse(std::string(decrypted.begin(), decrypted.end()));
+}
+
+std::string
+mtx::crypto::base642bin(const std::string &b64)
+{
+        std::size_t bin_maxlen = b64.size();
+        std::size_t bin_len;
+
+        const char *max_end;
+
+        auto ciphertext = create_buffer(bin_maxlen);
+
+        const int rc = sodium_base642bin(reinterpret_cast<unsigned char *>(ciphertext.data()),
+                                         ciphertext.size(),
+                                         b64.data(),
+                                         b64.size(),
+                                         nullptr,
+                                         &bin_len,
+                                         &max_end,
+                                         sodium_base64_VARIANT_ORIGINAL);
+        if (rc != 0)
+                throw std::runtime_error{"base642bin failed"};
+
+        if (bin_len != bin_maxlen)
+                ciphertext.resize(bin_len);
+
+        return std::string(std::make_move_iterator(ciphertext.begin()),
+                           std::make_move_iterator(ciphertext.end()));
+}
+
+std::string
+mtx::crypto::bin2base64(const std::string &bin)
+{
+        auto base64buf =
+          create_buffer(sodium_base64_encoded_len(bin.size(), sodium_base64_VARIANT_ORIGINAL));
+
+        sodium_bin2base64(reinterpret_cast<char *>(base64buf.data()),
+                          base64buf.size(),
+                          reinterpret_cast<const unsigned char *>(bin.data()),
+                          bin.size(),
+                          sodium_base64_VARIANT_ORIGINAL);
+
+        // Removing the null byte.
+        return std::string(base64buf.begin(), base64buf.end() - 1);
+}
+
+BinaryBuf
+mtx::crypto::derive_key(const std::string &pass, const BinaryBuf &salt)
+{
+        if (salt.size() != crypto_pwhash_SALTBYTES)
+                throw std::runtime_error{"derive_key: invalid buffer size for salt"};
+
+        auto key = create_buffer(crypto_secretbox_KEYBYTES);
+
+        // Derive a key from the user provided password.
+        if (crypto_pwhash(key.data(),
+                          key.size(),
+                          pass.data(),
+                          pass.size(),
+                          salt.data(),
+                          crypto_pwhash_OPSLIMIT_INTERACTIVE,
+                          crypto_pwhash_MEMLIMIT_INTERACTIVE,
+                          crypto_pwhash_ALG_DEFAULT) != 0) {
+                throw std::runtime_error{"crypto_pwhash: out of memory"};
+        }
+
+        return key;
+}
diff --git a/lib/crypto/types.cpp b/lib/crypto/types.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..b3f93ad5abb438f5b78b136d004d35cb1b6aa3cc
--- /dev/null
+++ b/lib/crypto/types.cpp
@@ -0,0 +1,77 @@
+#include "mtxclient/crypto/types.hpp"
+
+namespace mtx {
+namespace crypto {
+
+void
+to_json(nlohmann::json &obj, const ExportedSession &s)
+{
+        obj["sender_claimed_keys"]             = s.sender_claimed_keys;
+        obj["forwarding_curve25519_key_chain"] = s.forwarding_curve25519_key_chain;
+
+        obj["algorithm"]   = s.algorithm;
+        obj["room_id"]     = s.room_id;
+        obj["sender_key"]  = s.sender_key;
+        obj["session_id"]  = s.session_id;
+        obj["session_key"] = s.session_key;
+}
+
+void
+from_json(const nlohmann::json &obj, ExportedSession &s)
+{
+        s.room_id     = obj.at("room_id").get<std::string>();
+        s.sender_key  = obj.at("sender_key").get<std::string>();
+        s.session_id  = obj.at("session_id").get<std::string>();
+        s.session_key = obj.at("session_key").get<std::string>();
+
+        using ClaimedKeys = std::map<std::string, std::string>;
+        using KeyChain    = std::vector<std::string>;
+
+        if (obj.find("sender_claimed_keys") != obj.end())
+                s.sender_claimed_keys = obj.at("sender_claimed_keys").get<ClaimedKeys>();
+
+        if (obj.find("forwarding_curve25519_key_chain") != obj.end())
+                s.forwarding_curve25519_key_chain =
+                  obj.at("forwarding_curve25519_key_chain").get<KeyChain>();
+}
+
+void
+to_json(nlohmann::json &obj, const ExportedSessionKeys &keys)
+{
+        obj["sessions"] = keys.sessions;
+}
+
+void
+from_json(const nlohmann::json &obj, ExportedSessionKeys &keys)
+{
+        keys.sessions = obj.at("sessions").get<std::vector<ExportedSession>>();
+}
+
+void
+to_json(nlohmann::json &obj, const IdentityKeys &keys)
+{
+        obj[ED25519]    = keys.ed25519;
+        obj[CURVE25519] = keys.curve25519;
+}
+
+void
+from_json(const nlohmann::json &obj, IdentityKeys &keys)
+{
+        keys.ed25519    = obj.at(ED25519).get<std::string>();
+        keys.curve25519 = obj.at(CURVE25519).get<std::string>();
+}
+
+void
+to_json(nlohmann::json &obj, const OneTimeKeys &keys)
+{
+        obj[CURVE25519] = keys.curve25519;
+}
+
+void
+from_json(const nlohmann::json &obj, OneTimeKeys &keys)
+{
+        keys.curve25519 = obj.at(CURVE25519).get<std::map<std::string, std::string>>();
+}
+
+} // namespace crypto
+} // namespace mtx
diff --git a/tests/e2ee.cpp b/tests/e2ee.cpp
index fdaf9421579dc98d8394df319eac68f1f9dc229e..1a1f0e05ff484de3355680ee5322307c48328fda 100644
--- a/tests/e2ee.cpp
+++ b/tests/e2ee.cpp
@@ -6,6 +6,7 @@
 #include <gtest/gtest.h>
 
 #include "mtxclient/crypto/client.hpp"
+#include "mtxclient/crypto/types.hpp"
 #include "mtxclient/http/client.hpp"
 
 #include "mtx/requests.hpp"
@@ -1035,6 +1036,41 @@ TEST(Encryption, PickleMegolmSessions)
         EXPECT_EQ(std::string((char *)plaintext.data.data(), plaintext.data.size()), SECRET);
 }
 
+TEST(Base64, EncodingDecoding)
+{
+        std::string random_str =
+          "+7TE+9qmFWHPnrBLd03MtoXsRlhYaQt2tLBg4kZJI+NFcXVxqNUI1S3c97eV8aVgSj1/"
+          "eo8PsnRNO29c2TgPLXvah2GDl90ehHjzH/"
+          "vMBJKPdqyE31ch7NYBgvLBVoesrRyDoIYDlbRhHiRDTmLKMC55WN1YvDJu2Pvg3WxZiANobk"
+          "0EPzHABqOYLaYiVxFrdko7mm8pDZXlatys+dvLv9Zf6lxfd/5MPK1C52m/UhnrZ3shS/"
+          "XBzxRfBikZQjl7C9IMo7l170ffipN8QHb5LmZlj4V41DUJHCU=";
+
+        EXPECT_EQ(base642bin(bin2base64(random_str)), random_str);
+        EXPECT_EQ(bin2base64(base642bin(random_str)), random_str);
+}
+
+TEST(ExportSessions, EncryptDecrypt)
+{
+        constexpr auto PASS = "secret_passphrase";
+
+        ExportedSession s1;
+        s1.room_id     = "!room_id:example.org";
+        s1.session_id  = "sid";
+        s1.session_key = "skey";
+
+        ExportedSessionKeys keys;
+        keys.sessions = {s1, s1, s1};
+
+        std::string ciphertext = mtx::crypto::encrypt_exported_sessions(keys, PASS);
+        EXPECT_TRUE(ciphertext.size() > 0);
+
+        auto encoded = bin2base64(ciphertext);
+        auto decoded = base642bin(encoded);
+
+        auto restored_keys = mtx::crypto::decrypt_exported_sessions(decoded, PASS);
+        EXPECT_EQ(json(keys).dump(), json(restored_keys).dump());
+}
+
 TEST(Encryption, DISABLED_HandleRoomKeyEvent) {}
 TEST(Encryption, DISABLED_HandleRoomKeyRequestEvent) {}
 TEST(Encryption, DISABLED_HandleNewDevices) {}