# BastionGuardβ’ β Changelog
---
## [1.2] β 2026-03-21
---
# π¬π§ English
### β¨ New feature: Secure sandbox for payments and banking
Full implementation of the automatic isolation system for banking and payment
sites. When the user navigates to a domain on the bank or payment list,
BastionGuard intercepts the connection, opens an isolated CEF window on the
exact URL, and restores the normal proxy when the window is closed.
---
#### Proxy MITM and interception (`bastionguard_cef.cpp`, `tls_intercept.hpp`)
- **Full URL preserved** β the path and query string (e.g. `/checkoutnow?token=β¦`)
are extracted from the HTTP request line after the TLS handshake and passed to
the sandbox, instead of opening just `https://host/`. Fixes the PayPal checkout
link case that was opening only `paypal.com` and ignoring the token.
- **No tab loop** β separated "first interception" (Case 3, opens sandbox) from
"session already active" (Case 2, serves block page only). Added atomic guard
`sandbox_try_open/sandbox_close` for parallel requests arriving before the
first `do_intercept` completes.
- **`block_active_session()`** β new variant of `block_secure_request` used in
Case 2: serves the TLS block page with no callback and no `launch_sandbox`,
eliminating the root cause of the tab loop.
- **`launch_sandbox` is now blocking** β removed the `&` from the shell command.
`std::system` now waits for the child process to terminate, enabling
`session_close` on return.
- **`session_close(root)`** β new function that removes the session from
`g_sessions` immediately when the sandbox is closed. `SESSION_TTL` raised from
30 seconds to 30 minutes (safety net for crashes, not the primary mechanism).
- **`BG_SANDBOX=1`** β environment variable passed to sandbox processes
(`BastionGuard-bankopener`, `BastionGuard-secure`) to prevent `LocalWarningServer`
from starting inside them, eliminating interference with internal redirects
(e.g. the vendor's return URL).
- **`SandboxApiServer`** β new HTTP server on `127.0.0.1:3131` handling
`GET /open-sandbox?url=β¦`. Used by the JS banner injected into e-commerce pages
to open the sandbox on the correct URL without requiring any browser extension.
---
#### Credit card form detection on e-commerce sites (`card_form_injector.hpp`)
- **`CardFormTunnel`** β replaces `TunnelSession` for CONNECT connections to known
e-commerce domains. Performs double TLS MITM (browser β proxy β server), buffers
the first HTTP response (max 512 KB), analyses the HTML for card form patterns,
injects the BastionGuard banner if found, then switches to transparent relay.
- **`contains_card_form(html)`** β detects card fields via:
- HTML5 `autocomplete` attributes (`cc-number`, `cc-csc`, `cc-exp`)
- Common name/id values (`card_number`, `cardnumber`, `cc_number`, `pan`)
- Placeholder text combined with CVV fields
- SDK attributes from Stripe, Braintree, Adyen, Klarna
- **`inject_into_response()`** β inserts the JS script before ``, updates
`Content-Length`, removes `Transfer-Encoding: chunked` if present.
- **JS banner** β fixed at the bottom of the page, dark theme consistent with
BastionGuard UI. "Pay securely" button calls `SandboxApiServer`; β button
dismisses the banner without opening the sandbox.
- **Three-level filter** to minimise MITM overhead:
1. `should_inspect_for_card_forms(host)` β MITM only on known e-commerce hosts
(~80 sites: Amazon, eBay, Zalando, etc.); CDN and trackers excluded.
2. `is_checkout_path(path)` β inside the MITM, buffers body only when the path
contains checkout keywords (`/checkout`, `/cart`, `/payment`, `/cassa`, etc.).
Product and catalogue pages use transparent relay with no overhead.
3. `contains_card_form(html)` β injects only if the body actually contains a
card form; order confirmation pages without forms produce no banner.
---
#### CEF sandbox (`SecureBrowser.cpp`)
- **Vendor return URL** β after payment on PayPal (or any gateway), the final
redirect back to the vendor's site is shown directly inside the sandbox.
`LocalWarningServer` no longer interferes thanks to `BG_SANDBOX=1`.
- **Navigation in trusted session** β once the user has visited a trusted domain
(bank or payment), all subsequent redirects inside the sandbox are allowed
without any block, including automatic redirects to the return URL. Only manual
clicks toward external non-trusted domains show the inline block page.
- **`load_block_page_inline(browser, host)`** β new private function of
`SimpleHandler` that generates a block page as `data:text/html,β¦` directly
in CEF, with no dependency on `LocalWarningServer`.
- **Conditional `start_local_warning_server()`** β in `SecureBrowser::open()`,
the `LocalWarningServer` is started only if the `BG_SANDBOX` environment variable
is not set, preventing the sandbox process from starting a server that would
interfere with its own navigation.
---
#### Block page (`block_page.hpp`)
- **Inline BastionGuard logo** β `load_logo_base64()` loads
`/usr/share/BastionGuard/data/logo.png` and embeds it as a base64 data URI
directly in the HTML. No network request required; works offline and inside the
block page where the connection is already closed. Static cache: file is read
once per process.
- **Emoji fallback** β if the logo file is missing (non-standard installation or
development environment), `π‘οΈ` is used as before. No regressions.
- **Updated `.shield` CSS** β supports both `
` (PNG logo) and ``
(emoji fallback): fixed size `72Γ72px`, `object-fit:contain`, blue
`drop-shadow` for both.
---
#### Bug fixes
| Component | Bug | Fix |
|---|---|---|
| `bastionguard_cef.cpp` | Thousands of tabs opened on PayPal interception | Separated Case 2 and Case 3; atomic guard on `launch_sandbox` |
| `bastionguard_cef.cpp` | Sandbox opened on `https://paypal.com/` without token | Extracted path from request line after TLS handshake |
| `bastionguard_cef.cpp` | Session remained active after sandbox was closed | `session_close()` + blocking `launch_sandbox` |
| `SecureBrowser.cpp` | Red "Domain blocked" page shown on vendor return URL | `BG_SANDBOX=1` + free navigation in trusted session |
| `SecureBrowser.cpp` | `LocalWarningServer` started inside the sandbox | Conditioned on absence of `BG_SANDBOX` env var |
| `tls_intercept.hpp` | `do_intercept` ignored path and query string | Added `on_url_ready` callback with full URL |
---
#### Compatibility
- No changes to the public API of `SecureBrowser`, `DomainFilter`, `BlockPage`.
- `card_form_injector.hpp` is a new optional header; targets that do not include
it are unaffected.
- `BG_SANDBOX_API_PORT` (default `3131`) can be overridden at compile time via
`-DBG_SANDBOX_API_PORT=` in `CMakeLists.txt`.
---
### β¨ New feature: Password Manager
A fully integrated password manager with a secure encrypted vault, built directly
into BastionGuard. All credentials are stored locally β no cloud, no third-party
service.
---
#### Encryption & Storage (`PasswordVault.cpp`, `secrets.enc`)
- **AES-256-GCM** β all passwords are stored in a single encrypted file
`~/.config/BastionGuard/password_manager/secrets.enc`. The file contains the
ciphertext, a 12-byte random nonce, and a 16-byte GCM authentication tag.
- **PBKDF2-SHA256 key derivation** β the AES-256 key is derived from the master
password using PBKDF2-SHA256 with 100,000 iterations and a 16-byte random salt
regenerated on every write. Brute-force resistant even with direct file access.
- **Tamper detection** β the GCM authentication tag is verified on every decrypt.
Any modification to the ciphertext, nonce, salt, or tag makes the vault
**permanently unreadable**. This is enforced at the cryptographic level.
- **`master.json`** β stores only the PBKDF2 hash and salt of the master password.
The plaintext is never written to disk. Comparison uses constant-time logic to
prevent timing-based brute-force attacks.
- **Atomic write** β `secrets.enc` is written via `rename()` on a `.tmp` file.
No corruption can occur on crash or power loss mid-write.
- **Replaced libsecret per-entry storage** β the previous architecture stored one
secret per credential in GNOME Keyring via D-Bus. With 1,500 credentials this
caused high gnome-keyring CPU usage and write failures due to D-Bus queue
saturation. Now a single file write replaces all 1,500 D-Bus roundtrips.
---
#### Master password (`PasswordManagerPage.cpp`)
- **First-run wizard** β on first open, the user creates a master password with a
real-time strength indicator: Weak (red) / Medium (orange) / Strong (green).
Minimum 8 characters; confirmation field required.
- **Re-lock / unlock** β the RAM cache is cleared on lock; no plaintext remains in
memory when the vault is closed. An async warm-up thread reloads all secrets
from `secrets.enc` on the next unlock.
---
#### Performance (`PasswordVault.cpp`)
- **In-memory RAM cache** β on unlock, all secrets are loaded from `secrets.enc`
into an `unordered_map` protected by a `std::mutex`. Subsequent
`getSecretAsync()` calls are answered from cache β zero D-Bus roundtrips per
lookup.
- **Async warm-up** β a background thread decrypts and populates the cache
immediately after unlock. `cacheReady_` flag set on completion; the UI shows
"loading cacheβ¦" in the state badge during warm-up.
- **Bulk import O(n)** β `addEntriesBulk()` writes the index JSON once for the
entire batch instead of once per entry (was O(nΒ²)). Background thread persists
secrets to `secrets.enc` with a single encrypt + write operation.
- **`getSecretAsync()` β three-level lookup:**
1. Cache hit β instant response via `Glib::signal_idle`
2. Warm-up in progress β 50 ms polling via `Glib::signal_timeout` (max 5 s)
3. Cache ready but id not found β fallback for edge cases
---
#### Browser import (`PasswordImporter.cpp`, `PasswordImportDialog.cpp`)
- **Supported formats** β Chrome / Chromium, Edge (identical CSV format), Firefox
(Lockwise export), Safari.
- **Auto-detect** β browser identified from the CSV header line, case-insensitive,
BOM-aware (UTF-8 BOM stripped). Robust to column order variations.
- **RFC 4180 parser** β custom tokeniser handles quoted fields, embedded commas,
escaped double-quotes (`""`), and Windows `\r\n` line endings.
- **Preview dialog** β first 50 rows displayed before confirming; full import
(all rows) runs in a background thread with a live progress bar.
- **Progress bar** β updated via `Glib::Dispatcher` from the import thread.
The UI never blocks regardless of import size.
---
#### Vault view & export (`PasswordVaultView.cpp`)
- **Vault view dialog** β searchable table showing Name, Username, URL,
Last updated. Full-text filter updates live as the user types.
- **Per-entry detail** β clicking π opens a detail popup with all fields
(name, username, URL, password, notes, created/updated). Password field is
selectable for copy-paste.
- **CSV export** β runs on a background thread via `Glib::Dispatcher`. Format:
`name,url,username,password,notes` β RFC 4180 compliant with proper quoting.
File-save dialog with `.csv` filter.
---
#### UI & security indicators
- **Security banner** β always visible at the top of the manager page. Explains
AES-256-GCM protection and states that any file tampering renders the vault
permanently unreadable.
- **State badge** β top-right corner:
- π’ **Green** (`alert-success`) when vault is **locked** β credentials safe,
no plaintext in memory.
- π΄ **Red** (`alert-danger`) when vault is **unlocked** β vault open, credentials
accessible in RAM.
- **Two-row toolbar** β row 1: Unlock / Lock / state badge;
row 2: οΌ New / β¬ Import / π Vault / βΊ Refresh / π Delete-all / π Search.
- **Delete-all** β double confirmation dialog (WARNING β ERROR level) plus async
background removal of `secrets.enc`.
---
#### Bug fixes
| Component | Bug | Fix |
|---|---|---|
| `PasswordVault.cpp` | `secret_password_store` fired 1,500Γ in a loop, saturating D-Bus queue | Replaced per-entry libsecret with single AES-256-GCM file write |
| `PasswordVault.cpp` | `secret_password_lookup_sync` blocked GTK4 main loop on "Show password" | Replaced with in-memory RAM cache; fallback uses async `secret_password_lookup` |
| `PasswordVault.cpp` | `deleteAllEntries` froze UI (N Γ `clear_sync` on main thread) | Async background thread; index cleared atomically before keyring cleanup |
| `PasswordImportDialog.cpp` | SEGV on import: `dialog_->hide()` called after `delete dialog` | `onImportDone` no longer calls `hide()`; caller hides before `delete` |
| `PasswordVault.cpp` | Passwords empty after re-lock/unlock (cache not populated) | `warmUpCache()` thread started immediately after every `unlock()` |
---
### β¨ New feature: Dark mode & theme switcher
---
#### Theme files
- **`BastionGuard-dark.css`** β full dark theme. Background `#1a1d23`, text
`#e4e7eb`, cards `#23272e`, sidebar `#16191f`. Covers all UI components
including sidebar, cards, buttons, inputs, badges, and separators.
- **`BastionGuard.css`** β unchanged light theme, now the explicit default.
Background `#f8f9fa`, text `#212529`.
---
#### Runtime switching (`DashboardPage.cpp`)
- **β / π buttons** β two flat icon buttons in the Dashboard footer, left of the
update badge. Clicking applies the selected theme instantly via
`Gtk::StyleContext::add_provider_for_display()` at
`GTK_STYLE_PROVIDER_PRIORITY_USER` β no restart required.
- **Active highlight** β the button for the currently active theme receives the
CSS class `theme-btn-active` (full opacity); the inactive button is dimmed
(`opacity: 0.5`).
- **Preference saved** β theme choice written to
`~/.config/BastionGuard/theme.conf` on every click. File contains a single
word: `light` or `dark`.
---
#### Startup restore (`StyleProvider.cpp`)
- **`read_saved_theme()`** β new helper function reads
`~/.config/BastionGuard/theme.conf` before the main window is shown.
Returns `"dark"` or `"light"` (default if file absent or value unrecognised).
- **`load_style()` updated** β selects `BastionGuard-dark.css` or
`BastionGuard.css` based on the saved theme. Search order: `data/` (local
build), then `DATA_DIR` (installed path). Falls back to light theme if the
requested CSS file is not found β no crash, no blank window.
- **First launch** β if `theme.conf` does not exist, light theme is used.
No migration required from previous installations.
---
#### Settings β Options (`SettingsPage.cpp`)
- **Theme selector** β `Gtk::ComboBoxText` with two entries: β Chiaro (Light)
and π Scuro (Dark). Shows the current saved theme on open.
- **Preview label** β describes the selected theme (background colour, text
colour) and updates live as the user changes the selection.
- **"Applica tema" button** β loads the CSS file and applies it immediately via
`add_provider_for_display`. Shows a confirmation dialog on success; shows an
error dialog if the CSS file is not found. Saves preference to `theme.conf`.
---
#### Bug fixes
| Component | Bug | Fix |
|---|---|---|
| `StyleProvider.cpp` | App always started in light mode regardless of saved preference | `load_style()` now reads `theme.conf` before selecting the CSS file |
| `DashboardPage.cpp` | Theme buttons had no visual feedback for current selection | `theme-btn-active` CSS class added/removed on click and at startup |
| `IdentityLeakPage.cpp` | Fix dark mode visualization |
| `BankPage.cpp` | Fix update in silent mode |
---
#### Compatibility
- `StyleProvider.hpp` public API unchanged (`load_style()` signature identical).
- `BastionGuard.css` unchanged β existing installations continue to work without
any `theme.conf` file.
- `BastionGuard-dark.css` is a new optional file; absence does not affect the
light theme.
---