Security Architecture

Last updated: June 28, 2026 · applies to extension v0.0.3

This is written for whoever has to sign off on installing this extension across an org — not for a general audience. It names the exact mechanism behind each claim and tells you how to check it yourself, on the installed extension, without needing our source. Where we haven't verified something live or it has a known limitation, we say so. If a claim here doesn't hold up to your own inspection, that's a bug — security@trustevo.ai.

1. What we're claiming, precisely

Three separate guarantees, each enforced by a different mechanism:

  • Zero egress of your data — enforced by the extension's Content Security Policy (§2), not by application logic that could have a bug.
  • License/entitlement checks never touch your data — enforced by what the signing key can and cannot see (§3).
  • Nothing is persisted to disk — enforced by what the vault and audit log are built to be able to hold, not by a policy promising not to write it (§4).

Each section below: the mechanism, what it does and doesn't cover, and how to verify it on your own machine in under five minutes.

2. Zero egress — enforced by CSP, not by promise

The extension's manifest declares this Content Security Policy for every extension page (popup, options, and the offscreen document that runs the detection model):

"content_security_policy": {
  "extension_pages":
    "script-src 'self' 'wasm-unsafe-eval'; object-src 'self'; connect-src 'self' https://models.trustevo.ai"
}

connect-src is the directive Chrome uses to gate every fetch/XMLHttpRequest/WebSocket an extension page can open. It is allow-listed to two origins:

  • 'self' — same-origin chrome-extension:// resources only: the bundled detection model, tokenizer, and WASM runtime shipped inside the extension.
  • models.trustevo.ai — a CDN that serves only the ONNX model weights and ORT WASM binary (too large to fit in the Chrome Web Store's 128 MB package limit, so they're fetched once at install/update time rather than bundled). Nothing your detector reads — no prompt text, no detected values, no tokens — ever has a code path to this origin. There isn't a fetch call anywhere downstream of detection that could reach it even if it tried: the directive blocks it at the browser level regardless.

This is the load-bearing distinction: this isn't "our code chooses not to send your data." It's "Chrome will refuse the network call before it leaves the process," the same way it refuses a page script from calling an origin outside its own CSP. A bug in our detection or masking logic could leak data into a request — it cannot leak it to a destination outside this list, because the platform enforces the boundary, not us.

What this does not cover:

  • Your prompt itself still has to reach the AI provider (claude.ai, chatgpt.com, etc.) — that's the page's own request, not the extension's, and it's outside this CSP by design. What we control is what the extension rewrites that outgoing text to be before the page sends it — masked, not raw.
  • The content script that runs inside the AI site's page is governed by that page's CSP, not the extension's — this is why the on-device model runs in an offscreen document instead (MV3 extension pages get their own CSP; a content script embedded in claude.ai/chatgpt.com would inherit theirs, which blocks WASM compilation).

Verify it yourself:

  1. chrome://extensions → enable Developer mode → find PII Guardrail → click the extension's ID/path link, or just read ~/Library/Application Support/Google/Chrome/.../Extensions/<id>/.../manifest.json directly. The content_security_policy key is right there, unminified.
  2. Open DevTools → Network tab, type a prompt with a fake card number or SSN, send it, and read the actual request body sent to the AI provider — it will contain a token (e.g. a different 16-digit number), not what you typed.
  3. Open the extension's offscreen document in DevTools (chrome://extensions → Details → "Inspect views") and try fetch('https://anything-else.example') from its console — Chrome blocks it with a CSP violation, visible in the console, regardless of what our code does.

3. The license-signing key — what it can and can't see

Pro and Team plans are gated by a signed entitlement token (a standard JWT, algorithm EdDSA/Ed25519), checked entirely offline — verification never makes a network call. The extension embeds only the public half of the signing key:

-----BEGIN PUBLIC KEY-----
MCowBQYDK2VwAyEAC7c9ffu3EOptQXnMnRhK38oXQEJjrQCc383FguOUpLk=
-----END PUBLIC KEY-----

That key can verify a signature was produced by Trustevo's private key; it cannot produce new signatures, and it cannot decrypt anything (the token isn't encrypted — it's signed-but-readable, like every JWT). Verification checks the signature, issuer, audience, and expiry, and rejects the token entirely (degrading the extension to the Free tier) on any failure — there's no partial-trust path.

Why this can't become a side channel for your data:

  • The token's payload is a fixed, small shape: plan tier, which detector tiers are licensed, expiry, and — for Team seats — an org policy object (enforced tiers, a telemetry on/off flag, block-mode tiers). There is no field for prompt text, detected values, or tokens, and the verifier explicitly discards anything outside that shape.
  • License verification is a pure function over a string you already have locally (the cached token) and a public key. It does not run on, or receive, anything from the detection path. The two are architecturally disconnected — the verifier has no reference to the vault, the detector, or the page DOM.

Verify it yourself:

  1. A JWT is just base64url(header).base64url(payload).signature — paste the cached token (Options page → Account, or read it out of chrome.storage.local via the extension's console) into any JWT decoder and read the payload in plaintext. Confirm for yourself it's exactly the shape described above and nothing else.
  2. Watch the Network tab while the extension is idle (not refreshing a token): there is no outbound call at all on the detection path, license-gated or not — verification is synchronous and offline.

4. The vault — no-persist by construction, not by promise

Detected values live in a single in-memory structure (two Maps: value→token and token→value) for the life of the tab. It is not a cache with a TTL or a best-effort-cleared store — it has no method that writes anywhere but RAM. There is no save(), no chrome.storage call, no IndexedDB, no serialization path in that class at all. The only way data gets in is put(); the only way it leaves is a lookup or clear(), which empties both maps. Closing the tab or navigating away destroys the JS heap the vault lives in — there is nothing left to clean up, but the extension also explicitly wipes it on pagehide/beforeunload as a second guarantee independent of garbage collection.

The session audit log (the "N items protected" counter you see in the popup) is a separate, even narrower structure: its entry type has exactly three fields — entity type, timestamp, and source (auto/manual/file/undo). There is no value field and no token field in the type definition. This isn't redaction before display — the field doesn't exist in memory to redact. It is impossible to recover a PII value from the audit log, because the audit log never held one.

What persists (and is allowed to):

chrome.storage.local holds exactly two things across sessions: your policy configuration (which detector tiers are on, the master toggle) and, for paid seats, the signed entitlement token from §3. Neither can carry a detected value — the config schema has no field for one, and the token's shape is fixed and verified.

Verify it yourself:

  1. DevTools → Application → Storage, with the extension's background/offscreen context selected: inspect chrome.storage.local directly. You'll find config keys and (if licensed) the token — never a card number, name, or SSN you typed.
  2. Type a prompt containing fake PII, send it, then close the tab and reopen the AI site. The popup's protected-count resets to zero — there was nothing surviving to restore it from.

5. Scope and honest limitations

A document that only lists guarantees isn't credible. Here's what this architecture does not claim:

  • Detection is best-effort, not exhaustive. Structured PII (card numbers, SSNs, emails, phone numbers, IBANs) is covered by regex with tested precision floors. Contextual PII (names, job titles, organizations) relies on a small on-device model and is deliberately held to a lower accuracy bar — it will miss some. The manual "select text → protect" action exists specifically as the safety net for what auto-detection misses; it is not optional polish.
  • This protects the data path, not the endpoint. The threat model explicitly excludes a compromised OS, a compromised browser, or a keylogger — those are outside what a browser extension can defend against.
  • Team telemetry, when an admin opts in, is real but narrow. It sends an entity-type count and the AI host name — never a value, never a token. It is off by default and requires an explicit org-level opt-in.
  • This document, not source access, is the audit surface today. The codebase is not yet public. Everything claimed above is independently verifiable on the installed extension (manifest, CSP, storage, network traffic, the decoded token) without needing it to be. If your review requires a source walkthrough under NDA, email security@trustevo.ai.

Questions

If anything here doesn't match what you observe on the installed extension, that's a defect in either the product or this document — tell us: security@trustevo.ai. See also our Privacy Policy.

We use privacy-respecting analytics to understand how visitors use this site. No data is shared with AI providers and you can decline at any time.