Zion Boggan zionboggan.com ↗

Outlook: integration scaffold (manifest, task pane, design doc)

New integrations/outlook/ directory with the Office add-in 1.1
manifest (MailApp, read-mode, ReadItem only), task-pane HTML, and JS
that imports parseSealed / verifyManifestSignature / decryptSealed
directly from oversightprotocol.dev/viewer/ rather than reimplementing
crypto. Both classic and hybrid suites work. Architecture decisions
recorded in docs/OUTLOOK.md.

Roadmap and CHANGELOG updated to reflect scaffold-landed state.
Icons, tenant load-test, and manifest hosting deploy still pending.
e7e3320   Zion Boggan committed on May 7, 2026 (1 month ago)
CHANGELOG.md +8 -0
@@ -2,6 +2,14 @@
## Unreleased
+- **Outlook add-in scaffold landed (2026-05-07).** New `integrations/outlook/`
+ with the Office add-in 1.1 manifest (`MailApp`, read-mode task pane,
+ `ReadItem` only), task-pane HTML, and JS that imports the public viewer's
+ `parseSealed`, `verifyManifestSignature`, and `decryptSealed` directly
+ from `oversightprotocol.dev/viewer/...` rather than reimplementing crypto.
+ Decrypts both classic and hybrid suites. Architecture decision recorded in
+ `docs/OUTLOOK.md`. Status: scaffold; not yet load-tested in an Outlook
+ tenant. Icons (64/128 px) still pending.
- **Browser inspector: hybrid (post-quantum) decrypt shipped (2026-05-03).**
The viewer at `oversight-protocol.github.io/oversight/viewer/` now decrypts
`OSGT-HYBRID-v1` sealed files end-to-end, in addition to the
docs/OUTLOOK.md +119 -0
@@ -0,0 +1,119 @@
+# Outlook add-in design
+
+## Scope
+
+The Outlook add-in is a thin wrapper around the same parse / verify / decrypt
+pipeline that runs in the public browser inspector. It does not introduce a
+second crypto stack, a second container parser, or a second canonicalization.
+Where the inspector handles a `.sealed` file dropped onto a web page, the
+add-in handles a `.sealed` attachment on an open Outlook message.
+
+The MVP is read-only: a recipient who receives a sealed attachment by email
+can verify the issuer signature, read the manifest, and (optionally) decrypt
+the payload using their identity, all inside the Outlook task pane. Sealing
+new files from inside Outlook is a follow-up milestone, gated on a separate
+identity-key flow.
+
+## Architecture
+
+```
+Outlook task pane (HTML/JS)
+ |
+ +-- Office.js: get current message, enumerate attachments,
+ | fetch the .sealed attachment as base64.
+ |
+ +-- import { parseSealed, verifyManifestSignature, decryptSealed }
+ | from '/viewer/viewer.js'
+ +-- import { xchacha20poly1305 } from '/viewer/vendor/...'
+ +-- import { ml_kem768 } from '/viewer/vendor/...'
+ |
+ +-- Render the same kind of summary the inspector shows:
+ suite, signature status, recipient id, content hash,
+ decrypt panel for both classic and hybrid suites.
+```
+
+The task pane is a static HTML page hosted under the same origin as the
+public inspector, so the imports above are relative-path imports against the
+already-vendored modules. Nothing is duplicated.
+
+## Manifest
+
+`integrations/outlook/manifest.xml` is an Office add-in 1.1 manifest of type
+`MailApp`. It declares:
+
+- `Hosts`: `Mailbox`
+- `Requirements`: `Mailbox >= 1.5` (modern enough for `getAttachmentContentAsync`)
+- `Permissions`: `ReadItem`
+- A read-mode form with `SourceLocation` pointing to the hosted task pane
+ URL, currently `https://oversightprotocol.dev/integrations/outlook/taskpane.html`
+- A `Rule` that activates the add-in on items that have any attachment
+
+The `<Id>` GUID is `ee9beb3a-64a6-4656-b3f9-a8d0ad8c409c`. This is the
+stable identity of the add-in across versions; do not change it without
+also coordinating an AppSource update if the add-in ever ships there.
+
+## Hosting
+
+The task pane HTML, JS, and the existing viewer modules all live on
+`gh-pages`, served at `oversightprotocol.dev`. The path layout is:
+
+- `oversightprotocol.dev/integrations/outlook/taskpane.html`
+- `oversightprotocol.dev/integrations/outlook/taskpane.js`
+- `oversightprotocol.dev/viewer/viewer.js` (already deployed)
+- `oversightprotocol.dev/viewer/vendor/...` (already deployed)
+
+Same-origin imports keep the security model simple: the task pane is treated
+as one site by the browser, and Office's add-in sandbox enforces the rest.
+
+## Distribution
+
+For a pilot the manifest is sideloaded:
+
+- **Outlook on the web**: `Get Add-ins > My add-ins > Add a custom add-in
+ from URL/file`, point at the hosted manifest.
+- **Outlook desktop**: same dialog from the ribbon.
+- **Tenant-wide**: an admin uploads the manifest in the Microsoft 365 admin
+ centre and assigns it to a user group.
+
+AppSource publication is out of scope for the MVP. It requires a Partner
+Center account, validation submission, and review, none of which is on the
+critical path for the first regulated-industry deployment.
+
+## Identity model
+
+The recipient pastes or uploads their `identity.json` into the task pane,
+exactly the same shape the public inspector accepts. Hybrid identities
+include `mlkem_priv` and `mlkem_pub` alongside `x25519_priv` and
+`x25519_pub`. The identity stays in task-pane memory only; nothing is
+persisted to Outlook storage and nothing is sent to a server.
+
+This is deliberately the same UX as the public inspector. Recipients who
+have already used the inspector will recognize the flow.
+
+## What is intentionally not in the MVP
+
+- **Sealing from Outlook**: requires an issuer key on the user's machine
+ and a separate key-management story. Treat as v2.
+- **Auto-attribution on leak**: the attribute pipeline runs server-side
+ against the registry; not appropriate for an end-user task pane.
+- **Compose-mode rules**: would let the add-in inject metadata into
+ outgoing mail. Out of scope until a customer asks.
+- **Persistent identity storage**: until a hardware-key path is wired up
+ (see `docs/HARDWARE_KEYS.md`), persisting private keys in Office storage
+ is a regression versus the inspector's "memory only" guarantee.
+
+## Security caveats
+
+The add-in inherits the inspector's caveats. In particular:
+
+- The browser's WebCrypto + the vendored noble libraries are the only
+ crypto. Office.js is not used for any cryptographic operation.
+- The add-in trusts the page's same-origin scripts. Anyone who can ship
+ a malicious update to the task pane HTML/JS can subvert decryption.
+ The mitigation is the same as for the public inspector: vendor pinning
+ with SHA-256 fingerprints in `viewer/vendor/README.md`.
+- Outlook's message body and attachment metadata pass through Microsoft's
+ servers as a normal part of email transport. The sealed bundle is
+ end-to-end encrypted to the recipient's keys, but envelope metadata
+ (sender, subject line, attachment filenames) is visible to the email
+ provider as for any other message.
docs/ROADMAP.md +13 -5
@@ -46,7 +46,8 @@ threat-model honesty, not on a calendar date.
2. Browser inspector and drag-drop share workflow. **Shipped** -
inspector, classic-suite decrypt, and hybrid (post-quantum) decrypt
are all live.
-3. Outlook add-in. **Next up.**
+3. Outlook add-in. **Scaffold landed 2026-05-07** (`integrations/outlook/`,
+ `docs/OUTLOOK.md`); pilot in an Outlook tenant pending.
4. One regulated-industry design-partner deployment.
5. SOC 2 Type 1 scoping in parallel with the design partner.
6. Broad public launch (HN, Reddit, conferences). Not before the inspector,
@@ -190,10 +191,17 @@ place.
### Outlook add-in
-Microsoft add-in manifest, JS SDK surface, hosted manifest URL, and a
-pilot with one tenant. The manifest advertises seal and open against
-the user's configured registry URL. Browser inspector code already
-handles the open path; the add-in is primarily integration and UX work.
+**Scaffold landed 2026-05-07.** `integrations/outlook/` ships the Office
+add-in 1.1 manifest (`MailApp`, read-mode task pane, `ReadItem` only),
+the task-pane HTML, and JS that imports the public viewer's parse /
+verify / decrypt directly from `oversightprotocol.dev/viewer/`. No
+second crypto stack. Both classic and hybrid suites decrypt. Decision
+record at `docs/OUTLOOK.md`.
+
+Remaining for a real pilot: 64 px / 128 px icons in
+`integrations/outlook/assets/`, an Outlook tenant load-test, and the
+manifest hosting deploy under `oversightprotocol.dev/integrations/outlook/`.
+Sealing-from-Outlook (compose mode) is intentionally deferred to v2.
### Hardware `KeyProvider` in Rust
integrations/outlook/README.md +76 -0
@@ -0,0 +1,76 @@
+# Oversight Inspector for Outlook
+
+Read-mode Outlook task pane that verifies and decrypts `.sealed` attachments
+using the same parse/verify/decrypt pipeline as the public web inspector at
+<https://oversightprotocol.dev/viewer/>. No second crypto stack, no second
+container parser, no telemetry.
+
+Status: **scaffold**. The manifest, task pane HTML, and JS are wired up but
+nothing has been load-tested inside an Outlook tenant yet. The architecture
+decisions are recorded in [`docs/OUTLOOK.md`](../../docs/OUTLOOK.md).
+
+## Files
+
+| File | Purpose |
+|---|---|
+| `manifest.xml` | Office add-in 1.1 manifest, `MailApp` type, read-mode task pane |
+| `taskpane.html` | UI shell: status badge, attachment picker, manifest summary, decrypt panel |
+| `taskpane.js` | Office.js + viewer-module integration; reuses `parseSealed`, `verifyManifestSignature`, `decryptSealed` |
+| `assets/` | Icons referenced by `manifest.xml` (64 px, 128 px). Placeholders pending design. |
+
+## Hosting
+
+The task pane and its imports must be served over HTTPS from the URL declared
+in `manifest.xml` (`SourceLocation`). Production target is
+`https://oversightprotocol.dev/integrations/outlook/`, which lives under
+`gh-pages` next to `viewer/`.
+
+To deploy: copy this directory's contents into `P:\Oversight\site\integrations\outlook\`
+and push `gh-pages` (the standard site deploy step). Same-origin imports of
+`/viewer/viewer.js` and the vendored noble bundles work automatically once
+both paths are on the same host.
+
+## Sideload (developer)
+
+1. Build a local manifest with `SourceLocation` pointing at your dev URL
+ (e.g., `https://localhost:3000/integrations/outlook/taskpane.html` if you
+ are serving locally). Outlook requires HTTPS even for localhost; use
+ `office-addin-dev-certs` or your own self-signed pair.
+2. **Outlook on the web**: open any message > the More (`...`) menu >
+ `Get Add-ins` > `My add-ins` > `Add a custom add-in` > `Add from file...`
+ and pick your local `manifest.xml`.
+3. **Outlook desktop**: Home tab > `Get Add-ins` > same path.
+4. Open a message that has a `.sealed` or `.oversight` attachment. The task
+ pane will offer to load and verify it.
+
+## Tenant install
+
+For a pilot deployment a Microsoft 365 admin uploads `manifest.xml` in the
+admin centre under `Integrated apps > Upload custom apps > Office Add-in >
+Provide link to the manifest file` (or by uploading the XML directly). The
+admin assigns the add-in to a user group and Outlook surfaces it on the
+ribbon for those users.
+
+## Permissions
+
+`ReadItem` is the only requested scope. The add-in does not modify the
+message, send anything from the user's mailbox, or access any folders other
+than the open message. Decryption keys come from the user's pasted
+`identity.json` and stay in task-pane memory for the lifetime of that
+message view.
+
+## What's missing for a real pilot
+
+- [ ] Icons in `assets/` (64 px and 128 px PNG, transparent background).
+- [ ] A short demo video or screenshots for the AppSource listing once we
+ decide AppSource is in scope.
+- [ ] End-to-end test inside an Outlook dev tenant against a hybrid `.sealed`
+ attachment.
+- [ ] Decision: do we accept the `.oversight` extension Codex is shipping on
+ the mobile side as a synonym for `.sealed`? The activation rule already
+ covers any attachment, so this only affects the task pane's filename
+ filter.
+- [ ] Localization beyond `en-US` once a customer asks.
+
+Sealing-from-Outlook (compose mode) is intentionally out of scope for v1; see
+`docs/OUTLOOK.md` for the rationale.
integrations/outlook/manifest.xml +63 -0
@@ -0,0 +1,63 @@
+<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
+<OfficeApp
+ xmlns="http://schemas.microsoft.com/office/appforoffice/1.1"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xmlns:bt="http://schemas.microsoft.com/office/officeappbasictypes/1.0"
+ xsi:type="MailApp">
+
+ <!-- Stable identity. Do not regenerate; it is what AppSource and the
+ Microsoft 365 admin center key updates against. -->
+ <Id>ee9beb3a-64a6-4656-b3f9-a8d0ad8c409c</Id>
+ <Version>0.1.0</Version>
+ <ProviderName>Oversight Protocol</ProviderName>
+ <DefaultLocale>en-US</DefaultLocale>
+
+ <DisplayName DefaultValue="Oversight Inspector"/>
+ <Description DefaultValue="Verify Oversight .sealed attachments and decrypt them in the task pane. Private keys stay in memory; no content is sent to a server."/>
+
+ <IconUrl DefaultValue="https://oversightprotocol.dev/integrations/outlook/assets/icon-64.png"/>
+ <HighResolutionIconUrl DefaultValue="https://oversightprotocol.dev/integrations/outlook/assets/icon-128.png"/>
+
+ <SupportUrl DefaultValue="https://oversightprotocol.dev/about.html"/>
+
+ <AppDomains>
+ <AppDomain>https://oversightprotocol.dev</AppDomain>
+ </AppDomains>
+
+ <Hosts>
+ <Host Name="Mailbox"/>
+ </Hosts>
+
+ <Requirements>
+ <Sets>
+ <!-- 1.5 covers getAttachmentContentAsync across all modern Outlook
+ clients. Bump if we adopt newer item APIs. -->
+ <Set Name="Mailbox" MinVersion="1.5"/>
+ </Sets>
+ </Requirements>
+
+ <FormSettings>
+ <Form xsi:type="ItemRead">
+ <DesktopSettings>
+ <SourceLocation DefaultValue="https://oversightprotocol.dev/integrations/outlook/taskpane.html"/>
+ <RequestedHeight>360</RequestedHeight>
+ </DesktopSettings>
+ </Form>
+ </FormSettings>
+
+ <!-- ReadItem is the minimum scope to enumerate attachments and call
+ getAttachmentContentAsync. Do not request ReadWriteMailbox until the
+ seal-from-Outlook v2 flow lands; over-permissioning slows tenant
+ admin reviews and weakens the privacy story. -->
+ <Permissions>ReadItem</Permissions>
+
+ <!-- Activate on read-mode messages that have any attachment. The task
+ pane filters down to .sealed (and .oversight) attachments client-side
+ so messages with unrelated attachments don't get a misleading button. -->
+ <Rule xsi:type="RuleCollection" Mode="Or">
+ <Rule xsi:type="ItemHasAttachment"/>
+ </Rule>
+
+ <DisableEntityHighlighting>false</DisableEntityHighlighting>
+
+</OfficeApp>
integrations/outlook/taskpane.html +66 -0
@@ -0,0 +1,66 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="utf-8">
+ <meta http-equiv="X-UA-Compatible" content="IE=edge">
+ <meta name="viewport" content="width=device-width, initial-scale=1">
+ <title>Oversight Inspector</title>
+
+ <!-- Office.js: required by Outlook to host the task pane. Loaded from
+ Microsoft's CDN per Microsoft's guidance; it is not vendored. -->
+ <script src="https://appsforoffice.microsoft.com/lib/1.1/hosted/office.js"></script>
+
+ <link rel="stylesheet" href="https://oversightprotocol.dev/css/style.css">
+ <style>
+ body { font-family: 'Inter', system-ui, sans-serif; padding: 12px; font-size: 14px; }
+ h1 { font-size: 16px; margin: 0 0 8px; }
+ .badge { display:inline-block; padding:2px 8px; border-radius:4px; font-size:12px; font-weight:600; }
+ .badge.ok { background: rgba(60,180,100,0.15); color:#2ea870; }
+ .badge.bad { background: rgba(220,80,80,0.15); color:#cc4444; }
+ .badge.wait { background: rgba(150,150,150,0.15); color:#666; }
+ .row { margin: 8px 0; }
+ .kv { display:grid; grid-template-columns: 110px 1fr; gap:4px 8px; font-size:12px; }
+ .kv span:nth-child(odd) { color:#666; }
+ .kv code { word-break: break-all; }
+ button { padding:6px 12px; border-radius:4px; border:1px solid #ccc; background:#fafafa; cursor:pointer; }
+ button:disabled { opacity:0.5; cursor:not-allowed; }
+ textarea { width:100%; min-height:60px; font-family: monospace; font-size:11px; }
+ .err { color:#cc4444; font-size:12px; margin-top:8px; }
+ .info { color:#666; font-size:12px; margin-top:8px; }
+ </style>
+</head>
+<body>
+ <h1>Oversight Inspector</h1>
+
+ <div id="status" class="badge wait">waiting for message</div>
+
+ <div class="row" id="attachment-row" style="display:none;">
+ <label for="attachment-select"><strong>Sealed attachment:</strong></label>
+ <select id="attachment-select" style="width:100%;margin-top:4px;"></select>
+ <button id="btn-load" style="margin-top:6px;">Load + verify</button>
+ </div>
+
+ <div class="row" id="manifest-row" style="display:none;">
+ <h2 style="font-size:14px;margin-top:12px;">Manifest</h2>
+ <div class="kv" id="manifest-kv"></div>
+ </div>
+
+ <div class="row" id="decrypt-row" style="display:none;">
+ <h2 style="font-size:14px;margin-top:12px;">Decrypt</h2>
+ <p class="info">Paste your <code>identity.json</code>. Private keys stay in this task pane and are never sent to a server.</p>
+ <textarea id="identity-text" placeholder='{"x25519_priv":"...","x25519_pub":"..."} (hybrid identities also include mlkem_priv/mlkem_pub)'></textarea>
+ <div style="margin-top:6px;">
+ <button id="btn-decrypt">Decrypt</button>
+ </div>
+ <div id="plaintext-out" style="display:none;margin-top:8px;">
+ <strong>Plaintext SHA-256 matches manifest:</strong>
+ <pre id="plaintext-preview" style="max-height:140px;overflow:auto;font-size:11px;background:#f6f6f6;padding:6px;border-radius:4px;"></pre>
+ <button id="btn-download">Download plaintext</button>
+ </div>
+ </div>
+
+ <div id="error" class="err" style="display:none;"></div>
+
+ <script type="module" src="taskpane.js"></script>
+</body>
+</html>
integrations/outlook/taskpane.js +207 -0
@@ -0,0 +1,207 @@
+// Oversight Inspector for Outlook - task pane logic.
+//
+// Reuses the public viewer's parse/verify/decrypt pipeline so there is no
+// second crypto path. Office.js is used only for attachment access; all
+// cryptography happens against the vendored noble libraries the public
+// inspector already ships.
+//
+// Architecture decision: see ../../docs/OUTLOOK.md.
+
+import { parseSealed, verifyManifestSignature, decryptSealed } from 'https://oversightprotocol.dev/viewer/viewer.js';
+import { xchacha20poly1305 } from 'https://oversightprotocol.dev/viewer/vendor/noble-ciphers-chacha-1.3.0.js';
+import { ml_kem768 } from 'https://oversightprotocol.dev/viewer/vendor/noble-post-quantum-ml-kem-0.6.1.js';
+
+const SEAL_EXTS = ['.sealed', '.oversight'];
+
+let parsed = null;
+let plaintext = null;
+
+function setStatus(text, kind) {
+ const el = document.getElementById('status');
+ el.textContent = text;
+ el.className = 'badge ' + (kind || 'wait');
+}
+
+function setError(msg) {
+ const el = document.getElementById('error');
+ if (msg) {
+ el.textContent = msg;
+ el.style.display = 'block';
+ } else {
+ el.style.display = 'none';
+ }
+}
+
+function show(id, on) {
+ document.getElementById(id).style.display = on ? '' : 'none';
+}
+
+function isSealedName(name) {
+ const n = (name || '').toLowerCase();
+ return SEAL_EXTS.some(ext => n.endsWith(ext));
+}
+
+function base64ToBytes(b64) {
+ const bin = atob(b64);
+ const out = new Uint8Array(bin.length);
+ for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i);
+ return out;
+}
+
+function populateAttachmentSelect(attachments) {
+ const sel = document.getElementById('attachment-select');
+ sel.innerHTML = '';
+ for (const att of attachments) {
+ const opt = document.createElement('option');
+ opt.value = att.id;
+ opt.textContent = `${att.name} (${att.size} bytes)`;
+ opt.dataset.name = att.name;
+ sel.appendChild(opt);
+ }
+}
+
+function renderManifest(manifest, sigOk) {
+ const kv = document.getElementById('manifest-kv');
+ kv.innerHTML = '';
+ const rows = [
+ ['suite', manifest.suite || ''],
+ ['issuer_id', manifest.issuer_id || ''],
+ ['recipient', (manifest.recipient && manifest.recipient.id) || ''],
+ ['content_type', manifest.content_type || ''],
+ ['content_hash', manifest.content_hash || ''],
+ ['signature', sigOk ? 'verified' : 'INVALID'],
+ ];
+ for (const [k, v] of rows) {
+ const ks = document.createElement('span'); ks.textContent = k;
+ const vs = document.createElement('span');
+ const code = document.createElement('code'); code.textContent = v;
+ vs.appendChild(code);
+ kv.appendChild(ks); kv.appendChild(vs);
+ }
+}
+
+Office.onReady(info => {
+ if (info.host !== Office.HostType.Outlook) {
+ setStatus('not running in Outlook', 'bad');
+ setError('This task pane only runs inside Outlook.');
+ return;
+ }
+ refreshFromCurrentItem();
+
+ // Re-run when the user opens a different message in the same task pane session.
+ if (Office.context.mailbox && Office.context.mailbox.addHandlerAsync) {
+ try {
+ Office.context.mailbox.addHandlerAsync(
+ Office.EventType.ItemChanged,
+ refreshFromCurrentItem,
+ );
+ } catch (_) {
+ // Older clients don't expose ItemChanged; the task pane will simply
+ // need to be reopened on the next message.
+ }
+ }
+});
+
+function refreshFromCurrentItem() {
+ setError('');
+ show('attachment-row', false);
+ show('manifest-row', false);
+ show('decrypt-row', false);
+ show('plaintext-out', false);
+ parsed = null;
+ plaintext = null;
+
+ const item = Office.context.mailbox && Office.context.mailbox.item;
+ if (!item || !item.attachments) {
+ setStatus('no message selected', 'wait');
+ return;
+ }
+ const sealed = (item.attachments || []).filter(a => isSealedName(a.name));
+ if (sealed.length === 0) {
+ setStatus('no .sealed attachment on this message', 'wait');
+ return;
+ }
+ setStatus(`${sealed.length} sealed attachment(s) found`, 'ok');
+ populateAttachmentSelect(sealed);
+ show('attachment-row', true);
+}
+
+document.getElementById('btn-load').addEventListener('click', () => {
+ setError('');
+ const sel = document.getElementById('attachment-select');
+ const attId = sel.value;
+ if (!attId) return;
+
+ const item = Office.context.mailbox.item;
+ item.getAttachmentContentAsync(attId, { asyncContext: null }, async (result) => {
+ if (result.status !== Office.AsyncResultStatus.Succeeded) {
+ setError('Outlook refused to provide the attachment: ' + (result.error && result.error.message));
+ return;
+ }
+ const fmt = result.value && result.value.format;
+ const data = result.value && result.value.content;
+ if (fmt !== Office.MailboxEnums.AttachmentContentFormat.Base64 || !data) {
+ setError('unexpected attachment format: ' + fmt);
+ return;
+ }
+ let bytes;
+ try {
+ bytes = base64ToBytes(data);
+ } catch (e) {
+ setError('attachment was not valid base64: ' + e.message);
+ return;
+ }
+ try {
+ parsed = parseSealed(bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength));
+ } catch (e) {
+ setError('not a valid Oversight sealed file: ' + e.message);
+ return;
+ }
+ let sig;
+ try {
+ sig = await verifyManifestSignature(parsed.manifest);
+ } catch (e) {
+ setError('signature check failed to run: ' + e.message);
+ return;
+ }
+ renderManifest(parsed.manifest, !!(sig && sig.ok));
+ show('manifest-row', true);
+ show('decrypt-row', true);
+ setStatus(sig.ok ? `signature verified (${parsed.suiteName})` : 'SIGNATURE INVALID', sig.ok ? 'ok' : 'bad');
+ });
+});
+
+document.getElementById('btn-decrypt').addEventListener('click', async () => {
+ setError('');
+ show('plaintext-out', false);
+ if (!parsed) { setError('Load a sealed attachment first.'); return; }
+ const raw = document.getElementById('identity-text').value.trim();
+ if (!raw) { setError('Paste your identity JSON.'); return; }
+ let identity;
+ try { identity = JSON.parse(raw); }
+ catch (e) { setError('identity JSON could not be parsed: ' + e.message); return; }
+
+ try {
+ plaintext = await decryptSealed(parsed, identity, xchacha20poly1305, ml_kem768);
+ } catch (e) {
+ setError('decrypt failed: ' + e.message);
+ return;
+ }
+ const text = new TextDecoder('utf-8', { fatal: false }).decode(plaintext);
+ // Show first 1 KiB as a preview; the full plaintext is downloadable below.
+ document.getElementById('plaintext-preview').textContent = text.slice(0, 1024) + (text.length > 1024 ? '\n...' : '');
+ show('plaintext-out', true);
+});
+
+document.getElementById('btn-download').addEventListener('click', () => {
+ if (!plaintext) return;
+ const blob = new Blob([plaintext], { type: 'application/octet-stream' });
+ const url = URL.createObjectURL(blob);
+ const a = document.createElement('a');
+ a.href = url;
+ const name = (parsed && parsed.manifest && parsed.manifest.filename) || 'plaintext.bin';
+ a.download = name.replace(/\.sealed$|\.oversight$/i, '') || 'plaintext.bin';
+ document.body.appendChild(a);
+ a.click();
+ setTimeout(() => { URL.revokeObjectURL(url); a.remove(); }, 0);
+});