Skip to content

Commit

Permalink
Implement loading key backup from security phrase
Browse files Browse the repository at this point in the history
  • Loading branch information
TobiasFella committed Jul 29, 2023
1 parent 035daed commit f57ec5e
Show file tree
Hide file tree
Showing 6 changed files with 239 additions and 2 deletions.
213 changes: 213 additions & 0 deletions Quotient/connection.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,15 @@
#include <QtCore/QStringBuilder>
#include <QtNetwork/QDnsLookup>

#include <openssl/aes.h>
#include <openssl/evp.h>
#include <openssl/hmac.h>
#include <openssl/kdf.h>
#include <openssl/rand.h>
#include <openssl/sha.h>

#include <olm/pk.h>

using namespace Quotient;

// This is very much Qt-specific; STL iterators don't have key() and value()
Expand Down Expand Up @@ -1892,3 +1901,207 @@ Connection* Connection::makeMockConnection(const QString& mxId,
c->d->completeSetup(mxId, true);
return c;
}

QByteArray pbkdf2HmacSha512(const QString & password, const QByteArray& salt, int iterations, int keyLength)
{
QByteArray output(keyLength, u'\0');
PKCS5_PBKDF2_HMAC(password.toLocal8Bit().data(), password.size(), reinterpret_cast<const unsigned char *>(salt.data()), salt.size(), iterations, EVP_sha512(), keyLength, reinterpret_cast<unsigned char *>(output.data()));
return output;
}

QByteArray aesCtr256Encrypt(const QString& plaintext, const QByteArray& aes256Key, const QByteArray& iv)
{
EVP_CIPHER_CTX *ctx = nullptr;
int length = 0;
int ciphertextLength = 0;

auto encrypted = QByteArray(plaintext.size() + AES_BLOCK_SIZE, u'\0');
RAND_bytes(reinterpret_cast<unsigned char*>(encrypted.data()), encrypted.size());
auto data = encrypted.data();
constexpr auto mask = static_cast<std::uint8_t>(~(1U << (63 / 8)));
data[15 - 63 % 8] &= mask;
if (ctx = EVP_CIPHER_CTX_new(); !ctx) {
return {};
}

EVP_EncryptInit_ex(ctx, EVP_aes_256_ctr(), nullptr, reinterpret_cast<const unsigned char*>(aes256Key.data()), reinterpret_cast<const unsigned char*>(iv.data()));

EVP_EncryptUpdate(ctx, reinterpret_cast<unsigned char*>(encrypted.data()), &length, reinterpret_cast<const unsigned char *>(&plaintext.toLatin1().data()[0]), (int)plaintext.size());

ciphertextLength = length;
EVP_EncryptFinal_ex(ctx, reinterpret_cast<unsigned char*>(encrypted.data()) + length, &length);
ciphertextLength += length;
encrypted.resize(ciphertextLength);
EVP_CIPHER_CTX_free(ctx);
return encrypted;
}

struct HkdfKeys {
QByteArray aes;
QByteArray mac;
};

HkdfKeys hkdfSha256(const QByteArray& key, const QByteArray& salt, const QByteArray& info)
{
QByteArray result(64, u'\0');
auto context = EVP_PKEY_CTX_new_id(EVP_PKEY_HKDF, nullptr);

EVP_PKEY_derive_init(context);
EVP_PKEY_CTX_set_hkdf_md(context, EVP_sha256());
EVP_PKEY_CTX_set1_hkdf_salt(context, reinterpret_cast<const unsigned char *>(salt.data()), salt.size());
EVP_PKEY_CTX_set1_hkdf_key(context, reinterpret_cast<const unsigned char *>(key.data()), key.size());
EVP_PKEY_CTX_add1_hkdf_info(context, reinterpret_cast<const unsigned char *>(info.data()), info.size());
std::size_t outputLength = result.size();
EVP_PKEY_derive(context, reinterpret_cast<unsigned char *>(result.data()), &outputLength);
EVP_PKEY_CTX_free(context);

if (outputLength != 64) {
return {};
}

QByteArray macKey = result.mid(32);
result.resize(32);
return {std::move(result), std::move(macKey)};
}

QByteArray hmacSha256(const QByteArray& hmacKey, const QByteArray& data)
{
uint32_t len = SHA256_DIGEST_LENGTH;
QByteArray output(SHA256_DIGEST_LENGTH, u'\0');
HMAC(EVP_sha256(), hmacKey.data(), hmacKey.size(), reinterpret_cast<const unsigned char *>(data.data()), data.size(), reinterpret_cast<unsigned char *>(output.data()), &len);
return output;
}

namespace Quotient {
class QUOTIENT_API GetRoomKeysVersionJob : public BaseJob {
public:
using BaseJob::makeRequestUrl;
static QUrl makeRequestUrl(QUrl baseUrl, const QUrl& mxcUri,
QSize requestedSize);

GetRoomKeysVersionJob()
: BaseJob(HttpVerb::Get, {},
"/_matrix/client/v3/room_keys/version")
{}
};

class QUOTIENT_API GetRoomKeysJob : public BaseJob {
public:
using BaseJob::makeRequestUrl;
static QUrl makeRequestUrl(QUrl baseUrl, const QUrl& mxcUri,
QSize requestedSize);

GetRoomKeysJob(const QString& version)
: BaseJob(HttpVerb::Get, {},
"/_matrix/client/v3/room_keys/keys")
{
QUrlQuery query;
addParam<>(query, QStringLiteral("version"), version);
setRequestQuery(query);
}
};
} // namespace Quotient

QByteArray curve25519AesSha2Decrypt(QByteArray base64_ciphertext, const QByteArray &privateKey, const QByteArray &ephemeral, const QByteArray &mac)
{
auto context = makeCStruct(olm_pk_decryption, olm_pk_decryption_size, olm_clear_pk_decryption);

QByteArray publicKey(olm_pk_key_length(), u'\0');
olm_pk_key_from_private(context.get(), publicKey.data(), publicKey.size(), privateKey.data(), privateKey.size());

QByteArray plaintext(olm_pk_max_plaintext_length(context.get(), base64_ciphertext.size()), u'\0');
std::size_t decrypted_size = olm_pk_decrypt(context.get(), ephemeral.data(), ephemeral.size(), mac.data(), mac.size(), base64_ciphertext.data(), base64_ciphertext.size(), plaintext.data(), plaintext.size());

if (decrypted_size == olm_error()) {
return {};
}
plaintext.resize(decrypted_size);
return plaintext;
}

QByteArray aesCtr256Decrypt(const QByteArray& ciphertext, const QByteArray& aes256Key, const QByteArray& iv)
{
auto context = EVP_CIPHER_CTX_new();
Q_ASSERT(context);

int length = 0;
int plaintextLength = 0;
QByteArray decrypted(ciphertext.size(), u'\0');

EVP_DecryptInit_ex(context, EVP_aes_256_ctr(), nullptr, reinterpret_cast<const unsigned char *>(aes256Key.data()), reinterpret_cast<const unsigned char *>(iv.data()));

EVP_DecryptUpdate(context,
reinterpret_cast<unsigned char *>(decrypted.data()),
&length,
reinterpret_cast<const unsigned char *>(&ciphertext.data()[0]),
(int)ciphertext.size());
plaintextLength = length;

EVP_DecryptFinal_ex(context, reinterpret_cast<unsigned char *>(decrypted.data()) + length, &length);
plaintextLength += length;
decrypted.resize(plaintextLength);
EVP_CIPHER_CTX_free(context);
return decrypted;
}


void Connection::unlockSSSSFromPassword(const QString& password)
{
const auto defaultKey = accountData("m.secret_storage.default_key"_ls)->contentPart<QString>("key"_ls);
const auto &keyEvent = accountData("m.secret_storage.key."_ls + defaultKey);
if (keyEvent->contentPart<QString>("algorithm"_ls) != "m.secret_storage.v1.aes-hmac-sha2"_ls) {
qCWarning(E2EE) << "Unsupported SSSS key algorithm" << keyEvent->contentPart<QString>("algorithm"_ls) << " - aborting.";
return;
}
const auto &passphraseJson = keyEvent->contentPart<QJsonObject>("passphrase"_ls);
if (passphraseJson["algorithm"_ls].toString() != "m.pbkdf2"_ls) {
qCWarning(E2EE) << "Unsupported SSSS passphrase algorithm" << passphraseJson["algorithm"_ls].toString() << " - aborting.";
return;
}
const auto iterations = passphraseJson["iterations"_ls].toInt();
const auto &salt = passphraseJson["salt"_ls].toString();
const auto &iv = keyEvent->contentPart<QString>("iv"_ls);
const auto &mac = keyEvent->contentPart<QString>("mac"_ls);

const auto &decryptionKey = pbkdf2HmacSha512(password, salt.toLatin1(), iterations, 32);

const auto &testKeys = hkdfSha256(decryptionKey, QByteArray(32, u'\0'), QByteArray());
const auto &encrypted = aesCtr256Encrypt(QString(32, u'\0'), testKeys.aes, QByteArray::fromBase64(iv.toLatin1()));
if (hmacSha256(testKeys.mac, encrypted) != QByteArray::fromBase64(mac.toLatin1())) {
qCWarning(E2EE) << "MAC mismatch for secret storage key";
emit keyBackupPasswordWrong();
return;
}
emit keyBackupPasswordCorrect();

const auto& megolmBackupEncrypted = accountData("m.megolm_backup.v1"_ls)->contentPart<QJsonObject>("encrypted"_ls)[defaultKey];

auto keys = hkdfSha256(decryptionKey, QByteArray(32, u'\0'), QByteArrayLiteral("m.megolm_backup.v1"));

auto rawCipher = QByteArray::fromBase64(megolmBackupEncrypted["ciphertext"_ls].toString().toLatin1());
if (QString::fromLatin1(hmacSha256(keys.mac, rawCipher).toBase64()) != megolmBackupEncrypted["mac"_ls].toString()) {
qWarning() << "MAC mismatch for m.megolm_backup.v1";
return;
}
const auto& megolmDecryptionKey = QByteArray::fromBase64(aesCtr256Decrypt(rawCipher, keys.aes, QByteArray::fromBase64(megolmBackupEncrypted["iv"_ls].toString().toLatin1())));

auto job = callApi<GetRoomKeysVersionJob>();
connect(job, &BaseJob::finished, this, [=](){
auto keysJob = callApi<GetRoomKeysJob>(job->jsonData()["version"_ls].toString());
connect(keysJob, &BaseJob::finished, this, [=](){
const auto &rooms = keysJob->jsonData()["rooms"_ls].toObject();
for (const auto& roomId : rooms.keys()) {
if (!room(roomId)) {
continue;
}
const auto &sessions = rooms[roomId]["sessions"_ls].toObject();
for (const auto& sessionId : sessions.keys()) {
const auto &session = sessions[sessionId].toObject();
const auto &sessionData = session["session_data"_ls].toObject();
auto data = QJsonDocument::fromJson(curve25519AesSha2Decrypt(sessionData["ciphertext"_ls].toString().toLatin1(), megolmDecryptionKey, sessionData["ephemeral"_ls].toString().toLatin1(), sessionData["mac"_ls].toString().toLatin1())).object();
room(roomId)->addMegolmSessionFromBackup(sessionId.toLatin1(), data["session_key"_ls].toString().toLatin1(), session["first_message_index"_ls].toInt());
}
}
});
});
}
5 changes: 5 additions & 0 deletions Quotient/connection.h
Original file line number Diff line number Diff line change
Expand Up @@ -415,6 +415,9 @@ class QUOTIENT_API Connection : public QObject {
//! \sa encryptionEnabled
void enableEncryption(bool enable);

//! \brief Unlock the secret backup from the given password
void unlockSSSSFromPassword(const QString& password);

//! \brief Load room state from a previously saved file
//!
//! Call this before first sync.
Expand Down Expand Up @@ -903,6 +906,8 @@ public Q_SLOTS:
Quotient::KeyVerificationSession::State state);
void sessionVerified(const QString& userId, const QString& deviceId);
bool finishedQueryingKeys();
void keyBackupPasswordCorrect();
void keyBackupPasswordWrong();
#endif

protected:
Expand Down
4 changes: 4 additions & 0 deletions Quotient/database.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -273,6 +273,9 @@ UnorderedMap<QByteArray, QOlmInboundGroupSession> Database::loadMegolmSessions(
void Database::saveMegolmSession(const QString& roomId,
const QOlmInboundGroupSession& session)
{
auto deleteQuery = prepareQuery(QStringLiteral("DELETE FROM inbound_megolm_sessions WHERE roomId=:roomId AND sessionId=:sessionId;"));
deleteQuery.bindValue(":roomId"_ls, roomId);
deleteQuery.bindValue(":sessionId"_ls, session.sessionId());
auto query = prepareQuery(
QStringLiteral("INSERT INTO inbound_megolm_sessions(roomId, sessionId, pickle, senderId, olmSessionId) VALUES(:roomId, :sessionId, :pickle, :senderId, :olmSessionId);"));
query.bindValue(":roomId"_ls, roomId);
Expand All @@ -281,6 +284,7 @@ void Database::saveMegolmSession(const QString& roomId,
query.bindValue(":senderId"_ls, session.senderId());
query.bindValue(":olmSessionId"_ls, session.olmSessionId());
transaction();
execute(deleteQuery);
execute(query);
commit();
}
Expand Down
3 changes: 2 additions & 1 deletion Quotient/e2ee/qolminboundsession.h
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,9 @@ class QUOTIENT_API QOlmInboundGroupSession
OlmErrorCode lastErrorCode() const;
const char* lastError() const;

private:
//TODO: why?
QOlmInboundGroupSession();
private:
CStructPtr<OlmInboundGroupSession> m_groupSession;
QByteArray m_olmSessionId;
QString m_senderId;
Expand Down
14 changes: 13 additions & 1 deletion Quotient/room.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -404,7 +404,7 @@ class Q_DECL_HIDDEN Room::Private {
return {};
}
auto& senderSession = groupSessionIt->second;
if (senderSession.senderId() != senderId) {
if (!senderSession.senderId().isEmpty() && senderSession.senderId() != senderId) {
qCWarning(E2EE) << "Sender from event does not match sender from session";
return {};
}
Expand Down Expand Up @@ -3578,3 +3578,15 @@ void Room::activateEncryption()
}
setState<EncryptionEvent>(EncryptionType::MegolmV1AesSha2);
}

void Room::addMegolmSessionFromBackup(const QByteArray &sessionId, const QByteArray &sessionKey, uint32_t index)
{
if (d->groupSessions.contains(sessionId) && d->groupSessions[sessionId].firstKnownIndex() <= index) {
return;
}
d->groupSessions[sessionId] = QOlmInboundGroupSession::importSession(sessionKey).value();
d->groupSessions[sessionId].setOlmSessionId(QByteArrayLiteral("BACKUP"));
d->groupSessions[sessionId].setSenderId("BACKUP"_ls);
d->connection->saveMegolmSession(this, d->groupSessions[sessionId]);
}

2 changes: 2 additions & 0 deletions Quotient/room.h
Original file line number Diff line number Diff line change
Expand Up @@ -807,6 +807,8 @@ class QUOTIENT_API Room : public QObject {
return setState(EvT(std::forward<ArgTs>(args)...));
}

void addMegolmSessionFromBackup(const QByteArray &sessionId, const QByteArray &sessionKey, uint32_t index);

public Q_SLOTS:
/** Check whether the room should be upgraded */
void checkVersion();
Expand Down

0 comments on commit f57ec5e

Please sign in to comment.