add e2e encrypted messaging using RSA-4096

pull/176/head
larteyoh 10 months ago
parent 9d6147ec6d
commit 93cf1981d3

@ -63,7 +63,8 @@ The name _neroshop_ is a combination of the words _nero_, which is Italian for _
- there will be 0.5% fee for using one of the three payment options, specifically the 2-of-3 escrow system.
- [x] Pseudonymous identities
- sellers and buyers are identified by their unique ids and/or optional display names
- [ ] End-to-end encrypted messaging system for communications between sellers and buyers via matrix.org
- [x] End-to-end encrypted messaging system for communications between sellers and buyers via matrix.org
- generated RSA-4096 private keys will be used to decrypt messages so keep yours safe, secure and accessible!
- [ ] Subaddress generator for direct payments without an escrow
- a unique subaddress will be generated from a seller's synced wallet account for each order placed by a customer
- [x] Built-in Monero wallet with basic functionalities (`transaction history`, `send`, and `receive`)

@ -155,6 +155,7 @@ Page {
folder: (isWindows) ? StandardPaths.writableLocation(StandardPaths.DocumentsLocation) + "/neroshop" : StandardPaths.writableLocation(StandardPaths.HomeLocation) + "/neroshop"//StandardPaths.writableLocation(StandardPaths.AppDataLocation) // refer to https://doc.qt.io/qt-5/qstandardpaths.html#StandardLocation-enum
nameFilters: ["Wallet files (*.keys)"]
////options: FileDialog.ReadOnly // will not allow you to create folders while file dialog is opened
onAccepted: walletPasswordRestoreField.forceActiveFocus()
}
///////////////////////////
// for registration page

@ -23,6 +23,145 @@ Page {
anchors.bottomMargin: 10
spacing: 0//10 - topMargin already set for profilePictureRect
// Message dialog
Dialog {
id: messageDialog
anchors.centerIn: parent
width: 700; height: 500
visible: false
modal: true
closePolicy: Popup.NoAutoClose | Popup.CloseOnEscape
focus: true // The Popup will receive focus when it is shown
onAccepted: {
console.log("Ok clicked")
if(messageTextArea.text.length > 0) {
User.sendMessage(userModel.monero_address, messageTextArea.text, userModel.public_key)
messageTextArea.text = "" // clear message after sending it
}
}
onRejected: {
console.log("Cancel clicked")
messageTextArea.text = ""
}
// header
header: Rectangle {
id: titleBar
color: "#323232"
height: 40
width: parent.width
anchors.left: parent.left
anchors.right: parent.right
radius: 6
// Rounded top corners
Rectangle {
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: parent.bottom
height: parent.height / 2
color: parent.color
}
Label {
text: "Message seller"
color: "#ffffff"
font.bold: true
anchors.horizontalCenter: parent.horizontalCenter
anchors.verticalCenter: parent.verticalCenter
}
Button {
id: closeButton
width: 25//20
height: this.width
anchors.verticalCenter: titleBar.verticalCenter
anchors.right: titleBar.right
anchors.rightMargin: 10
text: qsTr(FontAwesome.xmark)
contentItem: Text {
text: closeButton.text
color: "#ffffff"
font.bold: true
font.family: FontAwesome.fontFamily
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
}
background: Rectangle {
color: "#ff4d4d"
radius: 100
}
onClicked: {
messageDialog.reject() // manually trigger onRejected signal and close dialog too
}
}
}
// footer
footer: Rectangle { // footer cant have margins cuz it is an integral part of the dialog
width: parent.width; height: 50
color: "transparent"
//border.color: "red" // <- for debug purposes
RowLayout {
id: standardButtonsRow
anchors.horizontalCenter: parent.horizontalCenter
width: dialogContent.width - spacing
Button {
Layout.fillWidth: true
text: "Cancel"
onClicked: {
messageDialog.reject()
}
}
Button {
Layout.fillWidth: true
text: "Send"
onClicked: {
messageDialog.accept()
}
}
}
}
// background
background: Rectangle {
color: "white" // Change this based on theme later
radius: 10
}
// Dialog content
Rectangle {
id: dialogContent
anchors.centerIn: parent
anchors.margins: 20
width: parent.width; height: parent.height
color: "transparent"
//border.color: "red" // <- for debug purposes
ColumnLayout {
anchors.fill: parent
TextArea {
id: messageTextArea
Layout.fillWidth: true
Layout.fillHeight: true
wrapMode: Text.Wrap
selectByMouse: true
focus: true // will receive focus when the Popup is shown (messageDialog must have focus set to true as well for this to work)
property int maximumLength: 470
background: Rectangle {
color: "lightblue"
radius: 5
}
}
Label {
id: characterCountLabel
text: "Characters: " + messageTextArea.text.length
color: (messageTextArea.text.length > messageTextArea.maximumLength) ? "red" : "black"
}
}
}
}
// Back button
Button {
id: backButton
@ -187,7 +326,9 @@ Page {
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
}
onClicked: {}
onClicked: {
messageDialog.open()
}
MouseArea {
anchors.fill: parent
onPressed: mouse.accepted = false

@ -9,4 +9,16 @@ Page {
background: Rectangle {
color: "transparent"
}
property var model: User.getMessages()
ColumnLayout {
Text {
id: content
text: messagesPage.model[0].content
}
Text {
id: sender_id
text: messagesPage.model[0].sender_id
}
}
}

@ -129,6 +129,8 @@ inline int get_category_id_by_name(const std::string& name) {
return -1;
}
//-----------------------------------------------------------------------------
inline std::vector<Subcategory> get_subcategories_by_category_id(unsigned int category_id) {
std::vector<Subcategory> matching_subcategories;

@ -24,6 +24,7 @@ neroshop::Mapper::~Mapper() {
order_ids.clear();
product_ratings.clear();
seller_ratings.clear();
messages.clear();
}
//-----------------------------------------------------------------------------
@ -135,6 +136,14 @@ void neroshop::Mapper::add(const std::string& key, const std::string& value) {
}
}
//-----------------------------------------------
if(metadata == "message") {
// Map a message's key to a recipient_id
if (json.contains("recipient_id") && json["recipient_id"].is_string()) {
std::string recipient_id = json["recipient_id"].get<std::string>();
messages[recipient_id].push_back(key);
}
}
//-----------------------------------------------
sync(); // Sync to database
}
@ -382,12 +391,31 @@ void neroshop::Mapper::sync() {
}
}
//-----------------------------------------------
// Insert data from 'messages'
for (const auto& entry : messages) {
const std::string& search_term = entry.first;
const std::vector<std::string>& keys = entry.second;
const std::string content = "message";
for (const std::string& key : keys) {
// Check if the record already exists
std::string select_query = "SELECT COUNT(*) FROM mappings WHERE search_term = ?1 AND key = ?2;";
bool exists = database->get_integer_params(select_query, { search_term, key });
// If no duplicate record found, perform insertion
if(!exists) {
std::string insert_query = "INSERT INTO mappings (search_term, key, content) VALUES (?1, ?2, ?3);";
database->execute_params(insert_query, { search_term, key, content });
}
}
}
//-----------------------------------------------
database->execute("COMMIT;");
}
//-----------------------------------------------------------------------------
std::pair<std::string, std::string> neroshop::Mapper::serialize() {
std::pair<std::string, std::string> neroshop::Mapper::serialize() { // no longer in use
nlohmann::json data;
//-----------------------------------------------
// Add user_ids

@ -25,6 +25,7 @@ struct Mapper { // maps search terms to DHT keys
std::unordered_map<std::string, std::vector<std::string>> order_ids; // maps a order uuid to the corresponding order key.
std::unordered_map<std::string, std::vector<std::string>> product_ratings;
std::unordered_map<std::string, std::vector<std::string>> seller_ratings;
std::unordered_map<std::string, std::vector<std::string>> messages;
void add(const std::string& key, const std::string& value); // must be JSON value
void sync(); // syncs mapping data to local database

@ -7,6 +7,10 @@
#include "protocol/p2p/serializer.hpp"
#include "protocol/transport/client.hpp"
#include "rating.hpp"
#include "crypto/rsa.hpp"
#include "crypto/sha3.hpp"
#include "tools/base64.hpp"
#include "tools/timestamp.hpp"
#include <fstream>
@ -459,6 +463,91 @@ void neroshop::User::delete_avatar() {
}
////////////////////
////////////////////
void neroshop::User::send_message(const std::string& recipient_id, const std::string& content, const std::string& public_key) {
if(recipient_id == this->id) {
neroshop::print("You cannot message yourself", 1);
return;
}
// Construct message
nlohmann::json data;
//----------------------------------------------------
int padding_overhead = 42; // only an estimate - probably accurate since I tested it once
int MAX_DATA_LENGTH_BYTES = (NEROSHOP_RSA_DEFAULT_BITS / 8) - padding_overhead; // RSA-OAEP padding reduces the max data size by 41 to 66 bytes :(
// Encrypt sender
std::string sender_encrypted = neroshop::crypto::rsa_public_encrypt(public_key, this->id);//std::cout << "sender (encrypted): " << sender_encrypted << std::endl;
// Convert to base64 (for transmission)
std::string sender_encoded = neroshop::base64_encode(sender_encrypted);
data["sender_id"] = sender_encoded;
#ifdef NEROSHOP_DEBUG0
std::cout << "sender (base64 encoded): " << sender_encoded << std::endl;
std::string sender_decoded = neroshop::base64_decode(sender_encoded);
std::cout << "sender (base64 decoded): " << sender_decoded << std::endl << std::endl;
#endif
//----------------------------------------------------
// Encrypt message
std::string message_encrypted = neroshop::crypto::rsa_public_encrypt(public_key, content);//std::cout << "message (encrypted): " << message_encrypted << std::endl;
if(message_encrypted.empty()) {
neroshop::print("Error encrypting message", 1);
return;
}
// Convert to base64 (for transmission)
std::string message_encoded = neroshop::base64_encode(message_encrypted);
data["content"] = message_encoded;
#ifdef NEROSHOP_DEBUG0
std::cout << "message (base64 encoded): " << message_encoded << std::endl;
std::string message_decoded = neroshop::base64_decode(message_encoded);
std::cout << "message (base64 decoded): " << message_decoded << std::endl << std::endl;
#endif
//----------------------------------------------------
data["recipient_id"] = recipient_id;
data["timestamp"] = neroshop::timestamp::get_current_utc_timestamp();
data["metadata"] = "message";
std::string value = data.dump();
std::string key = neroshop::crypto::sha3_256(value);
std::cout << "key: " << key << "\nvalue: " << value << "\n";
// Send put request to neighboring nodes (and your node too JIC)
Client * client = Client::get_main_client();
std::string response;
client->put(key, value, response);
#ifdef NEROSHOP_DEBUG
std::cout << "Received response: " << response << "\n";
#endif
}
////////////////////
std::pair<std::string, std::string> neroshop::User::decrypt_message(const std::string& content_encoded, const std::string& sender_encoded) {
// Decode encoded sender
std::string sender_decoded = neroshop::base64_decode(sender_encoded);
// Decrypt sender using your own private keys
std::string sender = neroshop::crypto::rsa_private_decrypt(this->private_key, sender_decoded);
#ifdef NEROSHOP_DEBUG0
std::cout << "sender (base64 decoded): " << sender_decoded << std::endl;
std::cout << "sender (decrypted): " << sender << std::endl << std::endl;
#endif
//----------------------------------------------------
// Decode encoded message
std::string message_decoded = neroshop::base64_decode(content_encoded);
// Decrypt message using your own private keys
std::string message = neroshop::crypto::rsa_private_decrypt(this->private_key, message_decoded);
#ifdef NEROSHOP_DEBUG0
std::cout << "message (base64 decoded): " << message_decoded << std::endl;
std::cout << "message (decrypted): " << message << std::endl << std::endl;
#endif
//----------------------------------------------------
return std::make_pair(message, sender);
}
////////////////////
////////////////////
void neroshop::User::set_public_key(const std::string& public_key) {
// TODO: validate public key before setting it
this->public_key = public_key;

@ -5,6 +5,7 @@
#include <memory> // std::unique_ptr
#include <string>
#include <vector>
#include <utility> // std::pair
#include "product.hpp"
#include "order.hpp"
@ -38,6 +39,9 @@ public:
// avatar-related stuff (10% complete)
void upload_avatar(const std::string& filename);
void delete_avatar();
// private messages
void send_message(const std::string& recipient_id, const std::string& content, const std::string& recipient_public_key);
std::pair<std::string, std::string> decrypt_message(const std::string& content_encoded, const std::string& sender_encoded);
// setters
void set_public_key(const std::string& public_key);
void set_private_key(const std::string& private_key);

@ -204,6 +204,22 @@ void neroshop::UserController::uploadAvatar(const QString& filename) {
_user->upload_avatar(filename.toStdString());
}
//----------------------------------------------------------------
void neroshop::UserController::sendMessage(const QString& recipient_id, const QString& content,
const QString& public_key) {
if (!_user) throw std::runtime_error("neroshop::User is not initialized");
_user->send_message(recipient_id.toStdString(), content.toStdString(), public_key.toStdString());
}
//----------------------------------------------------------------
QVariantMap neroshop::UserController::decryptMessage(const QString& content_encoded, const QString& sender_encoded) {
if (!_user) throw std::runtime_error("neroshop::User is not initialized");
std::pair<std::string, std::string> message_decrypted = _user->decrypt_message(content_encoded.toStdString(), sender_encoded.toStdString());
QVariantMap message;
message["content"] = QString::fromStdString(message_decrypted.first);
message["sender_id"] = QString::fromStdString(message_decrypted.second);
return message;
}
//----------------------------------------------------------------
//----------------------------------------------------------------
void neroshop::UserController::setStockQuantity(const QString& listing_key, int quantity) {
if (!_user) throw std::runtime_error("neroshop::User is not initialized");
@ -281,6 +297,7 @@ QVariantList neroshop::UserController::getInventory(InventorySorting sorting) co
while(sqlite3_step(stmt) == SQLITE_ROW) {
QVariantMap inventory_object; // Create an object for each row
QVariantList product_images;
QStringList product_categories;
for(int i = 0; i < sqlite3_column_count(stmt); i++) {
std::string column_value = (sqlite3_column_text(stmt, i) == nullptr) ? "NULL" : reinterpret_cast<const char *>(sqlite3_column_text(stmt, i));//std::cout << column_value << " (" << i << ")" << std::endl;
@ -324,7 +341,18 @@ QVariantList neroshop::UserController::getInventory(InventorySorting sorting) co
inventory_object.insert("product_uuid", QString::fromStdString(product_obj["id"].get<std::string>()));
inventory_object.insert("product_name", QString::fromStdString(product_obj["name"].get<std::string>()));
inventory_object.insert("product_description", QString::fromStdString(product_obj["description"].get<std::string>()));
inventory_object.insert("product_category_id", get_category_id_by_name(product_obj["category"].get<std::string>()));
// product category and subcategories
std::string category = product_obj["category"].get<std::string>();
product_categories.append(QString::fromStdString(category));
if (product_obj.contains("subcategories") && product_obj["subcategories"].is_array()) {
const auto& subcategories_array = product_obj["subcategories"];
for (const auto& subcategory : subcategories_array) {
if (subcategory.is_string()) {
product_categories.append(QString::fromStdString(subcategory.get<std::string>()));
}
}
inventory_object.insert("product_categories", product_categories);
}
//inventory_object.insert("", QString::fromStdString(product_obj[""].get<std::string>()));
if (product_obj.contains("images") && product_obj["images"].is_array()) {
const auto& images_array = product_obj["images"];
@ -332,7 +360,6 @@ QVariantList neroshop::UserController::getInventory(InventorySorting sorting) co
if (image.contains("name") && image.contains("id")) {
const auto& image_name = image["name"].get<std::string>();
const auto& image_id = image["id"].get<int>();
QVariantMap image_map;
image_map.insert("name", QString::fromStdString(image_name));
image_map.insert("id", image_id);
@ -448,6 +475,75 @@ QVariantList neroshop::UserController::getInventory(InventorySorting sorting) co
}
//----------------------------------------------------------------
//----------------------------------------------------------------
QVariantList neroshop::UserController::getMessages() const {
if (!_user) throw std::runtime_error("neroshop::User is not initialized");
Client * client = Client::get_main_client();
neroshop::db::Sqlite3 * database = neroshop::get_database();
if(!database) throw std::runtime_error("database is NULL");
std::string command = "SELECT DISTINCT key FROM mappings WHERE search_term = ?1 AND content = 'message'";
sqlite3_stmt * stmt = nullptr;
// Prepare (compile) statement
if(sqlite3_prepare_v2(database->get_handle(), command.c_str(), -1, &stmt, nullptr) != SQLITE_OK) {
neroshop::print("sqlite3_prepare_v2: " + std::string(sqlite3_errmsg(database->get_handle())), 1);
return {};
}
// Bind user_id to TEXT
std::string user_id = _user->get_id();
if(sqlite3_bind_text(stmt, 1, user_id.c_str(), user_id.length(), SQLITE_STATIC) != SQLITE_OK) {
neroshop::print("sqlite3_bind_text (arg: 1): " + std::string(sqlite3_errmsg(database->get_handle())), 1);
sqlite3_finalize(stmt);
return {};
}
QVariantList messages_array;
// Get all table values row by row
while(sqlite3_step(stmt) == SQLITE_ROW) {
QVariantMap message_object; // Create an object for each row
for(int i = 0; i < sqlite3_column_count(stmt); i++) {
std::string column_value = (sqlite3_column_text(stmt, i) == nullptr) ? "NULL" : reinterpret_cast<const char *>(sqlite3_column_text(stmt, i));//std::cout << column_value << " (" << i << ")" << std::endl;
if(column_value == "NULL") continue; // Skip invalid columns
QString key = QString::fromStdString(column_value);
// Get the value of the corresponding key from the DHT
std::string response;
client->get(key.toStdString(), response); // TODO: error handling
std::cout << "Received response (get): " << response << "\n";
// Parse the response
nlohmann::json json = nlohmann::json::parse(response);
if(json.contains("error")) {
int rescode = database->execute_params("DELETE FROM mappings WHERE key = ?1", { key.toStdString() });
if(rescode != SQLITE_OK) neroshop::print("sqlite error: DELETE failed", 1);
continue; // Key is lost or missing from DHT, skip to next iteration
}
const auto& response_obj = json["response"];
assert(response_obj.is_object());
if (response_obj.contains("value") && response_obj["value"].is_string()) {
const auto& value = response_obj["value"].get<std::string>();
nlohmann::json value_obj = nlohmann::json::parse(value);
assert(value_obj.is_object());//std::cout << value_obj.dump(4) << "\n";
std::string metadata = value_obj["metadata"].get<std::string>();
if (metadata != "message") { std::cerr << "Invalid metadata. \"message\" expected, got \"" << metadata << "\" instead\n"; continue; }
message_object.insert("key", key);
std::string content = value_obj["content"].get<std::string>();
std::string sender_id = value_obj["sender_id"].get<std::string>();
auto message_decrypted = _user->decrypt_message(content, sender_id);
message_object.insert("content", QString::fromStdString(message_decrypted.first));
message_object.insert("sender_id", QString::fromStdString(message_decrypted.second));
message_object.insert("recipient_id", QString::fromStdString(value_obj["recipient_id"].get<std::string>()));
if(message_object.contains("timestamp")) message_object.insert("timestamp", QString::fromStdString(value_obj["timestamp"].get<std::string>()));
}
messages_array.append(message_object);
}
}
sqlite3_finalize(stmt);
return messages_array;
}
//----------------------------------------------------------------
//----------------------------------------------------------------
bool neroshop::UserController::isUserLogged() const {
return (_user.get() != nullptr);
}

@ -84,6 +84,9 @@ public:
Q_INVOKABLE void setStockQuantity(const QString& listing_key, int quantity);
Q_INVOKABLE void uploadAvatar(const QString& filename);
Q_INVOKABLE void sendMessage(const QString& recipient_id, const QString& content, const QString& recipient_public_key);
Q_INVOKABLE QVariantMap decryptMessage(const QString& content_encoded, const QString& sender_encoded);
Q_INVOKABLE QString getId() const;//Q_INVOKABLE neroshop::WalletController * getWallet() const;
Q_INVOKABLE int getProductsCount() const;
@ -93,6 +96,8 @@ public:
Q_INVOKABLE QVariantList getInventory(InventorySorting sorting = SortNone) const;
Q_INVOKABLE QVariantList getMessages() const;
Q_INVOKABLE neroshop::User * getUser() const;
neroshop::Seller * getSeller() const;

Loading…
Cancel
Save