| @@ -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 |
| @@ -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. |
| @@ -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 | ||
| @@ -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. |
| @@ -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> |
| @@ -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> |
| @@ -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); | |
| + | }); |