Velnode/ Docs / Security Posturev2.3.3-beta.15
πŸ“¦ This guide also ships inside Velnode β€” open the ☰ menu β†’ Help in the browser; it works offline.

Velnode

Security Posture

Velnode Browser  ·  a product of Dignity New Zealand Limited

Operations  |  Version 1.6  |  Last updated 2026-06-27

Velnode


Security Posture

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


Purpose

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.

Scope

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.

1. Threat model

Velnode's threat model assumes:

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:

2. Electron hardening

Every webContents (the main window, the tab views, the internal HTML surfaces) is created with:

These 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.

3. The IPC allow-list

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:

  1. The channel name is added to the allow-list in preload.js.
  2. A handler is registered in main.js.
  3. The handler runs a permission check appropriate to the action.
  4. The change goes through a dedicated Electron-security review before merge.

This ordering is enforced in code review. We never add a handler without a check, and we never expose a channel without a handler.

4. Permissions deny-by-default

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.

5. The AI Gateway and the policy layer

All AI calls originate in the renderer and are routed to the main process over a single IPC channel. The Gateway:

  1. Loads the policy snapshot from data/settings.json.
  2. Applies the policy layer:
    • Kids Mode lock β€” when on, forces Local-Only mode.
    • Local-Only mode β€” when on, refuses cloud calls regardless of configured keys.
    • Token caps β€” refuses calls that would breach the configured budget.
  3. Selects a provider:
    • Cloud β€” API key from main-process secure storage; outbound TLS from the main process.
    • Local β€” node-llama-cpp against a model the user has explicitly downloaded.
  4. Streams the response back to the renderer.

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.

6. Navigation guardrails

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.

7. Plugin runtime

The Modular Runtime Core (Phase 8) enforces:

8. Secrets handling

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.

8a. Password Vault encryption

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.

8b. Cloud Account cryptographic contract (Phase 21 Stage 1)

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).

Split-password model

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.

Account data key and wrapping

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.

Recovery Kit wrapping

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.

Server re-hash of auth_hash

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.

Forgot-password blob retention (HIGH-3 guard)

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.

Password strength floor

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.

At-rest data key caching

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.

Vault opacity invariant

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.

IPC security

All VLR.ACCOUNT.* IPC channels are:

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.

Worker hardening

The velora-account Cloudflare Worker implements:

8c. Account sync security model (Phase 21 Stage 2)

This section documents the security controls for the optional account sync layer, which ships in v1.32.0 riding the Stage 1 data_key.

Zero-knowledge ciphertext storage

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:

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.

Settings allowlist (device-bound values never transmitted)

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:

Unknown 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.

Vault opacity invariant

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.

Rolling session

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.

IPC security for sync channels

All account:sync:* IPC channels follow the same pattern as Stage 1 account channels:

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.

Worker security

The sync endpoints on the velora-account Cloudflare Worker share the same hardening posture as the Stage 1 auth endpoints:

Deletion propagation and tombstone log

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 last-writer-wins (content-gated logical timestamp)

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.

Account deletion and cascade purge

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 union-merge and the append-only invariant

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.

8d. Fingerprint Protection (v2.3.0)

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.

Activation model

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.

Protected vectors

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

Known residuals (accepted, disclosed)

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.

Scope boundary

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.

9. State integrity

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:

A release that flipped any of these to false would fail pre-release checks.

10. Supply chain

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.

11. Vulnerability disclosure

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.

12. Records and review

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.

13. Contact

Dignity New Zealand Limited Attention: Security Email: [email protected] Subject line: "Security disclosure"


Document maintained by Dignity New Zealand Limited. Questions: [email protected].