GentzyCloak 0.1.1 documentation
User guide
Carry your privacy. Trust no host. — Powered by LordMoritz.
Written for a Senior Special Assistant (or chief of staff, protocol officer, close-protection lead) responsible for the devices and communications of a principal. No security background is assumed; where a step requires judgement, the guide says so.
Terms: the principal is the person you protect. The host is whatever computer GentzyCloak is running on. The drive is the USB stick that holds GentzyCloak and its GentzyCloak-Data folder.
0. Before you start — preparing the drive (once, on a trusted machine)
- Obtain the release file for each platform you may encounter (
.exefor Windows,.zipfor macOS,.AppImagefor Linux). Keep all three on the drive; they are small. - Verify the download hash. On a machine you trust, compute SHA-256 and compare with the value published on the release page:
- macOS/Linux:
shasum -a 256 GentzyCloak-* - Windows PowerShell:
Get-FileHash .\GentzyCloak-*.exe -Algorithm SHA256
If it does not match, do not use the file.
- Use an encrypted USB drive (BitLocker To Go on Windows, an encrypted volume on macOS, LUKS/VeraCrypt elsewhere). This protects anything you export if the drive is lost.
- Launch GentzyCloak once on the trusted machine. On the Dashboard note:
- Portable mode: on — the app is writing only to the drive.
- Integrity: OK and the root hash (first 16 characters are enough).
Write the root hash on paper or in your phone's notes. You will compare it later on untrusted hosts.
- Create the Vault (Section 6) now, on the trusted machine, and enter the items you will need while travelling.
- Agree a chat relay with your security team (Section 7) and enter its
wss://URL under Chat → Settings.
macOS note — "Not portable" right after unzipping. If the Dashboard shows NOT PORTABLE and the scanner reports GentzyCloak running from a temp folder, macOS Gatekeeper is running the downloaded, unsigned app from a read-only temporary location ("App Translocation"). GentzyCloak shows a banner when it detects this. Quit, move GentzyCloak.app with Finder to the drive (or run xattr -dr com.apple.quarantine GentzyCloak.app), and relaunch. Portable mode then works and the finding disappears.
1. The Dashboard
The first screen after launch. Read it every time.
| Indicator | Green means | Red / amber means | What to do |
|---|---|---|---|
| Portable mode | All data stays on the drive | The drive is read-only; the app is using the host's profile folder | Prefer to stop. If you must continue, treat the host as retaining traces and use the panic button before leaving |
| Integrity | Every bundled file matches the build manifest and the root hash equals the one you wrote down | A file was modified, is missing, or the root hash differs from your note | Stop. Someone altered the app on the drive. Use a fresh download on a trusted machine |
| Host | Hostname, OS, user, whether you are an administrator | — | Administrator rights let the scanner see more; as a normal user some collectors will report warnings |
| Last scan | Grade and time of the last scan in this session | — | Scans are not saved between launches by design; export a report if you need a record |
Panic button (top-right, red): wipes browser data, reports and downloads on the drive, locks the vault and exits. The encrypted vault file is kept. The same wipe runs automatically if the drive is unplugged while the app is open.
2. Pre-meeting checklist (principal's laptop or a loaner)
Allow 15 minutes. Do this before the principal uses the device for anything sensitive, and again after it has been out of your sight (hotel room, repair, customs).
- [ ] Drive plugged in, GentzyCloak launched from the drive, Dashboard shows Portable mode on and Integrity OK with the expected root hash.
- [ ] Device disconnected from Wi-Fi/Ethernet during the scan if possible (the scan is local; a disconnected device cannot exfiltrate while you look).
- [ ] Run a Full scan (Section 3).
- [ ] Grade A or B with no critical/high findings → proceed.
- [ ] Any critical finding → Section 5. Do not let the principal use the device.
- [ ] Any high finding → apply the remediation steps or escalate; re-scan.
- [ ] Export a report (Section 4) to the drive as a record, labelled with the device and date.
- [ ] Note in your log: device, date/time, grade, report filename, action taken.
3. Running a scan
- Open Scanner.
- Choose Quick (processes, ports, posture — about a minute) or Full (adds persistence enumeration, privacy grants, deeper checks — several minutes). Use Full for pre-meeting checks.
- Press Start. Progress shows each collector. You can Cancel.
- When finished you see the grade, the score and the findings list.
- Scroll to Collector warnings at the bottom. A warning means part of the device could not be inspected (usually because you are not an administrator). The grade may then be optimistic. If you can, re-run the scan as an administrator.
Reading the grade
| Grade | Score | Meaning for you |
|---|---|---|
| A | 90–100 | No significant indicators. Proceed. Re-scan after the device leaves your sight. |
| B | 75–89 | Minor hardening gaps (e.g. firewall off, updates pending). Proceed; fix when convenient. |
| C | 60–74 | Several weaknesses, typically one high finding. Fix the high items before sensitive use. |
| D | 40–59 | Serious. Treat the device as untrusted until findings are resolved. |
| F | 0–39 | Critical indicators present. Go to Section 5 now. |
Scoring: start at 100; minus 25 per critical, 12 per high, 6 per medium, 2 per low finding. A single critical finding therefore caps the grade at B, two at D. The grade is a summary; always read the top findings.
Reading a finding
Each finding has:
- a severity badge and a title;
- What this means — one paragraph written for the principal;
- Technical detail and Evidence — paths, commands, ports — for your security team;
- Remediation steps in order;
- Recommended tools — free or freemium, reputable, with links;
- a MITRE ATT&CK id your CERT will recognise.
Do not act on a critical finding's remediation steps before preserving evidence (Section 5).
4. Exporting a report
- After a scan, open Report.
- Fill in Asset label (e.g.
Governor's Office — Laptop 3) and your name as Operator. These appear on the cover page and in the filename. - Choose a format:
- HTML — the one to send to a principal or a decision-maker. Opens in
any browser, prints cleanly, contains no scripts and loads nothing from the internet.
- Markdown — for ticketing systems and e-mail.
- JSON — the complete machine-readable result for your security team.
- Tick Include process list only when sending to a technical team; it makes the report long.
- Press Export. The file is written to
GentzyCloak-Data/reports/GentzyCloak-Report-<asset>-<yyyymmdd-hhmm>.<ext>on the drive. The app shows the path and size.
The first page of the HTML report is an executive summary: asset, operator, date, grade, severity counts and the top risks in plain language. Hand that page to the principal; hand the whole file to the security team.
5. What to do on a critical finding
Critical means the scanner found an indicator that the device is being remotely controlled or actively monitored (for example a known remote-access tool running unsigned, a hidden persistence item launching from a temp folder, screen sharing enabled to an unknown party).
Do, in this order:
- Isolate. Turn off Wi-Fi and unplug Ethernet. Do not put the device in airplane mode via software if you can physically disconnect; if it is a loaner, simply stop using it.
- Do not reboot, log out, or "clean" the device. Volatile evidence (running processes, network connections) disappears on reboot, and malware may notice removal attempts. Leave it powered on and locked.
- Preserve. Export the JSON and HTML reports with the process list included. Photograph the screen if a message or pop-up is visible. Note the time.
- Protect the principal. Assume anything typed or displayed on this device recently is known to the adversary: change passwords from a different, clean device; cancel or move any meeting that depended on this device's confidentiality.
- Escalate to your organisation's security team or the national CERT / CSIRT with the reports. Keep the drive; do not hand it over without a copy. Do not unlock the GentzyCloak vault on that device.
- Only after the security team has taken images or told you to proceed, follow the finding's remediation steps or reinstall the OS.
High findings (e.g. unsigned persistence item, SSH enabled, firewall off): apply the steps, re-scan, and log it. Escalate if the item is unknown to you or reappears after removal.
6. The Vault
The vault is an encrypted file (vault.gcv) on the drive for the items you need on the road: Wi-Fi passwords, backup codes, contact numbers, short notes.
- Create: Vault → Create vault → choose a passphrase of at least 12 characters. Prefer four or five random words. There is no recovery: if you forget it, the contents are gone.
- Unlock / Lock: the vault locks automatically when the app quits or you press Panic. Lock it yourself as soon as you have what you need.
- Entries: Note, Credential (with username and URL) or Code. Body up to 64 KB.
- Where it is safe to unlock: on your own clean device, or on a host that just received grade A/B. Never on a host with critical findings — a monitoring tool would see what you display.
- Changing the passphrase: create a new vault on a trusted machine and re-enter items (rename or delete the old file first).
7. Secure chat — invite exchange procedure
GentzyCloak chat is end-to-end encrypted; the relay server only forwards scrambled data. Both sides must have the same invite code ("GentzyKey").
Creating a room (you):
- Chat → Create room → give it a name (only you see it).
- The app shows the invite code (
GZ-XXXXX-…) and an 8-character fingerprint. - Deliver the invite code to the other party by a channel the adversary does not control — in person, a phone call read aloud, a sealed envelope. Never over the same network or messaging app you are trying to avoid. The code has a built-in checksum: a mistyped character is detected when they enter it.
Joining (the other party): Chat → Join room → paste or type the code.
Verify before talking:
- Both sides read the fingerprint aloud over a phone call or compare in person. If the eight characters match, you are in the same room with the same key. If they differ, someone tampered with the code — create a new room.
- Set an alias each; aliases are self-declared and change each session. Trust the fingerprint, not the alias.
Using it:
- Messages are held in memory only; closing the app deletes them. Say what needs saying, then close.
- When IP exposure matters, run Tor on the device and set the proxy mode to Tor in Settings before connecting.
- Create a new room for each engagement; do not reuse invite codes.
8. The hardened browser
- Type a URL or a search term in the bar (search goes to a JavaScript-free DuckDuckGo endpoint). The tab shows a lock for HTTPS and a blocked count for trackers stopped.
- Defaults: HTTPS-only, DNS over HTTPS (Quad9), trackers and third-party cookies blocked, downloads off. Enable downloads only when needed; they go to the drive. The Downloads section in the sidebar lists every file the browser saved (this session and earlier), shows live progress, opens the folder, reveals a file, or shreds it. Reveal and delete refuse any path that is not on the GentzyCloak drive.
- Proxy: Settings → Direct, Tor (needs a running Tor client) or Custom SOCKS5. The dashboard shows whether Tor is connected.
- Wipe (browser menu) clears the whole browsing session immediately; quitting or Panic does the same.
- Do not sign in to the principal's accounts from a host that has not passed a scan. If the host has an unknown root certificate installed (the scanner flags this when it can detect it), HTTPS on that host can be read by the host's operator; the browser cannot fix that.
8a. Vetting a website (Site Scan)
Use Site Scan when a provider sends a demo site and asks the government to consider buying it, or whenever you need to assess a website's security posture.
- Open Site Scan and type the address (for example
vendor-demo.example.com). Press Scan site. - Passive analysis always runs and does not touch the target beyond loading it like a normal visitor: TLS certificate health, security headers, cookies, technology fingerprint (including exposed
.env/.git, source maps and other signs of a rushed or template deployment), DNS and email records, hosting provider, registration date and registrar, and the subdomain surface. These lookups honour your proxy setting, so under Tor mode they travel over Tor. - The active port scan is optional and gated. To include open-port findings, tick "I am authorized to actively test this target" and then "Run active port scan". Only tick the first box if you genuinely have authorization — a vendor inviting you to evaluate a demo is not always the same as permission to scan their infrastructure, and their site may share hosting with third parties you are not entitled to scan. The attestation, the target and the time are written into the report as your audit trail. The scan is connect-only and sends no attack payloads.
- Read the grade (A–F) and the findings, ordered by severity. Each finding explains what was found, why it matters, and what to do. Tabs show the full Intelligence (hosting, registration, TLS, fingerprint), Ports and Subdomains.
- Export a report (HTML, Markdown or JSON) for the procurement file. The HTML report is self-contained and printable; the JSON is machine-readable for a CERT.
A very new domain, a self-signed or expired certificate, an exposed .env, an open database port, or a session cookie without Secure are all reasons to withhold a purchase recommendation until the provider remediates.
9. Leaving
- Export any report you need.
- Lock the vault.
- Close the app (it wipes browser data and exits) — or press Panic.
- Unplug the drive. If you forget step 3, unplugging triggers the wipe.
10. Troubleshooting
| Symptom | Likely cause | Action |
|---|---|---|
| macOS: "cannot be opened because the developer cannot be verified" | Unsigned build | Right-click → Open, once. Check the integrity root hash afterwards |
| Portable mode off | Drive mounted read-only, or write-protect switch on | Re-mount / flip the switch; otherwise Panic before leaving |
| Integrity red on a host but green at home | App files changed on the drive, or the host's antivirus quarantined a file | Do not proceed; use a fresh copy |
| Scan shows many collector warnings | Not running as administrator | Re-run with elevated rights if policy allows; otherwise treat the grade as optimistic |
| Chat "invalid invite code" | Typo | The checksum caught it; re-enter carefully (O/0 and I/L/1 are interchangeable) |
| Chat will not connect | Relay URL wrong or not wss://; network blocks WebSockets | Check Settings; try Tor/SOCKS mode |
| Vault "Invalid passphrase or corrupted vault" | Wrong passphrase, or the file was altered | Retry; if the file was altered, restore from your own backup copy |
GentzyCloak — Powered by LordMoritz.
Threat model
Powered by LordMoritz.
This document states plainly what GentzyCloak protects, against whom, and where it stops. If a claim in marketing material contradicts this document, this document is right.
1. Assets
| # | Asset | Where it lives | Sensitivity |
|---|---|---|---|
| A1 | Vault contents (notes, credentials, codes) | vault.gcv (encrypted at rest); main-process memory while unlocked | Very high |
| A2 | Vault passphrase | user's head; renderer input → IPC → main; never stored | Very high |
| A3 | Chat plaintext, room secrets, room keys, session identity | main-process memory only | Very high |
| A4 | Browsing activity (URLs, cookies, cache, downloads) | in-memory partition; downloads on USB only if enabled | High |
| A5 | Scan results and exported reports | memory; reports/ on USB when exported | High (they describe a principal's device) |
| A6 | The app bundle itself (executable, bundled JS) | USB drive | High (a modified bundle compromises everything above) |
| A7 | Settings (relay URL, proxy, alias) | *-settings.json on USB | Medium |
| A8 | Relay availability and metadata | operator's server | Medium |
2. Adversaries
| Adversary | Capabilities assumed | Goal |
|---|---|---|
| ADV-1 Public-computer operator (café, hotel business centre, loaner-laptop provider, previous user) | Administrator on the host before you arrive; may have installed commodity RATs, stalkerware, keyloggers, browser extensions, proxy/CA certificates; can inspect the disk afterwards | Capture credentials, documents, communications; profile the user |
| ADV-2 Network observer (hostile Wi-Fi, ISP, national gateway) | Passive capture; active MITM attempts; DNS manipulation; traffic-analysis | Learn who the user talks to and which services they use; inject content |
| ADV-3 Malware already on the principal's own machine | User-level or admin-level persistence; may hide from casual inspection | Long-term surveillance of the principal |
| ADV-4 Physical theft or seizure of the USB drive | Full offline access to the drive's contents; unlimited time; forensic tooling | Recover vault contents, reports, browsing traces |
| ADV-5 Relay operator / relay compromise | Full control of the relay process and its network vantage point | Read messages; identify participants; disrupt |
| ADV-6 Supply-chain tampering (modified download, tampered drive contents) | Replace or patch the app before the user runs it | Turn the "security tool" into the implant |
3. What is mitigated
| Threat | Mitigation | Where |
|---|---|---|
| ADV-1 installed persistence / RAT / remote-access service | Scanner enumerates persistence mechanisms, remote-access services, listening ports, unsigned processes, privacy grants; rules flag known indicators with MITRE mapping and remediation steps | src/main/scanner |
| ADV-1 leaves data on the host for later | All Electron paths redirected onto the USB; browsing tabs use in-memory partitions; panic wipe; automatic wipe on drive removal | src/main/portable.ts, src/main/index.ts, browser |
| ADV-1 injected CA / proxy for MITM | HTTPS-only mode; DoH bypasses local DNS; tracker blocklist; (certificate pinning is not implemented — see §4) | browser |
| ADV-2 DNS snooping / manipulation | DNS over HTTPS (Quad9 / Cloudflare / Mullvad) | browser |
| ADV-2 tracker/beacon-based profiling | Host-based blocklist, third-party cookie blocking, user-agent normalisation, WebRTC leak prevention | browser |
| ADV-2 traffic content | TLS (browser); AES-256-GCM end-to-end (chat) | browser, chat |
| ADV-2 correlation via IP | Optional SOCKS/Tor proxy mode for the browser only — the relay connection is not proxied by the app (see §4) | browser |
| ADV-1 identity certificates planted in the host's certificate store | select-client-certificate is refused app-wide; no site can silently obtain a client certificate from the host | src/main/index.ts |
ADV-1 hostile process environment reaching scanner child processes (DYLD_INSERT_LIBRARIES, LD_PRELOAD, NODE_OPTIONS, user-writable PATH, foreign locale, forged SystemRoot) | Children get a sanitised environment: preload/interpreter hooks stripped, fixed system PATH, fixed locale; Windows SystemRoot must look like X:\Windows | src/main/scanner/exec.ts, collectors |
ADV-6 symlink planted on the drive (GentzyCloak-Data → the host's home directory) | The data directory must be a real directory physically inside the portable root (lstat + realpath), else the app falls back to host userData; the panic wipe never descends symlinked directories and opens files O_NOFOLLOW, sizing the overwrite from the open descriptor | src/main/portable.ts |
| ADV-5 relay resource exhaustion | Global and per-address connection caps, 256 members per room, slow-consumer drop, HTTP header timeout, per-socket rate limit and frame-size cap | relay/server.ts |
| ADV-4 offline vault attack | scrypt N=2^17 (≈134 MiB memory-hard per guess) + AES-256-GCM; 12-char minimum passphrase; tamper → authentication failure, not partial data | vault |
| ADV-4 recovering deleted reports/downloads | Panic wipe overwrites once then unlinks (see caveat) | portable |
| ADV-5 reading or forging messages | Relay only sees HKDF-derived room id and opaque ciphertext; messages are Ed25519-signed and replay-filtered; room key never leaves the clients | chat, relay |
| ADV-5 guessing rooms | Room id is 128-bit HKDF output of a 256-bit secret | chat |
| ADV-6 modified bundle | SHA-256 manifest of every bundled file, verified at startup; root hash displayed for out-of-band comparison; CI smoke test fails if integrity fails | scripts/build.mjs, portable |
| Renderer compromise via malicious page | Browsing tabs are sandboxed WebContentsViews with no preload and no IPC access; UI window has sender-guarded IPC (top-level file:// frame only), every IPC argument type/length/enum-validated in main (src/main/ipc-validate.ts), window-open denied, navigation and subframe navigation blocked, webview attach blocked, all web permissions denied; browser settings (including the download directory, which must stay on the GentzyCloak drive) are validated in main, never trusted from the UI; the plaintext-relay override cannot be set over IPC | main, preload |
| Host data injecting into reports (stored XSS in a report opened on another machine) | Every host/user string HTML/Markdown-escaped; report has CSP default-src 'none', no scripts; only http(s) tool links are clickable | report |
| Torn files on drive yank | Atomic temp+fsync+rename writes for vault and settings | settings, vault |
| Passphrase / vault contents lingering | Key buffer zeroed on lock; plaintext buffer zeroed after parse; entries cleared | vault |
4. What is NOT mitigated
Be honest with users about these.
| Gap | Why | Consequence |
|---|---|---|
| Hardware keyloggers / malicious USB hubs / BadUSB | Invisible to software | Passphrases and messages typed on that host are captured |
| Screen capture, camera, mic on a compromised host | If ADV-1/3 already run privileged code, they can read the screen and input regardless of our sandboxing | Anything displayed (decrypted vault entries, chat) is exposed |
| Kernel-level / firmware / hypervisor implants | The scanner uses userland OS tools (ps, launchctl, lsof, registry, systemctl, …); a rootkit that lies to those tools is not detected | A clean grade is "no known indicator found", not a guarantee |
| Zero-day or bespoke malware | Rules are heuristic and indicator-based | Same as above |
| Tor is not bundled | Licensing, size and update cadence; a stale bundled Tor is worse than none | "Tor mode" needs a Tor client running on the host or reachable via SOCKS; without it, IP is exposed to the relay and to sites |
| No certificate pinning / CA store hardening | We rely on Chromium's trust store, which an admin-level ADV-1 can have poisoned | An injected root CA on the host enables TLS interception of browser traffic; DoH does not help here. Chat E2E encryption is not affected by TLS interception |
| Relay metadata | The relay necessarily sees client IPs, connection timing, room ids and message sizes/timing | Traffic analysis can link participants; use Tor/SOCKS and a trusted relay operator |
| Relay availability | A hostile relay can drop or delay frames | Denial of service, not confidentiality loss |
| Alias / identity is self-declared and per-session | Anonymity is a feature | Without out-of-band fingerprint verification, a person holding the invite code can join as anyone |
| Panic wipe on flash media | SSD/USB wear-levelling remaps blocks; a single overwrite does not guarantee physical erasure | Forensic recovery possible; use an encrypted drive |
| Portable mode fallback | Read-only media forces use of the host's userData | Traces on host; the dashboard warns, the user must decide |
Files added to dist/ that are not in the manifest | The manifest lists what we built; it cannot list what an attacker adds. Extra files are not loaded by the bundle, but an attacker who can add files can usually also modify listed ones, which is detected | A tampered listed file is caught; a re-packaged app with a regenerated manifest is not — compare the root hash with a value obtained on a trusted machine |
The manifest covers dist/ only | package.json, the Electron binary/framework and the OS loader are outside it (all JS dependencies are bundled into dist/ by esbuild, so node_modules does not exist at runtime) | A patched Electron binary or a replaced package.json main entry is not detected by the in-app check; only the published download hash and OS code signing (not yet in place) would catch it |
| Relay traffic is not proxied by the app | The chat client uses Node's ws directly; no SOCKS/Tor agent is wired in yet | The relay operator sees the host's real IP even while the browser is in Tor mode. Use a system-wide VPN / Tor transparent proxy, or a Tor-capable network, when the fact of chatting must be hidden |
| Vault rollback | vault.gcv carries no version counter; an older copy decrypts under the same passphrase | ADV-4 with write access to the drive can silently revert entries to an earlier state (never read them). Keep a trusted copy if entry history matters |
| Unsigned builds | No Apple notarisation / Authenticode yet | OS warnings on first launch; the integrity root hash is the compensating control |
| Electron/Chromium vulnerabilities | Large attack surface | Keep the build current; portable builds are not auto-updated |
5. Residual risk table
| Scenario | Likelihood | Impact | Residual risk | Primary control |
|---|---|---|---|---|
| Commodity RAT/stalkerware on a public PC | High | High | Medium — detection likely for known families; user must act on findings | Scanner + report |
| Host with injected root CA | Medium | High | High for browsing; low for chat/vault | User guidance: do not enter credentials in the browser on such a host |
| Kernel rootkit on a loaner laptop | Low | Very high | High — undetected | Guidance: assume compromise; use device only for low-value tasks |
| Hostile Wi-Fi | High | Medium | Low — DoH + HTTPS-only + E2E chat | Browser, chat |
| USB drive stolen | Medium | High | Low for vault (scrypt + AES-GCM, strong passphrase); Medium for exported reports unless the drive is encrypted | Vault; encrypted drive |
| Relay operator malicious | Low–Medium | Medium (metadata) | Medium | System-level Tor/VPN (the app does not proxy the relay itself); run your own relay |
| Tampered download | Low | Very high | Low if hash verified on a trusted machine, High otherwise | Published hashes + integrity root |
| User unlocks vault on a compromised host | Medium | Very high | High | Operational guidance below |
6. Operational guidance
- Verify the build on a trusted machine. Compare the download's SHA-256 with the published value; after first launch, note the integrity root hash shown on the dashboard. On any other machine, if that hash differs or the check is red, stop.
- Use an encrypted USB drive. BitLocker To Go, an encrypted APFS volume, LUKS or VeraCrypt. This — not the panic wipe — is what protects exported reports and downloads from theft.
- Do not unlock the vault on an untrusted host. Run the scanner first. Only unlock on a device that is yours and clean, or accept that the entries you view may be captured. Prefer copying one entry at a time.
- Run Tor yourself when IP exposure matters, and set the browser to Tor mode; verify
torConnectedin the browser state. The relay connection is not routed through that proxy by the app (see §4) — if the relay operator must not learn your IP, use a system-wide VPN/Tor as well. - Verify the chat fingerprint out-of-band (voice call, in person) before discussing anything sensitive. Read the 8-character fingerprint aloud.
- Never type credentials into the browser on a host with a foreign root CA. The scanner's posture and integrity rules try to surface this; if in doubt, use the device only for reading public information.
- On a critical finding: isolate, preserve, escalate. Disconnect network, do not reboot or "clean" the machine (evidence lives in memory and in persistence entries), export the report to the USB, and hand the device to your security team or national CERT.
- Pull the drive when done. The app wipes ephemeral data and exits automatically; it also wipes on normal close. Use the panic button when you must leave in a hurry.
- Rotate. Invite codes are per room; create a new room for a new engagement. Vault passphrase changes require creating a new vault and re-entering items (no in-place rekey yet).
7. Assumptions
- Node's
crypto(OpenSSL) implementations of scrypt, AES-GCM, HKDF and Ed25519 are correct. - Chromium's sandbox holds against the sites the user visits.
- The user can obtain one trusted machine at some point to verify hashes.
- The USB drive's own firmware is not malicious.
Browser hardening
Powered by LordMoritz. Module: src/main/browser/. Verified by tests/browser.*.test.ts (pure logic) and scripts/browser-smoke.mjs (real Electron 44 / Chromium 152).
The hardened browser is an ephemeral, privacy-inclined browser that runs inside the GentzyCloak window using Electron WebContentsView. Its design goal is narrow and honest: leave nothing on the host disk and minimise what websites learn about you while you use a machine you do not trust. It is not a replacement for Tor Browser for high-risk anonymity — see "Comparison with Tor Browser" below.
1. What is protected
1.1 Nothing persists on the host
| Mechanism | Detail |
|---|---|
| In-memory partition | session.fromPartition('gentzy-ephemeral-<random>', { cache: false }). No persist: prefix, so Chromium never creates a profile directory: cookies, cache, localStorage, IndexedDB, service workers, history, HSTS state live in RAM only. |
| Fresh partition per wipe | wipe() (panic button, window close, manual) closes every tab, then runs closeAllConnections, clearAuthCache, clearHostResolverCache, clearCache, clearStorageData, clearData, clearCodeCaches, flushes the cookie store, and rotates to a brand-new random partition. |
| Verified, not assumed | The smoke test writes a localStorage canary and a cookie canary, loads a page, then byte-scans the whole userData tree: no file contains the page content or either canary; userData/Partitions does not exist. |
| Downloads off by default | allowDownloads=false cancels every download. When enabled, files go only to a folder on the GentzyCloak drive — <portableRoot>/GentzyCloak-Data/downloads by default. A custom downloadDir is accepted only if it resolves inside the portable root or the data directory; this is enforced in the main process (sanitizeBrowserSettings), not in the UI, so neither a tampered settings file nor a compromised renderer can point downloads at the host's Desktop/Downloads. Filenames are sanitised and timestamped. |
| No spellcheck, no preload | Spellchecker off (no dictionary downloads, no text sent to OS services). No preload scripts are registered in the browsing session. |
1.2 Network privacy
| Feature | Setting | What it does |
|---|---|---|
| Tracker / ad / telemetry blocking | blockTrackers | 560+ curated hosts (Google Analytics/Ads, Meta pixel & SDK, Microsoft Clarity/Bing, Amazon ads, Hotjar, Mixpanel, Segment, comScore, Nielsen, Criteo, Taboola, Outbrain, AppNexus, PubMatic, OpenX, …) blocked in onBeforeRequest, with suffix matching on label boundaries (stats.g.doubleclick.net blocked, notdoubleclick.net not). Plus path-scoped pixels on hosts that must stay reachable (facebook.com/tr, google.com/pagead/, youtube.com/api/stats/). Per-tab and total counters shown in the UI. |
| HTTPS-only | httpsOnly | http:// requests are rewritten to https:// before they leave. Exceptions: localhost, 127.0.0.0/8, [::1], RFC1918/link-local addresses, and .onion (Tor already encrypts end-to-end). |
| Referrer minimisation | always | Same-site requests carry origin only (https://site.tld/); cross-site requests carry no Referer; https→http never leaks. |
| Third-party cookies | blockThirdPartyCookies | Cookie request headers are stripped and Set-Cookie response headers are dropped when the request's registrable domain (eTLD+1, with a built-in Public Suffix subset — co.uk, com.au, co.jp, gov.ng, com.ng, com.br, github.io, …) differs from the top frame's. Main-frame navigations are never touched. |
| Privacy signals | always | DNT: 1 and Sec-GPC: 1 (Global Privacy Control) on every request. |
| Chrome telemetry headers | always | X-Client-Data and other Chrome-internal identity headers are removed. |
| DNS-over-HTTPS | dohEnabled, dohProvider | app.configureHostResolver({ secureDnsMode: 'secure' }) with Cloudflare (cloudflare-dns.com), Quad9 (dns.quad9.net, default) or Mullvad (dns.mullvad.net). secure mode means no fallback to the host's plaintext resolver. Note: this setting is process-wide in Chromium and must be applied after app.whenReady(). |
| Tor (SOCKS5) | proxyMode: 'tor' | Routes all traffic to socks5://127.0.0.1:9050 (tor daemon / Expert Bundle) or :9150 (Tor Browser), whichever answers. Chromium's socks5:// scheme resolves hostnames on the proxy side, so DNS goes through Tor and DoH is bypassed for proxied requests (dohEnabled shows as off in the state while Tor is active). Fails closed: if no Tor port answers, the session is still pinned to 127.0.0.1:9050, so nothing leaks to the direct network; the UI shows torConnected=false. |
| Custom proxy | proxyMode: 'custom' | socks5://, socks4://, http:// with optional credentials (answered via the login event). An invalid proxy string fails closed to an unroutable address. |
| WebRTC | always | setWebRTCIPHandlingPolicy('disable_non_proxied_udp') on every tab: no local/LAN IP leaks via ICE, and with a proxy configured no UDP escapes it. |
| Loopback bypass | always | <-loopback> is excluded from proxying so GentzyCloak's own local services keep working. |
1.3 Fingerprinting reduction (partial — see §2)
- User-Agent: a plain Chrome UA for the host OS with no Electron or app token, using the real Chromium major that is running (a mismatched UA vs. Client Hints is itself a fingerprint).
Accept-Languageis normalised toen-US,en;q=0.9. - WebGL disabled by default (
webgl: settings.webglEnabled, defaultfalse): removes GPU vendor/renderer strings and WebGL canvas hashing, the strongest passive fingerprint. Trade-off: WebGL-dependent sites (maps, 3D, some games) fail with "Failed to initialize WebGL". The operator can opt in per session (Settings → Browser → WebGL, or the GL button in the browser bar, which reopens the current page because the flag is fixed when a tab is created). The start page and the GL button both show when WebGL is on, so the exposure is never silent. - No plugins, no WebSQL, no experimental features, no DevTools,
navigator.webdriver === false. - All permissions denied except HTML fullscreen: camera, microphone, geolocation, notifications, MIDI, USB, HID, serial, Bluetooth (pairing cancelled), clipboard-read, display capture, idle detection, pointer lock, local fonts, sensors, storage-access, payment handler, etc. Both
setPermissionRequestHandlerandsetPermissionCheckHandlerdeny,setDevicePermissionHandlerreturns false, device-chooser events are cancelled. - Autoplay requires document-level user activation.
1.4 Process and navigation hardening
- Every tab:
sandbox: true,contextIsolation: true,nodeIntegration: false(including workers and subframes),webSecurity: true,allowRunningInsecureContent: false,webviewTag: false,navigateOnDragDrop: false,safeDialogs: true. - Navigation allow-list: only
http:,https:,about:blank.file:,chrome:,devtools:,javascript:,blob:and top-leveldata:are refused atwill-navigate,will-redirect,will-frame-navigate, in the omnibox, and inwindow.openhandling. Subframes may usedata:/blob:/about:srcdoc(needed by ordinary pages). - Pop-ups never create native windows; allowed URLs open as a new managed tab.
- Certificate errors are never bypassed (
callback(false)); Chromium's interstitial stays. - Client certificates are never offered:
select-client-certificateis refused app-wide, so a site (or a MITM) cannot make Chromium silently present an identity certificate from the host's store. - Settings are validated in main: every
BrowserSettingschange from the UI goes throughsanitizeBrowserSettings(enum values, booleans, proxy string length, download directory containment); unknown keys are refused. - At most 64 tabs (
MAX_TABS):window.openbeyond the cap is ignored, so a hostile page cannot exhaust memory by spawning views. - Only the active tab's
WebContentsViewis attached to the window; others are detached and background-throttled. - The omnibox never sends input to Google: non-URL input goes to
https://html.duckduckgo.com/html/?q=(a lightweight, JavaScript-free results page that also works withjavascriptEnabled=false).
2. What is NOT protected — read this before relying on it
- A compromised host defeats any browser. If the machine you plug into has a keylogger, screen recorder, malicious accessibility service, kernel rootkit, or a hostile administrator, everything you type and see is exposed regardless of how the browser is configured. GentzyCloak's scanner module can surface some of these indicators; it cannot make a hostile host safe. Do not enter credentials or sensitive text on a host you have reason to distrust.
- Tor is not bundled. Network-level anonymity (hiding your IP address from websites and your destinations from the local network/ISP) requires Tor, and GentzyCloak does not ship it. Without Tor mode, sites and the network see the host's real IP address; DoH hides DNS names from the local resolver but not your IP from the DoH provider or destination sites. To use Tor mode: run Tor Expert Bundle (
torlistens on127.0.0.1:9050) or simply start Tor Browser (its Tor listens on127.0.0.1:9150) on the same machine, then choose Tor in browser settings. The status pill shows whether a SOCKS listener answered — a listener on 9050/9150 is assumed to be Tor; the app cannot prove it. When the Tor port is not reachable the browser fails closed (no traffic) rather than leaking. - Fingerprinting is reduced, not eliminated. Screen size, timezone, installed-font metrics, canvas 2D, AudioContext, hardware concurrency, device memory, Client Hints and many behavioural signals are still exposed. Tor Browser's letterboxing, uniform UA, canvas prompt and disabled APIs make its users look alike; GentzyCloak users look like an ordinary Chrome on the host OS — which is a good disguise on a public machine, but not anonymity against a determined tracker.
- Anti-fraud and CAPTCHA vendors are deliberately not blocked (Arkose/FunCaptcha, hCaptcha/reCAPTCHA, ThreatMetrix, Sift, PerimeterX, DataDome). Blocking them silently breaks bank logins and checkouts; they do fingerprint you. Email link redirectors (SendGrid, Mailchimp) and podcast prefix hosts are likewise left alone so links keep working.
- RAM is not wiped on power loss or process crash. In-memory state is gone when the process exits, but a memory-forensics attacker with physical access to a running machine can read it. Also, the OS may swap process memory to disk; on a hostile host, swap/hibernation files can contain browsing data. GentzyCloak cannot control host swap settings.
- Chromium's default profile still exists. Chromium always creates a default profile directory (
Cache,GPUCache,Local Storage,DIPS…) underuserDatafor the app's own UI. The browsing session never uses it (verified: no fetched content or canaries appear there), and in portable modeuserDatais redirected onto the GentzyCloak drive, not the host. - Traffic analysis / local network observation. Without Tor, the local network sees the IPs and SNI hostnames you connect to (TLS 1.3 + DoH do not hide SNI unless Encrypted Client Hello is negotiated by both sides).
- DoH provider trust. Your chosen DoH provider sees every domain you resolve. Pick the one whose policy you trust (Quad9 is the default; Mullvad is the most privacy-focused).
- Hardware attacks — malicious USB ports (BadUSB/juice-jacking), firmware implants, hardware keyloggers in the keyboard cable, or a rogue display cable capturing video are entirely outside software control.
javascriptEnabledchanges apply to new tabs only (it is a per-renderer preference); open a new tab after toggling it.- Blocklist is static. It ships with the app and is not updated at runtime (no network fetch of lists — that would itself be a beacon). New trackers appear constantly; the list is a strong baseline, not an exhaustive filter like uBlock Origin's.
3. Comparison with Tor Browser
| GentzyCloak browser | Tor Browser | |
|---|---|---|
| Network anonymity | Only when an external Tor is running and selected | Built in, always on |
| Fingerprint model | "Look like normal Chrome on this OS" | "All users look identical" (letterboxing, uniform UA, RFP) |
| Persistence | None (RAM only, per-partition, wiped on close/panic) | Configurable; private by default |
| Tracker blocking | Curated host list + third-party cookie stripping | NoScript + first-party isolation; no ad-list |
| JavaScript control | Global on/off | Per-site security levels |
| Suitable for | Reading, research, webmail on a public/shared machine you distrust; leaving no traces on that machine | Whistleblowing, censorship circumvention, adversaries with network visibility |
If your threat model includes a network-level or state-level adversary, use Tor Browser (or Tails). GentzyCloak's browser is about the host not learning what you did, and websites learning less than usual.
4. Operational notes
- Panic (
app:panic) wipes the browser session before the app exits. - Closing the app window wipes the session.
- All settings (
BrowserSettings) can be changed live; proxy, DoH, UA and header policy are re-applied immediately. Tor connection status is re-probed on every proxy change. - When running GentzyCloak from tooling that exports
ELECTRON_RUN_AS_NODE=1(some IDE terminals do), Electron runs as plain Node and the app will not start; the smoke runner unsets it — do the same in your launcher.
Verification commands
npx tsc -p tsconfig.json --noEmit
npx eslint src/main/browser tests scripts/browser-smoke.mjs
npx vitest run tests/browser.policy.test.ts tests/browser.blocklist.test.ts
node scripts/browser-smoke.mjs # real Electron, hidden window, ~10 s, needs internet for example.comChat protocol
Anonymous, end-to-end encrypted group chat over a dumb relay. Powered by LordMoritz.
- Code:
src/main/chat/(client),relay/server.ts(relay),src/shared/relay-protocol.ts(wire format). - Crypto: Node built-in
cryptoonly — HKDF-SHA256, AES-256-GCM, Ed25519, SHA-256. No third-party crypto. - Protocol version: payload version byte
0x01, relay frame version 1.
1. Goals and non-goals
| Goal | Non-goal |
|---|---|
| Anyone with the invite code can talk; nobody without it can read anything | Accounts, phone numbers, contact discovery |
| The relay is a pipe: sees only opaque room ids and ciphertext | Hiding that you use the relay from your network / ISP |
| No persistent identity: a fresh Ed25519 key per app session | Message history; late joiners see nothing |
| No server-side storage or content logging | Forward secrecy against invite-code compromise (see §7) |
| Typos in invite codes are caught before anything is sent | Deniability against a peer who holds the invite |
2. Invite code ("GentzyKey")
GZ-XXXXX-XXXXX-XXXXX-XXXXX-XXXXX-XXXXX-XXXXX-XXXXX-XXXXX-XXXXX-XXXX
secret= 32 random bytes (crypto.randomBytes) — the only long-term secret of a room.- Encoded as 52 Crockford base32 symbols (
0-9 A-ZwithoutI L O U; 260 bits, last 4 bits are zero padding and must be zero on decode). - 2-symbol check appended (54 symbols total), then grouped 5-5-5-5-5-5-5-5-5-5-4 with the fixed
GZ-prefix. - Check value =
(Σ dᵢ · 32^(51−i)) mod 1021over the 52 data symbol values, written as two base32 symbols (check >> 5,check & 31). 1021 is the largest prime below 32²; because every symbol delta is< 32and32^k mod 1021 ≠ 0, every single-symbol substitution and every adjacent transposition is detected deterministically (the test suite exhaustively tries all 54 × 31 substitutions). Random garbage is rejected with probability ≈ 1 − 1/1021. - Parsing (
parseInvite) accepts any case, with or without dashes/spaces, with or without theGZprefix, and applies Crockford's look-alike mappingO→0,I→1,L→1.Uis always invalid.
3. Key derivation
All room material is derived with HKDF-SHA256, salt "gentzycloak/v1":
| Output | info | Length | Purpose |
|---|---|---|---|
roomId | "room-id" | 16 bytes | hex → 32 chars; public, sent to the relay |
roomKey | "room-key" | 32 bytes | AES-256-GCM key; never leaves the process |
fingerprint | "fingerprint" | 5 bytes | base32 → 8 chars, shown in the UI to verify out-of-band |
HKDF is one-way and the three outputs are independent: knowing roomId (which the relay and any network observer sees) reveals nothing about roomKey, and a fingerprint cannot be turned into either. The invite secret is zeroed in memory when a room is left or the manager is destroyed.
4. Identity
- On
ChatManagerconstruction an Ed25519 keypair is generated (generateKeyPairSync('ed25519')). It is never written anywhere; restarting the app yields a new identity. fromId = base32(SHA-256(rawPublicKey)[0..8])→ 13 chars. This is what peers see next to the alias.- The alias is self-declared, ≤ 64 chars, defaults to
anon-XXXX. It does not prove who a person is — onlyfromIdis bound to a key — but it is covered by the message signature (§5), so another room member cannot take a captured message and re-label it with a different alias while keeping the originalfromId. Receivers strip control characters from it before display. UIs should show both alias andfromId.
5. Message envelope
Plaintext (JSON, UTF-8):
{ "id": "<32 hex>", "alias": "alice", "fromId": "<13 base32>", "pub": "<base64 raw ed25519 pk>",
"body": "…≤ 8 KiB…", "sentAt": "2026-09-06T00:00:00.000Z", "sig": "<base64 ed25519 sig>" }
sig= Ed25519 overcanonical(roomId, id, sentAt, alias, body)wherecanonicalconcatenates each field asu32be(byteLength) || utf8(field)— unambiguous, so no field can be re-split.pubneeds no explicit coverage: it is the key the signature is verified under, andfromIdis recomputed from it.- Payload bytes =
0x01 || nonce(12, random) || AES-256-GCM(plaintext) || tag(16), with AAD = utf8(roomId). The payload is base64-encoded for the relay frame.
Receiver checks, in order — any failure drops the message silently except where noted:
- Version byte is
0x01; length ≥ 29. - AES-GCM authentication with the room key and AAD (wrong key, wrong room, or any flipped byte fails here).
- JSON shape;
body≤ 8 KiB;id≤ 64 chars;aliaspresent (may be empty) and ≤ 64 chars — a missing or oversized alias is a reject, never a default. pubdecodes to 32 bytes andfromId == base32(SHA-256(pub)[0..8])(constant-time compare).- Ed25519 signature verifies under
pub. |now − sentAt| > 10 min→ delivered withverified = false(not dropped; clocks drift).- Replay: per-room LRU of 5000 seen
ids; a repeated id is dropped. The sender records its own id too, so an echo is ignored.
The sender renders its own message locally (mine = true); the relay never echoes.
6. Relay wire protocol
One JSON object per WebSocket text frame. Room ids must match ^[0-9a-f]{32}$; payloads are base64, ≤ 16 KiB.
| Direction | Frame | Notes |
|---|---|---|
| client → relay | {t:'join', room} | idempotent; ≤ 64 rooms per socket |
| client → relay | {t:'leave', room} | |
| client → relay | {t:'msg', room, payload} | must have joined room first |
| client → relay | {t:'ping'} | application keepalive |
| relay → client | {t:'joined', room, peers} | ack for join |
| relay → client | {t:'peers', room, peers} | membership changed |
| relay → client | {t:'msg', room, payload} | fan-out to every other member |
| relay → client | {t:'error', code, message} | codes: bad_json bad_frame bad_room payload_too_large rate_limited too_many_rooms room_full not_joined server_error |
| relay → client | {t:'pong'} |
Relay policy: 20 frames/s per socket (token bucket, burst 20) → rate_limited + close 1008; WebSocket frames > 64 KiB are refused at the transport; at most 256 sockets per room (room_full); at most 10 000 open sockets and 64 per client address (further sockets are closed with 1013 before any frame is read; the per-address cap uses X-Forwarded-For only when the operator sets RELAY_TRUST_PROXY=1 behind their own TLS terminator); a member whose outbound queue exceeds 1 MiB is dropped rather than buffered without bound; the HTTP upgrade must complete within 15 s; WebSocket ping every 30 s, sockets that miss a pong are terminated; GET /healthz → 200 ok for load balancers; logs contain only connection/room counts, never payloads, room ids or addresses.
Client policy: wss:// required unless the host is loopback; ws:// elsewhere needs the allowInsecureRelay knob, which is a local ChatManager option only — it is deliberately not settable from the UI/IPC, so a compromised renderer cannot downgrade the relay link to plaintext. Relay URLs are validated when set (bad ones are rejected immediately). Reconnect with exponential backoff 1 s → 30 s plus jitter, rejoining all rooms; while disconnected send() fails fast (relay not connected) rather than queueing.
7. Threat model
What the relay operator (or anyone on the path to it) can see
| Visible | Hidden |
|---|---|
| Client IP addresses and connection times | Message text, aliases, identities, public keys, signatures |
| Room ids (opaque 128-bit values) | The invite secret and room key (HKDF is one-way) |
| Who is in which room, and when (by IP) | Whether two rooms belong to the same people |
| Message timing and ciphertext sizes | Message content — even after compromising the relay later |
| Nothing after the fact: no storage |
The relay can also drop, delay, duplicate or reorder ciphertext. Duplicates are caught by the replay LRU, forgeries by AES-GCM + Ed25519; dropping/delaying is a denial of service the protocol does not try to hide.
Mitigations
- Network metadata (IP, timing): run the app's traffic through Tor or a VPN, or host the relay as an onion service. Note that the browser module's SOCKS/Tor mode applies to browsing only — the relay WebSocket is opened by Node's
wsand is not proxied by the app yet. A system-wide VPN or Tor transparent proxy is currently the only way to hide the client IP from the relay. - Traffic analysis by size: not applied by default. Bodies are ≤ 8 KiB; a future version may pad payloads to fixed buckets (e.g. 512 B / 4 KiB / 8 KiB). Operators and users should assume message lengths are observable.
- Malicious relay injecting frames: every payload must authenticate under the room key and carry a valid Ed25519 signature from the claimed sender; a relay cannot produce either.
- Room id enumeration: ids are 128-bit HKDF outputs; guessing is infeasible, and joining a room without the key yields only undecryptable ciphertext.
What a peer with the invite can do
The invite is the room. Anyone holding it can read every message sent while they are connected, send messages under any alias of their own, and re-share the invite. What they cannot do is alter or re-label somebody else's message: the alias, body, id and timestamp are all under that sender's Ed25519 signature, and fromId (bound to the key) is stable within a session. Verify the 8-char fingerprint out-of-band before trusting a room, and rotate (create a new room) if an invite leaks.
8. Forward secrecy — known limitation
The room key is static for the life of the invite. Consequences:
- Anyone who obtains the invite can decrypt future traffic and any past traffic they captured while online.
- There is no server-side history, so past traffic is not retrievable from the relay after the fact — a later compromise of the invite does not expose messages nobody recorded.
- Per-session identity keys are ephemeral, so a device seized later cannot be tied to earlier sessions by key material (the invite, if still in memory, is the only link — leave rooms / quit the app to zero it).
There is no ratchet and no per-message key agreement. This is a deliberate trade-off for a zero-setup, invite-only design. Future work: hash-ratchet the room key per epoch, or run X3DH/Double-Ratchet pairwise sessions on top of the room channel.
9. Recommendations
- Exchange invites over a channel you already trust (in person, Signal, an encrypted note in the GentzyCloak vault). Never post them where a third party can read them.
- Compare fingerprints out-of-band; if they differ, someone has a different invite (or a typo passed a manual check).
- Use a
wss://relay you control or trust for availability; treat any relay as hostile for confidentiality — the design assumes it. - Prefer a system-wide Tor / VPN when the fact of communication is sensitive — the app does not proxy the relay connection itself.
- Keep sessions short: the identity and all room secrets are gone when the app exits (
ChatManager.destroy()zeroes them). - Do not rely on aliases; use
fromIdto correlate messages within a session, and expect it to change next session. - Message bodies are limited to 8 KiB — there is no file transfer, by design.
10. Test coverage
npx vitest run tests/chat.crypto.test.ts tests/relay.test.ts tests/chat.e2e.test.ts
- crypto: base32 round-trips, invite format/round-trip, exhaustive single-symbol typo and transposition detection, HKDF determinism and independence, encrypt/decrypt, tamper/wrong-key/AAD failures, forged-signature and impersonation rejection, fromId mismatch, 8 KiB limit both sides, skew →
verified=false, replay LRU. - relay: health endpoint, A→B delivery / room isolation / no echo, pings, error frames for bad JSON/frames/rooms, oversize payloads,
not_joined, rate limit + close, leave semantics, room cap, heartbeat termination. - e2e: two
ChatManagers exchanging verified messages via an in-process relay, invite typo rejection, garbage and wrong-key injection produce nomessageevent, replay suppression, relay-side socket kill → reconnect + rejoin + traffic resumes, relay URL policy, settings/constructor forms. - security (
tests/security.chat.test.ts,tests/security.relay.test.ts,tests/security.ipc.test.ts): alias re-labelling of a captured message is rejected, missing/oversized alias rejected, bad relay URL rejected at set time,allowInsecureRelayrefused over IPC, peer alias control characters stripped end-to-end; relay global/per-address connection caps (with and withoutX-Forwarded-Fortrust),room_full, slow-consumer drop.
Architecture
Powered by LordMoritz. Electron 44, TypeScript, Node 24.
This document is for reviewers who want to know what runs where, what talks to what, and what touches disk.
1. Process model
┌───────────────────────────── main process (Node) ──────────────────────────────┐
│ src/main/index.ts │
│ ├─ portable.ts resolve paths, integrity check, panic wipe, drive watch │
│ ├─ settings.ts atomic JSON stores (browser-settings.json, chat-settings) │
│ ├─ browser/ BrowserManager: WebContentsView tabs, in-memory partition │
│ ├─ scanner/ collectors (exec OS tools) → snapshot → pure rules → score │
│ ├─ chat/ ChatManager: invite/HKDF/AES-GCM/Ed25519, RelayClient (ws) │
│ ├─ vault/ Vault: scrypt + AES-256-GCM file, in-memory entries │
│ └─ report/ renderReport / exportReport (html, md, json) │
│ ipcMain.handle(...) with sender guard │
└──────────────────────────────────┬─────────────────────────────────────────────┘
│ contextBridge (typed, allow-listed channels)
┌──────────────────────────────────┴─────────────────────────────────────────────┐
│ preload src/preload/index.ts → window.gentzy : GentzyApi │
└──────────────────────────────────┬─────────────────────────────────────────────┘
│ (sandbox: true, contextIsolation: true,
│ nodeIntegration: false, no remote module)
┌──────────────────────────────────┴─────────────────────────────────────────────┐
│ renderer src/renderer/ (plain TS, no framework) shell: sidebar, topbar, │
│ statusbar, command palette · views: dashboard, browser, scan, chat, │
│ vault, reports, settings · design system: docs/DESIGN-SYSTEM.md │
└────────────────────────────────────────────────────────────────────────────────┘
│
┌──────────┴──────────┐
│ WebContentsView tabs │ one per browser tab; partition
│ (browser content) │ 'gentzy-<random>' (in-memory,
│ sandboxed, no preload│ no `persist:` prefix); shown
└─────────────────────┘ inside the renderer's layout rect
Key properties:
- One privileged process. Only
mainhas Node and OS access. The renderer UI is a sandboxed page with a fixed preload bridge; browsing tabs are separate sandboxedWebContentsViews that get no preload at all. - IPC sender guard. Every
ipcMain.handleverifiesevent.senderis the app window's ownwebContents; a browser tab cannot call app APIs. - Window hardening.
setWindowOpenHandler(() => deny),will-navigateprevented on the UI window,will-attach-webviewprevented globally, menu removed, devTools only when unpackaged. - Chromium switches set before
app.ready: WebRTC non-proxied UDP disabled, mDNS IP hiding, breakpad/crash reporter off, background networking, component update, domain reliability, sync, speech, pings off. - Ephemeral by design. Closing the last window wipes the browser session and quits;
before-quitlocks the vault.
2. Module map
| Module | Files | Pure / Electron | Tests |
|---|---|---|---|
| Shared contracts | src/shared/types.ts, ipc.ts, relay-protocol.ts | pure | via consumers |
| Portable core | src/main/portable.ts | pure (electron required lazily inside resolvePortablePaths()) | tests/portable.test.ts |
| Settings | src/main/settings.ts | pure | tests/settings.test.ts |
| Vault | src/main/vault/index.ts | pure (node:crypto) | tests/vault.test.ts |
| Report | src/main/report/index.ts | pure | tests/report.test.ts |
| Browser | src/main/browser/{index,policy,blocklist}.ts | policy, blocklist pure; index Electron | tests/browser.*.test.ts |
| Scanner | src/main/scanner/{collectors,parsers,rules,score,remediation,exec} | rules/score/parsers pure; collectors exec OS tools | tests/scanner.*.test.ts |
| Site scanner | src/main/sitescan/{target,catalog,net,intel,analyze,rules,index}.ts | target/catalog/intel-parsers/analyze/rules pure; net/index use the browser session + node:tls/net | tests/sitescan.*.test.ts, tests/security.sitescan.test.ts |
| Site report | src/main/report/site.ts | pure (reuses report escaping/CSS) | tests/sitescan.report.test.ts |
| Network status | src/main/netstatus.ts | Electron (session-bound egress probe) | e2e |
| Chat | src/main/chat/{crypto,index,lru,relayClient}.ts | pure + ws | tests/chat.*.test.ts |
| Relay | relay/server.ts | Node + ws | tests/relay.test.ts |
| Preload | src/preload/index.ts | Electron | smoke, e2e |
| Renderer | src/renderer/ | DOM | tests/renderer.*.test.ts |
| Main wiring | src/main/index.ts | Electron | smoke (--smoke, CI on Linux under xvfb); e2e (--e2e, src/main/e2e.ts, every feature through window.gentzy, CI on all three OSes) |
Rule of thumb enforced by review: anything with logic worth testing lives in a module that does not import electron at top level.
3. IPC contract (summary)
Channel names are constants in src/shared/ipc.ts; payload types in src/shared/types.ts. The renderer sees exactly the GentzyApi interface.
| Namespace | Renderer → main (invoke) | Main → renderer (events) |
|---|---|---|
app | hostInfo(), integrity(), panic(), openExternal(url) (https only) | — |
scan | start(mode), cancel(), last() | scan:progress, scan:done |
browser | tab CRUD, navigate, back/forward/reload/stop, setBounds, setVisible, get/setSettings, wipe | browser:stateChanged |
chat | createRoom, joinRoom(invite), leaveRoom, send, rooms, get/setSettings | chat:message, chat:roomChanged |
vault | status, create(pass), unlock(pass), lock, list, upsert(entry), remove(id) | — |
report | exportLast(opts) | — |
Fallible operations return Outcome<T> = { ok: true, value } | { ok: false, error } rather than throwing across the bridge, so error strings are controlled.
Vault specifics: passphrases cross the bridge once per unlock and are never stored; entries live in main-process memory only while unlocked; lock() zeroes the key buffer.
4. Data-at-rest map
With portable mode active, the only location GentzyCloak writes to is:
<USB>/GentzyCloak-Data/
├── browser-settings.json proxy mode, DoH provider, blocking toggles (atomic write)
├── chat-settings.json relay URL, alias (atomic write)
├── vault.gcv encrypted vault (scrypt + AES-256-GCM, 0600, atomic)
├── reports/ exported GentzyCloak-Report-*.{html,md,json} (0600)
├── downloads/ browser downloads, only if the user enables downloads
├── electron/ Electron userData + session (cache, cookies of the
│ └── session/ *UI* partition; browsing tabs use in-memory partitions)
├── tmp/ Electron temp + crash dumps (crash reporter disabled)
└── logs/ Electron logs path (app does not write logs itself)
What is never written:
- vault contents in plaintext, passphrases, room secrets, room keys, chat messages, session identities (memory only);
- browsing history / cookies / cache of the browsing tabs (in-memory partition, wiped on close and on panic);
- anything under the host user's profile (
~/Library,%APPDATA%,~/.config) when portable mode is active.
Fallback: if <root>/GentzyCloak-Data is not writable (read-only media, locked-down mount), the app uses Electron's default userData on the host and reports portableMode: false prominently on the dashboard. The user can then decide not to proceed.
panicWipe() overwrites every file under the data directory once with random bytes, unlinks it, removes emptied directories, and keeps vault.gcv (it is useless without the passphrase and the user may need it later). Symlinks are unlinked, never followed.
5. Build and integrity manifest
scripts/build.mjs (esbuild) produces:
dist/main/index.js(CJS,electronexternal),dist/preload/index.js(CJS),dist/renderer/app.js(IIFE) plus copiedindex.html,styles.css,assets/,dist/integrity.json={ root, files: { "<relpath>": "<sha256>" } }whereroot = sha256(JSON.stringify(files)).
At startup checkIntegrity(distDir) re-hashes every listed file, recomputes root, and reports { computed, expected, ok, checkedFiles }. ok requires every listed file to exist and match and the recomputed root to equal the recorded one. Files on disk that are not in the manifest are ignored — they are not loaded by the bundle, and the manifest cannot enumerate what an attacker might add; see THREAT-MODEL §4 for why this is still useful and where it stops. If the manifest is missing or corrupt, expected is null, ok is false, and computed is the root over everything found so a user can compare it with a value obtained on a trusted machine.
electron-builder.yml packs dist/** + package.json into an asar (Electron's patched fs reads inside it, so the check works in packaged builds). Targets: Windows portable (x64), macOS zip (universal), Linux AppImage (x64). Builds are not code-signed; see README limitations.
6. CI matrix
.github/workflows/ci.yml:
| Job | OS | Steps |
|---|---|---|
quality | ubuntu, macos, windows (fail-fast off) | npm ci → typecheck → lint → test → build → (Linux only) headless smoke launch under xvfb-run which asserts renderer readiness, host info, integrity ok and a browser tab open/wipe → headless end-to-end run (npm run e2e, xvfb-run on Linux) that drives browser, scanner, chat (with an in-process relay and a second peer), vault, reports and openExternal through the real preload/IPC bridge against a fresh GENTZY_DATA_DIR, and verifies the project's own GentzyCloak-Data was not touched |
package | one runner per target, main branch only, needs quality | package:win / package:mac / package:linux → upload release/* |
npm run ci locally is the same steps minus the smoke launch (typecheck, lint, test, build, e2e).
7. Cryptographic parameters (reference)
| Purpose | Primitive | Parameters |
|---|---|---|
| Vault KDF | scrypt | N = 2^17, r = 8, p = 1, 32-byte salt, maxmem 256 MiB; bounds enforced on unlock |
| Vault cipher | AES-256-GCM | 12-byte nonce (fresh per save), 16-byte tag, AAD gentzycloak-vault-v1 |
| Chat KDF | HKDF-SHA256 | salt gentzycloak/v1; infos room-id (16 B), room-key (32 B), fingerprint (5 B) |
| Chat cipher | AES-256-GCM | AAD = room id; payload 0x01 ‖ nonce ‖ ct ‖ tag |
| Chat identity | Ed25519 | per-session keypair; fromId = base32(SHA-256(pub)[0..8]) |
| Invite code | 32 random bytes | Crockford base32, 2-symbol mod-1021 checksum |
| Integrity | SHA-256 | per-file + root over canonical JSON |
All primitives are Node's built-in crypto; no third-party crypto libraries.
Security policy
GentzyCloak is a security tool used by people who may be at real risk. We treat every report seriously and we prefer an awkward disclosure over a silent bug.
Supported versions
Only the latest tagged release and the main branch receive fixes. Portable builds are not auto-updated; users must download a new build and re-verify its integrity hash (see docs/USER-GUIDE.md, "Verifying a build").
Reporting a vulnerability
Please do not open a public GitHub issue for anything that could be exploited before a fix ships.
- Open a private security advisory on the repository (GitHub → Security → "Report a vulnerability"), or
- If GitHub is unavailable to you, e-mail the maintainer address listed on the LordMoritz GitHub profile with the subject
GentzyCloak security. If you need to send sensitive details, ask for a PGP key first and we will reply with a fingerprint you can verify through a second channel.
Include, where you can:
- affected version / commit and platform (macOS, Windows, Linux),
- a minimal reproduction or proof of concept,
- your assessment of impact (what an attacker gains, which asset in
docs/THREAT-MODEL.mdis affected), - whether the issue is already public.
What to expect
| Step | Target time |
|---|---|
| Acknowledgement | within 3 working days |
| Triage + severity agreed | within 10 working days |
| Fix or mitigation published | depends on severity; critical issues are prioritised over all feature work |
| Public credit (if wanted) | in the release notes |
We do not run a bug bounty. We will credit you by name or handle unless you ask us not to.
Scope
In scope:
- the Electron application (
src/), its IPC surface and preload bridge, - the vault format and its cryptography (
src/main/vault), - the chat protocol, client crypto and the relay (
src/main/chat,relay/), - the report renderer (HTML injection through host data is a vulnerability),
- the build/integrity manifest pipeline (
scripts/build.mjs,src/main/portable.ts), - the CI workflow and release packaging.
Out of scope (documented limitations, not bugs): everything listed under "What is NOT mitigated" in docs/THREAT-MODEL.md, e.g. hardware keyloggers, screen capture by a compromised host, or kernel-level malware evading userland scanning.
Hardening expectations for contributors
- No new IPC channel without an entry in
src/shared/ipc.tsand a sender guard. - No string from the host or the user reaches HTML/Markdown output unescaped.
- No secrets written to disk outside the vault; no plaintext logging of vault contents, passphrases, invite codes or room keys.
npm run cimust be green on all three platforms before merge.
Rendered from the repository at build time · GentzyCloak 0.1.1 · commit 481b794 · Powered by LordMoritz