diff --git a/.github/workflows/build-linux-arm64.yml b/.github/workflows/build-linux-arm64.yml index d224f1c..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 @@ -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 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: | 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" 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; diff --git a/assets/passwords.html b/assets/passwords.html new file mode 100644 index 0000000..334da15 --- /dev/null +++ b/assets/passwords.html @@ -0,0 +1,1200 @@ + + + + + + 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

+
+ + + + +
+
+ + + + + + + + +
+ + + + diff --git a/assets/settings.html b/assets/settings.html index 4e003e9..7fac84e 100644 --- a/assets/settings.html +++ b/assets/settings.html @@ -13,69 +13,68 @@ } :root { - color-scheme: light dark; - --bg-primary: #1e1e1e; - --bg-secondary: #252525; - --bg-tertiary: #2a2a2a; - --bg-elevated: #2d2d2d; - - --border-subtle: rgba(255, 255, 255, 0.06); - --border-soft: rgba(255, 255, 255, 0.08); - --border-medium: rgba(255, 255, 255, 0.12); - - --text-primary: #e4e4e7; - --text-secondary: #a1a1aa; - --text-tertiary: #71717a; - - --accent-primary: #8b7cf5; - --accent-soft: rgba(139, 124, 245, 0.15); - --accent-softer: rgba(139, 124, 245, 0.08); + /* Dark purple theme to match Passwords UI */ + --bg-primary: #1e1e2e; + --bg-secondary: #282839; + --bg-tertiary: #32324a; + --bg-hover: #3d3d5c; + --text-primary: #e4e4ef; + --text-secondary: #9999b3; + --text-tertiary: #71718a; + --border-color: #404060; + --accent-color: #7c6aef; + --accent-hover: #9182f3; + --accent-light: rgba(124, 106, 239, 0.15); + --danger-color: #ef6a6a; + --success-color: #6aef8a; + --card-shadow: 0 2px 8px rgba(0, 0, 0, 0.3); } body { - font-family: system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, "Apple Color Emoji", "Segoe UI Emoji"; - background: var(--bg-primary); + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif; + background: linear-gradient(135deg, var(--bg-primary) 0%, #252540 100%); color: var(--text-primary); - padding: 40px 20px; - line-height: 1.6; + line-height: 1.5; min-height: 100vh; + padding: 24px; } .container { max-width: 900px; margin: 0 auto; - background: var(--bg-secondary); - border: 1px solid var(--border-soft); - border-radius: 12px; - padding: 0; - box-shadow: 0 4px 16px rgba(0, 0, 0, 0.3); - overflow: hidden; } header { - padding: 20px 30px; - border-bottom: 1px solid var(--border-subtle); - background: var(--bg-tertiary); + display: flex; + flex-direction: column; + margin-bottom: 24px; + padding-bottom: 16px; + border-bottom: 1px solid var(--border-color); } h1 { - color: var(--text-primary); - margin: 0 0 6px 0; + display: flex; + align-items: center; + gap: 12px; font-size: 24px; font-weight: 600; - letter-spacing: 0.01em; + margin-bottom: 4px; + } + + h1 svg { + width: 32px; + height: 32px; + fill: var(--accent-color); } .subtitle { color: var(--text-secondary); - margin: 0; - font-size: 13px; - font-weight: 400; + font-size: 14px; } /* Settings search */ .settings-search { - margin-top: 12px; + margin-top: 16px; display: flex; gap: 8px; align-items: center; @@ -83,31 +82,44 @@ .settings-search input { flex: 1; - padding: 8px 12px; - border-radius: 8px; - border: 1px solid var(--border-soft); - background: var(--bg-secondary); + padding: 12px 16px; + border-radius: 10px !important; + border: 1px solid var(--border-color); + background: var(--bg-tertiary); color: var(--text-primary); - font-size: 13px; + font-size: 14px; + -webkit-appearance: none; + appearance: none; + } + + .settings-search input::placeholder { + color: var(--text-secondary); + } + + .settings-search input:focus { + outline: none; + border-color: var(--accent-color); + box-shadow: 0 0 0 3px var(--accent-light); + border-radius: 10px !important; } .settings-search .clear-btn { - padding: 8px 10px; + padding: 10px 16px; border-radius: 8px; - border: 1px solid var(--border-soft); - background: transparent; + border: 1px solid var(--border-color); + background: var(--bg-tertiary); color: var(--text-secondary); cursor: pointer; + font-size: 13px; } #settings-container { - padding: 20px 30px 30px 30px; - max-height: calc(100vh - 280px); + max-height: calc(100vh - 220px); overflow-y: auto; } .settings-group { - margin-bottom: 28px; + margin-bottom: 24px; } .settings-group:last-child { @@ -115,32 +127,25 @@ } .settings-group h2 { - color: var(--accent-primary); - font-size: 12px; + color: var(--accent-color); + font-size: 13px; font-weight: 600; margin-bottom: 12px; padding-bottom: 8px; - border-bottom: 1px solid var(--border-subtle); - letter-spacing: 0.05em; + border-bottom: 1px solid var(--border-color); + letter-spacing: 0.03em; text-transform: uppercase; } .setting-item { - padding: 14px 18px; - background: var(--bg-tertiary); - border: 1px solid var(--border-subtle); - border-radius: 8px; + padding: 16px; + background: var(--bg-secondary); + border: 1px solid var(--border-color); + border-radius: 10px; margin-bottom: 8px; display: flex; align-items: center; justify-content: space-between; - transition: all 0.2s; - } - - .setting-item:hover { - background: var(--bg-elevated); - border-color: var(--border-soft); - transform: translateX(2px); } .setting-info { @@ -150,122 +155,105 @@ .setting-name { color: var(--text-primary); - font-weight: 600; - font-size: 13px; - margin-bottom: 3px; + font-weight: 500; + font-size: 14px; + margin-bottom: 2px; } .setting-description { - color: var(--text-tertiary); - font-size: 11px; - line-height: 1.4; + color: var(--text-secondary); + font-size: 12px; } .toggle-switch { position: relative; - width: 40px; - height: 20px; - background: #3a3a3a; - border: 1px solid var(--border-soft); - border-radius: 10px; + width: 48px; + height: 24px; + background: var(--bg-hover); + border-radius: 24px; cursor: pointer; - transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1); + transition: background 0.3s; flex-shrink: 0; } - .toggle-switch:hover { - background: #404040; - border-color: var(--border-medium); - } - - .toggle-switch.active { - background: var(--accent-primary); - border-color: var(--accent-primary); - box-shadow: 0 0 12px rgba(139, 124, 245, 0.3); - } - - .toggle-switch.active:hover { - background: #9688f7; - box-shadow: 0 0 16px rgba(139, 124, 245, 0.4); - } - .toggle-switch::after { content: ''; position: absolute; - top: 2px; - left: 2px; - width: 14px; - height: 14px; - background: linear-gradient(180deg, #ffffff, #f5f5f5); + top: 3px; + left: 3px; + width: 18px; + height: 18px; + background: white; border-radius: 50%; - box-shadow: 0 2px 4px rgba(0, 0, 0, 0.4); - transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1); + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2); + transition: transform 0.3s; + } + + .toggle-switch.active { + background: var(--accent-color); } .toggle-switch.active::after { - transform: translateX(20px); - box-shadow: 0 2px 6px rgba(0, 0, 0, 0.5); + transform: translateX(24px); } .actions { - padding: 20px 30px; - border-top: 1px solid var(--border-subtle); - background: var(--bg-tertiary); + margin-top: 24px; + padding-top: 16px; + border-top: 1px solid var(--border-color); display: flex; gap: 12px; justify-content: flex-end; } button { - padding: 10px 24px; - border: 1px solid rgba(255, 255, 255, 0.15); - border-radius: 7px; - font-size: 13px; - font-weight: 600; + display: inline-flex; + align-items: center; + gap: 6px; + padding: 10px 20px; + border: 1px solid var(--border-color); + border-radius: 8px; + font-size: 14px; + font-weight: 500; cursor: pointer; - transition: all 0.2s; + transition: background 0.2s; font-family: inherit; } .btn-primary { - background: var(--accent-soft); - color: var(--accent-primary); - border-color: var(--accent-primary); + background: var(--accent-color); + color: white; + border-color: var(--accent-color); } .btn-primary:hover:not(:disabled) { - background: var(--accent-primary); - color: #ffffff; - transform: translateY(-1px); - box-shadow: 0 4px 12px rgba(139, 124, 245, 0.3); + background: var(--accent-hover); } .btn-primary:disabled { - background: #3a3a3a; + background: var(--bg-tertiary); color: var(--text-tertiary); - border-color: var(--border-soft); + border-color: var(--border-color); cursor: not-allowed; - transform: none; } .btn-secondary { - background: transparent; - color: var(--text-secondary); + background: var(--bg-tertiary); + color: var(--text-primary); } .btn-secondary:hover { - background: var(--bg-elevated); - border-color: var(--border-medium); + background: var(--bg-hover); } .storage-path { margin-top: 20px; - padding: 14px 18px; - background: var(--bg-tertiary); - border: 1px solid var(--border-subtle); - border-radius: 8px; - font-size: 11px; - color: var(--text-tertiary); + padding: 14px 16px; + background: var(--bg-secondary); + border: 1px solid var(--border-color); + border-radius: 10px; + font-size: 12px; + color: var(--text-secondary); font-family: 'Consolas', 'Courier New', monospace; word-break: break-all; } @@ -278,25 +266,26 @@ } .debug-info { - margin-top: 20px; - padding: 14px 18px; - background: var(--bg-tertiary); - border: 1px solid var(--border-subtle); - border-radius: 8px; + margin-top: 16px; + padding: 14px 16px; + background: var(--bg-secondary); + border: 1px solid var(--border-color); + border-radius: 10px; font-family: 'Consolas', 'Courier New', monospace; - font-size: 10px; + font-size: 11px; color: var(--text-tertiary); - max-height: 200px; + max-height: 150px; overflow-y: auto; line-height: 1.5; } .drm-panel { - margin: 20px 30px 0 30px; + margin-top: 24px; padding: 20px; - background: var(--bg-tertiary); - border: 1px solid var(--border-subtle); + background: var(--bg-secondary); + border: 1px solid var(--border-color); border-radius: 12px; + box-shadow: var(--card-shadow); } .drm-header { @@ -310,6 +299,12 @@ .drm-header h2 { margin: 0; font-size: 18px; + font-weight: 600; + } + + .drm-header .subtitle { + font-size: 13px; + margin-top: 4px; } .status-chip { @@ -317,23 +312,23 @@ align-items: center; padding: 4px 12px; border-radius: 999px; - border: 1px solid var(--border-soft); + border: 1px solid var(--border-color); font-size: 11px; text-transform: uppercase; - letter-spacing: 0.08em; + letter-spacing: 0.05em; color: var(--text-secondary); } .status-chip.good { - color: #16a34a; - border-color: rgba(22, 163, 74, 0.4); - background: rgba(22, 163, 74, 0.12); + color: var(--success-color); + border-color: rgba(106, 239, 138, 0.4); + background: rgba(106, 239, 138, 0.1); } .status-chip.bad { - color: #f97316; - border-color: rgba(249, 115, 22, 0.4); - background: rgba(249, 115, 22, 0.1); + color: var(--danger-color); + border-color: rgba(239, 106, 106, 0.4); + background: rgba(239, 106, 106, 0.1); } .drm-status-grid { @@ -348,6 +343,7 @@ text-transform: uppercase; letter-spacing: 0.05em; color: var(--text-tertiary); + margin-bottom: 4px; } .drm-status-grid .value { @@ -371,8 +367,8 @@ .site-pill { padding: 4px 10px; border-radius: 999px; - background: var(--bg-secondary); - border: 1px solid var(--border-subtle); + background: var(--bg-tertiary); + border: 1px solid var(--border-color); font-size: 11px; color: var(--text-secondary); } @@ -390,54 +386,60 @@ } .drm-log { - background: #111; - border: 1px solid var(--border-soft); + background: var(--bg-primary); + border: 1px solid var(--border-color); border-radius: 8px; padding: 12px; - color: #d1d5db; + color: var(--text-secondary); font-family: 'Consolas', 'Courier New', monospace; font-size: 11px; - max-height: 220px; + max-height: 180px; overflow-y: auto; white-space: pre-wrap; } .ua-row { margin-top: 16px; - padding: 14px 18px; - background: var(--bg-tertiary); - border: 1px solid var(--border-subtle); - border-radius: 8px; + padding: 16px; + background: var(--bg-secondary); + border: 1px solid var(--border-color); + border-radius: 10px; display: flex; flex-direction: column; gap: 8px; } .ua-label { - font-size: 11px; + font-size: 12px; text-transform: uppercase; - letter-spacing: 0.06em; + letter-spacing: 0.03em; color: var(--text-secondary); + font-weight: 500; } .ua-help { - font-size: 11px; + font-size: 12px; color: var(--text-tertiary); } .ua-input { width: 100%; - padding: 8px 10px; - border-radius: 6px; - border: 1px solid var(--border-soft); - background: var(--bg-secondary); + padding: 10px 12px; + border-radius: 8px; + border: 1px solid var(--border-color); + background: var(--bg-tertiary); color: var(--text-primary); font-size: 12px; font-family: 'Consolas', 'Courier New', monospace; } + .ua-input:focus { + outline: none; + border-color: var(--accent-color); + } + .ua-input:disabled { - opacity: 0.6; + opacity: 0.5; cursor: default; } @@ -453,23 +455,22 @@ } ::-webkit-scrollbar-thumb { - background: rgba(255, 255, 255, 0.15); + background: var(--bg-hover); border-radius: 4px; } - - ::-webkit-scrollbar-thumb:hover { - background: rgba(255, 255, 255, 0.25); - }
-

Settings

+

+ + Settings +

Configure your browser preferences

diff --git a/assets/ui.html b/assets/ui.html index d4400f9..34bd438 100644 --- a/assets/ui.html +++ b/assets/ui.html @@ -509,6 +509,376 @@ window.addEventListener('resize', () => hide(true)); })(); + + + + + + + + + + + + + + \ No newline at end of file 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 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) { diff --git a/src/Tab.cpp b/src/Tab.cpp index 2cc34ac..bcf7bd6 100644 --- a/src/Tab.cpp +++ b/src/Tab.cpp @@ -4,25 +4,32 @@ #include "DownloadManager.h" #include "ExtensionManager.h" #include "AdBlocker.h" +#include "PasswordManager.h" #include #include #include #include +#include #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(); } @@ -473,6 +480,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 +507,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 +640,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 +1481,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..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; } @@ -141,6 +150,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 +174,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_; }; diff --git a/src/UI.cpp b/src/UI.cpp index 29b00cb..2647239 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" @@ -153,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", @@ -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); @@ -636,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; } @@ -1315,6 +1327,10 @@ 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); + // 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. @@ -1359,6 +1375,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 +1428,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 +1786,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")); @@ -2057,7 +2097,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); @@ -2079,7 +2125,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()); @@ -3067,8 +3119,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; @@ -3082,8 +3135,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; @@ -4693,3 +4746,413 @@ 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); +} + +// 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()) + 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..b8ce585 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; @@ -93,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); } @@ -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,35 @@ 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); + + // 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(); + 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 +324,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; 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_; }