From 4924b4fb2b5fb83c76a9a465e490a5f93f1093f9 Mon Sep 17 00:00:00 2001 From: Seyed Yahya Shirazi Date: Fri, 5 Dec 2025 18:09:43 -0800 Subject: [PATCH 1/3] Add security status display for encrypted streams - Add security_enabled and security_fingerprint fields to StreamItem - Display lock emoji prefix for encrypted streams in stream list - Update info_to_listName to include security status - Requires liblsl with security API (lsl_get_security_enabled) --- src/mainwindow.cpp | 6 ++++-- src/mainwindow.h | 16 +++++++++++----- 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index aec937f..421ff66 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -284,7 +284,8 @@ void MainWindow::save_config(QString filename) { } QString info_to_listName(const lsl::stream_info& info) { - return QString::fromStdString(info.name() + " (" + info.hostname() + ")"); + QString prefix = info.security_enabled() ? QString::fromUtf8("\xF0\x9F\x94\x92 ") : QString(""); // Lock emoji + return prefix + QString::fromStdString(info.name() + " (" + info.hostname() + ")"); } /** @@ -304,7 +305,8 @@ std::vector MainWindow::refreshStreams() { } if (!known) { bool found = missingStreams.contains(info_to_listName(s)); - knownStreams << StreamItem(s.name(), s.type(), s.source_id(), s.hostname(), found); + knownStreams << StreamItem(s.name(), s.type(), s.source_id(), s.hostname(), found, + s.security_enabled(), s.security_fingerprint()); if (found) { missingStreams.remove(info_to_listName(s)); } } } diff --git a/src/mainwindow.h b/src/mainwindow.h index abeaad4..2996783 100644 --- a/src/mainwindow.h +++ b/src/mainwindow.h @@ -19,18 +19,24 @@ class recording; class RemoteControlSocket; class StreamItem { - + public: StreamItem(std::string stream_name, std::string stream_type, std::string source_id, - std::string hostname, bool required) - : name(stream_name), type(stream_type), id(source_id), host(hostname), checked(required) {} - - QString listName() { return QString::fromStdString(name + " (" + host + ")"); } + std::string hostname, bool required, bool secure = false, std::string fingerprint = "") + : name(stream_name), type(stream_type), id(source_id), host(hostname), + checked(required), security_enabled(secure), security_fingerprint(fingerprint) {} + + QString listName() { + QString prefix = security_enabled ? QString::fromUtf8("\xF0\x9F\x94\x92 ") : QString(""); // Lock emoji + return prefix + QString::fromStdString(name + " (" + host + ")"); + } std::string name; std::string type; std::string id; std::string host; bool checked; + bool security_enabled; + std::string security_fingerprint; }; From 756ab618c9656db8ff451744a86e66a67efb87a1 Mon Sep 17 00:00:00 2001 From: Seyed Yahya Shirazi Date: Sun, 7 Dec 2025 14:16:33 -0800 Subject: [PATCH 2/3] Add version API info to About dialog Display security version information (base version, security version, full version) in the About dialog when using liblsl-secure. --- src/mainwindow.cpp | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index 421ff66..5fe58dc 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -45,7 +45,16 @@ MainWindow::MainWindow(QWidget *parent, const char *config_file) connect(ui->actionAbout, &QAction::triggered, this, [this]() { QString infostr = QStringLiteral("LSL library version: ") + QString::number(lsl::library_version()) + - "\nLSL library info:" + lsl::library_info(); + "\nLSL library info: " + lsl::library_info(); + // Add security version information if available + if (lsl::is_secure_build()) { + infostr += "\n\nSecurity: Enabled"; + infostr += "\nBase version: " + QString(lsl::base_version()); + infostr += "\nSecurity version: " + QString(lsl::security_version()); + infostr += "\nFull version: " + QString(lsl::full_version()); + } else { + infostr += "\n\nSecurity: Not available"; + } QMessageBox::about(this, "About this app", infostr); }); From 6c95797b09b4c3c1c691ad3d7b3cc0da4ee26dad Mon Sep 17 00:00:00 2001 From: Seyed Yahya Shirazi Date: Wed, 17 Dec 2025 09:49:46 -0800 Subject: [PATCH 3/3] Add security mismatch detection with informative error dialogs - Detect security mismatches before recording starts - Show clear error dialog with stream names in blue, red warning text - Provide actionable instructions (run lsl-keygen or import credentials) - Add error reporting infrastructure to recording class for future use - Move lock icon to end of stream name for better readability --- src/mainwindow.cpp | 76 ++++++++++++++++++++++++++++++++++++++++++++-- src/mainwindow.h | 2 +- src/recording.cpp | 32 +++++++++++++++++-- src/recording.h | 21 +++++++++++++ 4 files changed, 125 insertions(+), 6 deletions(-) diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index 5fe58dc..38d63bf 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -103,8 +103,35 @@ MainWindow::MainWindow(QWidget *parent, const char *config_file) load_config(cfgfilepath); } -void MainWindow::statusUpdate() const { +void MainWindow::statusUpdate() { if (currentRecording) { + // Check for errors from the recording threads + auto errors = currentRecording->getErrors(); + for (const auto& error : errors) { + QString errorMsg = QString::fromStdString(error.error_message); + QString streamName = QString::fromStdString(error.stream_name); + + if (error.is_security_error) { + // Extract the most relevant part of the security error message + QString displayMsg = errorMsg; + if (errorMsg.contains("403")) { + // Extract the 403 message for cleaner display + int idx = errorMsg.indexOf("403"); + if (idx >= 0) { + displayMsg = errorMsg.mid(idx); + } + } + // Show security errors prominently + QMessageBox::warning(const_cast(this), + "Security Error - " + streamName, + "Failed to connect to stream '" + streamName + "':\n\n" + displayMsg + + "\n\nMake sure all devices have matching security configurations."); + } else { + // Log non-security errors to console + qWarning() << "Stream error for" << streamName << ":" << errorMsg; + } + } + auto elapsed = static_cast(lsl::local_clock() - startTime); QString recFilename = replaceFilename(QDir::cleanPath(ui->lineEdit_template->text())); auto fileinfo = QFileInfo(QDir::cleanPath(ui->rootEdit->text()) + '/' + recFilename); @@ -293,8 +320,8 @@ void MainWindow::save_config(QString filename) { } QString info_to_listName(const lsl::stream_info& info) { - QString prefix = info.security_enabled() ? QString::fromUtf8("\xF0\x9F\x94\x92 ") : QString(""); // Lock emoji - return prefix + QString::fromStdString(info.name() + " (" + info.hostname() + ")"); + QString suffix = info.security_enabled() ? QString::fromUtf8(" \xF0\x9F\x94\x92") : QString(""); // Lock emoji at end + return QString::fromStdString(info.name() + " (" + info.hostname() + ")") + suffix; } /** @@ -403,6 +430,49 @@ void MainWindow::startRecording() { msgBox.setDefaultButton(QMessageBox::No); if (msgBox.exec() != QMessageBox::Yes) return; } + + // Check for security mismatches between local config and selected streams + bool localSecurityEnabled = lsl::local_security_enabled(); + QStringList securityMismatchStreams; + for (const auto& stream : requestedAndAvailableStreams) { + bool streamSecure = stream.security_enabled(); + if (streamSecure && !localSecurityEnabled) { + // Stream requires security but local security is not enabled + securityMismatchStreams.append(QString::fromStdString(stream.name())); + } else if (!streamSecure && localSecurityEnabled) { + // Stream is insecure but local security is enabled + securityMismatchStreams.append(QString::fromStdString(stream.name()) + " (insecure)"); + } + } + if (!securityMismatchStreams.isEmpty()) { + QString errorMsg; + // Format stream names as a bulleted list with blue color + QString streamList; + for (const auto& s : securityMismatchStreams) { + streamList += "  • " + s + "
"; + } + if (!localSecurityEnabled) { + errorMsg = "The following streams require security, but Lab Recorder does not have " + "security credentials configured:

" + streamList + + "
To fix this:
" + "  1. Run 'lsl-keygen' to generate credentials, or
" + "  2. Import shared credentials from an authorized device

" + "Recording cannot proceed with mismatched security settings."; + } else { + errorMsg = "Security mismatch detected for the following streams:

" + + streamList + + "
All devices must have the same security configuration " + "(either all secure or all insecure).

" + "Recording cannot proceed with mismatched security settings."; + } + QMessageBox msgBox(this); + msgBox.setWindowTitle("Security Mismatch"); + msgBox.setIcon(QMessageBox::Critical); + msgBox.setTextFormat(Qt::RichText); + msgBox.setText(errorMsg); + msgBox.exec(); + return; + } } // don't hide critical errors. diff --git a/src/mainwindow.h b/src/mainwindow.h index 2996783..588c659 100644 --- a/src/mainwindow.h +++ b/src/mainwindow.h @@ -48,7 +48,7 @@ class MainWindow : public QMainWindow { ~MainWindow() noexcept override; private slots: - void statusUpdate(void) const; + void statusUpdate(void); void closeEvent(QCloseEvent *ev) override; void blockSelected(const QString &block); std::vector refreshStreams(void); diff --git a/src/recording.cpp b/src/recording.cpp index 0b8b43e..f2ad39c 100644 --- a/src/recording.cpp +++ b/src/recording.cpp @@ -121,6 +121,30 @@ void recording::requestStop() noexcept shutdown_ = true; } +void recording::reportError(const std::string& stream_name, const std::string& error_msg) { + std::lock_guard lock(error_mut_); + // Check if this is a security-related error + bool is_security = (error_msg.find("403") != std::string::npos) || + (error_msg.find("security") != std::string::npos) || + (error_msg.find("Security") != std::string::npos) || + (error_msg.find("encryption") != std::string::npos) || + (error_msg.find("public key") != std::string::npos) || + (error_msg.find("Public key") != std::string::npos); + errors_.push_back({stream_name, error_msg, is_security}); +} + +std::vector recording::getErrors() { + std::lock_guard lock(error_mut_); + std::vector result(errors_.begin(), errors_.end()); + errors_.clear(); + return result; +} + +bool recording::hasErrors() const { + std::lock_guard lock(error_mut_); + return !errors_.empty(); +} + void recording::record_from_query_results(const std::string &query) { try { std::set known_uids; // set of previously seen stream uid's @@ -154,7 +178,9 @@ void recording::record_from_query_results(const std::string &query) { // wait for all our threads to join timed_join_or_detach(threads, max_join_wait); } catch (std::exception &e) { - std::cout << "Error in the record_from_query_results thread: " << e.what() << std::endl; + std::string error_msg = e.what(); + std::cout << "Error in the record_from_query_results thread: " << error_msg << std::endl; + reportError("query: " + query, error_msg); } } @@ -277,7 +303,9 @@ void recording::record_from_streaminfo(const lsl::stream_info &src, bool phase_l throw; } } catch (std::exception &e) { - std::cout << "Error in the record_from_streaminfo thread: " << e.what() << std::endl; + std::string error_msg = e.what(); + std::cout << "Error in the record_from_streaminfo thread: " << error_msg << std::endl; + reportError(src.name(), error_msg); } } diff --git a/src/recording.h b/src/recording.h index 0b198ba..0446ee8 100644 --- a/src/recording.h +++ b/src/recording.h @@ -5,6 +5,7 @@ #include #include #include +#include #include #include #include @@ -43,6 +44,12 @@ using offset_list = std::list>; // a map from streamid to offset_list using offset_lists = std::map; +// Structure to hold stream errors +struct StreamError { + std::string stream_name; + std::string error_message; + bool is_security_error; +}; /** * A recording process using the lab streaming layer. @@ -74,7 +81,17 @@ class recording { void requestStop() noexcept; + /// Get and clear any errors that occurred during recording + /// Thread-safe, returns errors since last call + std::vector getErrors(); + + /// Check if there are any errors without removing them + bool hasErrors() const; + private: + /// Report an error from a recording thread + void reportError(const std::string& stream_name, const std::string& error_msg); + // the file stream XDFWriter file_; // the file output stream // static information @@ -104,6 +121,10 @@ class recording { offset_lists_; // the clock offset lists for each stream (to be written into the footer) std::mutex offset_mut_; // a mutex to protect the offset lists + // error reporting for the UI + std::deque errors_; + mutable std::mutex error_mut_; + // data for shutdown / final joining std::list stream_threads_; // the spawned stream handling threads thread_p boundary_thread_; // the spawned boundary-recording thread