Skip to content

Latest commit

 

History

1 Commit

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

ImageSteg

steghide in the browser. Hide a file or a message inside an image, a PCM WAV or an MP3 and reveal it again with a passphrase — entirely client side, with no upload and no server.

Live demo — 73hi.com/image-steg.html — the hosted build of this project, so you can try it before cloning anything.

ImageSteg is a TypeScript port of steghide 0.5.1 that keeps both of its embedding methods and adds two audio carriers:

carrier where the data lives one payload byte needs
BMP, PNG (and anything the browser can decode) least significant bits of pixel values, swapped by graph matching ~8 pixels
JPEG (baseline, progressive, arithmetic coded) parity of the non-zero quantised DCT coefficients 3 coefficients
PCM WAV (8/16/24/32 bit int) least significant bit of the samples, swapped the same way 16 samples
MP3 the unused tail of the audio frames 1 spare frame byte

Everything is byte compatible with the steghide command line tool on the BMP and JPEG paths: the bit layout, the passphrase seeded permutation of the sample positions and the Rijndael/CBC encryption all follow 0.5.1, so files produced by either side can be read by the other.

Why this exists

  • No upload. The cover image, the secret and the passphrase never leave the tab.
  • Real steghide, not naive LSB. Samples are grouped into vertices and a minimum weight matching decides what to change, so only about one sample per hidden value is touched and a swap keeps the value histogram of the carrier intact.
  • Real JPEG DCT steganography. Browsers expose no API for quantised DCT coefficients, so the project ships its own JPEG entropy codec (baseline, progressive and arithmetic coded input, Huffman output) instead of falling back to pixels.

Quick start

Want to see it first? Open the hosted demo — 73hi.com/image-steg.html — no install needed. To run it yourself:

git clone <this repo>
cd imagesteg
npm install          # only TypeScript, for the build step
npm run build        # src/*.ts -> dist/*.js
npm start            # http://localhost:8080

Then open http://localhost:8080/. ES modules cannot be loaded from file://, so the page has to be served — npm start runs a small zero dependency static server (scripts/serve.mjs).

npm test builds and runs the regression gate (test/roundtrip.mjs), which round-trips every carrier that works without a canvas under Node.

Using it as a library

dist/index.js is a plain ES module with no bundler involved. In the browser:

import { embed, capacityBytes, decodeImageBlob, encodeBitmap } from './dist/index.js';

const cover = await decodeImageBlob(file);                 // File or Blob
const payload = new TextEncoder().encode('meet me at noon');

const result = await embed(cover, payload, {
    passphrase: 'correct horse battery staple',
    fileName: 'note.txt',
    compression: 9,      // zlib level, 0 = off
    checksum: true,      // CRC32
    encrypt: true        // AES-256 / Rijndael-CBC, libmcrypt compatible
});

const bytes = encodeBitmap(result.image);                  // 24-bit BMP

…and to read it back:

import { extract, decodeImageBlob } from './dist/index.js';

const stego = await decodeImageBlob(file);
const data = await extract(stego, 'correct horse battery staple');
console.log(data.fileName, data.checksumOk, new TextDecoder().decode(data.data));

The same calls work under Node for the JPEG, WAV and MP3 carriers — they are pure computation. Only imageio.ts (pixel decode/encode) needs a browser canvas:

import { parseJpeg, writeJpeg, JpegCoverFile } from 'imagesteg';

const jpeg = parseJpeg(bytes);                 // Uint8Array
const cover = new JpegCoverFile(jpeg);
await embedIn(cover, payload, options);
fs.writeFileSync('stego.jpg', writeJpeg(jpeg));

Extraction from an unknown container dispatches on the file itself:

import { extractAny, coverFromBlob, extractFrom } from 'imagesteg';

const data = await extractAny(blob, passphrase);           // JPEG / BMP / PNG
const file = await coverFromBlob(blob);                    // any CoverFile

The audio carriers have their own entry points:

import { parseWav, encodeWav, embedAudio, extractAudio, audioCapacityBytes } from 'imagesteg';
import { parseMp3, embedMp3, extractMp3, mp3CapacityBytes } from 'imagesteg';

onProgress?: (ratio: number, label: string) => void is accepted by every embed and extract call, which is how the demo page drives its progress bar.

Project layout

index.html            the demo page (all element ids live here)
styles/               base.css (design tokens) + page.css + steg.css
src/
  app.ts              the page controller: DOM wiring only, no algorithm
  index.ts            the public API of the library (re-exports)
  core.ts             embed / extract on any cover, capacity estimates
  embdata.ts          steghide's EmbData header: compression, encryption, CRC32
  bitstring.ts        the bit oriented buffer the header is streamed through
  crypto.ts           MD5, CRC32, libmcrypt key derivation, Rijndael/CBC, zlib
  graph.ts            the sample graph and the RGB adjacency lists
  matching.ts         the minimum weight matching heuristic
  selector.ts         the passphrase seeded permutation of the sample positions
  sampledomain.ts     the two sample domains (RGB pixel, DCT coefficient)
  imageio.ts          24-bit BMP I/O plus browser image conversion (needs canvas)
  jpegdct.ts          baseline / progressive / arithmetic JPEG entropy codec
  jpegcover.ts        the JPEG sample access layer
  jpegarith.ts        the QM (binary arithmetic) decoder
  audioio.ts          the WAV (RIFF) container
  audiocover.ts       the PCM sample domain and its carrier
  mp3ancillary.ts     post-encoding hiding in the unused tail of MP3 frames
test/roundtrip.mjs    regression gate
scripts/serve.mjs     zero dependency static server for the demo

src/ is the source of truth; dist/ is generated. Never edit dist/ by hand.

How the embedding works

  1. The payload is compressed (zlib), encrypted (Rijndael-256 in CBC mode with libmcrypt's key derivation) and wrapped in steghide's EmbData header, optionally with a CRC32.
  2. An MD5 seeded permutation (selector.ts) spreads the resulting bits over the whole carrier, so the payload is not stored in a predictable order.
  3. Samples are grouped into vertices and a minimum weight matching (matching.ts) pairs them. For each pair the two sample values are swapped, which is what leaves the distribution of the carrier alone. Vertices that find no partner nudge one sample to the nearest value carrying the wanted embedded value.
  4. The result is written back into the same container: pixels for BMP/PNG, re-encoded DCT coefficients for JPEG, PCM samples for WAV, frame padding for MP3.

Compatibility notes

  • steghide extract -sf image.bmp -p passphrase and -sf image.jpg read the BMP and JPEG outputs of this tool, and this tool reads what the CLI produces.
  • The CLI does not read PNG — the PNG download exists because it is much smaller, and it is only readable here.
  • JPEG output is always Huffman coded (annex K tables), exactly like jpeg_write_coefficients(), even when the source was arithmetic coded.
  • Any re-compression destroys the payload: re-saving a JPEG, resizing, filtering, a screenshot round-trip, converting a WAV to MP3 or letting a tag editor rewrite an MP3.

Tests

npm test

test/roundtrip.mjs builds a synthetic baseline JPEG, a raw pixel array, a 16 bit PCM WAV and a chain of MPEG-1 Layer III frames and checks that:

  • the JPEG entropy codec reproduces the coefficients it wrote,
  • a payload survives embed → write → parse → extract on all four carriers,
  • the recovered file name, CRC32 and encryption flag come back intact,
  • a wrong passphrase is rejected instead of returning garbage,
  • the number of touched samples stays inside the graph matching bound.

Requirements

  • Browser: any current browser with ES modules, canvas and WebCrypto. WebCrypto needs a secure context — https:// or http://localhost.
  • Node (library and tests): 18 or newer, for crypto.subtle, CompressionStream and Blob.

License

GNU General Public License v2 or later — see LICENSE. This project is a port of steghide 0.5.1 by Stefan Hetzl and inherits its licence.

The demo page loads Font Awesome from a CDN for its icons. Nothing else is requested from the network, and no file, passphrase or payload is ever sent anywhere.

About

Your file and your secret never leave this device. Decoding, embedding, encryption and extraction all run inside this browser tab. Nothing is ever uploaded to a server — so there is nothing to leak.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages