FWC-1§8Normative

Runtime loader

The host page is a constant. The loader inside it is under 1 KB, has no dependencies, and turns a favicon into a document.

The host page

A host page MUST contain exactly one <link rel="icon"> pointing at the FWC-1 PNG, and a script that implements the decoding procedure of §7 against that link. It SHOULD contain a <noscript> explanation and nothing else that could be mistaken for content. The reference host page is generated by buildHostPage():

<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Site title</title>
<link rel="icon" type="image/png" href="/s/username/favicon.png?v=1725800000">
<script>/* loader, see below */</script>
</head>
<body>
<noscript>This page lives inside its favicon. Enable JavaScript to unpack it.</noscript>
</body>
</html>

Loader requirements

  • MUST load the icon with crossOrigin = "anonymous" so that getImageData is permitted when the PNG is served from a CDN or another origin. Hosts MUST therefore serve the PNG with Access-Control-Allow-Origin: *.
  • MUST size the canvas from naturalWidth / naturalHeight and MUST NOT scale, smooth, or otherwise resample the image before reading pixels.
  • MUST implement the mode mapping (§4) that matches the icon. A loader MAY be specialised to a single mode at generation time to save bytes — the reference encoder emits a noise-only or lsb-only loader — but MUST then be paired only with icons of that mode.
  • MUST replace the document via document.open(); document.write(html); document.close() or an equivalent that yields a fresh parsing context, so that the payload's own <html>, <head> and inline scripts behave as if the page had been served directly.
  • MUST fail silently on not-a-container (leave the host page as-is) and MUST surface bad-password to the user.

Reference loader (noise mode, unencrypted)

(async () => {
  const L = document.querySelector('link[rel~="icon"]'); if (!L) return
  const I = new Image; I.crossOrigin = 'anonymous'; I.src = L.href; await I.decode()
  const C = document.createElement('canvas'); C.width = I.naturalWidth; C.height = I.naturalHeight
  const X = C.getContext('2d', { willReadFrequently: true }); X.drawImage(I, 0, 0)
  const D = X.getImageData(0, 0, C.width, C.height).data
  const R = n => { const o = new Uint8Array(n); for (let i = 0, j = 0; j < n; i++) { if (i % 4 != 3) o[j++] = D[i] } return o }
  const H = R(12); if (H[0] != 70 || H[1] != 87) return
  const f = H[3]; const n = (H[4] << 24 | H[5] << 16 | H[6] << 8 | H[7]) >>> 0
  let b = R(12 + n).slice(12)
  if (f & 2) b = new Uint8Array(await new Response(
    new Blob([b]).stream().pipeThrough(new DecompressionStream('deflate-raw'))).arrayBuffer())
  const h = new TextDecoder().decode(b)
  document.open(); document.write(h); document.close()
})()

Minified, this is ~620 bytes; the encrypted variant adds ~400 bytes of PBKDF2/AES-GCM glue. The loader deliberately omits the CRC check to stay small — the host page trusts its own origin to serve an intact icon — whereas general-purpose decoders such as the Unpack panel MUST implement it.

Behaviour of the unpacked document

  • The unpacked document inherits the host page's origin and URL. Relative links resolve against the host URL; location is unchanged. Authors SHOULD use absolute URLs for external resources.
  • The <link rel="icon"> is replaced along with everything else. If the payload wants the tab to keep showing the same icon, it MUST include its own <link rel="icon"> — pointing at the same PNG is the idiomatic choice.
  • Search engines and social crawlers see the host page, not the payload. §9 describes the optional server-side rendering path for bots.