Dev log · 2026-07-07

Hide a plastron link in an image with ChaosEdgeSteg 🐢

The ChaosEdgeSteg banner from the crypt0lith project
The banner is from crypt0lith/ChaosEdgeSteg.

plastron now includes a sheetapp that hides a plastron.ca/#f=… share link in a PNG. The PNG looks the same as an ordinary image. The method is the edge-adaptive, chaos-keyed LSB steganography from crypt0lith/ChaosEdgeSteg. The upstream Python code runs without changes in the browser through Pyodide. Post the image to X. Your friend then uses the original cover and the password to recover the link.

What is ChaosEdgeSteg?

ChaosEdgeSteg is a Python tool from the crypt0lith project. It does chaos-based, edge-adaptive LSB steganography. Bits hidden in every pixel are visible in a statistical analysis. Thus ChaosEdgeSteg does not use every pixel. It does three steps:

  1. It finds the edge pixels with an adaptive Canny pass.
  2. It puts the edge pixels in an order with a Hénon chaotic map. Your password is the key for the map.
  3. It writes the payload into the least significant bits of only those locations.

The scheme is non-blind and keyed. Three items are necessary to decode:

The decode operation uses the cover to find the set of edge pixels again. It uses the password to make the Hénon order. If the cover, the image sequence, or the password is incorrect, the decode operation fails quickly. The check on the CES header then shows bad password.

The upstream project supplies a CLI:

pip install git+https://github.com/crypt0lith/ChaosEdgeSteg.git

# embed files into a cover image
chaosedgesteg embed cover.png secret.txt -O steg.png

# extract from the steg image (needs the same cover + password)
chaosedgesteg extract cover.png steg.png -O extracted.zip

The plastron integration is byte-compatible in the two directions. The desktop CLI can extract a steg PNG that the browser made. The plastron workbook can decode a steg image that the upstream CLI made.

Why ChaosEdgeSteg is on plastron

The use case is specific. You hide a https://plastron.ca/#f=<base64url> f-string link in a small PNG that you can post to Twitter/X. An f-string link is a deep link that you can share. It opens a live sketch (for example, boids or kanban). If the image stays the same after X re-encodes it, each person who has the cover and the password can recover the link. That person then clicks the link, and your sketch opens.

ChaosEdgeSteg is not a new plastron application segment. It is a sheetapp document: a workbook of positioned cells. Ordinary spreadsheet formulas and one Python source cell make all of these parts:

Open ChaosEdgeSteg from the desktop the same as any other document.

The plastron origin desktop shows the ChaosEdgeSteg icon and the icons of other applications
The 🕵️ ChaosEdgeSteg icon on the plastron origin desktop opens doc:chaosedgesteg.

How Pyodide runs the upstream code

The py compiler cel is already a part of plastron. Use it in three steps:

  1. Paste the Python code into a cell.
  2. Bind the code with =def('name', 'py', sourceCell).
  3. Call the new function from formulas.

For ChaosEdgeSteg, we put the bodies of the upstream henon.py and steg.py files into cell A21 without changes. We added a small quantity of interface code for the PNG↔BGR conversion and the upstream zip container. Cell A22 binds the code: =def('chaosedgesteg.ces', 'py', A21).

The runtime is Pyodide v0.29.4 (CPython 3.13 in WebAssembly). When you open the workbook for the first time, the browser downloads the Pyodide core and the packages that the source code imports:

The download is approximately 30 MB and comes from jsDelivr. The browser keeps it in the cache after the first run.

Pillow and numpy read and write the PNG data. OpenCV does four operations:

We did not write the chaotic-map math again. The plastron results are equal to the upstream results because the code bytes are the upstream bytes.

One small native change made this possible. The py compiler now calls pyodide.loadPackagesFromImports(source) before runPython. Thus each import line in a py cell automatically downloads its wheel. ChaosEdgeSteg is the worked example. Before this change, a cell that imported numpy failed with ModuleNotFoundError and showed no message.

The algorithm is policy in cells. There are only two native additions: the automatic package load and fs.pickToCel. (fs.pickToCel changes a file-picker upload into base64 in a named cell.) There are no new verbs for steganography.
The open ChaosEdgeSteg workbook: the worksheet is on the left, and the Decode and Embed view panes are on the right
The workbook layout. The grid of cells is on the left, and the Python source code is visible in A21. The 🔓 Decode and 🖼 Embed view panes are on the right.

How to use ChaosEdgeSteg

Figure 1 shows the full procedure. To decode the link later, three items are necessary: the original cover, the image from X, and the password. Keep the cover. Send the password to your friend through a different channel.

Embed, post to X, download, decode The embed operation uses a cover PNG, a plastron link, and a password to make a steg PNG. You post the steg PNG to X and then download the image from X. The decode operation uses the original cover, the image from X, and the password to recover the link. Cover PNG plastron link Password Embed in plastron Keep the cover Different channel Steg PNG Post X Download Original cover Image from X Password Decode in plastron plastron link
Figure 1. The decode operation uses three items: the original cover, the image from X, and the password.

1. Open ChaosEdgeSteg

  1. Go to plastron.ca.
  2. Open the origin desktop.
  3. Click 🕵️ ChaosEdgeSteg.

A workbook opens. It has two tabs on the right: 🔓 Decode and 🖼 Embed.

2. Embed a link

  1. On the 🖼 Embed pane, paste your plastron.ca/#f=… link. (As an alternative, keep the default boids link.)
  2. Select a PNG cover image. The sheet immediately does a check of the dimensions.
  3. Set a password. (The default is SECRET_PASSWORD, the same as upstream.)
  4. Wait until the embed operation is complete. (The first run loads OpenCV through Pyodide in a few seconds.)
  5. Click ⬇ Download steg PNG.
The Embed pane after the embed operation is complete, with the Download steg PNG button
After the embed operation, you can download the steg PNG. It looks the same as the cover image.

3. The ≤680px rule (necessary for X)

We did a round-trip test on the real X service. We posted seven steg PNGs to X, downloaded them, and decoded them. X did not change the PNGs with a maximum dimension ≤ 680px: the downloaded files were byte-for-byte identical. We recovered the link exactly. X recompressed the images of approximately 900px or larger as JPEG. That recompression destroyed the LSBs.

The Embed pane applies this rule. If your cover is too large, the pane shows TOO BIG: 1200×630 …. The sheet then automatically downscales the cover to ≤680px before the embed operation. If the sheet used a downscaled copy, a second download link gives the cover that the sheet used. The person who decodes must have that exact file, not the original file that was too large.

The Embed pane shows the size OK confirmation for a 680px cover
The size check passed: size OK — 680×357, ≤680px. The lossless re-encode on X will not change this cover.
The original cover PNG before steganography
Original cover
The steg PNG with the hidden link. It looks the same as the cover.
Steg PNG after the X round trip. It looks identical.

4. Post to X

  1. Upload the downloaded steg PNG to X without changes. Do not crop the image, and do not apply a filter.
  2. Later, download the image that X serves.

You do this step on X, not in plastron. The application intentionally does not download from twimg.com because of CORS.

5. Decode

  1. On the 🔓 Decode pane, upload the original cover and the image from X. The sheet automatically tries the two images in the two possible sequences.
  2. Enter the password. The recovered text appears. If the text is a URL, it appears as a clickable link.
  3. Click the link. Your boids sketch opens. If you hid a different link, that link opens.
The Decode pane shows the recovered plastron.ca link as a clickable link
The decode operation is complete. The sheet recovered each character of the hidden plastron.ca/#f=… link and shows it as a clickable link.

6. Compatibility between the tools

The upstream CLI can also extract a steg PNG that plastron made:

chaosedgesteg extract cover.png plastron-steg.png -O recovered.zip

The opposite direction is also possible. The plastron workbook can decode an image that the upstream CLI made, with the same cover and the same password. The zip container is exactly the same as the upstream format (comment b"0", member 0.bin).

How the sheetapp operates (technical details)

Figure 2 shows the embed operation. It has four steps:

  1. It changes the cover to grayscale.
  2. It finds the edge pixels with adaptive Canny. Canny uses a bilateral-filtered image. A bisection on the thresholds gives the target edge density.
  3. It puts those pixels in a Hénon-map order. The key for the map is a BLAKE2b-8 hash of the password.
  4. It writes the LSBs at those pixels: a CES magic header and a 4-byte length, then the payload bits.
The four steps of the embed operation The embed operation changes the cover PNG to grayscale. It finds the edge pixels with adaptive Canny. It puts the pixels in a Hénon-map order, and the key is a hash of the password. Then it writes the header, the length, and the payload bits into the LSBs. The result is the steg PNG. Cover PNG Change the cover to grayscale Find the edge pixels adaptive Canny, target edge density Password Put the pixels in Hénon-map order key: BLAKE2b-8 hash of the password plastron link Write bits into the LSBs CES header, 4-byte length, then the payload bits Steg PNG
Figure 2. The embed operation writes the data only into the LSBs of the edge pixels.

The decode operation has three steps:

  1. It runs Canny again on the original cover to find the same set of pixels.
  2. It reads the LSBs from the steg image.
  3. It does a check of the header.

No special code is necessary. Formulas connect the parts:

When you close the workbook, plastron flushes and evicts every chaosedgesteg.* cell. The shared Pyodide runtime stays in the cache at process level. The next py cell that uses Pyodide can then use that runtime.

Status

ChaosEdgeSteg is in plastron-examples/origin as apps/docs/chaosedgesteg.json. A capstone e2e test (e2e/chaosedgesteg.mjs) is also there. The test does three things:

🕵️ NanoSteg is a smaller, related tool. It hides data in LSBs and has no dependencies. It is available for comparison. It uses a different algorithm and is intentionally not compatible with ChaosEdgeSteg.

This sheetapp shows the "algorithm as spreadsheet policy" model of plastron:

Do not use this sheetapp as a security product. Steganography is obscurity, not encryption. The sheetapp is a demonstration for enjoyment. It is also a good method to send hidden share links through the image pipelines of social networks.

References

More from the dev log