From 269558995d75f23af81fb512e96bb64566898151 Mon Sep 17 00:00:00 2001 From: ovsky Date: Thu, 4 Dec 2025 14:53:19 +0100 Subject: [PATCH 01/19] Add PasswordManager implementation and header Introduces PasswordManager.cpp and PasswordManager.h, providing a Chrome-like password manager with secure credential storage, autofill, password generation, import/export, blacklist management, and optional master password protection. Includes platform-specific encryption, form detection, and password strength checking. --- src/PasswordManager.cpp | 1483 +++++++++++++++++++++++++++++++++++++++ src/PasswordManager.h | 252 +++++++ 2 files changed, 1735 insertions(+) create mode 100644 src/PasswordManager.cpp create mode 100644 src/PasswordManager.h diff --git a/src/PasswordManager.cpp b/src/PasswordManager.cpp new file mode 100644 index 0000000..9c5e9f8 --- /dev/null +++ b/src/PasswordManager.cpp @@ -0,0 +1,1483 @@ +#include "PasswordManager.h" +#include "Utils.h" +#include +#include +#include +#include +#include +#include +#include +#include + +#ifdef _WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#include +#include +#pragma comment(lib, "crypt32.lib") +#elif defined(__APPLE__) +#include +#include +#else +#include +#include +#include +#include +#endif + +namespace password { + +namespace { + +// Simple Base64 encoding/decoding +static const std::string base64_chars = + "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + "abcdefghijklmnopqrstuvwxyz" + "0123456789+/"; + +std::string Base64Encode(const std::string& input) { + std::string ret; + int i = 0; + int j = 0; + unsigned char char_array_3[3]; + unsigned char char_array_4[4]; + size_t in_len = input.size(); + const unsigned char* bytes_to_encode = reinterpret_cast(input.data()); + + while (in_len--) { + char_array_3[i++] = *(bytes_to_encode++); + if (i == 3) { + char_array_4[0] = (char_array_3[0] & 0xfc) >> 2; + char_array_4[1] = ((char_array_3[0] & 0x03) << 4) + ((char_array_3[1] & 0xf0) >> 4); + char_array_4[2] = ((char_array_3[1] & 0x0f) << 2) + ((char_array_3[2] & 0xc0) >> 6); + char_array_4[3] = char_array_3[2] & 0x3f; + + for (i = 0; i < 4; i++) + ret += base64_chars[char_array_4[i]]; + i = 0; + } + } + + if (i) { + for (j = i; j < 3; j++) + char_array_3[j] = '\0'; + + char_array_4[0] = (char_array_3[0] & 0xfc) >> 2; + char_array_4[1] = ((char_array_3[0] & 0x03) << 4) + ((char_array_3[1] & 0xf0) >> 4); + char_array_4[2] = ((char_array_3[1] & 0x0f) << 2) + ((char_array_3[2] & 0xc0) >> 6); + + for (j = 0; j < i + 1; j++) + ret += base64_chars[char_array_4[j]]; + + while (i++ < 3) + ret += '='; + } + + return ret; +} + +std::string Base64Decode(const std::string& encoded_string) { + size_t in_len = encoded_string.size(); + int i = 0; + int j = 0; + int in_ = 0; + unsigned char char_array_4[4], char_array_3[3]; + std::string ret; + + while (in_len-- && encoded_string[in_] != '=' && + (isalnum(encoded_string[in_]) || encoded_string[in_] == '+' || encoded_string[in_] == '/')) { + char_array_4[i++] = encoded_string[in_]; in_++; + if (i == 4) { + for (i = 0; i < 4; i++) + char_array_4[i] = static_cast(base64_chars.find(char_array_4[i])); + + char_array_3[0] = (char_array_4[0] << 2) + ((char_array_4[1] & 0x30) >> 4); + char_array_3[1] = ((char_array_4[1] & 0xf) << 4) + ((char_array_4[2] & 0x3c) >> 2); + char_array_3[2] = ((char_array_4[2] & 0x3) << 6) + char_array_4[3]; + + for (i = 0; i < 3; i++) + ret += char_array_3[i]; + i = 0; + } + } + + if (i) { + for (j = 0; j < i; j++) + char_array_4[j] = static_cast(base64_chars.find(char_array_4[j])); + + char_array_3[0] = (char_array_4[0] << 2) + ((char_array_4[1] & 0x30) >> 4); + char_array_3[1] = ((char_array_4[1] & 0xf) << 4) + ((char_array_4[2] & 0x3c) >> 2); + + for (j = 0; j < i - 1; j++) + ret += char_array_3[j]; + } + + return ret; +} + +// Simple JSON parsing helpers +std::string ParseJsonString(const std::string& json, const std::string& key) { + std::string search = "\"" + key + "\""; + auto pos = json.find(search); + if (pos == std::string::npos) return ""; + + pos = json.find(':', pos); + if (pos == std::string::npos) return ""; + + pos = json.find('"', pos); + if (pos == std::string::npos) return ""; + pos++; + + std::string result; + while (pos < json.size() && json[pos] != '"') { + if (json[pos] == '\\' && pos + 1 < json.size()) { + pos++; + switch (json[pos]) { + case 'n': result += '\n'; break; + case 'r': result += '\r'; break; + case 't': result += '\t'; break; + case '"': result += '"'; break; + case '\\': result += '\\'; break; + default: result += json[pos]; break; + } + } else { + result += json[pos]; + } + pos++; + } + return result; +} + +uint64_t ParseJsonUint64(const std::string& json, const std::string& key) { + std::string search = "\"" + key + "\""; + auto pos = json.find(search); + if (pos == std::string::npos) return 0; + + pos = json.find(':', pos); + if (pos == std::string::npos) return 0; + pos++; + + while (pos < json.size() && std::isspace(json[pos])) pos++; + + std::string num; + while (pos < json.size() && std::isdigit(json[pos])) { + num += json[pos++]; + } + + return num.empty() ? 0 : std::stoull(num); +} + +bool ParseJsonBool(const std::string& json, const std::string& key, bool default_val = false) { + std::string search = "\"" + key + "\""; + auto pos = json.find(search); + if (pos == std::string::npos) return default_val; + + pos = json.find(':', pos); + if (pos == std::string::npos) return default_val; + pos++; + + while (pos < json.size() && std::isspace(json[pos])) pos++; + + if (json.compare(pos, 4, "true") == 0) return true; + if (json.compare(pos, 5, "false") == 0) return false; + return default_val; +} + +std::string EscapeJsonStr(const std::string& s) { + std::string result; + for (char c : s) { + switch (c) { + case '"': result += "\\\""; break; + case '\\': result += "\\\\"; break; + case '\n': result += "\\n"; break; + case '\r': result += "\\r"; break; + case '\t': result += "\\t"; break; + default: result += c; break; + } + } + return result; +} + +} // anonymous namespace + +// SavedCredential implementation +SavedCredential::SavedCredential() + : date_created(0) + , date_last_used(0) + , date_password_modified(0) + , times_used(0) + , blacklisted(false) { +} + +bool SavedCredential::IsValid() const { + return !origin.empty() && !username.empty() && (!password.empty() || !encrypted_password.empty()); +} + +std::string SavedCredential::GetDisplayName() const { + if (!username.empty()) return username; + return origin; +} + +// DetectedForm implementation +DetectedForm::DetectedForm() + : has_remember_me(false) + , is_signup_form(false) + , is_change_password_form(false) { +} + +bool DetectedForm::IsLoginForm() const { + return !is_signup_form && !is_change_password_form && + !username_value.empty() && !password_value.empty(); +} + +std::string DetectedForm::GetFormKey() const { + return origin + "|" + action_url + "|" + username_field_name; +} + +// PasswordGeneratorOptions implementation +std::string PasswordGeneratorOptions::GeneratePassword() const { + std::string chars; + + if (include_lowercase) chars += "abcdefghijklmnopqrstuvwxyz"; + if (include_uppercase) chars += "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; + if (include_numbers) chars += "0123456789"; + if (include_symbols) chars += "!@#$%^&*()_+-=[]{}|;:,.<>?"; + + // Remove excluded characters + for (char c : excluded_chars) { + chars.erase(std::remove(chars.begin(), chars.end(), c), chars.end()); + } + + if (chars.empty()) { + chars = "abcdefghijklmnopqrstuvwxyz"; + } + + std::random_device rd; + std::mt19937 gen(rd()); + std::uniform_int_distribution<> dis(0, static_cast(chars.size()) - 1); + + std::string password; + password.reserve(length); + + for (int i = 0; i < length; i++) { + password += chars[dis(gen)]; + } + + return password; +} + +// PasswordManager implementation +PasswordManager::PasswordManager() + : is_locked_(false) + , initialized_(false) { +} + +PasswordManager::~PasswordManager() { + Shutdown(); +} + +bool PasswordManager::Initialize(const std::filesystem::path& data_directory) { + std::lock_guard lock(mutex_); + + if (initialized_) return true; + + data_dir_ = data_directory / "passwords"; + + try { + std::filesystem::create_directories(data_dir_); + } catch (...) { + return false; + } + + passwords_file_ = data_dir_ / "credentials.dat"; + blacklist_file_ = data_dir_ / "blacklist.json"; + settings_file_ = data_dir_ / "settings.json"; + + // Initialize encryption key + encryption_key_ = GetEncryptionKey(); + + LoadSettings(); + LoadBlacklist(); + + if (!Load()) { + // Start with empty credentials if file doesn't exist + credentials_.clear(); + } + + last_activity_ = std::chrono::steady_clock::now(); + initialized_ = true; + + return true; +} + +void PasswordManager::Shutdown() { + std::lock_guard lock(mutex_); + + if (!initialized_) return; + + Save(); + SaveBlacklist(); + SaveSettings(); + + // Clear sensitive data + for (auto& cred : credentials_) { + std::fill(cred.password.begin(), cred.password.end(), '\0'); + cred.password.clear(); + } + + std::fill(encryption_key_.begin(), encryption_key_.end(), '\0'); + encryption_key_.clear(); + + initialized_ = false; +} + +bool PasswordManager::SaveCredential(const SavedCredential& credential) { + std::lock_guard lock(mutex_); + + if (!initialized_ || !credential.IsValid()) return false; + + SavedCredential cred = credential; + + // Generate ID if not set + if (cred.id.empty()) { + cred.id = GenerateUUID(); + } + + // Set timestamps + uint64_t now = GetCurrentTimestamp(); + if (cred.date_created == 0) { + cred.date_created = now; + } + cred.date_password_modified = now; + + // Encrypt password + if (!cred.password.empty()) { + cred.encrypted_password = Encrypt(cred.password); + } + + // Check for existing credential with same origin+username + auto it = std::find_if(credentials_.begin(), credentials_.end(), + [&cred](const SavedCredential& c) { + return c.origin == cred.origin && c.username == cred.username; + }); + + if (it != credentials_.end()) { + // Update existing + cred.date_created = it->date_created; + cred.times_used = it->times_used; + *it = cred; + } else { + credentials_.push_back(cred); + } + + // Remove from blacklist if it was there + auto bl_it = std::find(blacklisted_origins_.begin(), blacklisted_origins_.end(), cred.origin); + if (bl_it != blacklisted_origins_.end()) { + blacklisted_origins_.erase(bl_it); + SaveBlacklist(); + } + + return Save(); +} + +bool PasswordManager::UpdateCredential(const SavedCredential& credential) { + std::lock_guard lock(mutex_); + + if (!initialized_) return false; + + auto it = std::find_if(credentials_.begin(), credentials_.end(), + [&credential](const SavedCredential& c) { return c.id == credential.id; }); + + if (it == credentials_.end()) return false; + + SavedCredential updated = credential; + updated.date_password_modified = GetCurrentTimestamp(); + + if (!updated.password.empty() && updated.password != it->password) { + updated.encrypted_password = Encrypt(updated.password); + } + + *it = updated; + return Save(); +} + +bool PasswordManager::DeleteCredential(const std::string& id) { + std::lock_guard lock(mutex_); + + if (!initialized_) return false; + + auto it = std::find_if(credentials_.begin(), credentials_.end(), + [&id](const SavedCredential& c) { return c.id == id; }); + + if (it == credentials_.end()) return false; + + // Clear sensitive data + std::fill(it->password.begin(), it->password.end(), '\0'); + + credentials_.erase(it); + return Save(); +} + +bool PasswordManager::DeleteCredentialsForOrigin(const std::string& origin) { + std::lock_guard lock(mutex_); + + if (!initialized_) return false; + + auto it = std::remove_if(credentials_.begin(), credentials_.end(), + [&origin](SavedCredential& c) { + if (c.origin == origin) { + std::fill(c.password.begin(), c.password.end(), '\0'); + return true; + } + return false; + }); + + if (it == credentials_.end()) return false; + + credentials_.erase(it, credentials_.end()); + return Save(); +} + +std::vector PasswordManager::GetCredentialsForOrigin(const std::string& origin) const { + std::lock_guard lock(mutex_); + + std::vector result; + + if (!initialized_) return result; + + std::string normalized_origin = ExtractOriginFromURL(origin); + + for (const auto& cred : credentials_) { + if (cred.origin == normalized_origin && !cred.blacklisted) { + SavedCredential decrypted = cred; + if (!decrypted.encrypted_password.empty() && decrypted.password.empty()) { + decrypted.password = const_cast(this)->Decrypt(decrypted.encrypted_password); + } + result.push_back(decrypted); + } + } + + // Sort by times_used (most used first), then by date_last_used + std::sort(result.begin(), result.end(), + [](const SavedCredential& a, const SavedCredential& b) { + if (a.times_used != b.times_used) return a.times_used > b.times_used; + return a.date_last_used > b.date_last_used; + }); + + return result; +} + +std::vector PasswordManager::GetAllCredentials() const { + std::lock_guard lock(mutex_); + + std::vector result; + + if (!initialized_) return result; + + for (const auto& cred : credentials_) { + SavedCredential decrypted = cred; + if (!decrypted.encrypted_password.empty() && decrypted.password.empty()) { + decrypted.password = const_cast(this)->Decrypt(decrypted.encrypted_password); + } + result.push_back(decrypted); + } + + return result; +} + +SavedCredential* PasswordManager::FindCredential(const std::string& id) { + for (auto& cred : credentials_) { + if (cred.id == id) return &cred; + } + return nullptr; +} + +const SavedCredential* PasswordManager::FindCredential(const std::string& id) const { + for (const auto& cred : credentials_) { + if (cred.id == id) return &cred; + } + return nullptr; +} + +bool PasswordManager::HasCredentialsForOrigin(const std::string& origin) const { + std::lock_guard lock(mutex_); + + std::string normalized = ExtractOriginFromURL(origin); + + return std::any_of(credentials_.begin(), credentials_.end(), + [&normalized](const SavedCredential& c) { + return c.origin == normalized && !c.blacklisted; + }); +} + +bool PasswordManager::IsOriginBlacklisted(const std::string& origin) const { + std::lock_guard lock(mutex_); + + std::string normalized = ExtractOriginFromURL(origin); + + return std::find(blacklisted_origins_.begin(), blacklisted_origins_.end(), normalized) + != blacklisted_origins_.end(); +} + +void PasswordManager::BlacklistOrigin(const std::string& origin) { + std::lock_guard lock(mutex_); + + std::string normalized = ExtractOriginFromURL(origin); + + if (std::find(blacklisted_origins_.begin(), blacklisted_origins_.end(), normalized) + == blacklisted_origins_.end()) { + blacklisted_origins_.push_back(normalized); + SaveBlacklist(); + } +} + +void PasswordManager::RemoveFromBlacklist(const std::string& origin) { + std::lock_guard lock(mutex_); + + std::string normalized = ExtractOriginFromURL(origin); + + auto it = std::find(blacklisted_origins_.begin(), blacklisted_origins_.end(), normalized); + if (it != blacklisted_origins_.end()) { + blacklisted_origins_.erase(it); + SaveBlacklist(); + } +} + +std::vector PasswordManager::GetBlacklistedOrigins() const { + std::lock_guard lock(mutex_); + return blacklisted_origins_; +} + +void PasswordManager::OnFormDetected(const DetectedForm& form) { + // Store detected form for potential later use + std::lock_guard lock(mutex_); + pending_forms_[form.GetFormKey()] = form; +} + +void PasswordManager::OnFormSubmitted(const DetectedForm& form) { + if (!settings_.offer_to_save_passwords) return; + if (form.username_value.empty() || form.password_value.empty()) return; + if (IsOriginBlacklisted(form.origin)) return; + + std::string origin = ExtractOriginFromURL(form.origin); + + // Check if this is a new credential or an update + auto existing = GetCredentialsForOrigin(origin); + + bool found_exact_match = false; + bool found_username_match = false; + SavedCredential matched_cred; + + for (const auto& cred : existing) { + if (cred.username == form.username_value) { + found_username_match = true; + matched_cred = cred; + if (cred.password == form.password_value) { + found_exact_match = true; + break; + } + } + } + + if (found_exact_match) { + // Credentials already saved and match - just update usage + RecordAutofillUsage(matched_cred.id); + return; + } + + if (found_username_match) { + // Password changed - store pending form for later update when user responds + std::string form_key = origin + "|" + form.username_value; + pending_forms_[form_key] = form; + + // Notify UI to show update prompt (the callback will be called with user's response) + // For now, auto-update if callback not set + if (!update_prompt_callback_) { + SavedCredential updated = matched_cred; + updated.password = form.password_value; + UpdateCredential(updated); + } + } else { + // New credential - store pending form for later save when user responds + std::string form_key = origin + "|" + form.username_value; + pending_forms_[form_key] = form; + + // Notify UI to show save prompt (the callback will be called with user's response) + // For now, auto-save if callback not set + if (!save_prompt_callback_) { + SavedCredential cred; + cred.origin = origin; + cred.signon_realm = form.action_url.empty() ? origin : form.action_url; + cred.username = form.username_value; + cred.password = form.password_value; + cred.username_field = form.username_field_name; + cred.password_field = form.password_field_name; + cred.form_action = form.action_url; + SaveCredential(cred); + } + } +} + +void PasswordManager::OnLoginSuccessful(const std::string& origin) { + // Could be used to confirm that a recently-submitted credential worked + (void)origin; +} + +void PasswordManager::OnLoginFailed(const std::string& origin) { + // Could be used to detect if a saved password is no longer valid + (void)origin; +} + +bool PasswordManager::ShouldOfferAutofill(const std::string& origin) const { + if (!settings_.auto_signin) return false; + if (IsOriginBlacklisted(origin)) return false; + return HasCredentialsForOrigin(origin); +} + +std::vector PasswordManager::GetAutofillSuggestions( + const std::string& origin, const std::string& username_hint) const { + + auto creds = GetCredentialsForOrigin(origin); + + if (!username_hint.empty()) { + std::string hint_lower = username_hint; + std::transform(hint_lower.begin(), hint_lower.end(), hint_lower.begin(), ::tolower); + + std::vector filtered; + for (const auto& cred : creds) { + std::string username_lower = cred.username; + std::transform(username_lower.begin(), username_lower.end(), username_lower.begin(), ::tolower); + + if (username_lower.find(hint_lower) == 0) { + filtered.push_back(cred); + } + } + return filtered; + } + + return creds; +} + +void PasswordManager::RecordAutofillUsage(const std::string& credential_id) { + std::lock_guard lock(mutex_); + + auto cred = FindCredential(credential_id); + if (cred) { + cred->times_used++; + cred->date_last_used = GetCurrentTimestamp(); + Save(); + } +} + +std::string PasswordManager::GeneratePassword(const PasswordGeneratorOptions& options) { + return options.GeneratePassword(); +} + +PasswordStrengthResult PasswordManager::CheckPasswordStrength(const std::string& password) const { + PasswordStrengthResult result; + result.score = 0; + + if (password.empty()) { + result.strength = PasswordStrength::VeryWeak; + result.feedback = "Password is empty"; + return result; + } + + // Length score + int length = static_cast(password.length()); + if (length >= 16) result.score += 30; + else if (length >= 12) result.score += 25; + else if (length >= 8) result.score += 15; + else result.suggestions.push_back("Use at least 8 characters"); + + // Character variety + bool has_lower = false, has_upper = false, has_digit = false, has_special = false; + for (char c : password) { + if (std::islower(c)) has_lower = true; + else if (std::isupper(c)) has_upper = true; + else if (std::isdigit(c)) has_digit = true; + else has_special = true; + } + + int variety_count = (has_lower ? 1 : 0) + (has_upper ? 1 : 0) + + (has_digit ? 1 : 0) + (has_special ? 1 : 0); + + result.score += variety_count * 15; + + if (!has_lower) result.suggestions.push_back("Add lowercase letters"); + if (!has_upper) result.suggestions.push_back("Add uppercase letters"); + if (!has_digit) result.suggestions.push_back("Add numbers"); + if (!has_special) result.suggestions.push_back("Add special characters"); + + // Check for common patterns (simplified) + std::string lower_pass = password; + std::transform(lower_pass.begin(), lower_pass.end(), lower_pass.begin(), ::tolower); + + std::vector common_patterns = { + "password", "123456", "qwerty", "abc123", "letmein", "welcome", + "admin", "login", "pass", "1234" + }; + + for (const auto& pattern : common_patterns) { + if (lower_pass.find(pattern) != std::string::npos) { + result.score -= 20; + result.suggestions.push_back("Avoid common words and patterns"); + break; + } + } + + // Repeated characters penalty + int repeats = 0; + for (size_t i = 1; i < password.length(); i++) { + if (password[i] == password[i-1]) repeats++; + } + if (repeats > 2) { + result.score -= 10; + result.suggestions.push_back("Avoid repeated characters"); + } + + // Ensure score is in range + result.score = std::max(0, std::min(100, result.score)); + + // Determine strength level + if (result.score >= 80) { + result.strength = PasswordStrength::VeryStrong; + result.feedback = "Very strong password"; + } else if (result.score >= 60) { + result.strength = PasswordStrength::Strong; + result.feedback = "Strong password"; + } else if (result.score >= 40) { + result.strength = PasswordStrength::Fair; + result.feedback = "Fair password - could be stronger"; + } else if (result.score >= 20) { + result.strength = PasswordStrength::Weak; + result.feedback = "Weak password - please improve"; + } else { + result.strength = PasswordStrength::VeryWeak; + result.feedback = "Very weak password"; + } + + return result; +} + +bool PasswordManager::ExportToCSV(const std::filesystem::path& filepath, const std::string& master_password) const { + (void)master_password; // Could be used to encrypt the export + + std::lock_guard lock(mutex_); + + std::ofstream file(filepath); + if (!file.is_open()) return false; + + // Write header (Chrome format) + file << "name,url,username,password,note\n"; + + for (const auto& cred : credentials_) { + if (cred.blacklisted) continue; + + std::string password = cred.password; + if (password.empty() && !cred.encrypted_password.empty()) { + password = const_cast(this)->Decrypt(cred.encrypted_password); + } + + // Escape fields for CSV + auto escape_csv = [](const std::string& s) { + if (s.find_first_of(",\"\n\r") != std::string::npos) { + std::string escaped = "\""; + for (char c : s) { + if (c == '"') escaped += "\"\""; + else escaped += c; + } + escaped += "\""; + return escaped; + } + return s; + }; + + file << escape_csv(cred.GetDisplayName()) << "," + << escape_csv(cred.origin) << "," + << escape_csv(cred.username) << "," + << escape_csv(password) << "," + << escape_csv(cred.notes) << "\n"; + } + + return true; +} + +bool PasswordManager::ImportFromCSV(const std::filesystem::path& filepath) { + std::ifstream file(filepath); + if (!file.is_open()) return false; + + std::string line; + bool first_line = true; + int imported = 0; + + while (std::getline(file, line)) { + if (first_line) { + first_line = false; + // Skip header + continue; + } + + if (line.empty()) continue; + + // Simple CSV parsing (doesn't handle all edge cases) + std::vector fields; + std::string current; + bool in_quotes = false; + + for (size_t i = 0; i < line.size(); i++) { + char c = line[i]; + if (c == '"') { + if (in_quotes && i + 1 < line.size() && line[i+1] == '"') { + current += '"'; + i++; + } else { + in_quotes = !in_quotes; + } + } else if (c == ',' && !in_quotes) { + fields.push_back(current); + current.clear(); + } else { + current += c; + } + } + fields.push_back(current); + + if (fields.size() >= 4) { + SavedCredential cred; + cred.origin = ExtractOriginFromURL(fields.size() > 1 ? fields[1] : ""); + cred.signon_realm = cred.origin; + cred.username = fields.size() > 2 ? fields[2] : ""; + cred.password = fields.size() > 3 ? fields[3] : ""; + cred.notes = fields.size() > 4 ? fields[4] : ""; + + if (cred.IsValid()) { + SaveCredential(cred); + imported++; + } + } + } + + return imported > 0; +} + +bool PasswordManager::ExportToJSON(const std::filesystem::path& filepath) const { + std::lock_guard lock(mutex_); + + std::ofstream file(filepath); + if (!file.is_open()) return false; + + file << "{\n \"credentials\": [\n"; + + bool first = true; + for (const auto& cred : credentials_) { + if (cred.blacklisted) continue; + + if (!first) file << ",\n"; + first = false; + + std::string password = cred.password; + if (password.empty() && !cred.encrypted_password.empty()) { + password = const_cast(this)->Decrypt(cred.encrypted_password); + } + + file << " {\n" + << " \"origin\": \"" << EscapeJsonStr(cred.origin) << "\",\n" + << " \"username\": \"" << EscapeJsonStr(cred.username) << "\",\n" + << " \"password\": \"" << EscapeJsonStr(password) << "\",\n" + << " \"notes\": \"" << EscapeJsonStr(cred.notes) << "\"\n" + << " }"; + } + + file << "\n ]\n}\n"; + + return true; +} + +bool PasswordManager::ImportFromJSON(const std::filesystem::path& filepath) { + std::ifstream file(filepath); + if (!file.is_open()) return false; + + std::stringstream buffer; + buffer << file.rdbuf(); + std::string content = buffer.str(); + + // Very simple JSON array parsing + size_t pos = 0; + int imported = 0; + + while ((pos = content.find("{", pos)) != std::string::npos) { + size_t end = content.find("}", pos); + if (end == std::string::npos) break; + + std::string obj = content.substr(pos, end - pos + 1); + + SavedCredential cred; + cred.origin = ParseJsonString(obj, "origin"); + if (cred.origin.empty()) { + cred.origin = ParseJsonString(obj, "url"); + } + cred.origin = ExtractOriginFromURL(cred.origin); + cred.signon_realm = cred.origin; + cred.username = ParseJsonString(obj, "username"); + cred.password = ParseJsonString(obj, "password"); + cred.notes = ParseJsonString(obj, "notes"); + + if (cred.IsValid()) { + SaveCredential(cred); + imported++; + } + + pos = end + 1; + } + + return imported > 0; +} + +bool PasswordManager::SetMasterPassword(const std::string& password) { + std::lock_guard lock(mutex_); + + master_password_hash_ = HashMasterPassword(password); + settings_.require_master_password = true; + + // Re-encrypt all passwords with new key + std::string new_key = DeriveKey(password); + std::string old_key = encryption_key_; + encryption_key_ = new_key; + + for (auto& cred : credentials_) { + if (!cred.encrypted_password.empty()) { + // Decrypt with old key + std::string temp = encryption_key_; + encryption_key_ = old_key; + cred.password = Decrypt(cred.encrypted_password); + encryption_key_ = temp; + + // Re-encrypt with new key + cred.encrypted_password = Encrypt(cred.password); + } + } + + SaveSettings(); + return Save(); +} + +bool PasswordManager::VerifyMasterPassword(const std::string& password) const { + if (master_password_hash_.empty()) return true; + return HashMasterPassword(password) == master_password_hash_; +} + +bool PasswordManager::HasMasterPassword() const { + return !master_password_hash_.empty(); +} + +bool PasswordManager::RemoveMasterPassword(const std::string& current_password) { + if (!VerifyMasterPassword(current_password)) return false; + + std::lock_guard lock(mutex_); + + master_password_hash_.clear(); + settings_.require_master_password = false; + + // Re-encrypt with default key + std::string new_key = GetEncryptionKey(); + std::string old_key = encryption_key_; + encryption_key_ = new_key; + + for (auto& cred : credentials_) { + if (!cred.encrypted_password.empty()) { + std::string temp = encryption_key_; + encryption_key_ = old_key; + cred.password = Decrypt(cred.encrypted_password); + encryption_key_ = temp; + cred.encrypted_password = Encrypt(cred.password); + } + } + + SaveSettings(); + return Save(); +} + +bool PasswordManager::ChangeMasterPassword(const std::string& old_password, const std::string& new_password) { + if (!VerifyMasterPassword(old_password)) return false; + return SetMasterPassword(new_password); +} + +bool PasswordManager::IsLocked() const { + return is_locked_; +} + +bool PasswordManager::Unlock(const std::string& master_password) { + if (!VerifyMasterPassword(master_password)) return false; + + std::lock_guard lock(mutex_); + is_locked_ = false; + last_activity_ = std::chrono::steady_clock::now(); + + if (!master_password.empty()) { + encryption_key_ = DeriveKey(master_password); + } + + return true; +} + +void PasswordManager::Lock() { + std::lock_guard lock(mutex_); + is_locked_ = true; + + // Clear decrypted passwords from memory + for (auto& cred : credentials_) { + std::fill(cred.password.begin(), cred.password.end(), '\0'); + cred.password.clear(); + } +} + +PasswordManager::Settings& PasswordManager::GetSettings() { + return settings_; +} + +const PasswordManager::Settings& PasswordManager::GetSettings() const { + return settings_; +} + +void PasswordManager::SaveSettings() { + std::ofstream file(settings_file_); + if (!file.is_open()) return; + + file << "{\n" + << " \"offer_to_save_passwords\": " << (settings_.offer_to_save_passwords ? "true" : "false") << ",\n" + << " \"auto_signin\": " << (settings_.auto_signin ? "true" : "false") << ",\n" + << " \"check_passwords_leaked\": " << (settings_.check_passwords_leaked ? "true" : "false") << ",\n" + << " \"generate_passwords_automatically\": " << (settings_.generate_passwords_automatically ? "true" : "false") << ",\n" + << " \"auto_lock_timeout_minutes\": " << settings_.auto_lock_timeout_minutes << ",\n" + << " \"require_master_password\": " << (settings_.require_master_password ? "true" : "false") << ",\n" + << " \"master_password_hash\": \"" << EscapeJsonStr(master_password_hash_) << "\"\n" + << "}\n"; +} + +PasswordManager::Stats PasswordManager::GetStats() const { + std::lock_guard lock(mutex_); + + Stats stats = {}; + stats.total_passwords = credentials_.size(); + stats.blacklisted_sites = blacklisted_origins_.size(); + + std::map password_counts; + uint64_t ninety_days_ago = GetCurrentTimestamp() - (90ULL * 24 * 60 * 60 * 1000); + + for (const auto& cred : credentials_) { + if (cred.blacklisted) continue; + + // Check for weak passwords + std::string password = cred.password; + if (password.empty() && !cred.encrypted_password.empty()) { + password = const_cast(this)->Decrypt(cred.encrypted_password); + } + + auto strength = CheckPasswordStrength(password); + if (strength.strength <= PasswordStrength::Weak) { + stats.weak_passwords++; + } + + // Check for reused passwords + if (!password.empty()) { + password_counts[password]++; + } + + // Check for old passwords + if (cred.date_password_modified < ninety_days_ago) { + stats.old_passwords++; + } + } + + // Count reused passwords + for (const auto& [pass, count] : password_counts) { + if (count > 1) { + stats.reused_passwords += count; + } + } + + return stats; +} + +bool PasswordManager::Load() { + if (!std::filesystem::exists(passwords_file_)) { + return false; + } + + std::ifstream file(passwords_file_, std::ios::binary); + if (!file.is_open()) return false; + + std::stringstream buffer; + buffer << file.rdbuf(); + std::string content = buffer.str(); + + // Decrypt the file content + if (!content.empty()) { + content = Decrypt(content); + } + + credentials_.clear(); + + // Parse JSON array + size_t pos = 0; + while ((pos = content.find("{", pos)) != std::string::npos) { + size_t end = content.find("}", pos); + if (end == std::string::npos) break; + + std::string obj = content.substr(pos, end - pos + 1); + + SavedCredential cred; + cred.id = ParseJsonString(obj, "id"); + cred.origin = ParseJsonString(obj, "origin"); + cred.signon_realm = ParseJsonString(obj, "signon_realm"); + cred.username = ParseJsonString(obj, "username"); + cred.encrypted_password = ParseJsonString(obj, "encrypted_password"); + cred.username_field = ParseJsonString(obj, "username_field"); + cred.password_field = ParseJsonString(obj, "password_field"); + cred.form_action = ParseJsonString(obj, "form_action"); + cred.date_created = ParseJsonUint64(obj, "date_created"); + cred.date_last_used = ParseJsonUint64(obj, "date_last_used"); + cred.date_password_modified = ParseJsonUint64(obj, "date_password_modified"); + cred.times_used = static_cast(ParseJsonUint64(obj, "times_used")); + cred.blacklisted = ParseJsonBool(obj, "blacklisted"); + cred.notes = ParseJsonString(obj, "notes"); + + if (!cred.id.empty() && !cred.origin.empty()) { + credentials_.push_back(cred); + } + + pos = end + 1; + } + + return true; +} + +bool PasswordManager::Save() { + std::ostringstream json; + json << "[\n"; + + bool first = true; + for (const auto& cred : credentials_) { + if (!first) json << ",\n"; + first = false; + + json << " {\n" + << " \"id\": \"" << EscapeJsonStr(cred.id) << "\",\n" + << " \"origin\": \"" << EscapeJsonStr(cred.origin) << "\",\n" + << " \"signon_realm\": \"" << EscapeJsonStr(cred.signon_realm) << "\",\n" + << " \"username\": \"" << EscapeJsonStr(cred.username) << "\",\n" + << " \"encrypted_password\": \"" << EscapeJsonStr(cred.encrypted_password) << "\",\n" + << " \"username_field\": \"" << EscapeJsonStr(cred.username_field) << "\",\n" + << " \"password_field\": \"" << EscapeJsonStr(cred.password_field) << "\",\n" + << " \"form_action\": \"" << EscapeJsonStr(cred.form_action) << "\",\n" + << " \"date_created\": " << cred.date_created << ",\n" + << " \"date_last_used\": " << cred.date_last_used << ",\n" + << " \"date_password_modified\": " << cred.date_password_modified << ",\n" + << " \"times_used\": " << cred.times_used << ",\n" + << " \"blacklisted\": " << (cred.blacklisted ? "true" : "false") << ",\n" + << " \"notes\": \"" << EscapeJsonStr(cred.notes) << "\"\n" + << " }"; + } + + json << "\n]\n"; + + // Encrypt and write + std::string encrypted = Encrypt(json.str()); + + std::ofstream file(passwords_file_, std::ios::binary | std::ios::trunc); + if (!file.is_open()) return false; + + file.write(encrypted.data(), encrypted.size()); + return file.good(); +} + +void PasswordManager::SetSavePromptCallback(SavePromptCallback callback) { + save_prompt_callback_ = std::move(callback); +} + +void PasswordManager::SetUpdatePromptCallback(UpdatePromptCallback callback) { + update_prompt_callback_ = std::move(callback); +} + +void PasswordManager::SetCredentialSelectedCallback(CredentialSelectedCallback callback) { + credential_selected_callback_ = std::move(callback); +} + +std::string PasswordManager::ExtractOriginFromURL(const std::string& url) { + if (url.empty()) return ""; + + // Find protocol + size_t proto_end = url.find("://"); + if (proto_end == std::string::npos) { + // No protocol, assume https + return "https://" + url.substr(0, url.find('/')); + } + + std::string protocol = url.substr(0, proto_end); + size_t host_start = proto_end + 3; + + // Find end of host (port or path) + size_t host_end = url.find_first_of(":/", host_start); + if (host_end == std::string::npos) { + host_end = url.length(); + } + + std::string host = url.substr(host_start, host_end - host_start); + + // Include port if non-standard + std::string port; + if (host_end < url.length() && url[host_end] == ':') { + size_t port_end = url.find('/', host_end); + if (port_end == std::string::npos) port_end = url.length(); + port = url.substr(host_end, port_end - host_end); + + // Skip default ports + if ((protocol == "http" && port == ":80") || + (protocol == "https" && port == ":443")) { + port.clear(); + } + } + + return protocol + "://" + host + port; +} + +std::string PasswordManager::GenerateUUID() { + std::random_device rd; + std::mt19937 gen(rd()); + std::uniform_int_distribution<> dis(0, 15); + + const char* hex = "0123456789abcdef"; + std::string uuid = "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx"; + + for (char& c : uuid) { + if (c == 'x') { + c = hex[dis(gen)]; + } else if (c == 'y') { + c = hex[(dis(gen) & 0x3) | 0x8]; + } + } + + return uuid; +} + +uint64_t PasswordManager::GetCurrentTimestamp() { + auto now = std::chrono::system_clock::now(); + auto duration = now.time_since_epoch(); + return std::chrono::duration_cast(duration).count(); +} + +std::string PasswordManager::Encrypt(const std::string& plaintext) const { + if (plaintext.empty()) return ""; + +#ifdef _WIN32 + // Use Windows DPAPI + DATA_BLOB input; + input.pbData = reinterpret_cast(const_cast(plaintext.data())); + input.cbData = static_cast(plaintext.size()); + + DATA_BLOB output; + if (CryptProtectData(&input, nullptr, nullptr, nullptr, nullptr, + CRYPTPROTECT_UI_FORBIDDEN, &output)) { + std::string result(reinterpret_cast(output.pbData), output.cbData); + LocalFree(output.pbData); + return Base64Encode(result); + } + return Base64Encode(plaintext); // Fallback: just encode +#elif defined(__APPLE__) + // Simple XOR encryption with key for macOS (Keychain would be better for production) + std::string result = plaintext; + for (size_t i = 0; i < result.size(); i++) { + result[i] ^= encryption_key_[i % encryption_key_.size()]; + } + return Base64Encode(result); +#else + // Simple XOR encryption for Linux + std::string result = plaintext; + for (size_t i = 0; i < result.size(); i++) { + result[i] ^= encryption_key_[i % encryption_key_.size()]; + } + return Base64Encode(result); +#endif +} + +std::string PasswordManager::Decrypt(const std::string& ciphertext) const { + if (ciphertext.empty()) return ""; + +#ifdef _WIN32 + std::string decoded = Base64Decode(ciphertext); + + DATA_BLOB input; + input.pbData = reinterpret_cast(const_cast(decoded.data())); + input.cbData = static_cast(decoded.size()); + + DATA_BLOB output; + if (CryptUnprotectData(&input, nullptr, nullptr, nullptr, nullptr, + CRYPTPROTECT_UI_FORBIDDEN, &output)) { + std::string result(reinterpret_cast(output.pbData), output.cbData); + LocalFree(output.pbData); + return result; + } + return decoded; // Fallback +#elif defined(__APPLE__) + std::string decoded = Base64Decode(ciphertext); + for (size_t i = 0; i < decoded.size(); i++) { + decoded[i] ^= encryption_key_[i % encryption_key_.size()]; + } + return decoded; +#else + std::string decoded = Base64Decode(ciphertext); + for (size_t i = 0; i < decoded.size(); i++) { + decoded[i] ^= encryption_key_[i % encryption_key_.size()]; + } + return decoded; +#endif +} + +std::string PasswordManager::HashMasterPassword(const std::string& password) const { + // Simple SHA-256 like hash (production should use bcrypt/argon2) + std::string salted = "UltralightBrowser_" + password + "_Salt2024"; + + // Simple hash function + uint64_t hash = 14695981039346656037ULL; // FNV offset basis + for (char c : salted) { + hash ^= static_cast(c); + hash *= 1099511628211ULL; // FNV prime + } + + std::ostringstream ss; + ss << std::hex << std::setfill('0') << std::setw(16) << hash; + return ss.str(); +} + +std::string PasswordManager::DeriveKey(const std::string& password) const { + // Simple key derivation (production should use PBKDF2/scrypt) + std::string key = "UltralightPWKey_" + password; + + // Stretch to 32 bytes + while (key.size() < 32) { + key += key; + } + + return key.substr(0, 32); +} + +void PasswordManager::LoadBlacklist() { + blacklisted_origins_.clear(); + + if (!std::filesystem::exists(blacklist_file_)) return; + + std::ifstream file(blacklist_file_); + if (!file.is_open()) return; + + std::stringstream buffer; + buffer << file.rdbuf(); + std::string content = buffer.str(); + + // Simple JSON array parsing + size_t pos = 0; + while ((pos = content.find('"', pos)) != std::string::npos) { + pos++; + size_t end = content.find('"', pos); + if (end == std::string::npos) break; + + std::string origin = content.substr(pos, end - pos); + if (!origin.empty() && origin.find("://") != std::string::npos) { + blacklisted_origins_.push_back(origin); + } + + pos = end + 1; + } +} + +void PasswordManager::SaveBlacklist() { + std::ofstream file(blacklist_file_); + if (!file.is_open()) return; + + file << "[\n"; + for (size_t i = 0; i < blacklisted_origins_.size(); i++) { + file << " \"" << EscapeJsonStr(blacklisted_origins_[i]) << "\""; + if (i < blacklisted_origins_.size() - 1) file << ","; + file << "\n"; + } + file << "]\n"; +} + +void PasswordManager::LoadSettings() { + settings_ = Settings(); // Defaults + + if (!std::filesystem::exists(settings_file_)) return; + + std::ifstream file(settings_file_); + if (!file.is_open()) return; + + std::stringstream buffer; + buffer << file.rdbuf(); + std::string content = buffer.str(); + + settings_.offer_to_save_passwords = ParseJsonBool(content, "offer_to_save_passwords", true); + settings_.auto_signin = ParseJsonBool(content, "auto_signin", true); + settings_.check_passwords_leaked = ParseJsonBool(content, "check_passwords_leaked", false); + settings_.generate_passwords_automatically = ParseJsonBool(content, "generate_passwords_automatically", true); + settings_.auto_lock_timeout_minutes = static_cast(ParseJsonUint64(content, "auto_lock_timeout_minutes")); + settings_.require_master_password = ParseJsonBool(content, "require_master_password", false); + master_password_hash_ = ParseJsonString(content, "master_password_hash"); +} + +std::string PasswordManager::GetEncryptionKey() const { + // Generate a machine-specific key + std::string key; + +#ifdef _WIN32 + // Use machine GUID on Windows + HKEY hKey; + if (RegOpenKeyExA(HKEY_LOCAL_MACHINE, "SOFTWARE\\Microsoft\\Cryptography", + 0, KEY_READ | KEY_WOW64_64KEY, &hKey) == ERROR_SUCCESS) { + char buffer[256]; + DWORD bufferSize = sizeof(buffer); + if (RegQueryValueExA(hKey, "MachineGuid", nullptr, nullptr, + reinterpret_cast(buffer), &bufferSize) == ERROR_SUCCESS) { + key = std::string(buffer, bufferSize - 1); + } + RegCloseKey(hKey); + } +#elif defined(__APPLE__) + // Use a fixed identifier for macOS (could use hardware UUID) + key = "UltralightBrowser_MacOS_Key_2024"; +#else + // Use machine-id on Linux + std::ifstream machine_id("/etc/machine-id"); + if (machine_id.is_open()) { + std::getline(machine_id, key); + } +#endif + + if (key.empty()) { + key = "UltralightBrowser_DefaultKey_2024"; + } + + // Ensure key is 32 bytes + while (key.size() < 32) { + key += key; + } + + return key.substr(0, 32); +} + +void PasswordManager::RegenerateEncryptionKey() { + encryption_key_ = GetEncryptionKey(); +} + +// Global instance +static std::unique_ptr g_password_manager; + +PasswordManager& GetPasswordManager() { + if (!g_password_manager) { + g_password_manager = std::make_unique(); + } + return *g_password_manager; +} + +} // namespace password diff --git a/src/PasswordManager.h b/src/PasswordManager.h new file mode 100644 index 0000000..e77d7a7 --- /dev/null +++ b/src/PasswordManager.h @@ -0,0 +1,252 @@ + #pragma once + +#include +#include +#include +#include +#include +#include +#include +#include + +/** + * Password Manager - Chrome-like password save and autofill functionality + * + * Features: + * - Secure storage with encryption + * - Form detection and credential capture + * - Auto-fill support + * - Password generation + * - Import/Export capabilities + * - Master password protection (optional) + */ + +namespace password { + +// Represents a saved credential +struct SavedCredential { + std::string id; // Unique identifier (UUID) + std::string origin; // Website origin (e.g., https://example.com) + std::string signon_realm; // Full login realm URL + std::string username; // Username/email + std::string encrypted_password; // Encrypted password (stored) + std::string password; // Decrypted password (in memory only) + std::string username_field; // Form field name for username + std::string password_field; // Form field name for password + std::string form_action; // Form submission URL + uint64_t date_created; // Timestamp when created + uint64_t date_last_used; // Timestamp when last used + uint64_t date_password_modified; // Timestamp when password changed + uint32_t times_used; // Number of times used for autofill + bool blacklisted; // True if user chose "Never save" for this site + std::string notes; // Optional user notes + + SavedCredential(); + bool IsValid() const; + std::string GetDisplayName() const; +}; + +// Represents a detected login form on a page +struct DetectedForm { + std::string origin; + std::string action_url; + std::string username_field_name; + std::string username_field_id; + std::string password_field_name; + std::string password_field_id; + std::string username_value; + std::string password_value; + std::string form_id; + bool has_remember_me; + bool is_signup_form; // Detected as registration form + bool is_change_password_form; // Detected as password change form + + DetectedForm(); + bool IsLoginForm() const; + std::string GetFormKey() const; +}; + +// Password generation options +struct PasswordGeneratorOptions { + int length = 16; + bool include_uppercase = true; + bool include_lowercase = true; + bool include_numbers = true; + bool include_symbols = true; + std::string excluded_chars; // Characters to exclude + + std::string GeneratePassword() const; +}; + +// Password strength result +enum class PasswordStrength { + VeryWeak = 0, + Weak = 1, + Fair = 2, + Strong = 3, + VeryStrong = 4 +}; + +struct PasswordStrengthResult { + PasswordStrength strength; + int score; // 0-100 + std::string feedback; + std::vector suggestions; +}; + +// Callback types +using SavePromptCallback = std::function; +using UpdatePromptCallback = std::function; +using CredentialSelectedCallback = std::function; + +// Main Password Manager class +class PasswordManager { +public: + PasswordManager(); + ~PasswordManager(); + + // Initialization + bool Initialize(const std::filesystem::path& data_directory); + void Shutdown(); + + // Core password operations + bool SaveCredential(const SavedCredential& credential); + bool UpdateCredential(const SavedCredential& credential); + bool DeleteCredential(const std::string& id); + bool DeleteCredentialsForOrigin(const std::string& origin); + + // Lookup operations + std::vector GetCredentialsForOrigin(const std::string& origin) const; + std::vector GetAllCredentials() const; + SavedCredential* FindCredential(const std::string& id); + const SavedCredential* FindCredential(const std::string& id) const; + bool HasCredentialsForOrigin(const std::string& origin) const; + + // Blacklist management (sites where user chose "Never save") + bool IsOriginBlacklisted(const std::string& origin) const; + void BlacklistOrigin(const std::string& origin); + void RemoveFromBlacklist(const std::string& origin); + std::vector GetBlacklistedOrigins() const; + + // Form detection and submission handling + void OnFormDetected(const DetectedForm& form); + void OnFormSubmitted(const DetectedForm& form); + void OnLoginSuccessful(const std::string& origin); + void OnLoginFailed(const std::string& origin); + + // Autofill support + bool ShouldOfferAutofill(const std::string& origin) const; + std::vector GetAutofillSuggestions(const std::string& origin, + const std::string& username_hint = "") const; + void RecordAutofillUsage(const std::string& credential_id); + + // Password generation + std::string GeneratePassword(const PasswordGeneratorOptions& options = PasswordGeneratorOptions()); + PasswordStrengthResult CheckPasswordStrength(const std::string& password) const; + + // Import/Export + bool ExportToCSV(const std::filesystem::path& filepath, const std::string& master_password = "") const; + bool ImportFromCSV(const std::filesystem::path& filepath); + bool ExportToJSON(const std::filesystem::path& filepath) const; + bool ImportFromJSON(const std::filesystem::path& filepath); + + // Master password (optional additional security) + bool SetMasterPassword(const std::string& password); + bool VerifyMasterPassword(const std::string& password) const; + bool HasMasterPassword() const; + bool RemoveMasterPassword(const std::string& current_password); + bool ChangeMasterPassword(const std::string& old_password, const std::string& new_password); + bool IsLocked() const; + bool Unlock(const std::string& master_password); + void Lock(); + + // Settings + struct Settings { + bool offer_to_save_passwords = true; + bool auto_signin = true; + bool check_passwords_leaked = false; // Future: integration with breach databases + bool generate_passwords_automatically = true; + int auto_lock_timeout_minutes = 15; // 0 = never auto-lock + bool require_master_password = false; + }; + + Settings& GetSettings(); + const Settings& GetSettings() const; + void SaveSettings(); + + // Statistics + struct Stats { + size_t total_passwords; + size_t weak_passwords; + size_t reused_passwords; + size_t old_passwords; // Not changed in 90+ days + size_t blacklisted_sites; + }; + + Stats GetStats() const; + + // Persistence + bool Load(); + bool Save(); + + // Event callbacks + void SetSavePromptCallback(SavePromptCallback callback); + void SetUpdatePromptCallback(UpdatePromptCallback callback); + void SetCredentialSelectedCallback(CredentialSelectedCallback callback); + + // Utility + static std::string ExtractOriginFromURL(const std::string& url); + static std::string GenerateUUID(); + static uint64_t GetCurrentTimestamp(); + +private: + // Encryption helpers + std::string Encrypt(const std::string& plaintext) const; + std::string Decrypt(const std::string& ciphertext) const; + std::string HashMasterPassword(const std::string& password) const; + std::string DeriveKey(const std::string& password) const; + + // Internal helpers + void LoadBlacklist(); + void SaveBlacklist(); + void LoadSettings(); + bool ShouldPromptToSave(const DetectedForm& form) const; + bool IsNewCredential(const DetectedForm& form) const; + bool IsUpdatedCredential(const DetectedForm& form, const SavedCredential& existing) const; + + // Platform-specific encryption key management + std::string GetEncryptionKey() const; + void RegenerateEncryptionKey(); + + // Member variables + std::filesystem::path data_dir_; + std::filesystem::path passwords_file_; + std::filesystem::path blacklist_file_; + std::filesystem::path settings_file_; + + std::vector credentials_; + std::vector blacklisted_origins_; + Settings settings_; + + std::string encryption_key_; + std::string master_password_hash_; + bool is_locked_; + std::chrono::steady_clock::time_point last_activity_; + + mutable std::mutex mutex_; + + // Pending form submissions awaiting user decision + std::map pending_forms_; + + // Callbacks + SavePromptCallback save_prompt_callback_; + UpdatePromptCallback update_prompt_callback_; + CredentialSelectedCallback credential_selected_callback_; + + bool initialized_; +}; + +// Global password manager instance +PasswordManager& GetPasswordManager(); + +} // namespace password From 8aca2235fb7640db32929a0d8e148d5fc4cdc9ab Mon Sep 17 00:00:00 2001 From: ovsky Date: Thu, 4 Dec 2025 14:53:29 +0100 Subject: [PATCH 02/19] Add password manager integration to Tab Implements password manager callbacks and JS bindings for password form detection, autofill, save/update prompts, and password management UI. Adds support for exporting/importing passwords, password statistics, and credential CRUD operations. Integrates password manager features with the tab's lifecycle and exposes relevant JS APIs for passwords.html and page scripts. --- src/Tab.cpp | 779 ++++++++++++++++++++++++++++++++++++++++++++++++++++ src/Tab.h | 22 ++ 2 files changed, 801 insertions(+) diff --git a/src/Tab.cpp b/src/Tab.cpp index 2cc34ac..09296ce 100644 --- a/src/Tab.cpp +++ b/src/Tab.cpp @@ -4,10 +4,12 @@ #include "DownloadManager.h" #include "ExtensionManager.h" #include "AdBlocker.h" +#include "PasswordManager.h" #include #include #include #include +#include #define INSPECTOR_DRAG_HANDLE_HEIGHT 10 @@ -473,6 +475,7 @@ void Tab::OnDOMReady(View *caller, uint64_t frame_id, bool is_main_frame, const // Check if this is the settings page bool is_settings_page = url_utf8.data() && std::strstr(url_utf8.data(), "settings.html") != nullptr; bool is_extensions_page = url_utf8.data() && std::strstr(url_utf8.data(), "extensions.html") != nullptr; + bool is_passwords_page = url_utf8.data() && std::strstr(url_utf8.data(), "passwords.html") != nullptr; if (is_settings_page) { @@ -499,6 +502,23 @@ void Tab::OnDOMReady(View *caller, uint64_t frame_id, bool is_main_frame, const global["OnOpenExtensionsFolder"] = BindJSCallback(&Tab::JS_OpenExtensionsFolder); } + if (is_passwords_page) + { + // Bind password manager functions when passwords page loads in a tab + global["getPasswords"] = BindJSCallbackWithRetval(&Tab::JS_GetPasswords); + global["getPasswordStats"] = BindJSCallbackWithRetval(&Tab::JS_GetPasswordStats); + global["savePassword"] = BindJSCallback(&Tab::JS_SavePassword); + global["deletePassword"] = BindJSCallback(&Tab::JS_DeletePassword); + global["getDecryptedPassword"] = BindJSCallbackWithRetval(&Tab::JS_GetDecryptedPassword); + global["savePasswordSettings"] = BindJSCallback(&Tab::JS_SavePasswordSettings); + global["exportPasswords"] = BindJSCallback(&Tab::JS_ExportPasswords); + global["importPasswords"] = BindJSCallback(&Tab::JS_ImportPasswords); + global["isDarkModeEnabled"] = BindJSCallbackWithRetval(&Tab::JS_IsDarkModeEnabled); + + // Notify the page that native bindings are ready, so it can reload passwords + caller->EvaluateScript("(function(){ if(typeof loadPasswords === 'function') loadPasswords(); })();", nullptr); + } + // Expose a unified native bridge on window.__ul using global function proxies global["__ul_back"] = BindJSCallback(&Tab::JS_Back); global["__ul_forward"] = BindJSCallback(&Tab::JS_Forward); @@ -615,6 +635,251 @@ void Tab::OnDOMReady(View *caller, uint64_t frame_id, bool is_main_frame, const std::string wrapped = "(function(){\ntry{\n" + script_code + "\n}catch(e){console.error('Extension error:',e);}\n})();"; caller->EvaluateScript(String(wrapped.c_str()), nullptr); } + + // Inject password form detection and autofill script + { + RefPtr ctx = caller->LockJSContext(); + SetJSContext(ctx->ctx()); + JSObject global = JSGlobalObject(); + + // Bind password manager callbacks + global["NativePasswordFormDetected"] = BindJSCallback(&Tab::OnPasswordFormDetected); + global["NativePasswordFormSubmitted"] = BindJSCallback(&Tab::OnPasswordFormSubmitted); + global["NativeGetPasswordSuggestions"] = BindJSCallbackWithRetval(&Tab::OnGetPasswordSuggestions); + global["NativePasswordSelected"] = BindJSCallback(&Tab::OnPasswordSelected); + global["NativePasswordSaveResponse"] = BindJSCallback(&Tab::OnPasswordSaveResponse); + + // Inject the password form detection script + const char *passwordScript = R"JS((function(){ + if (window.__ul_password_manager_installed) return; + window.__ul_password_manager_installed = true; + + var origin = window.location.origin; + var pendingForms = []; + var lastSubmittedCredentials = null; + + // Find password fields + function findPasswordFields() { + return document.querySelectorAll('input[type="password"]'); + } + + // Find associated username field for a password field + function findUsernameField(passwordField) { + var form = passwordField.closest('form'); + var fields = form ? form.querySelectorAll('input') : document.querySelectorAll('input'); + var usernameTypes = ['text', 'email', 'tel']; + var usernameNames = ['user', 'email', 'login', 'name', 'account', 'id']; + + for (var i = 0; i < fields.length; i++) { + var f = fields[i]; + if (f === passwordField) continue; + var type = (f.type || '').toLowerCase(); + var name = ((f.name || '') + (f.id || '')).toLowerCase(); + + if (usernameTypes.indexOf(type) >= 0) { + for (var j = 0; j < usernameNames.length; j++) { + if (name.indexOf(usernameNames[j]) >= 0) { + return f; + } + } + // If no specific name match, return the first text/email field before password + if (f.compareDocumentPosition(passwordField) & Node.DOCUMENT_POSITION_FOLLOWING) { + return f; + } + } + } + return null; + } + + // Create autofill dropdown + var dropdown = null; + function showAutofillDropdown(field, suggestions) { + hideAutofillDropdown(); + if (!suggestions || suggestions.length === 0) return; + + dropdown = document.createElement('div'); + dropdown.className = '__ul_password_dropdown'; + dropdown.style.cssText = 'position:absolute;z-index:999999;background:#fff;border:1px solid #ccc;border-radius:4px;box-shadow:0 2px 10px rgba(0,0,0,0.2);max-height:200px;overflow-y:auto;min-width:200px;'; + + var rect = field.getBoundingClientRect(); + dropdown.style.top = (window.scrollY + rect.bottom + 2) + 'px'; + dropdown.style.left = (window.scrollX + rect.left) + 'px'; + dropdown.style.width = Math.max(rect.width, 200) + 'px'; + + suggestions.forEach(function(s) { + var item = document.createElement('div'); + item.style.cssText = 'padding:8px 12px;cursor:pointer;border-bottom:1px solid #eee;'; + item.innerHTML = '
' + escapeHtml(s.username) + '
Password saved
'; + item.addEventListener('mouseenter', function() { this.style.background = '#f0f0f0'; }); + item.addEventListener('mouseleave', function() { this.style.background = '#fff'; }); + item.addEventListener('click', function(e) { + e.preventDefault(); + e.stopPropagation(); + if (window.NativePasswordSelected) { + window.NativePasswordSelected(s.username, s.password); + } + hideAutofillDropdown(); + }); + dropdown.appendChild(item); + }); + + document.body.appendChild(dropdown); + } + + function hideAutofillDropdown() { + if (dropdown && dropdown.parentNode) { + dropdown.parentNode.removeChild(dropdown); + } + dropdown = null; + } + + function escapeHtml(text) { + var div = document.createElement('div'); + div.textContent = text; + return div.innerHTML; + } + + // Fill form with credentials + window.__ul_fill_password_form = function(username, password) { + var pwFields = findPasswordFields(); + pwFields.forEach(function(pwField) { + var userField = findUsernameField(pwField); + if (userField) { + userField.value = username; + userField.dispatchEvent(new Event('input', {bubbles: true})); + userField.dispatchEvent(new Event('change', {bubbles: true})); + } + pwField.value = password; + pwField.dispatchEvent(new Event('input', {bubbles: true})); + pwField.dispatchEvent(new Event('change', {bubbles: true})); + }); + }; + + // Submit credentials to native + function submitCredentials(username, password) { + // Avoid duplicate submissions + var credKey = username + '|' + password; + if (lastSubmittedCredentials === credKey) return; + lastSubmittedCredentials = credKey; + + if (username && password && window.NativePasswordFormSubmitted) { + console.log('[PasswordManager] Submitting credentials for:', origin, username); + window.NativePasswordFormSubmitted(JSON.stringify({ + origin: origin, + username: username, + password: password + })); + } + } + + // Detect and handle password forms + function setupPasswordFields() { + var pwFields = findPasswordFields(); + if (pwFields.length === 0) return; + + // Notify native that we found password forms + if (window.NativePasswordFormDetected) { + window.NativePasswordFormDetected(JSON.stringify({origin: origin})); + } + + pwFields.forEach(function(pwField) { + if (pwField.__ul_pw_setup) return; + pwField.__ul_pw_setup = true; + + var userField = findUsernameField(pwField); + + // Show autofill dropdown on focus + function showDropdown(field) { + if (window.NativeGetPasswordSuggestions) { + var suggestionsJson = window.NativeGetPasswordSuggestions(origin); + try { + var suggestions = JSON.parse(suggestionsJson); + if (suggestions && suggestions.length > 0) { + showAutofillDropdown(field, suggestions); + } + } catch(e) {} + } + } + + pwField.addEventListener('focus', function() { showDropdown(pwField); }); + if (userField) { + userField.addEventListener('focus', function() { showDropdown(userField); }); + } + + // Hide dropdown when clicking elsewhere + document.addEventListener('click', function(e) { + if (dropdown && !dropdown.contains(e.target) && e.target !== pwField && e.target !== userField) { + hideAutofillDropdown(); + } + }); + + // Handle form submission + var form = pwField.closest('form'); + if (form && !form.__ul_pw_submit_setup) { + form.__ul_pw_submit_setup = true; + + // Traditional form submit + form.addEventListener('submit', function(e) { + var username = userField ? userField.value : ''; + var password = pwField.value; + submitCredentials(username, password); + }); + + // Also capture click on submit buttons (for JS-based form handling) + var submitBtns = form.querySelectorAll('button[type="submit"], input[type="submit"], button:not([type])'); + submitBtns.forEach(function(btn) { + if (btn.__ul_pw_click_setup) return; + btn.__ul_pw_click_setup = true; + btn.addEventListener('click', function(e) { + // Small delay to let form validation happen + setTimeout(function() { + var username = userField ? userField.value : ''; + var password = pwField.value; + if (username && password) { + submitCredentials(username, password); + } + }, 100); + }); + }); + } + + // Also handle Enter key on password field + pwField.addEventListener('keydown', function(e) { + if (e.key === 'Enter') { + setTimeout(function() { + var username = userField ? userField.value : ''; + var password = pwField.value; + if (username && password) { + submitCredentials(username, password); + } + }, 100); + } + }); + }); + } + + // Run on page load + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', setupPasswordFields); + } else { + setupPasswordFields(); + } + + // Also watch for dynamically added forms + var observer = new MutationObserver(function(mutations) { + var hasNewInputs = mutations.some(function(m) { + return m.addedNodes.length > 0; + }); + if (hasNewInputs) { + setTimeout(setupPasswordFields, 100); + } + }); + observer.observe(document.body || document.documentElement, {childList: true, subtree: true}); + + })();)JS"; + + caller->EvaluateScript(passwordScript, nullptr); + } } } @@ -1211,4 +1476,518 @@ void Tab::JS_OpenExtensionsFolder(const JSObject &obj, const JSArgs &args) ui_->OnOpenExtensionsFolder(obj, args); } +// --- Password Manager callbacks --- + +void Tab::OnPasswordFormDetected(const JSObject &obj, const JSArgs &args) +{ + // Called when a login form is detected on the page + // This is informational - we'll autofill if we have credentials + if (!ui_ || !ui_->password_manager() || args.empty()) + return; + + ultralight::String json_ul = args[0].ToString(); + auto json_str = json_ul.utf8(); + std::string data = json_str.data() ? json_str.data() : ""; + + // Parse origin from the JSON + auto extract_string = [&data](const std::string &key) -> std::string + { + std::string search_key = "\"" + key + "\":\""; + size_t pos = data.find(search_key); + if (pos == std::string::npos) + return ""; + pos += search_key.length(); + std::string result; + while (pos < data.length() && data[pos] != '"') + { + if (data[pos] == '\\' && pos + 1 < data.length()) + { + pos++; + result += data[pos]; + } + else + { + result += data[pos]; + } + pos++; + } + return result; + }; + + std::string origin = extract_string("origin"); + if (origin.empty()) + return; + + // Check if we have saved credentials for this origin + auto creds = ui_->password_manager()->GetCredentialsForOrigin(origin); + if (!creds.empty()) + { + // Notify JS that autofill is available + std::ostringstream ss; + ss << "(function(){ if(window.__ul_password_autofill_available) window.__ul_password_autofill_available(" << creds.size() << "); })();"; + view()->EvaluateScript(String(ss.str().c_str()), nullptr); + } +} + +void Tab::OnPasswordFormSubmitted(const JSObject &obj, const JSArgs &args) +{ + // Called when a login form is submitted - offer to save the password + if (!ui_ || !ui_->password_manager() || args.empty()) + return; + + ultralight::String json_ul = args[0].ToString(); + auto json_str = json_ul.utf8(); + std::string data = json_str.data() ? json_str.data() : ""; + + auto extract_string = [&data](const std::string &key) -> std::string + { + std::string search_key = "\"" + key + "\":\""; + size_t pos = data.find(search_key); + if (pos == std::string::npos) + return ""; + pos += search_key.length(); + std::string result; + while (pos < data.length() && data[pos] != '"') + { + if (data[pos] == '\\' && pos + 1 < data.length()) + { + pos++; + if (data[pos] == 'n') + result += '\n'; + else if (data[pos] == 't') + result += '\t'; + else if (data[pos] == '"') + result += '"'; + else if (data[pos] == '\\') + result += '\\'; + else + result += data[pos]; + } + else + { + result += data[pos]; + } + pos++; + } + return result; + }; + + std::string origin = extract_string("origin"); + std::string username = extract_string("username"); + std::string password = extract_string("password"); + + if (origin.empty() || username.empty() || password.empty()) + return; + + // Check if this origin is blacklisted + if (ui_->password_manager()->IsOriginBlacklisted(origin)) + return; + + // Check if we already have this exact credential + auto existing = ui_->password_manager()->GetCredentialsForOrigin(origin); + for (const auto &cred : existing) + { + if (cred.username == username && cred.password == password) + { + // Already saved, just update last used time + ui_->password_manager()->RecordAutofillUsage(cred.id); + return; + } + if (cred.username == username && cred.password != password) + { + // Password changed - store pending and show update prompt + pending_save_origin_ = origin; + pending_save_username_ = username; + pending_save_password_ = password; + + // Show update prompt in the UI overlay (same as save, but will update) + if (ui_) + { + ui_->ShowPasswordSavePrompt(origin, username); + } + return; + } + } + + // New credential - store pending and show save prompt + pending_save_origin_ = origin; + pending_save_username_ = username; + pending_save_password_ = password; + + // Notify the UI overlay to show the save prompt bar + if (ui_) + { + ui_->ShowPasswordSavePrompt(origin, username); + } +} + +JSValue Tab::OnGetPasswordSuggestions(const JSObject &obj, const JSArgs &args) +{ + // Return list of saved passwords for autofill dropdown + if (!ui_ || !ui_->password_manager() || args.empty()) + return JSValue("[]"); + + ultralight::String origin_ul = args[0].ToString(); + auto origin_str = origin_ul.utf8(); + std::string origin = origin_str.data() ? origin_str.data() : ""; + + if (origin.empty()) + return JSValue("[]"); + + auto creds = ui_->password_manager()->GetCredentialsForOrigin(origin); + + std::ostringstream ss; + ss << "["; + bool first = true; + for (const auto &cred : creds) + { + if (!first) + ss << ","; + first = false; + ss << "{"; + ss << "\"id\":\"" << util::EscapeJsonString(cred.id) << "\","; + ss << "\"username\":\"" << util::EscapeJsonString(cred.username) << "\","; + ss << "\"password\":\"" << util::EscapeJsonString(cred.password) << "\""; + ss << "}"; + } + ss << "]"; + + return JSValue(String(ss.str().c_str())); +} + +void Tab::OnPasswordSelected(const JSObject &obj, const JSArgs &args) +{ + // User selected a password from the dropdown - fill it in + if (!ui_ || !ui_->password_manager() || args.size() < 2) + return; + + ultralight::String username_ul = args[0].ToString(); + ultralight::String password_ul = args[1].ToString(); + + auto username_str = username_ul.utf8(); + auto password_str = password_ul.utf8(); + + std::string username = username_str.data() ? username_str.data() : ""; + std::string password = password_str.data() ? password_str.data() : ""; + + // Fill the form via JS + std::ostringstream ss; + ss << "(function(){ if(window.__ul_fill_password_form) window.__ul_fill_password_form(" + << "'" << util::EscapeJsonString(username) << "'," + << "'" << util::EscapeJsonString(password) << "'" + << "); })();"; + view()->EvaluateScript(String(ss.str().c_str()), nullptr); +} + +void Tab::OnPasswordSaveResponse(const JSObject &obj, const JSArgs &args) +{ + // User responded to save/update password prompt + if (!ui_ || !ui_->password_manager() || args.empty()) + return; + + ultralight::String response_ul = args[0].ToString(); + auto response_str = response_ul.utf8(); + std::string response = response_str.data() ? response_str.data() : ""; + + if (response == "save" || response == "update") + { + if (!pending_save_origin_.empty() && !pending_save_username_.empty()) + { + // Check if updating existing or saving new + auto existing = ui_->password_manager()->GetCredentialsForOrigin(pending_save_origin_); + bool found = false; + for (auto &cred : existing) + { + if (cred.username == pending_save_username_) + { + // Update existing credential + cred.password = pending_save_password_; + cred.date_password_modified = password::PasswordManager::GetCurrentTimestamp(); + ui_->password_manager()->UpdateCredential(cred); + found = true; + break; + } + } + + if (!found) + { + // Save new credential + password::SavedCredential cred; + cred.id = password::PasswordManager::GenerateUUID(); + cred.origin = pending_save_origin_; + cred.signon_realm = pending_save_origin_; + cred.username = pending_save_username_; + cred.password = pending_save_password_; + cred.date_created = password::PasswordManager::GetCurrentTimestamp(); + cred.date_password_modified = cred.date_created; + cred.date_last_used = 0; + cred.times_used = 0; + cred.blacklisted = false; + ui_->password_manager()->SaveCredential(cred); + } + } + } + else if (response == "never") + { + // Add to blacklist + if (!pending_save_origin_.empty()) + { + ui_->password_manager()->BlacklistOrigin(pending_save_origin_); + } + } + + // Clear pending + pending_save_origin_.clear(); + pending_save_username_.clear(); + pending_save_password_.clear(); + + // Hide the prompt + if (ui_) + { + ui_->HidePasswordSavePrompt(); + } +} + +// ============================================================================ +// Password Page JS Callbacks (for passwords.html) +// ============================================================================ + +JSValue Tab::JS_GetPasswords(const JSObject &obj, const JSArgs &args) +{ + if (!ui_ || !ui_->password_manager()) + return JSValue("[]"); + + auto credentials = ui_->password_manager()->GetAllCredentials(); + std::ostringstream ss; + ss << "["; + bool first = true; + for (const auto &cred : credentials) + { + if (!first) + ss << ","; + first = false; + + ss << "{"; + ss << "\"id\":\"" << util::EscapeJsonString(cred.id) << "\","; + ss << "\"origin\":\"" << util::EscapeJsonString(cred.origin) << "\","; + ss << "\"username\":\"" << util::EscapeJsonString(cred.username) << "\","; + ss << "\"password\":\"" << util::EscapeJsonString(cred.password) << "\","; + ss << "\"notes\":\"" << util::EscapeJsonString(cred.notes) << "\","; + ss << "\"created\":" << cred.date_created << ","; + ss << "\"modified\":" << cred.date_password_modified << ","; + ss << "\"last_used\":" << cred.date_last_used; + ss << "}"; + } + ss << "]"; + return JSValue(String(ss.str().c_str())); +} + +JSValue Tab::JS_GetPasswordStats(const JSObject &obj, const JSArgs &args) +{ + if (!ui_ || !ui_->password_manager()) + return JSValue("{}"); + + auto credentials = ui_->password_manager()->GetAllCredentials(); + int total = static_cast(credentials.size()); + int weak = 0; + int reused = 0; + std::unordered_map password_counts; + + for (const auto &cred : credentials) + { + auto strength = ui_->password_manager()->CheckPasswordStrength(cred.password); + if (strength.score < 3) + weak++; + + password_counts[cred.password]++; + } + + for (const auto &p : password_counts) + { + if (p.second > 1) + reused += p.second; + } + + int blacklisted = static_cast(ui_->password_manager()->GetBlacklistedOrigins().size()); + + std::ostringstream ss; + ss << "{"; + ss << "\"total_passwords\":" << total << ","; + ss << "\"weak_passwords\":" << weak << ","; + ss << "\"reused_passwords\":" << reused << ","; + ss << "\"blacklisted_sites\":" << blacklisted; + ss << "}"; + + return JSValue(String(ss.str().c_str())); +} + +void Tab::JS_SavePassword(const JSObject &obj, const JSArgs &args) +{ + if (!ui_ || !ui_->password_manager() || args.empty()) + return; + + ultralight::String json_ul = args[0].ToString(); + auto json_str = json_ul.utf8(); + std::string data = json_str.data() ? json_str.data() : ""; + + auto extract_string = [&data](const std::string &key) -> std::string + { + std::string search_key = "\"" + key + "\":\""; + size_t pos = data.find(search_key); + if (pos == std::string::npos) + return ""; + pos += search_key.length(); + std::string result; + while (pos < data.length() && data[pos] != '"') + { + if (data[pos] == '\\' && pos + 1 < data.length()) + { + pos++; + if (data[pos] == 'n') + result += '\n'; + else if (data[pos] == 't') + result += '\t'; + else if (data[pos] == '"') + result += '"'; + else if (data[pos] == '\\') + result += '\\'; + else + result += data[pos]; + } + else + { + result += data[pos]; + } + pos++; + } + return result; + }; + + std::string id = extract_string("id"); + std::string origin = extract_string("origin"); + std::string username = extract_string("username"); + std::string password = extract_string("password"); + std::string notes = extract_string("notes"); + + if (origin.empty() || username.empty()) + return; + + password::SavedCredential cred; + cred.id = id.empty() ? password::PasswordManager::GenerateUUID() : id; + cred.origin = origin; + cred.signon_realm = origin; + cred.username = username; + cred.password = password; + cred.notes = notes; + cred.date_created = password::PasswordManager::GetCurrentTimestamp(); + cred.date_password_modified = cred.date_created; + cred.date_last_used = 0; + cred.times_used = 0; + cred.blacklisted = false; + + if (id.empty()) + { + ui_->password_manager()->SaveCredential(cred); + } + else + { + ui_->password_manager()->UpdateCredential(cred); + } +} + +void Tab::JS_DeletePassword(const JSObject &obj, const JSArgs &args) +{ + if (!ui_ || !ui_->password_manager() || args.empty()) + return; + + ultralight::String id_ul = args[0].ToString(); + auto id_str = id_ul.utf8(); + std::string id = id_str.data() ? id_str.data() : ""; + + if (!id.empty()) + ui_->password_manager()->DeleteCredential(id); +} + +JSValue Tab::JS_GetDecryptedPassword(const JSObject &obj, const JSArgs &args) +{ + if (!ui_ || !ui_->password_manager() || args.empty()) + return JSValue(""); + + ultralight::String id_ul = args[0].ToString(); + auto id_str = id_ul.utf8(); + std::string id = id_str.data() ? id_str.data() : ""; + + auto credentials = ui_->password_manager()->GetAllCredentials(); + for (const auto &cred : credentials) + { + if (cred.id == id) + return JSValue(String(cred.password.c_str())); + } + + return JSValue(""); +} + +void Tab::JS_SavePasswordSettings(const JSObject &obj, const JSArgs &args) +{ + // TODO: Implement password settings storage +} + +void Tab::JS_ExportPasswords(const JSObject &obj, const JSArgs &args) +{ + if (!ui_ || !ui_->password_manager() || args.empty()) + return; + + ultralight::String format_ul = args[0].ToString(); + auto format_str = format_ul.utf8(); + std::string format = format_str.data() ? format_str.data() : "json"; + + std::filesystem::path export_path = std::filesystem::path(ui_->SettingsDirectory()) / "passwords_export"; + if (format == "csv") + { + export_path += ".csv"; + ui_->password_manager()->ExportToCSV(export_path.string()); + } + else + { + export_path += ".json"; + ui_->password_manager()->ExportToJSON(export_path.string()); + } +} + +void Tab::JS_ImportPasswords(const JSObject &obj, const JSArgs &args) +{ + if (!ui_ || !ui_->password_manager() || args.size() < 2) + return; + + ultralight::String content_ul = args[0].ToString(); + ultralight::String format_ul = args[1].ToString(); + + auto content_str = content_ul.utf8(); + auto format_str = format_ul.utf8(); + + std::string content = content_str.data() ? content_str.data() : ""; + std::string format = format_str.data() ? format_str.data() : "json"; + + // Create a temp file and import from it + std::filesystem::path temp_path = std::filesystem::temp_directory_path() / "passwords_import_temp"; + if (format == "csv") + temp_path += ".csv"; + else + temp_path += ".json"; + + std::ofstream temp_file(temp_path); + if (temp_file.is_open()) + { + temp_file << content; + temp_file.close(); + + if (format == "csv") + ui_->password_manager()->ImportFromCSV(temp_path.string()); + else + ui_->password_manager()->ImportFromJSON(temp_path.string()); + + std::filesystem::remove(temp_path); + } +} + // (Disable-history removed) diff --git a/src/Tab.h b/src/Tab.h index c337e92..0fd8bcb 100644 --- a/src/Tab.h +++ b/src/Tab.h @@ -141,6 +141,23 @@ class Tab : public ViewListener, void JS_CreateExtension(const JSObject &obj, const JSArgs &args); void JS_OpenExtensionsFolder(const JSObject &obj, const JSArgs &args); + // Passwords page callbacks (for passwords.html UI) + JSValue JS_GetPasswords(const JSObject &obj, const JSArgs &args); + JSValue JS_GetPasswordStats(const JSObject &obj, const JSArgs &args); + void JS_SavePassword(const JSObject &obj, const JSArgs &args); + void JS_DeletePassword(const JSObject &obj, const JSArgs &args); + JSValue JS_GetDecryptedPassword(const JSObject &obj, const JSArgs &args); + void JS_SavePasswordSettings(const JSObject &obj, const JSArgs &args); + void JS_ExportPasswords(const JSObject &obj, const JSArgs &args); + void JS_ImportPasswords(const JSObject &obj, const JSArgs &args); + + // Password Manager callbacks (called from page scripts) + void OnPasswordFormDetected(const JSObject &obj, const JSArgs &args); + void OnPasswordFormSubmitted(const JSObject &obj, const JSArgs &args); + JSValue OnGetPasswordSuggestions(const JSObject &obj, const JSArgs &args); + void OnPasswordSelected(const JSObject &obj, const JSArgs &args); + void OnPasswordSaveResponse(const JSObject &obj, const JSArgs &args); + protected: UI *ui_; RefPtr overlay_; @@ -148,4 +165,9 @@ class Tab : public ViewListener, uint64_t id_; bool ready_to_close_ = false; uint32_t container_width_, container_height_; + + // Password manager state for this tab + std::string pending_save_origin_; + std::string pending_save_username_; + std::string pending_save_password_; }; From ee13c5821cfe96c7bf583f6be425b0719d523acc Mon Sep 17 00:00:00 2001 From: ovsky Date: Thu, 4 Dec 2025 14:53:38 +0100 Subject: [PATCH 03/19] Add PasswordManager to build sources Included PasswordManager.h and PasswordManager.cpp in the SOURCES list in CMakeLists.txt to ensure they are compiled with the project. --- CMakeLists.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CMakeLists.txt b/CMakeLists.txt index c953dd6..c88e971 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -73,6 +73,8 @@ set(SOURCES "src/DownloadManager.cpp" "src/ExtensionManager.h" "src/ExtensionManager.cpp" + "src/PasswordManager.h" + "src/PasswordManager.cpp" "src/Tab.h" "src/Tab.cpp" "src/UI.h" From c77444b55a3d44c3d1a944c91e6a5e6921e5decc Mon Sep 17 00:00:00 2001 From: ovsky Date: Thu, 4 Dec 2025 14:53:48 +0100 Subject: [PATCH 04/19] Add password manager integration to UI Integrates a password manager into the UI, including initialization, JS bindings for password management actions, and support for password save prompts. Adds methods for handling password CRUD operations, import/export, autofill suggestions, and password statistics. Updates UI.h and UI.cpp to support these new features. --- src/UI.cpp | 376 +++++++++++++++++++++++++++++++++++++++++++++++++++++ src/UI.h | 29 +++++ 2 files changed, 405 insertions(+) diff --git a/src/UI.cpp b/src/UI.cpp index 29b00cb..bfb5561 100644 --- a/src/UI.cpp +++ b/src/UI.cpp @@ -18,6 +18,7 @@ #include #include #include "DownloadManager.h" +#include "PasswordManager.h" #include "Settings.h" #include "Utils.h" #include "AdBlocker.h" @@ -476,6 +477,10 @@ UI::UI(RefPtr window) download_manager_->SetOnChangeCallback([this]() { NotifyDownloadsChanged(); }); + // Initialize password manager + password_manager_ = std::make_unique(); + password_manager_->Initialize(SettingsDirectory()); + // Apply runtime toggles (visual sync happens on DOMReady via SyncSettingsStateToUI) ApplySettings(true, true); @@ -518,6 +523,10 @@ UI::UI(RefPtr window, AdBlocker *adblock, AdBlocker *tracker) download_manager_->SetOnChangeCallback([this]() { NotifyDownloadsChanged(); }); + // Initialize password manager + password_manager_ = std::make_unique(); + password_manager_->Initialize(SettingsDirectory()); + // Apply runtime toggles (visual sync happens on DOMReady via SyncSettingsStateToUI) ApplySettings(true, true); @@ -1315,6 +1324,8 @@ void UI::OnDOMReady(View *caller, uint64_t frame_id, bool is_main_frame, const S global["GetAdblockEnabled"] = BindJSCallbackWithRetval(&UI::OnGetAdblockEnabled); global["OnOpenSettingsPanel"] = BindJSCallback(&UI::OnOpenSettingsPanel); global["OnCloseSettingsPanel"] = BindJSCallback(&UI::OnCloseSettingsPanel); + // Password save bar callback + global["OnPasswordSaveBarResponse"] = BindJSCallback(&UI::OnPasswordSaveBarResponse); // Allow UI documents (including settings) to request a chrome overlay reload. global["OnReloadChromeUI"] = BindJSCallback(&UI::OnReloadChromeUI); // Allow UI documents to request reloading the active non-settings tab. @@ -1359,6 +1370,7 @@ void UI::OnDOMReady(View *caller, uint64_t frame_id, bool is_main_frame, const S global["OnAddressBarNavigate"] = BindJSCallback(&UI::OnAddressBarNavigate); global["OnOpenHistoryNewTab"] = BindJSCallback(&UI::OnOpenHistoryNewTab); global["OnOpenDownloadsNewTab"] = BindJSCallback(&UI::OnOpenDownloadsNewTab); + global["OnOpenPasswordsNewTab"] = BindJSCallback(&UI::OnOpenPasswordsNewTab); global["OnOpenExtensionsNewTab"] = BindJSCallback(&UI::OnOpenExtensionsNewTab); global["GetDownloadsSnapshot"] = BindJSCallbackWithRetval(&UI::OnDownloadsOverlayGet); global["ClearDownloadsSnapshot"] = BindJSCallback(&UI::OnDownloadsOverlayClear); @@ -1411,6 +1423,22 @@ void UI::OnDOMReady(View *caller, uint64_t frame_id, bool is_main_frame, const S global["OnOpenExtensionsFolder"] = BindJSCallback(&UI::OnOpenExtensionsFolder); } + // Passwords page bindings + bool is_passwords_page_view = url_utf8.data() && std::strstr(url_utf8.data(), "passwords.html") != nullptr; + if (is_passwords_page_view) + { + // Password management callbacks - bind directly to global object + global["getPasswords"] = BindJSCallbackWithRetval(&UI::OnGetPasswords); + global["getPasswordStats"] = BindJSCallbackWithRetval(&UI::OnGetPasswordStats); + global["savePassword"] = BindJSCallback(&UI::OnSavePassword); + global["deletePassword"] = BindJSCallback(&UI::OnDeletePassword); + global["getDecryptedPassword"] = BindJSCallbackWithRetval(&UI::OnGetDecryptedPassword); + global["savePasswordSettings"] = BindJSCallback(&UI::OnSavePasswordSettings); + global["exportPasswords"] = BindJSCallback(&UI::OnExportPasswords); + global["importPasswords"] = BindJSCallback(&UI::OnImportPasswords); + global["isDarkModeEnabled"] = BindJSCallbackWithRetval(&UI::OnIsDarkModeEnabled); + } + if (!is_menu_view && !is_ctx_view && !is_sugg_view && !is_downloads_overlay_view && !is_settings_page_view && !is_extensions_page_view) { SyncAdblockStateToUI(); @@ -1753,6 +1781,13 @@ void UI::OnOpenDownloadsNewTab(const JSObject &obj, const JSArgs &args) child->LoadURL("file:///downloads.html"); } +void UI::OnOpenPasswordsNewTab(const JSObject &obj, const JSArgs &args) +{ + RefPtr child = CreateNewTabForChildView(String("file:///passwords.html")); + if (child) + child->LoadURL("file:///passwords.html"); +} + void UI::OnOpenExtensionsNewTab(const JSObject &obj, const JSArgs &args) { RefPtr child = CreateNewTabForChildView(String("file:///extensions.html")); @@ -4693,3 +4728,344 @@ std::filesystem::path UI::LegacySettingsFilePath() namespace fs = std::filesystem; return fs::path("data") / "settings.json"; } + +// ============================================================================ +// Password Manager Implementation +// ============================================================================ + +ultralight::JSValue UI::OnGetPasswords(const JSObject &obj, const JSArgs &args) +{ + if (!password_manager_) + return JSValue("[]"); + + auto credentials = password_manager_->GetAllCredentials(); + std::ostringstream ss; + ss << "["; + bool first = true; + for (const auto &cred : credentials) + { + if (!first) + ss << ","; + first = false; + + ss << "{"; + ss << "\"id\":\"" << util::EscapeJsonString(cred.id) << "\","; + ss << "\"origin\":\"" << util::EscapeJsonString(cred.origin) << "\","; + ss << "\"username\":\"" << util::EscapeJsonString(cred.username) << "\","; + ss << "\"password\":\"" << util::EscapeJsonString(cred.password) << "\","; + ss << "\"notes\":\"" << util::EscapeJsonString(cred.notes) << "\","; + ss << "\"created\":" << cred.date_created << ","; + ss << "\"modified\":" << cred.date_password_modified << ","; + ss << "\"last_used\":" << cred.date_last_used; + ss << "}"; + } + ss << "]"; + return JSValue(String(ss.str().c_str())); +} + +ultralight::JSValue UI::OnGetPasswordStats(const JSObject &obj, const JSArgs &args) +{ + if (!password_manager_) + return JSValue("{}"); + + auto credentials = password_manager_->GetAllCredentials(); + int total = static_cast(credentials.size()); + int weak = 0; + int reused = 0; + std::unordered_map password_counts; + + for (const auto &cred : credentials) + { + auto strength = password_manager_->CheckPasswordStrength(cred.password); + if (strength.score < 3) + weak++; + + password_counts[cred.password]++; + } + + for (const auto &pair : password_counts) + { + if (pair.second > 1) + reused += pair.second; + } + + int blacklisted = static_cast(password_manager_->GetBlacklistedOrigins().size()); + + std::ostringstream ss; + ss << "{"; + ss << "\"total_passwords\":" << total << ","; + ss << "\"weak_passwords\":" << weak << ","; + ss << "\"reused_passwords\":" << reused << ","; + ss << "\"blacklisted_sites\":" << blacklisted; + ss << "}"; + return JSValue(String(ss.str().c_str())); +} + +void UI::OnSavePassword(const JSObject &obj, const JSArgs &args) +{ + if (!password_manager_ || args.empty()) + return; + + ultralight::String json = args[0].ToString(); + auto json_str = json.utf8(); + std::string data = json_str.data() ? json_str.data() : ""; + + // Parse JSON manually + auto extract_string = [&data](const std::string &key) -> std::string + { + std::string search_key = "\"" + key + "\":\""; + size_t pos = data.find(search_key); + if (pos == std::string::npos) + return ""; + pos += search_key.length(); + std::string result; + while (pos < data.length() && data[pos] != '"') + { + if (data[pos] == '\\' && pos + 1 < data.length()) + { + pos++; + if (data[pos] == 'n') + result += '\n'; + else if (data[pos] == 't') + result += '\t'; + else if (data[pos] == '"') + result += '"'; + else if (data[pos] == '\\') + result += '\\'; + else + result += data[pos]; + } + else + { + result += data[pos]; + } + pos++; + } + return result; + }; + + std::string id = extract_string("id"); + std::string origin = extract_string("origin"); + std::string username = extract_string("username"); + std::string password = extract_string("password"); + std::string notes = extract_string("notes"); + + if (origin.empty() || username.empty() || password.empty()) + return; + + password::SavedCredential cred; + cred.id = id.empty() ? password_manager_->GenerateUUID() : id; + cred.origin = origin; + cred.signon_realm = origin; + cred.username = username; + cred.password = password; + cred.notes = notes; + cred.date_created = static_cast(std::time(nullptr)); + cred.date_password_modified = cred.date_created; + cred.date_last_used = 0; + cred.times_used = 0; + cred.blacklisted = false; + + if (id.empty()) + { + password_manager_->SaveCredential(cred); + } + else + { + password_manager_->UpdateCredential(cred); + } +} + +void UI::OnDeletePassword(const JSObject &obj, const JSArgs &args) +{ + if (!password_manager_ || args.empty()) + return; + + ultralight::String id_ul = args[0].ToString(); + auto id_str = id_ul.utf8(); + std::string id = id_str.data() ? id_str.data() : ""; + + if (!id.empty()) + password_manager_->DeleteCredential(id); +} + +ultralight::JSValue UI::OnGetDecryptedPassword(const JSObject &obj, const JSArgs &args) +{ + if (!password_manager_ || args.empty()) + return JSValue(""); + + ultralight::String id_ul = args[0].ToString(); + auto id_str = id_ul.utf8(); + std::string id = id_str.data() ? id_str.data() : ""; + + auto credentials = password_manager_->GetAllCredentials(); + for (const auto &cred : credentials) + { + if (cred.id == id) + return JSValue(String(cred.password.c_str())); + } + return JSValue(""); +} + +void UI::OnSavePasswordSettings(const JSObject &obj, const JSArgs &args) +{ + // Password settings are stored in browser settings, not password manager + // This is a placeholder for future implementation +} + +void UI::OnExportPasswords(const JSObject &obj, const JSArgs &args) +{ + if (!password_manager_ || args.empty()) + return; + + ultralight::String format_ul = args[0].ToString(); + auto format_str = format_ul.utf8(); + std::string format = format_str.data() ? format_str.data() : "csv"; + + std::string filename = "passwords_export." + format; + std::filesystem::path export_path = SettingsDirectory() / filename; + + if (format == "json") + password_manager_->ExportToJSON(export_path.string()); + else + password_manager_->ExportToCSV(export_path.string()); +} + +void UI::OnImportPasswords(const JSObject &obj, const JSArgs &args) +{ + if (!password_manager_ || args.size() < 2) + return; + + ultralight::String content_ul = args[0].ToString(); + ultralight::String format_ul = args[1].ToString(); + + auto content_str = content_ul.utf8(); + auto format_str = format_ul.utf8(); + + std::string content = content_str.data() ? content_str.data() : ""; + std::string format = format_str.data() ? format_str.data() : "csv"; + + // Write to temp file and import + std::filesystem::path temp_path = SettingsDirectory() / ("temp_import." + format); + { + std::ofstream out(temp_path, std::ios::binary); + out << content; + } + + if (format == "json") + password_manager_->ImportFromJSON(temp_path.string()); + else + password_manager_->ImportFromCSV(temp_path.string()); + + std::filesystem::remove(temp_path); +} + +void UI::OnShowPasswordSavePrompt(const JSObject &obj, const JSArgs &args) +{ + // Placeholder for showing password save prompt overlay +} + +void UI::OnHidePasswordSavePrompt(const JSObject &obj, const JSArgs &args) +{ + // Placeholder for hiding password save prompt overlay +} + +void UI::OnPasswordSaveResponse(const JSObject &obj, const JSArgs &args) +{ + // Placeholder for handling user response to password save prompt +} + +// Non-JS versions called from Tab +void UI::ShowPasswordSavePrompt(const std::string &origin, const std::string &username) +{ + // Show password save prompt bar in the UI + std::ostringstream js; + js << "(function(){ " + << "if(typeof window.showPasswordSaveBar === 'function') { " + << " window.showPasswordSaveBar('" << util::EscapeJsonString(origin) << "', '" << util::EscapeJsonString(username) << "'); " + << "} " + << "})();"; + view()->EvaluateScript(String(js.str().c_str()), nullptr); +} + +void UI::HidePasswordSavePrompt() +{ + // Hide password save prompt bar in the UI + view()->EvaluateScript("(function(){ if(typeof window.hidePasswordSaveBar === 'function') window.hidePasswordSaveBar(); })();", nullptr); +} + +void UI::OnPasswordSaveBarResponse(const JSObject &obj, const JSArgs &args) +{ + // Called when user clicks Save/Never on the password save bar + if (!password_manager_ || args.size() < 3) + return; + + ultralight::String action_ul = args[0].ToString(); + ultralight::String origin_ul = args[1].ToString(); + ultralight::String username_ul = args[2].ToString(); + + auto action_str = action_ul.utf8(); + auto origin_str = origin_ul.utf8(); + auto username_str = username_ul.utf8(); + + std::string action = action_str.data() ? action_str.data() : ""; + std::string origin = origin_str.data() ? origin_str.data() : ""; + std::string username = username_str.data() ? username_str.data() : ""; + + // Get the active tab to retrieve pending credentials + if (active_tab_id_ && tabs_.count(active_tab_id_) && tabs_[active_tab_id_]) + { + auto &tab = tabs_[active_tab_id_]; + // Call the tab's password save response handler + JSArgs response_args; + response_args.push_back(JSValue(String(action.c_str()))); + tab->OnPasswordSaveResponse(JSObject(), response_args); + } +} + +void UI::OnPasswordNeverSave(const JSObject &obj, const JSArgs &args) +{ + if (!password_manager_ || args.empty()) + return; + + ultralight::String origin_ul = args[0].ToString(); + auto origin_str = origin_ul.utf8(); + std::string origin = origin_str.data() ? origin_str.data() : ""; + + if (!origin.empty()) + password_manager_->BlacklistOrigin(origin); +} + +ultralight::JSValue UI::OnGetAutofillSuggestions(const JSObject &obj, const JSArgs &args) +{ + if (!password_manager_ || args.empty()) + return JSValue("[]"); + + ultralight::String origin_ul = args[0].ToString(); + auto origin_str = origin_ul.utf8(); + std::string origin = origin_str.data() ? origin_str.data() : ""; + + auto credentials = password_manager_->GetCredentialsForOrigin(origin); + + std::ostringstream ss; + ss << "["; + bool first = true; + for (const auto &cred : credentials) + { + if (!first) + ss << ","; + first = false; + + ss << "{"; + ss << "\"id\":\"" << util::EscapeJsonString(cred.id) << "\","; + ss << "\"username\":\"" << util::EscapeJsonString(cred.username) << "\""; + ss << "}"; + } + ss << "]"; + return JSValue(String(ss.str().c_str())); +} + +ultralight::JSValue UI::OnIsDarkModeEnabled(const JSObject &obj, const JSArgs &args) +{ + return JSValue(dark_mode_enabled_); +} diff --git a/src/UI.h b/src/UI.h index 638104f..680a0c4 100644 --- a/src/UI.h +++ b/src/UI.h @@ -17,6 +17,11 @@ namespace drm class DRMWebViewTab; } +namespace password +{ + class PasswordManager; +} + using ultralight::JSArgs; using ultralight::JSFunction; using ultralight::JSObject; @@ -128,6 +133,7 @@ class UI : public WindowListener, // Open History page in a new tab (used by menu button and shortcuts) void OnOpenHistoryNewTab(const JSObject &obj, const JSArgs &args); void OnOpenDownloadsNewTab(const JSObject &obj, const JSArgs &args); + void OnOpenPasswordsNewTab(const JSObject &obj, const JSArgs &args); void OnAddressBarBlur(const JSObject &obj, const JSArgs &args); void OnAddressBarFocus(const JSObject &obj, const JSArgs &args); void OnMenuOpen(const JSObject &obj, const JSArgs &args); @@ -176,8 +182,30 @@ class UI : public WindowListener, void OnCreateExtension(const JSObject &obj, const JSArgs &args); void OnOpenExtensionsFolder(const JSObject &obj, const JSArgs &args); + // Password Manager callbacks + ultralight::JSValue OnGetPasswords(const JSObject &obj, const JSArgs &args); + ultralight::JSValue OnGetPasswordStats(const JSObject &obj, const JSArgs &args); + void OnSavePassword(const JSObject &obj, const JSArgs &args); + void OnDeletePassword(const JSObject &obj, const JSArgs &args); + ultralight::JSValue OnGetDecryptedPassword(const JSObject &obj, const JSArgs &args); + void OnSavePasswordSettings(const JSObject &obj, const JSArgs &args); + void OnExportPasswords(const JSObject &obj, const JSArgs &args); + void OnImportPasswords(const JSObject &obj, const JSArgs &args); + void OnShowPasswordSavePrompt(const JSObject &obj, const JSArgs &args); + void OnHidePasswordSavePrompt(const JSObject &obj, const JSArgs &args); + void OnPasswordSaveResponse(const JSObject &obj, const JSArgs &args); + void OnPasswordNeverSave(const JSObject &obj, const JSArgs &args); + void OnPasswordSaveBarResponse(const JSObject &obj, const JSArgs &args); + ultralight::JSValue OnGetAutofillSuggestions(const JSObject &obj, const JSArgs &args); + ultralight::JSValue OnIsDarkModeEnabled(const JSObject &obj, const JSArgs &args); + + // Password save prompt methods (called from Tab) + void ShowPasswordSavePrompt(const std::string &origin, const std::string &username); + void HidePasswordSavePrompt(); + RefPtr window() { return window_; } DownloadManager *download_manager() { return download_manager_.get(); } + password::PasswordManager *password_manager() { return password_manager_.get(); } AdBlocker *network_blocker() { return adblock_; } protected: @@ -291,6 +319,7 @@ class UI : public WindowListener, AdBlocker *adblock_ = nullptr; AdBlocker *trackerblock_ = nullptr; std::unique_ptr download_manager_; + std::unique_ptr password_manager_; bool downloads_overlay_had_active_ = false; bool downloads_overlay_user_dismissed_ = false; uint64_t downloads_last_sequence_seen_ = 0; From 71fa832d506f5e8c151f38fd1d869c00d3ce1941 Mon Sep 17 00:00:00 2001 From: ovsky Date: Thu, 4 Dec 2025 14:53:59 +0100 Subject: [PATCH 05/19] Add password save bar UI component Introduces a password save bar with UI, styles, and JavaScript logic for prompting users to save passwords. Includes auto-save countdown, domain extraction, and response handling for save, never, and close actions. --- assets/ui.html | 197 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 197 insertions(+) diff --git a/assets/ui.html b/assets/ui.html index d4400f9..a7304bf 100644 --- a/assets/ui.html +++ b/assets/ui.html @@ -509,6 +509,203 @@ window.addEventListener('resize', () => hide(true)); })(); + + + + + + + \ No newline at end of file From 4136ddad8c0eed34545e7b4b425d6515338eeafa Mon Sep 17 00:00:00 2001 From: ovsky Date: Thu, 4 Dec 2025 14:54:07 +0100 Subject: [PATCH 06/19] Add passwords management HTML page Introduces a new passwords.html file for Ultralight Browser, providing a UI for viewing, adding, editing, deleting, importing, and exporting saved passwords. Includes password strength checking, settings modal, and support for dark mode. --- assets/passwords.html | 1145 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 1145 insertions(+) create mode 100644 assets/passwords.html diff --git a/assets/passwords.html b/assets/passwords.html new file mode 100644 index 0000000..437906e --- /dev/null +++ b/assets/passwords.html @@ -0,0 +1,1145 @@ + + + + + + Passwords - Ultralight Browser + + + +
+
+

+ + Passwords +

+
+ + +
+
+ +
+
+
0
+
Saved Passwords
+
+
+
0
+
Weak Passwords
+
+
+
0
+
Reused
+
+
+
0
+
Never Save
+
+
+ +
+
+ + +
+
+ +
+
+ +

No saved passwords

+

When you save passwords in the browser, they'll appear here.

+
+
+ +

Import & Export

+
+ + + + +
+
+ + + + + + + + +
+ + + + From 137d0c22b6c85964c6c72f4a32c2f0009b533377 Mon Sep 17 00:00:00 2001 From: ovsky Date: Thu, 4 Dec 2025 14:54:12 +0100 Subject: [PATCH 07/19] Add 'Passwords' option to menu Introduces a new 'Passwords' menu item and corresponding action handler to open passwords in a new tab if available. --- assets/menu.html | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/assets/menu.html b/assets/menu.html index 2d04e10..2d5af00 100644 --- a/assets/menu.html +++ b/assets/menu.html @@ -69,6 +69,7 @@ + @@ -147,6 +148,9 @@ case 'downloads': if (window.OnOpenDownloadsNewTab) OnOpenDownloadsNewTab(); break; + case 'passwords': + if (window.OnOpenPasswordsNewTab) OnOpenPasswordsNewTab(); + break; case 'extensions': if (window.OnOpenExtensionsNewTab) OnOpenExtensionsNewTab(); break; From fb488883a39353dd1491100eeb34ac7f4ecbd04f Mon Sep 17 00:00:00 2001 From: ovsky Date: Thu, 4 Dec 2025 14:54:32 +0100 Subject: [PATCH 08/19] Improve clipboard copy fallback in passwords.html Enhanced the copyToClipboard function to use a fallback method with a temporary textarea when navigator.clipboard is unavailable or fails, improving compatibility across browsers and devices. --- assets/passwords.html | 40 +++++++++++++++++++++++++++++++++++----- 1 file changed, 35 insertions(+), 5 deletions(-) diff --git a/assets/passwords.html b/assets/passwords.html index 437906e..1478ce7 100644 --- a/assets/passwords.html +++ b/assets/passwords.html @@ -1000,11 +1000,41 @@

Password Settings

} function copyToClipboard(text, label) { - navigator.clipboard.writeText(text).then(() => { - showToast(`${label} copied to clipboard`); - }).catch(() => { - showToast('Failed to copy'); - }); + // Try multiple methods for clipboard access + if (navigator.clipboard && navigator.clipboard.writeText) { + navigator.clipboard.writeText(text).then(() => { + showToast(`${label} copied to clipboard`); + }).catch(() => { + fallbackCopyToClipboard(text, label); + }); + } else { + fallbackCopyToClipboard(text, label); + } + } + + function fallbackCopyToClipboard(text, label) { + // Fallback using a temporary textarea + const textarea = document.createElement('textarea'); + textarea.value = text; + textarea.style.position = 'fixed'; + textarea.style.left = '-9999px'; + textarea.style.top = '-9999px'; + document.body.appendChild(textarea); + textarea.focus(); + textarea.select(); + + try { + const successful = document.execCommand('copy'); + if (successful) { + showToast(`${label} copied to clipboard`); + } else { + showToast('Failed to copy - please copy manually'); + } + } catch (err) { + showToast('Failed to copy - please copy manually'); + } + + document.body.removeChild(textarea); } function generatePassword() { From 90e849282318449bf10ea8b5d08e848abc9ca1d5 Mon Sep 17 00:00:00 2001 From: ovsky Date: Thu, 4 Dec 2025 15:06:45 +0100 Subject: [PATCH 09/19] Revamp password manager UI with improved theming Updated CSS variables and styles for a more modern dark theme by default, refined light theme, and enhanced UI elements such as buttons, modals, and lists. Improved color palette, shadows, border radii, and transitions for better visual consistency and accessibility. Adjusted dark mode logic to default to dark unless light is explicitly requested. --- assets/passwords.html | 101 ++++++++++++++++++++++++++---------------- 1 file changed, 63 insertions(+), 38 deletions(-) diff --git a/assets/passwords.html b/assets/passwords.html index 1478ce7..334da15 100644 --- a/assets/passwords.html +++ b/assets/passwords.html @@ -6,28 +6,35 @@ Passwords - Ultralight Browser
-

Settings

+

+ + Settings +

Configure your browser preferences

From 877bf8f1f39d828e437db4762e0e123f656f0f3a Mon Sep 17 00:00:00 2001 From: ovsky Date: Thu, 4 Dec 2025 15:52:48 +0100 Subject: [PATCH 12/19] Improve settings search input styling Added !important to border-radius and set appearance to none for better cross-browser consistency in the settings search input. --- assets/settings.html | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/assets/settings.html b/assets/settings.html index 0cf0830..7fac84e 100644 --- a/assets/settings.html +++ b/assets/settings.html @@ -83,11 +83,13 @@ .settings-search input { flex: 1; padding: 12px 16px; - border-radius: 10px; + border-radius: 10px !important; border: 1px solid var(--border-color); background: var(--bg-tertiary); color: var(--text-primary); font-size: 14px; + -webkit-appearance: none; + appearance: none; } .settings-search input::placeholder { @@ -98,7 +100,7 @@ outline: none; border-color: var(--accent-color); box-shadow: 0 0 0 3px var(--accent-light); - border-radius: 10px; + border-radius: 10px !important; } .settings-search .clear-btn { From 67db695c965925f0f5ad4903750d34f390fec3bb Mon Sep 17 00:00:00 2001 From: ovsky Date: Thu, 4 Dec 2025 15:58:29 +0100 Subject: [PATCH 13/19] Add TabViewSettings for per-tab view configuration Introduces a TabViewSettings struct to encapsulate settings like JavaScript and hardware acceleration for each tab. These settings are now passed at tab creation and applied to the ViewConfig, allowing new tabs to reflect current browser settings while existing tabs retain their original configuration. --- src/Tab.cpp | 9 +++++++-- src/Tab.h | 11 ++++++++++- src/UI.cpp | 25 +++++++++++++++++++------ 3 files changed, 36 insertions(+), 9 deletions(-) diff --git a/src/Tab.cpp b/src/Tab.cpp index 09296ce..bcf7bd6 100644 --- a/src/Tab.cpp +++ b/src/Tab.cpp @@ -14,17 +14,22 @@ #define INSPECTOR_DRAG_HANDLE_HEIGHT 10 Tab::Tab(UI *ui, uint64_t id, uint32_t width, uint32_t height, int x, int y, - const std::string &user_agent) + const std::string &user_agent, const TabViewSettings &view_settings) : ui_(ui), id_(id), container_width_(width), container_height_(height) { // Create a ViewConfig with the user agent - always set one ultralight::ViewConfig cfg; cfg.initial_device_scale = ui->window_->scale(); + // Apply view settings from browser settings + cfg.enable_javascript = view_settings.enable_javascript; + // Match acceleration/display settings with main UI view to avoid GPU driver issues + // But allow user to override via settings (hardware_acceleration) if (ui->overlay_ && ui->overlay_->view()) { - cfg.is_accelerated = ui->overlay_->view()->is_accelerated(); + // Use hardware acceleration if both the main UI supports it AND user has it enabled + cfg.is_accelerated = ui->overlay_->view()->is_accelerated() && view_settings.hardware_acceleration; cfg.display_id = ui->overlay_->view()->display_id(); } diff --git a/src/Tab.h b/src/Tab.h index 0fd8bcb..e6a38bf 100644 --- a/src/Tab.h +++ b/src/Tab.h @@ -6,6 +6,15 @@ class UI; using namespace ultralight; +/** + * Settings that affect View/ViewConfig creation for a tab. + * These must be provided at tab creation time since ViewConfig is immutable. + */ +struct TabViewSettings { + bool enable_javascript = true; + bool hardware_acceleration = true; +}; + /** * Browser Tab UI implementation. Renders the actual page content in bottom pane. */ @@ -14,7 +23,7 @@ class Tab : public ViewListener, { public: Tab(UI *ui, uint64_t id, uint32_t width, uint32_t height, int x, int y, - const std::string &user_agent = ""); + const std::string &user_agent = "", const TabViewSettings &view_settings = TabViewSettings()); ~Tab(); void set_ready_to_close(bool ready) { ready_to_close_ = ready; } diff --git a/src/UI.cpp b/src/UI.cpp index bfb5561..218aef5 100644 --- a/src/UI.cpp +++ b/src/UI.cpp @@ -2092,7 +2092,13 @@ void UI::CreateNewTab() int tab_height = window->height() - ui_height_; if (tab_height < 1) tab_height = 1; - tabs_[id] = std::make_unique(this, id, window->width(), (uint32_t)tab_height, 0, ui_height_, active_user_agent_); + + // Build view settings from current browser settings + TabViewSettings view_settings; + view_settings.enable_javascript = settings_.enable_javascript; + view_settings.hardware_acceleration = settings_.hardware_acceleration; + + tabs_[id] = std::make_unique(this, id, window->width(), (uint32_t)tab_height, 0, ui_height_, active_user_agent_, view_settings); // Load local static start page const char *kStartPage = "file:///static-sties/google-static.html"; tabs_[id]->view()->LoadURL(kStartPage); @@ -2114,7 +2120,13 @@ RefPtr UI::CreateNewTabForChildView(const String &url) int tab_height = window->height() - ui_height_; if (tab_height < 1) tab_height = 1; - tabs_[id] = std::make_unique(this, id, window->width(), (uint32_t)tab_height, 0, ui_height_, active_user_agent_); + + // Build view settings from current browser settings + TabViewSettings view_settings; + view_settings.enable_javascript = settings_.enable_javascript; + view_settings.hardware_acceleration = settings_.hardware_acceleration; + + tabs_[id] = std::make_unique(this, id, window->width(), (uint32_t)tab_height, 0, ui_height_, active_user_agent_, view_settings); { RefPtr lock(view()->LockJSContext()); @@ -3102,8 +3114,9 @@ void UI::ApplySettings(bool initial, bool snapshot_is_baseline) adblock_enabled_cached_ = settings_.enable_adblock; clear_history_on_exit_ = settings_.clear_history_on_exit; - // Note: JavaScript, web security, cookies, DNT would require View config changes - // These settings are stored and can be applied on next tab creation + // Note: enable_javascript and hardware_acceleration are applied to NEW tabs via TabViewSettings + // Existing tabs keep their original settings since ViewConfig is immutable after creation. + // enable_web_security, block_third_party_cookies, do_not_track are not yet implemented. // Address Bar & Suggestions suggestions_enabled_ = settings_.enable_suggestions; @@ -3117,8 +3130,8 @@ void UI::ApplySettings(bool initial, bool snapshot_is_baseline) // ask_download_location would be checked when download starts // Performance - // smooth_scrolling, hardware_acceleration, local_storage, database - // These would typically be applied during View/Config creation + // enable_javascript and hardware_acceleration are applied during Tab creation (see CreateNewTab) + // smooth_scrolling, local_storage, database - would require additional Ultralight session config // Accessibility reduce_motion_enabled_ = settings_.reduce_motion; From 693f00732d744adf4577d580f808228a16386606 Mon Sep 17 00:00:00 2001 From: ovsky Date: Thu, 4 Dec 2025 16:18:37 +0100 Subject: [PATCH 14/19] Add DRM enable prompt bar to UI Introduces a prompt bar for enabling DRM (Widevine) required for video playback. Includes UI elements, styling, and JavaScript logic to handle user responses for enabling DRM once, always, or dismissing the prompt. --- assets/ui.html | 158 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 158 insertions(+) diff --git a/assets/ui.html b/assets/ui.html index 61ac986..34bd438 100644 --- a/assets/ui.html +++ b/assets/ui.html @@ -721,6 +721,164 @@ } })(); + + + + + + + \ No newline at end of file From 3321f220b3942a85202f173f9f979967dabf4492 Mon Sep 17 00:00:00 2001 From: ovsky Date: Thu, 4 Dec 2025 16:23:49 +0100 Subject: [PATCH 15/19] Add IsDrmSite method to DRMSettings Introduces the IsDrmSite method to check if a URL matches a DRM site, independent of the enabled_ flag. Updates both DRMSettings.cpp and DRMSettings.h to support this functionality. --- src/drm/DRMSettings.cpp | 6 ++++++ src/drm/DRMSettings.h | 3 +++ 2 files changed, 9 insertions(+) diff --git a/src/drm/DRMSettings.cpp b/src/drm/DRMSettings.cpp index 3882fb0..4745e93 100644 --- a/src/drm/DRMSettings.cpp +++ b/src/drm/DRMSettings.cpp @@ -205,6 +205,12 @@ namespace drm { if (!enabled_) return false; + return IsDrmSite(url); + } + + bool DRMSettings::IsDrmSite(const std::string &url) const + { + // Check if URL matches a DRM site (ignores enabled_ flag) std::string host = NormalizeHost(ExtractHost(url)); if (host.empty()) return false; diff --git a/src/drm/DRMSettings.h b/src/drm/DRMSettings.h index c8a1351..2be1eea 100644 --- a/src/drm/DRMSettings.h +++ b/src/drm/DRMSettings.h @@ -27,6 +27,9 @@ namespace drm void SetSiteRule(const std::string &host, const SiteRule &rule); bool IsDRMRequired(const std::string &url) const; + + // Check if URL matches a DRM site (ignores enabled_ flag) + bool IsDrmSite(const std::string &url) const; const std::filesystem::path &storage_path() const { return storage_path_; } From 9d992c7cede0d3a8fa78255f0681ec60ba62d6b2 Mon Sep 17 00:00:00 2001 From: ovsky Date: Thu, 4 Dec 2025 16:23:57 +0100 Subject: [PATCH 16/19] Add DRM prompt bar and update DRM WebView logic Introduces a DRM prompt bar to allow users to enable DRM WebView temporarily or permanently when visiting DRM-protected sites. The DRM WebView setting is now disabled by default, requiring user opt-in. The MaybeOpenDrmTab logic is updated to show the prompt if DRM is required but not enabled, and new methods for showing, hiding, and handling responses from the DRM prompt bar are added. --- src/UI.cpp | 84 ++++++++++++++++++++++++++++++++++++++++++++++++++---- src/UI.h | 9 ++++-- 2 files changed, 86 insertions(+), 7 deletions(-) diff --git a/src/UI.cpp b/src/UI.cpp index 218aef5..2647239 100644 --- a/src/UI.cpp +++ b/src/UI.cpp @@ -154,7 +154,7 @@ namespace // DRM subsystem SettingDescriptor{"enable_drm_webview", "Enable DRM WebView", "Automatically switch Widevine-protected sites to a native DRM-capable WebView.", - "drm", "Requires native runtime", false, &UI::BrowserSettings::enable_drm_webview, true}, + "drm", "Requires native runtime", false, &UI::BrowserSettings::enable_drm_webview, false}, // Networking / User Agent SettingDescriptor{"use_custom_user_agent", "Use custom user agent", @@ -645,14 +645,17 @@ void UI::EnsureDrmManager() bool UI::MaybeOpenDrmTab(uint64_t tab_id, const std::string &url, bool user_initiated) { - if (!settings_.enable_drm_webview) + // Check if URL matches a DRM site (ignores DRMSettings enabled_ flag) + if (!drm_settings_.IsDrmSite(url)) { - // DRM webview is disabled in settings + // URL is not a DRM site return false; } - if (!drm_settings_.IsDRMRequired(url)) + + if (!settings_.enable_drm_webview) { - // URL is not a DRM site + // DRM webview is disabled in browser settings - show prompt to user + ShowDrmPrompt(url, tab_id); return false; } @@ -1326,6 +1329,8 @@ void UI::OnDOMReady(View *caller, uint64_t frame_id, bool is_main_frame, const S global["OnCloseSettingsPanel"] = BindJSCallback(&UI::OnCloseSettingsPanel); // Password save bar callback global["OnPasswordSaveBarResponse"] = BindJSCallback(&UI::OnPasswordSaveBarResponse); + // DRM prompt bar callback + global["OnDrmPromptResponse"] = BindJSCallback(&UI::OnDrmPromptResponse); // Allow UI documents (including settings) to request a chrome overlay reload. global["OnReloadChromeUI"] = BindJSCallback(&UI::OnReloadChromeUI); // Allow UI documents to request reloading the active non-settings tab. @@ -5049,6 +5054,75 @@ void UI::OnPasswordNeverSave(const JSObject &obj, const JSArgs &args) password_manager_->BlacklistOrigin(origin); } +// DRM Prompt functionality +void UI::ShowDrmPrompt(const std::string &url, uint64_t tab_id) +{ + // Show DRM prompt bar in the UI + std::ostringstream js; + js << "(function(){ " + << "if(typeof window.showDrmPromptBar === 'function') { " + << " window.showDrmPromptBar('" << util::EscapeJsonString(url) << "', " << tab_id << "); " + << "} " + << "})();"; + view()->EvaluateScript(String(js.str().c_str()), nullptr); +} + +void UI::HideDrmPrompt() +{ + // Hide DRM prompt bar in the UI + view()->EvaluateScript("(function(){ if(typeof window.hideDrmPromptBar === 'function') window.hideDrmPromptBar(); })();", nullptr); +} + +void UI::OnDrmPromptResponse(const JSObject &obj, const JSArgs &args) +{ + // Called when user clicks Enable DRM / Always Enable / Dismiss on the DRM prompt bar + if (args.size() < 3) + return; + + ultralight::String action_ul = args[0].ToString(); + ultralight::String url_ul = args[1].ToString(); + int tab_id_int = args[2].ToInteger(); + + auto action_str = action_ul.utf8(); + auto url_str = url_ul.utf8(); + + std::string action = action_str.data() ? action_str.data() : ""; + std::string url = url_str.data() ? url_str.data() : ""; + uint64_t tab_id = static_cast(tab_id_int); + + if (action == "enable_once") + { + // Temporarily enable DRM for this navigation only + // We'll directly open the DRM tab without changing the setting + bool old_setting = settings_.enable_drm_webview; + settings_.enable_drm_webview = true; + + // Try to open the DRM tab + if (tab_id > 0 && tabs_.count(tab_id)) + { + // Force open DRM tab for this URL + MaybeOpenDrmTab(tab_id, url, true); + } + + // Restore the setting (user didn't want it permanently enabled) + settings_.enable_drm_webview = old_setting; + } + else if (action == "enable_always") + { + // Permanently enable DRM setting + settings_.enable_drm_webview = true; + ApplySettings(false, false); + SaveSettingsToDisk(); + + // Now open the DRM tab + if (tab_id > 0 && tabs_.count(tab_id)) + { + MaybeOpenDrmTab(tab_id, url, true); + } + } + // "dismiss" action - do nothing, just close the bar +} + ultralight::JSValue UI::OnGetAutofillSuggestions(const JSObject &obj, const JSArgs &args) { if (!password_manager_ || args.empty()) diff --git a/src/UI.h b/src/UI.h index 680a0c4..b8ce585 100644 --- a/src/UI.h +++ b/src/UI.h @@ -98,8 +98,8 @@ class UI : public WindowListener, // When false, user must press "Save Changes" in the Settings UI. bool auto_save_settings = true; - // DRM WebView subsystem toggle - bool enable_drm_webview = true; + // DRM WebView subsystem toggle (disabled by default, user must opt-in) + bool enable_drm_webview = false; bool operator==(const BrowserSettings &other) const; bool operator!=(const BrowserSettings &other) const { return !(*this == other); } @@ -199,6 +199,11 @@ class UI : public WindowListener, ultralight::JSValue OnGetAutofillSuggestions(const JSObject &obj, const JSArgs &args); ultralight::JSValue OnIsDarkModeEnabled(const JSObject &obj, const JSArgs &args); + // DRM prompt methods + void OnDrmPromptResponse(const JSObject &obj, const JSArgs &args); + void ShowDrmPrompt(const std::string &url, uint64_t tab_id); + void HideDrmPrompt(); + // Password save prompt methods (called from Tab) void ShowPasswordSavePrompt(const std::string &origin, const std::string &username); void HidePasswordSavePrompt(); From 44f28a174d1f9af516b860bf8e478eb85103c067 Mon Sep 17 00:00:00 2001 From: ovsky Date: Thu, 4 Dec 2025 16:30:37 +0100 Subject: [PATCH 17/19] Sync DRM webview state from browser settings DRM settings are now loaded and their enabled state is set based on the browser's settings.json, making browser settings the source of truth for enable_drm_webview. --- src/Settings.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Settings.cpp b/src/Settings.cpp index 07734e7..9dbb0d8 100644 --- a/src/Settings.cpp +++ b/src/Settings.cpp @@ -156,9 +156,10 @@ bool SettingsManager::LoadSettingsFromDisk(UI &ui) ui.saved_settings_ = ui.settings_; ui.settings_dirty_ = false; + // Load DRM settings file, then sync its enabled state FROM the browser settings + // (browser settings.json is the source of truth for enable_drm_webview) ui.drm_settings_.Load(); - ui.settings_.enable_drm_webview = ui.drm_settings_.IsEnabled(); - ui.saved_settings_.enable_drm_webview = ui.settings_.enable_drm_webview; + ui.drm_settings_.SetEnabled(ui.settings_.enable_drm_webview); if (migrated) { From 3ccadf647f022f000b1da630448e465acf4a8c47 Mon Sep 17 00:00:00 2001 From: ovsky Date: Thu, 4 Dec 2025 16:45:01 +0100 Subject: [PATCH 18/19] Limit build parallelism to 2 in ARM64 workflow Restricts cmake build parallelism to 2 jobs in the ARM64 GitHub Actions workflow to prevent compiler crashes under QEMU emulation. --- .github/workflows/build-linux-arm64.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build-linux-arm64.yml b/.github/workflows/build-linux-arm64.yml index d224f1c..12d899d 100644 --- a/.github/workflows/build-linux-arm64.yml +++ b/.github/workflows/build-linux-arm64.yml @@ -148,7 +148,8 @@ jobs: echo "${VER:-Unknown}" > build/ultralight_sdk_version.txt echo "== ARM64: Configure & Build ==" cmake -S . -B build -DCMAKE_BUILD_TYPE=Release -DULTRALIGHT_SDK_ROOT="$ROOT" -DBUILD_TESTING=OFF -DAUTO_INSTALL_CURL=ON -DWEBBROWSER_VERSION="$WEBBROWSER_VERSION" - cmake --build build --parallel + # Limit parallelism to 2 jobs to avoid compiler crashes under QEMU emulation + cmake --build build --parallel 2 echo "== ARM64: Package (TGZ) ==" cpack --config build/CPackConfig.cmake -C Release -G TGZ -D CPACK_OUTPUT_FILE_PREFIX="$GITHUB_WORKSPACE/build" # Rename package to final name From caf5d3468b69c2515249bdd9c01f3ef540e41b04 Mon Sep 17 00:00:00 2001 From: ovsky Date: Thu, 4 Dec 2025 16:56:05 +0100 Subject: [PATCH 19/19] Add libssl-dev to Linux build dependencies Added libssl-dev to the package installation steps in both build-linux and build-linux-arm64 GitHub Actions workflows to ensure required SSL libraries are available during builds. --- .github/workflows/build-linux-arm64.yml | 2 +- .github/workflows/build-linux.yml | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build-linux-arm64.yml b/.github/workflows/build-linux-arm64.yml index 12d899d..0cac5f9 100644 --- a/.github/workflows/build-linux-arm64.yml +++ b/.github/workflows/build-linux-arm64.yml @@ -89,7 +89,7 @@ jobs: apt-get update DEBIAN_FRONTEND=noninteractive apt-get install -y \ git curl zip unzip cmake build-essential pkg-config \ - p7zip-full \ + p7zip-full libssl-dev \ libgtk-3-dev libwebkit2gtk-4.1-dev libnss3-dev libgdk-pixbuf2.0-dev libxtst-dev libxss-dev libdbus-glib-1-dev libcurl4-openssl-dev run: | set -euo pipefail diff --git a/.github/workflows/build-linux.yml b/.github/workflows/build-linux.yml index a141903..cefcad6 100644 --- a/.github/workflows/build-linux.yml +++ b/.github/workflows/build-linux.yml @@ -320,7 +320,8 @@ jobs: libxtst-dev \ libxss-dev \ libdbus-glib-1-dev \ - libcurl4-openssl-dev + libcurl4-openssl-dev \ + libssl-dev - name: 5.5 AUTO_INSTALL_CURL (helper script) run: |