Velnode Browser · a product of Dignity New Zealand Limited
Operations | Version 1.6 | Last updated 2026-06-27
Document type: Operations Owner: Dignity New Zealand Limited Applies to: Velnode Browser v2.3.0 and later Last updated: 2026-06-27 Document version: 1.6 Status: Approved
This document is the operational view of the security controls in Velnode Browser, a product of Dignity New Zealand Limited. It is written for security reviewers and customer security teams evaluating Velnode's AI / voice / permissions code paths.
The legal version of the same posture is in the Privacy Policy. The architectural view is in the Architecture Overview.
This document covers Velnode Browser as distributed by Dignity New Zealand Limited and the official plugins published through the Velnode Plugin Manager. It does not cover websites you visit using Velnode, third-party AI providers you configure under your own credentials, or third-party plugins outside the Plugin Manager.
Velnode's threat model assumes:
data/*.json without further cryptographic gating.The threats we explicitly defend against:
| Threat | Where it is blocked |
|---|---|
| A malicious web page calls a Velnode privileged API | IPC allow-list; sandbox; no nodeIntegration |
| A malicious page exfiltrates a Vee prompt by triggering an unwanted call | Single AI Gateway; policy layer in main proc |
| A misbehaving plugin bypasses Kids Mode | Policy layer in main proc; Kids Mode lock |
| A misbehaving plugin reads other plugins' data | Per-plugin sandbox; permissions deny-by-default |
| State tampering by a plugin | Append-only timeline; canon hash audit |
| Credential theft via the renderer | API keys in main-proc storage; never in renderer |
| Remote code execution from a downloaded model | Models loaded by node-llama-cpp in main proc; no eval, no remote-loaded scripts |
| Vault credential exfiltration via a plugin or malicious page | vault:reveal/vault:cards:reveal IPC channels are sender-verified; plugins are denied |
| Vault file read by another OS user account | OS keystore layer; only the owning user session can unlock the keystore |
| Cross-site canvas / WebGL fingerprint linking | Farble engine injects per-origin noise into canvas and WebGL read-backs; GPU cohort strings replace real GPU identity; font-enumeration APIs return a cohorted set |
| User-Agent / client-hintsβbased browser identification on guest pages | A common Chrome identity is presented to guest pages; Velnode account and sync endpoints are excluded from this substitution |
The threats we do not defend against, and what we say about them:
Every webContents (the main window, the tab views, the internal HTML surfaces) is created with:
nodeIntegration: falsecontextIsolation: truesandbox: trueThese three flags together prevent the renderer from reaching the host OS, prevent web scripts from sharing context with the preload script, and force every privileged action to cross a sandbox boundary.
preload.js exposes a single object to the renderer: window.veloraBridge. Every method on veloraBridge corresponds to a named IPC channel handled in main.js. New features add a channel only after:
preload.js.main.js.This ordering is enforced in code review. We never add a handler without a check, and we never expose a channel without a handler.
Browser permissions (geolocation, camera, microphone, notifications, clipboard, persistent storage, etc.) ship denied. The user is prompted on first use. The decision is stored per-origin in data/settings.json.
Plugin permissions are declared in the plugin manifest and surfaced to the user before enable. A plugin cannot request a new permission at runtime; new requirements need a new release.
All AI calls originate in the renderer and are routed to the main process over a single IPC channel. The Gateway:
data/settings.json.node-llama-cpp against a model the user has explicitly downloaded.Because the policy decision is in the main process and the renderer has no provider credentials, a malicious page cannot make Vee send a prompt that bypasses the user's settings.
Navigation to non-HTTPS and non-HTTP schemes is reviewed. Unknown protocol handlers are denied by default. file:// URLs from outside the install directory are denied except via the explicit "Open file" command.
In Kids Mode, navigation is restricted to an allow-list. The allow-list is enforced in the main process; the renderer cannot circumvent it.
The Modular Runtime Core (Phase 8) enforces:
API keys are written to main-process secure storage. The renderer never sees them. They are never written to localStorage, sessionStorage, IndexedDB, or any place the renderer can read.
Plugins do not see the host's API keys. If a plugin needs its own credentials, it requests them from the user and stores them in its own scope under the same main-process secure-storage interface.
The Password Vault applies a two-layer encryption model.
| Layer | Mechanism | What it protects |
|---|---|---|
| OS keystore | Windows Credential Manager / macOS Keychain / Linux secret service | The vault file at rest against other OS user accounts |
| Master passphrase | AES-256-GCM + async scrypt (key derivation, off-main-thread + TOCTOU re-check since v1.30.1) + RSA-OAEP (RSA-4096, new vaults) | Passwords, card numbers, and other sensitive fields inside the vault |
Zero-knowledge design. The master passphrase and the 24-word BIP39 recovery phrase are never stored or transmitted. Dignity New Zealand Limited has no copy of either. A user who loses both credentials cannot recover vault contents.
Recovery phrase. The recovery phrase is generated locally from a BIP39 word list and shown once during setup. Either the master passphrase or the recovery phrase can decrypt the vault. The recovery phrase is generated only when the user explicitly requests it via Settings β Vault β Recovery phrase.
Portable export. The .velora-vault export file is encrypted under the same keys and is safe to copy to external media. The file is opaque without the correct credential; it contains no metadata that reveals its contents.
Automatic snapshots. Automatic local snapshots are encrypted using the same mechanism as the primary vault file. They are stored in a user-chosen local directory only. They are not transmitted anywhere.
IPC guard. The IPC channels that return plaintext vault credentials (vault:reveal, vault:cards:reveal) are sender-verified. They return data only to the internal vault page; plugin-originated calls are denied.
Threat model note. The vault defends against an attacker who has a copy of the encrypted vault file but does not have the user's master passphrase or recovery phrase. It does not defend against an attacker who has already compromised the user's OS session with the same user-account permissions, because that attacker can invoke the OS keystore on the user's behalf. OS-level device security (full-disk encryption, screen lock) is the recommended control for that threat.
This section documents the cryptographic controls for the optional Velnode Account service, reviewed and approved by the velora-crypto-reviewer (verdict: SHIP-WITH-FIXES; all fixes applied).
The account login password is never transmitted or stored. Two independent values are derived from it using scrypt, each with its own 16-byte random salt and a domain-separation label:
| Derivation | Label | Destination |
|---|---|---|
KEK = scrypt(password, "velora-account-v1-kek" β kek_salt, N=2^17, r=8, p=1) β 32 B |
Key Encryption Key | Never leaves the device |
auth_hash = scrypt(password, "velora-account-v1-auth" β auth_salt, N=2^17, r=8, p=1) β 32 B |
Auth credential | Sent to the server |
N=2^17 (~128 MB, ~125 ms per derivation) is the minimum. A brief "Securing your accountβ¦" indicator is shown during the double derivation.
A random 32-byte data_key is generated client-side using crypto.randomBytes(32). It is wrapped with AES-256-GCM using the KEK:
blob = { v: 1, nonce: base64(12-byte random), ct: base64, tag: base64(16-byte) }
AAD = JSON({ v: 1, accountId, purpose: "account-data-key" })
The opaque blob is stored in D1. The server never receives the KEK or the plaintext data_key.
A recovery_secret = crypto.randomBytes(32) is generated client-side and shown as a 24-word BIP-39 phrase (consistent with the vault's existing 24-word UX). A recovery_KEK is derived from it:
recovery_KEK = scrypt(recovery_secret, "velora-account-v1-recovery" β recovery_salt, N=2^17, r=8, p=1)
A second copy of the data_key is wrapped with recovery_KEK and stored alongside the primary blob in D1 (same AES-256-GCM shape, AAD purpose:"account-data-key-recovery"). The recovery_secret is shown once and never stored by Velnode or the server.
The server treats the received auth_hash as input to a second KDF β PBKDF2-HMAC-SHA256 (available natively in the Workers runtime) with a per-row server-generated salt β and stores only the resulting auth_verifier. Login re-runs the same derivation and compares in constant time. If D1 is ever breached, the auth_verifier rows do not directly yield the original auth_hash, which is itself a one-way scrypt of the password.
The POST /account/reset endpoint updates login only (auth_verifier, auth_verifier_salt, auth_salt). It never touches wrapped_data_key or wrapped_data_key_recovery. The wrapped blobs are replaced only by POST /account/rewrap after the user completes the Recovery-Kit re-wrap flow. Silent data loss on reset is an explicit non-goal of this design.
Signup and password-change both call lib/vault.js assessStrength. The submission is blocked client-side unless length β₯ 12 AND score β₯ 3. Enforced at the UI layer; a weak password would collapse the KEK security margin.
The unwrapped data_key is cached at rest via Electron safeStorage (consistent with sync-identity.json and the vault file). It is cleared on sign-out, on lock, and on device-session revocation. This avoids prompting the user for their password on every launch while keeping the plaintext key out of files readable by other OS user accounts.
The vault blob travels as an opaque encrypted payload through the account sync layer. The account data_key is never used to encrypt, decrypt, or wrap vault contents. Vault contents open only via the vault passphrase on the receiving device.
All VLR.ACCOUNT.* IPC channels are:
preload.js on the explicit allow-list.The account.html renderer page handles presentation only. All secrets are processed in the main process; no passphrase, KEK, or plaintext data key is ever passed to the renderer.
The velora-account Cloudflare Worker implements:
file:// origin (reflected null Origin) and an optional allowlist for the future web dashboard.Cache-Control: no-store on authenticated responses.ENUM_SALT Worker secret seeds decoy-salt HMAC at /account/prelogin to prevent account enumeration. Production deployment must set this secret.This section documents the security controls for the optional account sync layer, which ships in v1.32.0 riding the Stage 1 data_key.
Each collection's changes are encrypted client-side with the account data_key using AES-256-GCM before leaving the device. The authenticated additional data (AAD) binds {v, accountId, collection} to every ciphertext, so:
data_key.bookmarks collection is rejected if presented as a history blob.The Cloudflare Worker backend stores only the opaque ciphertext, record sizes, collection names, and timestamps. Dignity New Zealand Limited has no access to the plaintext of any synced collection.
The settings collection syncs only an explicit allowlist of user-level preferences (theme, search engine, homepage, ad-block state, reader defaults, dashboard layout, language). The following are permanently excluded:
/key|token|secret|path/i patternUnknown settings keys are excluded by default. The allowlist is an explicit constant enforced by lib/sync/settings-sync.js and is covered by unit tests; a future setting is not silently synced just because it is written to settings.json.
The vault blob travels through the sync layer as an opaque payload. The account data_key wraps it for transport (defense in depth), but the inner vault ciphertext β sealed by the vault master passphrase and the OS keystore β is never decrypted by the sync layer. On a receiving device the blob lands as passwords.json in its already-sealed state; it becomes usable only when the user enters the vault passphrase. A "vault locked on this device" prompt guides the user through that step. The account password and data_key cannot unlock the vault under any circumstances.
The session token rotates on every launch (and periodically while the app is open) by calling POST /account/refresh. The server issues a new token and a fresh 30-day expiry; the previous token is invalidated. A revoked or expired token pauses sync and surfaces a re-sign-in prompt β it never deletes local data. Account key material is cleared from RAM and from account.json on sign-out; if the user switches accounts, the current session's data_key is wiped before the new account's key is cached, so two accounts' keys never coexist in memory or on disk.
The opt-in "Always ask me to sign in on launch" toggle (off by default) prevents silent session restore for users who prefer to re-authenticate explicitly on each launch.
All account:sync:* IPC channels follow the same pattern as Stage 1 account channels:
preload.js on the explicit allow-list.The data_key and session token are processed in the main process; neither is exposed to the renderer or passed as arguments to renderer callbacks. The sync event broadcast (account-sync:event) carries only display-safe status strings and counts β never key material.
The sync endpoints on the velora-account Cloudflare Worker share the same hardening posture as the Stage 1 auth endpoints:
QUOTA (413) when exceeded.file:// origin.Velnode maintains a local tombstone log at data/sync-tombstones.json. When an item is deleted β a bookmark, a pinned command, a workspace, or a note β a deletion marker is written to this log rather than simply removing the record. The tombstone log is age-pruned (180 days, capped at 1 000 entries per collection). During compaction, current tombstones are embedded in the snapshot so that devices coming back online after a compaction still receive outstanding deletions.
Residual edge case: a device that has been offline for longer than the 180-day tombstone-retention window may not receive deletions made before the window opened. This is accepted as an inherent limitation of a bounded retention scheme and is documented here for transparency. Everyday online use is not affected.
Settings sync uses a last-writer-wins policy. The winning write is determined by a logical timestamp β an incrementing counter stored in a non-synced bookkeeping key β that advances only when a synced preference actually changes. File-system modification-time is not used because no-op saves and filesystem clock skew can produce spurious overwrites under an mtime-based scheme. A serialized write-queue protects the applyBlob path and the main user-facing settings writers from concurrent updates overwriting each other.
When a user deletes their account, all sync data associated with that account is purged from the server in the same transaction: all rows in sync_records, sync_snapshots, sync_blobs, and sync_meta are deleted alongside the account row, sessions, and email tokens. No orphaned sync data remains on the server after an account deletion. The purge covers all collections: bookmarks, history, notes, workspaces, pinned commands, settings, and vault blobs.
History sync preserves the timeline_append_only canon invariant. Incoming history entries are merged by union: an entry is appended only if it is absent (deduplicated by entry ID, or by type + URL + timestamp). Remote entries are never used to delete or overwrite local entries. If local recording is paused (Network Privacy on), no new entries are captured and therefore nothing is pushed; incoming remote entries from other devices still apply.
Browser fingerprinting allows third-party websites and analytics networks to build a stable identifier for a browser by combining signals like canvas rendering, GPU identity strings, available fonts, and HTTP request headers β without using cookies. Velnode ships a farble engine that addresses the highest-entropy vectors.
| Context | Behaviour |
|---|---|
| Incognito (Enhanced) windows | Always protected, regardless of user settings |
| Tor windows | Always protected |
| Normal windows | Opt-in via Settings β Privacy β Fingerprint Protection (off by default) |
| When Network Privacy is active | Activates automatically in normal windows |
A per-site allowlist (Settings β Privacy β Fingerprint Protection β Manage exceptions) lets users disable protection for specific sites.
| Vector | Mechanism | Files |
|---|---|---|
| Canvas read-back | Per-origin deterministic noise seed, injected at the getImageData / toDataURL boundary |
lib/farble.js |
WebGL readPixels read-back |
Same per-origin noise seed applied to WebGL pixel read-back | lib/farble.js |
| WebGL GPU identity strings | UNMASKED_VENDOR_WEBGL / UNMASKED_RENDERER_WEBGL overridden with a hardware cohort string |
lib/fingerprint-cohort.js |
| Audio context | Per-origin noise on getChannelData / copyFromChannel |
lib/farble.js |
| Font enumeration APIs | queryLocalFonts() and document.fonts.check() return a stable cohorted font set |
lib/fingerprint-cohort.js |
| User-Agent | Guest pages receive a common Chrome UA string; Velnode/Electron UA is suppressed | lib/fingerprint-cohort.js |
| Sec-CH-UA client hints | Harmonised to match the common Chrome identity on guest pages | lib/fingerprint-cohort.js |
| navigator.userAgentData | Returns cohorted brand list and platform string consistent with the UA | lib/fingerprint-cohort.js |
The following vectors are not addressed in this release:
| Residual | Reason not addressed |
|---|---|
| WebGL capability parameters (extension list, max texture size, precision values) | Hardware-specific; cohortion without breaking WebGL-heavy pages requires a per-GPU compatibility study. The rendered-image vector is addressed; this residual is lower-entropy. |
Metric-based font enumeration (measureText, offsetWidth layout-engine metrics) |
Cannot be spoofed without altering page layout. API-based enumeration (queryLocalFonts, document.fonts.check) is addressed. |
This posture is consistent with Brave's privacy browser implementation. Protection reduces the practical cross-site linkability of the rendered-image fingerprint. It is not full anonymity and is not represented as such in any Velnode documentation.
The User-Agent harmonisation applies only to guest (third-party) page loads. Velnode's own account, sync, and update calls are never subject to UA substitution β they use the real Velnode UA to avoid compatibility breakage with Dignity New Zealand Limited's own infrastructure.
State lives in plain JSON files under data/. timeline.json is append-only by design. The canon_manifest.json records SHA-256 hashes for tracked files; an automated hash audit checks the manifest before and after edits. A drift between an entry's hash and a file's actual hash blocks the release until the drift is reviewed.
architecture_flags in the canon manifest record invariants that cannot regress:
echo_only_architectureaskvee_removedai_single_gateway_enabledai_local_only_enforced_main_levelai_policy_layer_enabledtimeline_append_onlyfile_backed_single_source_of_truthpermissions_deny_by_defaultsafe_kids_mode_enabledA release that flipped any of these to false would fail pre-release checks.
Runtime dependencies are pinned in package.json. Build-time dependencies are pinned in the same file. New dependencies require an explicit review. The Velnode installer is built from the locked dependency tree; no npm install happens at user time.
Models for the optional local LLM brain and the optional wake-word are downloaded by the user from a fixed URL the first time the user opts in. The download is hash-checked against a manifest published alongside the release.
Security disclosures go to [email protected] with the subject Security disclosure. We do not pursue legal action against good-faith researchers who follow the responsible-disclosure path in the Acceptable Use Policy Β§5. The Incident Response procedure describes what happens next.
This document is reviewed every Velnode major release and on demand if a control changes. Material changes are called out in the release notes for the version that introduces them.
Dignity New Zealand Limited
Attention: Security
Email: [email protected]
Subject line: "Security disclosure"
Document maintained by Dignity New Zealand Limited. Questions: [email protected].