Project QRE Blog

Updates, Research, and Technical Deep Dives

Security Hardening Report: QRE Privacy Toolkit v2.6.9

With every release, we conduct a thorough security review of the entire stack. This post documents the hardening work completed for qre-gui v2.6.9 (Tauri V2 + React/TypeScript + Rust), from dependency auditing all the way through to fuzz testing infrastructure. Transparency is a core value for a security tool, so we are publishing the full findings here.

1. Dependency Security

rand 0.8 → 0.9 Migration

We upgraded rand and rand_chacha to version 0.9 across the entire project, eliminating one of three duplicate rand versions in the dependency tree. A residual rand 0.7.3 copy remains as a deep transitive dependency inside Tauri itself and cannot be controlled from our side.

The migration required adapting to a breaking API change: OsRng no longer implements RngCore directly in 0.9 — it now implements TryRngCore because OS random number generation can theoretically fail. Every OsRng.fill_bytes() call across crypto.rs, crypto_stream.rs, and keychain.rs was updated to use .try_fill_bytes(...).expect("OS RNG failed"). The .expect() is intentional: if the OS random number generator is unavailable during key or nonce generation, there is no safe fallback and the operation must abort immediately.

A version conflict in keychain.rs was resolved by aliasing the two incompatible OsRng types, since argon2 re-exports an older rand_core 0.6 snapshot internally:

use argon2::password_hash::{rand_core::OsRng as Argon2OsRng, SaltString};
use rand::{rngs::OsRng, TryRngCore};

cargo-audit — Advisory Review

Running cargo audit produced 19 warnings and 0 errors. All warnings were triaged: the GTK3 bindings (10 crates), unic-* crates (5 crates), proc-macro-error, and fxhash are all transitive Tauri dependencies and were deferred to upstream.

The one advisory worth calling out is RUSTSEC-2024-0429 for glib — an unsound Iterator implementation buried inside webkit2gtk internals. It is not directly callable from application code, but is flagged for monitoring until a Tauri release integrates glib 0.19+.

bincode 1.3.3 — a direct project dependency — is flagged as unmaintained and left as an active error in deny.toml as a deliberate reminder to migrate to bincode v2.

cargo-deny — License Compliance

658 license errors and 2 unlicense warnings were resolved. The package was marked license = "proprietary", and the deny.toml allowlist was expanded to cover the full Tauri/Rust ecosystem: MIT, Apache-2.0, ISC, BSD-2-Clause, BSD-3-Clause, Unicode-3.0, MPL-2.0, OpenSSL, Zlib, CC0-1.0, and others. A [[licenses.clarify]] block was added for the ring TLS crypto library, which bundles its license non-standardly and cannot be auto-detected.

2. Rust Backend — Clippy Hardening

All 20 Clippy errors were resolved across 9 files, with cargo clippy -- -D warnings now exiting clean. The most security-relevant fix was in utils.rs: the set_readonly(false) call was replaced with a platform-gated implementation. On Unix, file permissions are now set explicitly to 0o600 (owner read/write only) via PermissionsExt. Other fixes across the codebase involved collapsing nested matches, simplifying boolean logic, removing needless borrows, and fixing range patterns.

3. Dead Code Removal

Two dead modules were identified and deleted from the Rust backend:

  • entropy.rs — contained a blocking stdin read (io::stdin().read_line()) that is architecturally impossible to call in a GUI application.
  • secure_rng.rs — a ChaCha20 wrapper never called from production code. Its two unit tests were migrated to tests.rs before deletion.

Two files were also flagged for removal from version control: src/components/layout/Sidebar.css (orphaned stylesheet) and tauri.conf.json.backup (a backup configuration file that should never be committed to a repository).

4. Eliminating Panics in Production Code

A full grep across all source files identified every .unwrap(), .expect(), panic!(), unimplemented!(), and unreachable!() call. These were sorted into two categories: legitimate (test code, truly unrecoverable startup failures, hardcoded regex) and production code that needed fixing.

The most significant fixes were 17 mutex panics across commands/files.rs and commands/vault.rs. Every .lock().unwrap() was replaced with:

state.master_key.lock().unwrap_or_else(|e| e.into_inner())

A poisoned mutex (caused by a thread panicking while holding the lock) would previously cause every subsequent caller to panic forever. .into_inner() recovers the data safely. Eight .parent().unwrap() path calls and nine path.to_str().unwrap() UTF-8 path calls were similarly converted to propagate clean errors to the frontend rather than crashing the backend.

5. Filesystem Scope Hardening

This was the most impactful security fix in the release. The original Tauri default.json contained "**" in the fs:scope allow list, granting the application unrestricted read/write access to the entire filesystem — including system directories, SSH keys, browser profiles, and credential stores.

The fix replaces "**" with "$HOME/**" for all user content, and adds an explicit deny list blocking the most sensitive subdirectories even within the home folder:

"deny": [
  "$HOME/.ssh/**",
  "$HOME/.gnupg/**",
  "$HOME/.aws/**",
  "$HOME/.config/google-chrome/**",
  "$HOME/.mozilla/**",
  "$HOME/AppData/Roaming/Microsoft/**",
  "$HOME/AppData/Local/Microsoft/**",
  "$HOME/Library/Keychains/**",
  "$HOME/Library/Passwords/**"
]

It is worth emphasising that the Tauri capability scope is enforced in Rust on the IPC bridge — it cannot be bypassed by JavaScript running in the WebView, regardless of XSS, malicious npm packages, or compromised dependencies. The frontend isDangerousPath() blacklist is retained as a UX layer only and is not relied upon as a security boundary.

6. Frontend Fixes

Two frontend issues were resolved. In VaultView.tsx, the password input field in the vault edit modal was missing type="password", causing entered passwords to be displayed in plaintext. An Eye/EyeOff toggle was added using icons already present in the project.

In useFileSystem.ts, platform() was being called 5 times across the hook in inconsistent contexts. The result is now cached once at hook initialisation via useRef. This also fixed a bug where navigating to "/" on Linux or macOS would call the Windows drive enumeration command, returning an empty view with no way to navigate back.

7. Fuzz Testing Infrastructure

A fuzz target was created at src-tauri/fuzz/fuzz_targets/fuzz_decrypt.rs targeting the decrypt pipeline — the highest-value attack surface since it processes untrusted, user-provided file bytes. The target exercises three attack surfaces in a single pass:

  • Malformed bincode — adversarially crafted container deserialization
  • Invalid ciphertext — AES-GCM authentication tag verification with random data
  • Decompression safety — zstd payloads that could exhaust memory

cargo-fuzz requires Linux and Rust nightly. A GitHub Actions workflow was added to run fuzzing automatically on a weekly schedule against ubuntu-latest, keeping it independent of the development platform.

Remaining Action Items

The following items were identified but not resolved in this session and are carried forward as tracked work:

  • [High] Migrate bincode 1.x → 2.x (direct dep, flagged unmaintained)
  • [High] Monitor glib RUSTSEC-2024-0429 and update Tauri when patched
  • [Medium] Add proptest roundtrip invariants to complement fuzz testing
  • [Low] Audit os:default capability for environment variable exposure
  • [Low] Align rand versions with Tauri to eliminate the rand 0.7.3 duplicate