Help us shape the future of Go! Take the 2026 Go Developer Survey

mist

package module
v1.2.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 12, 2026 License: MIT Imports: 30 Imported by: 0

README

mist logo

mist

CI Coverage Go Version License

Asymmetric-key audio steganography for Go — a library and a command-line tool.

A sender hides a message using nothing but the recipient's public key. Only the matching private key can read it back. The payload lives inside the audio itself — not in tags, comments, or container metadata — so a plain digital recording of a stream carries the message with it.

Output is Ogg Vorbis or any lossless format your FFmpeg can write: FLAC, WAV, ALAC, WavPack, TTA, AIFF, CAF and the rest. The extension of --output picks it, exactly as it does for ffmpeg.

Mist is built for learning: a place to explore steganography, hybrid cryptography, and bitstream-level audio embedding in a codebase small enough to read in an afternoon. Contributions and design discussion are very welcome.


Contents


How it works

The message is encrypted before it is hidden. Mist uses a hybrid box in the shape of age or NaCl's box: a freshly generated ephemeral X25519 key agrees a secret with the recipient's public key, HKDF-SHA256 splits that secret into an AEAD key, a position-selection key and a length mask, and ChaCha20-Poly1305 seals the payload. The sender key travels as an Elligator 2 representative, which is uniformly random bytes, because a bare X25519 key has a structure anyone can test for. Because that sender key is ephemeral and generated per message, a sender needs no long-term identity at all. An optional Ed25519 signature can be added when you do want the recipient to know who sent it.

The hiding place is the audio, and where exactly depends on the format. Mist always decodes the carrier to PCM and re-encodes it. For Ogg Vorbis it then modifies the quantized residue values inside the resulting packets; extraction is the mirror image and is far cheaper, Huffman-decoding the bitstream and stopping before the inverse MDCT, so it never reanalyses PCM. For a lossless format there is no bitstream surgery at all: a lossless encoder returns its input samples bit for bit, so the bits go straight into the PCM before encoding and are read back out of the decoded samples.

Everything else is identical either way. The values that carry bits are taken in a keyed order, never sequentially, and a syndrome-trellis code chooses which of them to change: about one value for every seven bits, preferring the changes that disturb the audio least. Every encode carries the same number of bits per value whether or not there is a real message — short payloads are padded with CSPRNG filler. Presence and absence are meant to leave the same statistical footprint. A lossless output also leaves digital silence alone, exactly as a plain encoder does.

Audio is divided into frames, and the message goes into the first one with room for it. The carrier is split into self-contained 8-second frames. A message that fits in one frame goes there and stops; a longer one spreads across as many consecutive frames as it needs, each independently sealed with its own ephemeral key. Every frame not carrying a piece of the message is filled with CSPRNG bytes at exactly the same density, so a carrying frame and an empty one are statistically indistinguishable — the padding is not decoration, it is what stops the payload's location being obvious. There is no sync marker either: both sides derive frame boundaries from the timestamps already present in the stream.

One consequence is worth knowing: a listener who joins a live broadcast after the message's frame (or, for a longer message, after its first frame) has passed recovers nothing, and a recording that loses that frame — or any one frame of a longer message's span — loses the message.

Requirements

  • Go 1.27+
  • A C compiler and system FFmpeg, discoverable via pkg-config. All codec I/O goes through libav; Mist deliberately adds no other Vorbis or Ogg library.
# macOS
brew install ffmpeg pkg-config

# Debian / Ubuntu
sudo apt-get install pkg-config libavformat-dev libavcodec-dev libavutil-dev libswresample-dev libvorbis-dev

Building with CGO_ENABLED=0 still type-checks against a pure-Go stub, which keeps editors and vet happy, but every encode and decode then reports unimplemented. Real use needs cgo.

Install

# library
go get github.com/iSerganov/mist

# command-line tool
go install github.com/iSerganov/mist/cmd/mist@latest

On macOS the repository is also a Homebrew tap. The formula builds from source against Homebrew's FFmpeg:

brew tap iSerganov/mist https://github.com/iSerganov/mist
brew install mist

The explicit URL is needed because the tap lives in this repository rather than in a homebrew-mist one. Publishing a GitHub release runs bump-formula.yml, which points url and sha256 in Formula/mist.rb at the new tag and commits to master.

Command line

mist --version prints the version. make build stamps it from git describe; a go install reports the module version, and a plain go build reports dev.

The mist binary has several commands. Colour is enabled for terminals and turned off automatically for pipes, TERM=dumb, and NO_COLOR; --no-color forces it off.

embed
mist embed --input song.ogg --data "the eagle lands at dawn"
  mist · embed

  carrier     song.ogg
  payload     23 bytes  text
  output      song.stego.ogg  vorbis · lossy, bits in the residues
  recipient   new keypair  no --key given

  → re-encoding carrier and embedding…

  ✓ embedded into song.stego.ogg  (129.8 KiB in 273ms)

  public key  song.stego.pub  share with senders
  private key song.stego.key  keep secret — required to read the message
  ! without the private key the message cannot be recovered
Flag Short Required Description
--input -i yes Carrier audio: path, file:// or http(s):// URL
--data -d one of Text to hide
--file -f one of Image, audio or other file to hide; give this or --data, not both
--key -k no Recipient public key file; a keypair is generated when omitted
--output -o no Destination file; its extension picks the format (default <input>.stego.ogg)
--out-codec no Encoder to use when the container holds more than one, e.g. alac for .m4a

The output format follows the --output extension, the same way ffmpeg's does:

mist embed -i song.mp3 -d "the eagle lands at dawn" -o song.flac
mist embed -i song.mp3 -d "the eagle lands at dawn" -o song.m4a --out-codec alac

A lossless output is worth preferring when you have the disk space: it is far roomier and very nearly transparent. Measured against the carrier, a FLAC stego file scores about 85 dB SDR where the Vorbis path scores about 26 dB — a lossless encode adds no loss of its own, and the only change is one step in the last bit of one percent of the samples. Run mist formats to see every target your FFmpeg build can write. An unwritable one — a lossy format other than Vorbis, or an extension FFmpeg does not know — fails immediately, before the carrier is even read.

--file hides a file instead of text. The payload type follows the file's content, not its extension: an image or audio file travels as such and comes back under a generated name (recovered-<frame>.jpg), anything else keeps its base name. A payload larger than one frame is spread across as many as it needs, so a photo fits when the carrier is long enough; mist estimate reports how much. Lossless output holds far more than Vorbis.

mist embed -i song.flac -f photo.jpg -o song.stego.flac

When --key is omitted, Mist generates a keypair and writes it beside the output, named after it: song.stego.pub and song.stego.key. Keys are hex-encoded so they can be inspected and pasted, and the private key is written with mode 0600. They are written only after the embed succeeds, so a failed run leaves nothing behind. Give the public key to anyone who should be able to send you messages; keep the private key — without it the message is gone.

catch
mist catch --input song.stego.ogg --key song.stego.key
  mist · catch

  source      song.stego.ogg
  key         song.stego.key
  timeout     none, read to end of stream

  → listening…
  ▸ frame 0     23 B  the eagle lands at dawn

  ✓ 1 frame(s) recovered · end of stream
Flag Short Required Description
--input -i yes Source to read: path, file:// or http(s):// URL
--key -k yes Private key file
--timeout -t no Go duration; 0 (the default) reads until the stream ends
--out-dir no Directory for recovered images, audio and files (default .)
--partial no Keep what arrived of a spanned message that was cut short

Text is printed. Any other payload is written to --out-dir and its path is printed. Only the last element of a stored name is used, and an existing file is never replaced: a taken name becomes name-1.ext, name-2.ext.

A message spread over several frames is lost when one of its frames is. With --partial, catch keeps the part that arrived before the gap instead: text is printed, anything else is saved with a .partial suffix, and a warning says how much of the message that is. On a terminal, embed and catch show which frame they are on and how much of a spanned message has arrived.

A successful scan reports one message, whether it rode in a single frame or spread across several — FrameIdx names the frame where it started. catch keeps reading to the end regardless, because that frame may be anywhere in the file. A file ends by itself; a live stream runs until --timeout elapses or you interrupt it. The command exits non-zero when nothing was recovered.

Frames that carry no message and frames sealed for somebody else's key are indistinguishable, so both are skipped in silence. catch will never tell you "something was here but I could not read it" — that distinction would itself be a signal worth detecting.

catch takes no format flag. The stream itself says whether the bits are in Vorbis residues or in PCM samples, so a FLAC and an Ogg are read by the same command.

formats
mist formats
  mist · formats

  .aif        aiff        pcm_s16be · lossless, bits in the samples
  .caf        caf         alac · lossless, bits in the samples
  .flac       flac        flac · lossless, bits in the samples
  .ogg        ogg         vorbis · lossy, bits in the residues
  .wav        wav         pcm_s16le · lossless, bits in the samples
  .wv         wv          wavpack · lossless, bits in the samples
  …

The list is not kept inside Mist — it is what the libraries on your machine report. Give embed --output one of those extensions, and name the encoder with --out-codec where a container holds more than one. --out-codec accepts either name libav knows a codec by, the encoder's or the codec's, so both dca and dts work.

estimate
mist estimate --input song.ogg
  mist · estimate

  carrier     song.ogg
  target      ogg/vorbis  vorbis · lossy, bits in the residues

  → decoding and probing capacity…

  source      vorbis · lossy  44100 Hz · 2ch · 128 kbps · 24s

  frames      ████████████████████████  3/3 usable  3 total
  frame cap   77 B  largest message that fits in a single frame
  total cap   241 B  largest message spanning every usable frame

  ✓ this carrier can be used with `mist embed`
Flag Short Required Description
--input -i yes Carrier audio: path, file:// or http(s):// URL
--output -o no Target container, or an output path whose extension names one (default ogg)
--out-codec no Encoder to use when the container holds more than one, e.g. alac for .m4a

estimate decodes the carrier and re-encodes it for the same target embed would write — --output/--out-codec pick that target the same way, and no --data or --key is needed, since capacity does not depend on the message or the recipient. source is read straight from libav: the carrier's own codec, container, sample rate, channel count, bitrate (when the source reports one) and duration.

It reports the real limit embed enforces: for Vorbis it groups the encoder's actual packets into stego frames and counts each one's genuinely eligible residues, and for a lossless target it computes each frame's sample capacity directly. frame cap is the largest message a single, unspanned frame can hold — what embed uses when a message fits in one frame outright. total cap is the largest message embed could carry overall by spreading it across every usable frame in turn, for one that doesn't. A carrier that cannot fit even the protocol envelope in any frame exits non-zero with the same hint embed would give:

mist estimate --input tiny.wav
  mist · estimate

  carrier     tiny.wav
  target      ogg/vorbis  vorbis · lossy, bits in the residues

  → decoding and probing capacity…

  source      pcm_s16le · lossless  44100 Hz · 2ch · 1411 kbps · 0s

  frames      ░░░░░░░░░░░░░░░░░░░░░░░░  0/1 usable  1 total
  frame cap   0 B  largest message that fits in a single frame
  total cap   0 B  largest message spanning every usable frame

  ✗ mist: carrier has no frame with room for the payload — quiet or tonal audio
    yields too few usable residues; try a longer or richer carrier, or a
    lossless --output format, which holds far more

A lossless target is cheap to check — capacity there is arithmetic over the sample count, so no full encode is needed — while a Vorbis target costs what embed costs, since only the encoder's real residues say how many are eligible.

analyze
mist analyze ~/Music/incoming            # default: the current folder
mist analyze . --reference originals/carrier.wav

analyze is interactive. It browses a folder: a .. row for its parent, its subfolders, then the audio files in it. Each file is shown with what libav reports about it (the fields ffprobe shows: codec, container, sample rate, channels, bitrate, duration) and its size. Files libav cannot decode as audio are left out.

Key Does
↑/↓ (k/j) move
enter (→/l) open the folder, or analyze the file
←/backspace (h) go up to the parent folder
/ filter by name
r mark the file as the original to compare against (again to clear)
pgup/pgdown page through a long folder
b/esc back to the list from a report; esc also cancels a running analysis
q quit

Every audio format is analyzed, and the report asks two separate questions.

Does it carry a Mist message? This part runs every stage of make harness that one file supports, in the harness's order, and gives a calibrated probability.

Stage What it asks
format Could Mist have written this at all? Mist writes only Ogg Vorbis and lossless audio, so any other lossy file (MP3, AAC, Opus) is 0% for Mist. Its other-tool checks still run
chi-square, spa, rs, hcf-com The classical LSB detectors, on the values Mist embeds into: grid samples for lossless audio, residue indices for Vorbis
classifier, markov, rich The trained wardens, scoring the file with the models the calibration run fitted
dead tail Is the end constant while the start is not, as embedding without filler would leave it?
fingerprint Re-encodes the file with the ffmpeg command line at its own settings and compares container and metadata field by field. Context only: any encoder other than ffmpeg differs too. Needs ffmpeg and ffprobe on PATH
known cover, sdr Only with a reference (below)
selection, key-aware Not run: they need the recipient's public key, which analyze neither needs nor accepts
invariance, perceptual Not run: they need two embeddings of the original, or ViSQOL/PEAQ

Did any other tool hide data in it? These checks look where steganography tools and file-joining tricks put data outside the audio itself. None of them is calibrated, since there is no corpus of other tools' output to calibrate against, so each reports a level (none, low, medium, high) with the reasons for it, never a percentage.

Check What it looks for
container Bytes after the end of the audio that no tag format accounts for (MP3, AAC, Ogg, WAV, AIFF, MP4), bytes between frames, unknown WAV/AIFF chunks and MP4 atoms, padding that is not empty, files appended to cover art (ID3 APIC, FLAC PICTURE), large binary PRIV/GEOB frames, and tag text that reads like base64 or hex. ID3v1, APEv2, Lyrics3 and MusicMatch tags, XMP packets, Content Credentials (C2PA), Pro Tools and broadcast chunks, and runs of 0x00/0xFF fill are recognised as ordinary
mp3 header bits The private, copyright, original and emphasis bits of every frame. An encoder sets them once per file; changing from frame to frame they carry one bit per frame
mp3 ancillary The bits between one frame's audio and the next. Encoders leave them zero or write their own name and padding (LAME writes LAME3.100UUUU…); anything else, at the entropy of encrypted data, is a payload
mp3 granules Context only: the parity of each granule's length, where MP3Stego hides its bits. An honest encoder's lengths have random parity too, so this cannot be scored without the encoder that made the file
silence Lossless files only: isolated ±1 samples dropped into digital silence, the trace LSB-replacement tools leave. Dither fills silence densely and a fade reaches ±1 in runs, so neither counts

A tool that changes audio samples as carefully as Mist does leaves nothing these checks can see; they catch the far more common tricks.

  mist · analysis · 02 - Ride the Lightning.mp3
  mp3 · lossy · 44100 Hz · 2ch · 320 kbps · 6m57s · 15.9 MiB

  Mist's own embedding  calibrated
  ✓ format           mp3 is lossy and not Vorbis: Mist cannot write it, so it carries no Mist message

  Any steganography tool  structure and format checks, not calibrated
  ✓ container        nothing hidden found
  ✓ mp3 header bits  nothing hidden found
  ✓ mp3 ancillary    nothing hidden found; 214484 bytes of ancillary data, 0 bytes of it neither zero…
  i mp3 granules     context: 63910 granules, 49.7% of lengths odd; no Xing/Info header, …
  · silence          not run: needs exact samples; a lossy decode is not
  ╭──────────────────────────────────────────────────────────────────────╮
  │  Summary                                                             │
  │  Not found                                                           │
  │    ○ mp3 is lossy and not Vorbis: Mist cannot write it, so it        │
  │      carries no Mist message                                         │
  │    ○ no data hidden outside the audio: container, mp3 header bits,   │
  │      mp3 ancillary                                                   │
  │  Mist message:          0%  Mist cannot write this format            │
  │  Other steganography:   NONE  nothing found by the structure and     │
  │                         format checks                                │
  ╰──────────────────────────────────────────────────────────────────────╯

The checks were tuned against 1,851 real files: radio captures, streamed AAC segments, ads, rips and editor exports. Two were flagged high, both radio files with genuinely unexplained bytes after the last frame, and one medium, a WAV with a non-standard chunk. Read high as "data is here that no player reads", not as proof of a hidden message.

What the Mist percentage means. Each detector's score is read against the calibration built into the binary (make calibrate, below): how often clean and stego files of the same format scored like this one in that run. That gives a likelihood ratio per detector, capped at 20 to 1. The probability is the average of their logarithms applied to a 50% prior. It is an average, not a sum, because the detectors read overlapping features of the same file and summing would count that evidence several times. 50% means no evidence either way, and that is what most files score: Mist is designed so these detectors stay at chance, and the harness measures that they do. Read a blind result as "a warden without the original could tell this much", not as proof either way. The report names the calibration run it used. A lossless codec the calibration has no entry for borrows another lossless one, and the report says so.

With the original. Pass --reference <file>, or highlight a file in the list and press r to mark it (a REF badge appears; r again clears it). The next analysis then also encodes that original through Mist's own encoder with nothing embedded and compares its values with the file's. Encoding is deterministic, so a clean encode of the same original matches exactly, while a Mist file differs in a small share of values (about 0.13%), each sample by exactly ±1. Either result decides the Mist probability (1% or 99%) and the summary says so. A reference that is not the file's original differs in far more values than Mist ever changes. It is reported and ignored, and the blind result stands. For Vorbis this also needs the same libvorbis build that made the file. With a reference, the fingerprint compares against ffmpeg's encode of the original, which is the harness's own comparison, and sdr reports the signal-to-distortion against it.

analyze needs a terminal: it refuses to run with its input or output redirected.

Interrupting a run

Both commands install a signal handler for SIGINT, SIGTERM and SIGHUP, which cancels the context they run under. catch stops listening and reports whatever it recovered up to that point; embed abandons the run without writing an output file or any keys, since a half-embedded carrier is of no use to anybody. SIGKILL cannot be trapped by any process, so it terminates the tool immediately as it would anything else.

Library

Runnable Godoc examples live in the example package (go test ./example).

Emitter
pub, priv, err := mist.GenerateKeyPair()

emitter, err := mist.NewEmitter(pub)
stego, err := emitter.EmbedReader(ctx, carrier, mist.Text("hello"))
defer stego.Close()
io.Copy(dst, stego)

Embed(ctx, source, payload) takes a path or URL, EmbedReader an open io.Reader, and EmbedFile an *os.File. All three return an io.ReadCloser producing a new stego stream, which the caller must close.

A Payload is a type and bytes. mist.Text(s), mist.Image(b) and mist.Audio(b) build the first three; mist.File(name, b) also remembers a file name, and Payload.FileName() splits it back into name and content on the other side. The type is encrypted with the data, and a payload larger than one frame is spread across several, so a photo fits in a long enough carrier.

file, err := mist.File("report.pdf", pdf)
stego, err := emitter.EmbedReader(ctx, carrier, file)

The output format is Ogg Vorbis unless you ask for another:

emitter, err := mist.NewEmitter(pub, mist.WithFormat("song.flac"))

WithFormat takes what ffmpeg -f takes — a container short name, or the output path whose extension names one — and WithCodec is -c:a, for the containers that hold more than one encoder. An unwritable target is ErrUnsupportedCodec from NewEmitter, before any carrier is read. mist.Formats() lists what the installed FFmpeg can write, and mist.LookupFormat(name, codec) resolves one without constructing an Emitter — useful for deriving an output filename from its Ext.

mist.EstimateCapacity(ctx, source, format, codec) decodes source the way Embed would for that target and reports a Capacity, without writing anything out or needing a recipient key: Source (the carrier's own codec, container, rate, channels, bitrate and duration, straight from libav), Frames and Usable (how many have room to contribute), FrameCapacity (the largest plaintext a single, unspanned frame could carry), and TotalCapacity (the largest plaintext Embed could carry overall by spreading it across every usable frame when it doesn't fit in one).

Catcher
catcher, err := mist.NewCatcher(priv)
results, err := catcher.Listen(ctx, "https://radio.example.com/live.ogg")
for r := range results {
    fmt.Println(r.FrameIdx, r.Payload.Type, len(r.Payload.Data))
}
if ctx.Err() != nil {
    // stopped by the caller rather than by end of stream
}

Listen accepts a path or URL and streams results as they are recovered. The channel closes when the source ends or when the context is cancelled; check ctx.Err() afterwards to tell the two apart. ListenReader does the same over an open reader, and Extract scans an *os.File synchronously and returns a slice. Prefer Listen for live sources: ListenReader buffers a non-seekable reader in full before it starts.

Options
Option Applies to Purpose
WithSenderAuth(priv) Emitter Add an Ed25519 signature over the plaintext
WithFormat(name) Emitter Output container, or the output path naming one (ffmpeg -f)
WithCodec(name) Emitter Encoder override for a container holding several (ffmpeg -c:a)
WithMaxRetries(n) Catcher Tolerate transient read failures on a live source
WithBackoff(d) Catcher Pause between those retries
WithLogger(*slog.Logger) Catcher Receive diagnostics such as joining mid-frame
WithEmitProgress(f) Emitter Call f after each frame is embedded
WithCatchProgress(f) Catcher Call f for each frame scanned and each chunk of a spanned message received
WithPartial() Catcher Return the start of a spanned message cut short, with Result.Partial set

The logger is discarded by default and never receives failed decrypts. Progress never reports one either: a frame that fails to decrypt is scanned exactly like a frame that carries nothing. A Partial result holds the bytes that arrived, in order up to the first missing frame, and Total is the length the sender declared. It can never carry a signature.

Errors
Sentinel Meaning
ErrInvalidKey Key missing or the wrong size
ErrInvalidPayload Payload malformed or too large for a frame
ErrInvalidSource Empty, nil or unusable source argument
ErrUnsupportedCodec Carrier codec Mist cannot decode, or an output format it cannot embed into
ErrCarrier Source could not be opened, demuxed or decoded
ErrNoCapacity No frame had room for the message, so nothing was embedded

ErrNoCapacity is deliberately loud. Everything on the reading side fails silently, but an Embed that quietly produced a file containing nothing would be a much worse outcome than an error.

Choosing a carrier

This matters more than it might seem — for an Ogg Vorbis output. A lossless output has none of the constraints below: every sample can carry a bit, so any audio will do and the only limit is length. If a carrier is rejected or the quality cost bothers you, writing FLAC instead of Ogg is usually the answer.

For Vorbis, the payload rides in quantized residues, and quiet or purely tonal audio simply does not produce many usable ones. A pure 440 Hz sine wave yields so few that an 8-second frame cannot hold even the 64-byte envelope, and embed fails with ErrNoCapacity rather than writing a file that silently carries nothing.

Music, speech, and anything with broadband content are all comfortable carriers. As a rough sense of scale, an 8-second frame of ordinary stereo music holds a little over a hundred bytes — a sentence or two, not a document. Capacity is deliberately modest: embedding is confined to the frequencies where it cannot be heard, and every extra byte is extra distortion.

A message that fits in one frame only ever cares about that frame's own room, so for it a longer carrier buys nothing — a richer one does. A longer message that needs to spread across several frames is different: it draws on every usable frame in turn, so there a longer carrier directly means more room. Either way, if your message does not fit at all, shorten it or pick a carrier with more high-frequency content.

Capacity depends on the carrier and the target, so there is no fixed per-frame figure: on two real songs a Vorbis frame held 106 and 176 bytes and a FLAC frame 812 and 890. For the real number ahead of time, without producing any output, use mist estimate (or EstimateCapacity from Go); see estimate.

Audio quality

Mist always decodes and re-encodes, so some loss is unavoidable for a lossy output — but the embedding itself should be inaudible. For Vorbis, Mist keeps the carrier's quality: it encodes in VBR at the -q:a level whose nominal rate is nearest the source's bitrate scaled by how efficient its codec is (MP3 ×0.75, AAC ×1, Opus ×1.3, Vorbis ×1), and at q8 for a lossless source. A 192 kbps MP3 becomes q5 (160 kbps nominal). The payload is written only above 6 kHz at a low density. Lossless output keeps the carrier's depth as ffmpeg would: a 24-bit or float source is written at 24 bits wherever the codec allows it.

Measured on a 128 kbps MP3, against a plain FFmpeg transcode of the same decoded audio (signal-to-distortion, higher is better), under the earlier rate rule (source ×1.5 ABR); the table has not been re-measured since:

SDR
plain transcode, no embedding 22.75 dB
mist re-encode, no embedding 22.76 dB
mist with a message embedded 22.58 dB

So the message costs under 0.2 dB — the re-encode itself dominates, and that is the price of the format, not of the steganography.

A lossless output removes even that. There is no transcode underneath, and the only change is one step in the last bit of one percent of the samples:

SDR
mist FLAC with a message embedded 85.1 dB

Measured against the same decoded carrier, 20 s of pink noise at 44.1 kHz stereo.

Supported formats

Input is whatever your FFmpeg can decode. The carrier is decoded to PCM and re-encoded, so it need not already be in the output format, and Mist keeps no list of acceptable inputs: it carries libav's own codec id through and asks libav whether a decoder exists. MP3, FLAC, WAV, Opus, AAC/M4A, ALAC and the rest all work if your build supports them, and a format yours cannot decode is reported as ErrUnsupportedCodec naming the codec. Sample format does not matter either — FLAC's integer samples and Vorbis's floats are normalised alike.

Output is Ogg Vorbis or any lossless codec your FFmpeg can encode. That list is not Mist's either: a format qualifies when libav reports an encoder for it and its codec descriptor carries AV_CODEC_PROP_LOSSLESS. mist formats prints what yours offers — typically FLAC, WAV, ALAC, WavPack, TTA, AIFF, CAF, W64, AU and the raw PCM containers.

The two kinds of output are not two features, they are two hiding places for the same protocol:

Ogg Vorbis any lossless format
bits live in quantized residues PCM sample LSBs
extraction Huffman-decode, stop before the iMDCT decode; the samples are unchanged
room in an 8-second frame ~130 bytes ~1.7 kB at 44.1 kHz stereo
SDR against the carrier ~26 dB ~85 dB
carrier must be broadband; tonal audio may not fit long enough; silence carries nothing
file size small large

Frame layout, keyed positions, density, filler and crypto are identical in both.

A lossy output other than Vorbis is refused. Mist can only hide bits it can still find afterwards, and it has no way to rewrite an MP3 or AAC bitstream — ErrUnsupportedCodec, raised by NewEmitter before the carrier is read.

Two things follow from re-encoding. An Ogg Vorbis input is still decoded and re-encoded, with the generation loss that implies: Mist owns the whole encode path and does not patch somebody else's existing bitstream. And a stego file survives being copied byte for byte, but not being transcoded — converting a stego FLAC to MP3, or even back to Vorbis, destroys the message.

Status and limitations

Phase 1 is complete end to end. Crypto, wire framing, the libav cgo layer, Vorbis residue parse and rewrite, lossless sample-domain embedding, keyed position selection, constant-density embedding, the Emitter, the Catcher, and the CLI are all implemented and tested, including multi-frame carriers and live streams. The lossless path is verified end to end for FLAC, WAV, ALAC, WavPack, TTA, AIFF and CAF.

Known gaps, all recorded in CLAUDE.md:

  • Embed over http(s) buffers a finite file rather than streaming a live source, and the lossless path buffers the whole carrier before encoding.
  • A listener that joins mid-frame cannot align to that frame; it says so once and resumes cleanly from the next one.
  • A scan goroutine parked in a blocking libav read outlives its context until the read returns. Listen's channel still closes immediately on cancellation, so callers are unaffected.

Security notes

The design target is a passive warden who knows exactly how Mist works and has the file, but not the private key — Kerckhoffs's principle, with all security resting on the key. Encrypting the payload stops it being read; the keyed position selection and constant embedding density are what aim to stop it being noticed.

One consequence is worth spelling out: position selection derives from the recipient's public key, which is not a secret. Anyone holding it can work out which residues to look at, so everything they would find there must be indistinguishable from noise — which is why the ciphertext length is masked with a subkey derived from the ECDH shared secret rather than stored in the clear.

Explicitly not goals in Phase 1: surviving a digital-to-analog-to-digital round trip, surviving re-encoding by a different encoder, embedding into pre-existing files produced elsewhere, and deniability under coercion (undetectability and deniability are different guarantees).

What is and is not claimed. Mist is not proven undetectable. The measure used is Cachin's: the system is ε-secure when the relative entropy between innocent files and Mist's output is at most ε, and by Pinsker's inequality a detector with AUC a proves ε ≥ 2(a − 0.5)². make harness can only produce such lower bounds, from the detectors it runs, so an AUC at chance means those detectors found nothing, not that nothing exists. The square-root law also applies: at a fixed 1% rate, detectability grows with the amount of audio a warden collects, and the harness pools at most six files.

What the harness measured on 84 carriers in five kinds of audio (MP3 and Opus decodes, 16-bit and 24-bit FLAC, and a few WAVs), the first five minutes of each, against a plain ffmpeg encode of the same carrier. File-level AUC, with 95% intervals bootstrapped by carrier:

  • Over the whole corpus no detector reaches more than 0.55 on FLAC or WAV. The largest is the classifier on FLAC, 0.548 (0.523–0.577), an ε lower bound of 0.005 (up to 0.012). The warden who knows the public key sits at chance, 0.512 (0.494–0.535); it scored 1.000 until the ephemeral key was sent as an Elligator 2 representative.
  • Within one kind of audio the picture is worse. Trained and scored on a single folder, the classifier separates FLAC from 24-bit live recordings (9 carriers) with AUC between 0.89 and 1.00, Opus-sourced carriers at 0.98 and MP3-sourced ones at 0.94. Chunk by chunk the signal is faint (AUC about 0.51); it shows once a file's chunks are averaged. A warden who knows what kind of audio to expect can do this, so the whole-corpus numbers understate what a targeted one sees.
  • Ogg Vorbis is detected more clearly: HCF-COM reaches 0.584 (0.543–0.631), ε ≥ 0.014 (up to 0.034), and a warden who pools files does better, 0.60, 0.69 and 0.78 for 1, 3 and 6 files. That is the square-root law at work. Ogg keeps the source's quality, so it also differs from ffmpeg's default q3 (all MP3-sourced carriers map to q8, 256 kbps against 112); a warden can read the level from the header.
  • A small CNN trained across the whole corpus finds nothing in FLAC (0.501, 0.496–0.508) or WAV (0.503) but does find Ogg Vorbis: 0.612 (0.559–0.677), an ε lower bound of 0.025 (up to 0.063).
  • Length. Mist used to keep the decoder delay and padding of an MP3 or Opus carrier, so its output ran a few hundred samples longer than ffmpeg's encode on 41 of the 84 carriers. It now drops them the way ffmpeg does.

Known-cover attack. A warden who has the original carrier, or can get it (public music), re-encodes it and diffs. The embedded ±1 changes are exactly the difference that finds, in both domains, with certainty. No embedding that changes the file prevents this, and the harness cannot measure it. Do not embed into audio that is publicly available.

Development

make build                       # -> bin/mist
make keys                        # X25519 keypair via OpenSSL -> keys/mist.{pub,key}
make embed INPUT=song.mp3 DATA="hello" OUTPUT=out.flac KEY=keys/mist.pub
make catch INPUT=out.flac KEY=keys/mist.key TIMEOUT=30s
make formats                     # output formats this FFmpeg build can write
make estimate INPUT=song.mp3 OUTPUT=out.flac   # capacity check, no key needed
make analyze DIR=~/Music REFERENCE=original.wav  # interactive warden; REFERENCE optional
make test                        # verbose: -v -race -cover, full tracebacks
make test LOG=trace              # ...and libav logging turned all the way up
make test-quiet                  # same run, results only
make lint                        # golangci-lint run --timeout=5m
make harness CORPUS="$CORPUS" CORPUS_MANIFEST=testdata/harness/corpus.example.json
make cnn-warden CORPUS="$CORPUS" CORPUS_MANIFEST=testdata/harness/corpus.example.json
make calibrate CORPUS="$CORPUS" CORPUS_MANIFEST=testdata/harness/corpus.example.json

make harness re-encodes every file in CORPUS (or a built-in synthetic set when it is empty) per output format three ways: with the ffmpeg command line at its own defaults, which is the clean file a warden would compare against; with Mist's own encoder and nothing embedded; and with a hidden message. It first checks that Mist's output matches the ffmpeg copy on length, trailing digital zeros, sample format and nominal bitrate. Then it reports how well four classical detectors, two cross-validated classifiers — one on general features, one on second-difference Markov features — and a warden who knows the recipient's public key tell the stego copy from each clean one, and how much embedding costs in SDR. Point CORPUS_MANIFEST at a JSON file of public ids, categories and lineage groups (see testdata/harness/corpus.example.json) so reports never contain local filenames. Without a manifest, carriers are renamed carrier-0001 and grouped as external. After make cnn-warden, the report also includes a small CNN (tools/cnn_warden). ffmpeg is taken from FFMPEG / MIST_FFMPEG (default: ffmpeg on PATH). FORMATS=ogg,flac or FORMATS=all picks the targets (default: Ogg Vorbis and the verified lossless set), JOBS= sets how many carriers run at once, MAX_SECONDS= cuts each carrier to its first seconds (every carrier is held decoded several times, so hour-long tracks need it), HARNESS_OUT= chooses the output directory, BASELINE= points at an earlier report.json to show what changed, and MERGE= joins the report.json files of separate runs (one per format, run side by side) into one report. It writes report.md, report.json, manifest.json and scores.json. It is build-tagged, so make test and CI never run it.

make calibrate is a harness run that also writes calibration.json (or CALIBRATION=), the reference mist analyze reads a single file against. Per output format it holds every detector's per-file scores on the stego and clean copies, and the trained wardens refit on every chunk. A trained warden's scores are out-of-fold scores under the same training rule, so they describe files the model never saw. Carrier names are not written. The file is embedded at build time, so rebuild the CLI after recalibrating. It names the harness commit and corpus it came from, and analyze shows that in every report. A run that only merges reports (MERGE=) writes no calibration.

make test is deliberately loud: every test and subtest is named, t.Log output is shown, coverage is reported per package, GOTRACEBACK=all dumps every goroutine on a panic, and MIST_AV_LOG lets libav report what the codec layer is doing. Set MIST_AV_LOG on the mist binary too when a carrier misbehaves — it defaults to silent so codec chatter never lands in the CLI's output.

make keys writes the raw key bytes as hex, which is the format the CLI reads. OpenSSL stores X25519 keys as PKCS#8/SPKI DER whose final 32 bytes are the key itself, so the pair it produces is interchangeable with one mist embed generates for itself.

CLAUDE.md is the design reference: package layout, protocol constants, the invariants the stego layer must preserve, and the conventions used throughout. Read it before changing anything under internal/.

CI runs golangci-lint v2.11.4 and race tests with a coverage badge, matching gocue.

Ethics

This project exists for legitimate confidential communication, digital watermarking, and steganography/steganalysis research. It is not designed for, and should not be used for, concealing illegal content or evading its detection.

Contributing

Issues, design notes, and pull requests are very welcome — this project is meant to be learned from and built with. If you are adding to internal/, please read the invariants in CLAUDE.md first; several of them are load-bearing in non-obvious ways, and the tests exist to catch exactly those cases.

Documentation

Overview

Package mist is an asymmetric-key audio steganography library.

A sender embeds a payload using only the recipient's X25519 public key. Only the matching private key can recover it. The payload lives in the audio itself — not container metadata — so a digital recording of a live stream is enough to extract the message.

Output is Ogg Vorbis, where the bits go into quantized residues, or any lossless format the installed FFmpeg can write (FLAC, WAV, ALAC, WavPack and the rest), where they go into PCM sample LSBs. Formats lists them; WithFormat and WithCodec choose one. Carriers are whatever FFmpeg can decode, whatever the output is.

Hybrid encryption follows the age / NaCl box shape: an ephemeral X25519 key agreement derives a ChaCha20-Poly1305 key. Each stego frame is sealed independently so repeating a payload across a live stream does not produce a correlatable ciphertext. Unused capacity is filled with CSPRNG bytes at the same embedding density as a real message, so presence and absence have the same statistical footprint.

Codec I/O goes through libav (libavformat, libavcodec, libavutil) via cgo. Which codecs qualify is libav's answer, not a list kept here: a format is usable when FFmpeg reports an encoder for it and marks it lossless.

Index

Constants

View Source
const (
	PublicKeySize  = 32
	PrivateKeySize = 32
)

X25519 key sizes in bytes.

View Source
const (
	SigningPublicKeySize  = 32
	SigningPrivateKeySize = 64
	SignatureSize         = 64
)

Ed25519 sizes for optional sender authentication.

View Source
const (
	NonceSize = 12
	TagSize   = 16
)

ChaCha20-Poly1305 sizes in bytes.

View Source
const DefaultFormat = "ogg"

DefaultFormat is the target Embed writes when none is named.

View Source
const EnvelopeOverhead = PublicKeySize + LengthSize + NonceSize + TagSize

EnvelopeOverhead is the number of bytes added by hybrid encryption before embedding: ephemeral public key + masked length + nonce + Poly1305 tag.

View Source
const FrameDuration = 8 * time.Second

FrameDuration is the fixed stego-frame length. Embed loops the payload across consecutive frames of this duration; Listen uses the same value to recover frame phase. It is a protocol constant, not a per-call option.

View Source
const LengthSize = 4

LengthSize is the masked ciphertext-length field that lets a receiver read exactly the right number of bytes instead of searching for the end.

MaxLR is the most one stage's likelihood ratio can say either way: a Stage's LR lies in [1/MaxLR, MaxLR].

View Source
const Version byte = 1

Version is the payload framing version written inside the ciphertext.

Variables

View Source
var (
	ErrInvalidKey       = errors.New("mist: invalid key")
	ErrInvalidPayload   = errors.New("mist: invalid payload")
	ErrInvalidSource    = errors.New("mist: invalid source")
	ErrUnsupportedCodec = errors.New("mist: unsupported codec")
	ErrCarrier          = errors.New("mist: unreadable carrier")
	ErrClosed           = errors.New("mist: already closed")
	// ErrNoCapacity means no frame in the carrier had room for the
	// envelope, so nothing was embedded. Quiet or tonal audio yields too
	// few usable residues; a richer or longer carrier fixes it.
	ErrNoCapacity = errors.New("mist: carrier has no frame with room for the payload")
)

Sentinel errors returned by the public API. Per-frame decrypt failures inside Listen are not surfaced — they are indistinguishable from frames that carry no payload and must stay silent.

Functions

func GenerateKeyPair

func GenerateKeyPair() (pub, priv []byte, err error)

GenerateKeyPair creates a new X25519 keypair for a recipient. pub is shared freely with senders; priv is kept secret by the recipient.

func GenerateSigningKeyPair

func GenerateSigningKeyPair() (pub, priv []byte, err error)

GenerateSigningKeyPair creates an Ed25519 keypair for optional sender authentication. Pass the private key to NewEmitter via WithSenderAuth.

Types

type Analysis added in v1.1.0

type Analysis struct {
	Source      Source
	Stages      []Stage
	Probability float64
	Basis       Basis
	Calibration string
	Suspicion   Suspicion
}

Analysis is how a warden reads one file. Probability is the chance of a Mist message, from the calibrated stages. Suspicion is the strongest sign of data hidden by any other tool, from the uncalibrated ones.

func Analyze added in v1.1.0

func Analyze(ctx context.Context, source string, opts AnalyzeOptions) (Analysis, error)

Analyze runs every stage of the harness one file supports and reports the probability that it carries a Mist message. Blind, that probability comes from the detectors read against a calibration run, and stays near 0.5 wherever the harness found them at chance. With opts.Reference, the file is also diffed against Mist's own clean encode of that original, which is decisive when the two are the same recording.

type AnalyzeOptions added in v1.1.0

type AnalyzeOptions struct {
	Reference string
	FFmpeg    string
	FFprobe   string
	Progress  func(Stage)
}

AnalyzeOptions configures Analyze. Reference is the original carrier, when the caller has it. FFmpeg and FFprobe name the binaries the fingerprint stage runs, found on PATH when empty. Progress, when set, sees each stage as it finishes.

type Basis added in v1.1.0

type Basis int

Basis names what decided an Analysis's probability.

const (
	// BasisBlind is the calibrated detectors alone.
	BasisBlind Basis = iota
	// BasisFormat is a format Mist cannot write.
	BasisFormat
	// BasisKnownCover is a diff against a clean re-encode of the reference.
	BasisKnownCover
)

type Capacity added in v0.2.0

type Capacity struct {
	Source Source
	Format Format
	// Frames is the number of stego frames the carrier splits into.
	Frames int
	// Usable is how many of those frames have room to contribute to
	// TotalCapacity — the protocol envelope plus at least one byte, paid
	// once by whichever usable frame Embed writes first.
	Usable int
	// FrameCapacity is the largest plaintext, in bytes, a single unspanned
	// frame could carry — what Embed uses when the payload fits in one
	// frame outright.
	FrameCapacity int
	// TotalCapacity is the largest plaintext, in bytes, Embed could carry
	// overall by spanning the payload across every usable frame in turn
	// when it does not fit in just one.
	TotalCapacity int
}

Capacity reports how much of a carrier Embed could use for a given target format, without producing any output.

func EstimateCapacity added in v0.2.0

func EstimateCapacity(ctx context.Context, source, formatName, codecName string) (Capacity, error)

EstimateCapacity decodes source the way Embed would for the target picked by formatName/codecName (LookupFormat's rules) and reports how large a text payload Embed could carry into it, without writing anything out. It costs what Embed costs: the carrier still has to be decoded and, for a Vorbis target, re-encoded, because capacity depends on what the target encoder actually produces, not on the source.

type Catcher

type Catcher struct {
	// contains filtered or unexported fields
}

Catcher extracts payloads with a recipient private key. Construct it once with NewCatcher and share it; methods do not mutate it.

func NewCatcher

func NewCatcher(privKey []byte, opts ...CatcherOption) (*Catcher, error)

NewCatcher returns an immutable Catcher for privKey. The key and any option-held secrets are copied; later writes to the caller's slices are not observed.

func (*Catcher) Extract

func (c *Catcher) Extract(ctx context.Context, source *os.File) ([]Result, error)

Extract scans source synchronously and returns every frame that decrypts and authenticates. It stops at EOF or when ctx is cancelled. source stays owned by the caller.

func (*Catcher) Listen

func (c *Catcher) Listen(ctx context.Context, source string) (<-chan Result, error)

Listen opens source (a local file path, file:// URL, or http(s):// URL), continuously scans for stego frames, and sends each successfully decrypted payload to the returned channel.

The returned channel is closed when either:

  • the source reaches EOF (finite file or stream ended by the server), or
  • ctx is cancelled.

The caller distinguishes the two after the channel closes:

  • ctx.Err() == nil → source reached EOF naturally
  • ctx.Err() != nil → context was cancelled by the caller

The error return covers only immediate setup failures (invalid source, unreadable file, malformed key). Per-frame decrypt failures are silently skipped — they are indistinguishable from frames carrying no payload and must not surface as errors.

func (*Catcher) ListenReader

func (c *Catcher) ListenReader(ctx context.Context, r io.Reader) (<-chan Result, error)

ListenReader is Listen over an already-open bitstream (pipe, HTTP body, in-memory buffer). The caller retains ownership of r and must close it if it is an io.Closer.

type CatcherOption

type CatcherOption func(*Catcher)

CatcherOption configures NewCatcher.

func WithBackoff

func WithBackoff(backoff time.Duration) CatcherOption

WithBackoff configures the pause between read retries.

func WithCatchProgress added in v1.2.0

func WithCatchProgress(f ProgressFunc) CatcherOption

WithCatchProgress reports each frame scanned and each chunk of a spanned payload assembled to f. Frames that fail to decrypt are reported exactly like frames that carry nothing.

func WithLogger

func WithLogger(log *slog.Logger) CatcherOption

WithLogger directs diagnostics — such as joining a stream mid-frame — to log. Failed decrypts are never logged: they are indistinguishable from frames carrying no payload. Discarded by default.

func WithMaxRetries

func WithMaxRetries(retries int) CatcherOption

WithMaxRetries configures how many consecutive read failures a live source may produce before Listen gives up and closes the channel.

func WithPartial added in v1.2.0

func WithPartial() CatcherOption

WithPartial makes a spanned payload that stops arriving part way through a Result with Partial set, holding the bytes that did arrive, instead of nothing. Off by default.

type Emitter

type Emitter struct {
	// contains filtered or unexported fields
}

Emitter encrypts a payload for a recipient public key and embeds it into a carrier. Construct it once with NewEmitter and share it. Methods do not mutate the Emitter.

func NewEmitter

func NewEmitter(recipientPubKey []byte, opts ...EmitterOption) (*Emitter, error)

NewEmitter returns an Emitter for recipientPubKey. The key and any option-held secrets are copied; later writes to the caller's slices are not observed. An output format this FFmpeg cannot write, or a lossy one other than Vorbis, is ErrUnsupportedCodec here rather than after a carrier has been decoded.

func (*Emitter) Embed

func (e *Emitter) Embed(ctx context.Context, source string, payload Payload) (io.ReadCloser, error)

Embed opens source (a local file path, file:// URL, or http(s):// URL) as the carrier, encrypts payload for the recipient, and returns a new stego stream in the Emitter's output format.

The payload is looped across fixed-duration stego frames, each re-encrypted with a fresh ephemeral X25519 key, for the full duration of the carrier. Suitable for both finite files and wrapping a live encoder output — the returned reader produces output as fast as the carrier does.

func (*Emitter) EmbedFile

func (e *Emitter) EmbedFile(ctx context.Context, carrier *os.File, payload Payload) (io.ReadCloser, error)

EmbedFile is Embed over an *os.File carrier. The file stays owned by the caller.

func (*Emitter) EmbedReader

func (e *Emitter) EmbedReader(ctx context.Context, carrier io.Reader, payload Payload) (io.ReadCloser, error)

EmbedReader is Embed over an already-open carrier (pipe, HTTP body, in-memory buffer). The caller retains ownership of carrier and must close it if it is an io.Closer.

type EmitterOption

type EmitterOption func(*Emitter)

EmitterOption configures NewEmitter.

func WithCodec added in v0.2.0

func WithCodec(name string) EmitterOption

WithCodec overrides the encoder the container would default to, the way ffmpeg's -c:a does — an m4a holds AAC unless told to hold ALAC.

func WithEmitProgress added in v1.2.0

func WithEmitProgress(f ProgressFunc) EmitterOption

WithEmitProgress reports each frame Embed writes to f.

func WithFormat added in v0.2.0

func WithFormat(name string) EmitterOption

WithFormat selects the output target the way ffmpeg's -f does: a container short name ("flac", "wav", "ogg") or the output path whose extension names one ("song.flac"). Ogg Vorbis by default.

func WithSenderAuth

func WithSenderAuth(senderPrivKey []byte) EmitterOption

WithSenderAuth attaches an Ed25519 signature over the plaintext, itself encrypted before embedding. Off by default — an unauthenticated ciphertext is shorter and one step closer to looking like nothing.

type Format added in v0.2.0

type Format struct {
	Container string
	Codec     string
	Ext       string
	Lossless  bool
}

Format is an output target Embed can write: a container, the encoder inside it, and the extension a file of that kind normally carries.

func Formats added in v0.2.0

func Formats() []Format

Formats lists every output target this FFmpeg build can write, sorted by container name. A container and its default encoder are reachable by either name, so the same target is reported once.

func LookupFormat added in v0.2.0

func LookupFormat(name, codec string) (Format, error)

LookupFormat resolves an output target the way ffmpeg's command line does: name is an output path whose extension names a container ("song.flac"), a container short name ("flac"), or an encoder name ("alac"); codec, when not empty, overrides the encoder the container would default to. An empty name means DefaultFormat.

Ogg Vorbis and every lossless codec the installed FFmpeg can encode are supported. Any other lossy codec is ErrUnsupportedCodec: Mist can only hide bits it can still find afterwards, and a lossy re-encode it does not own the bitstream of destroys them.

func (Format) String added in v0.2.0

func (f Format) String() string

String names the format the way a caller would ask for it.

type Payload

type Payload struct {
	Type PayloadType
	Data []byte
}

Payload is the unit that Emitter encrypts and Catcher emits. A PayloadFile carries its name in Data; build and read it with File and FileName. For every other type Data is the content itself.

func Audio added in v1.2.0

func Audio(data []byte) Payload

Audio returns an audio payload. Data is the encoded audio as it is on disk.

func File added in v1.2.0

func File(name string, data []byte) (Payload, error)

File returns a file payload that remembers name. The name travels inside Data as a big-endian u16 length and the name bytes, ahead of the content.

func Image added in v1.2.0

func Image(data []byte) Payload

Image returns an image payload. Data is the encoded image as it is on disk.

func Text

func Text(s string) Payload

Text returns a text payload.

func (Payload) FileName added in v1.2.0

func (p Payload) FileName() (name string, data []byte, err error)

FileName splits a PayloadFile into the name it was built with and its content. It returns ErrInvalidPayload for any other type or truncated Data.

type PayloadType

type PayloadType byte

PayloadType identifies the kind of data inside a Payload. The type byte is encrypted with the payload, so a warden who isolates the embedded bytes still cannot read it without the private key.

const (
	PayloadText  PayloadType = 0x01
	PayloadImage PayloadType = 0x02
	PayloadAudio PayloadType = 0x03
	PayloadFile  PayloadType = 0x04
)

Payload types. The values are part of the wire format.

type Progress added in v1.2.0

type Progress struct {
	Stage ProgressStage
	Done  int
	Total int
}

Progress counts frames for ProgressEmbed and ProgressScan and message bytes for ProgressAssemble. Total is 0 when it is not known, as on a live stream.

type ProgressFunc added in v1.2.0

type ProgressFunc func(Progress)

ProgressFunc receives Progress reports on the goroutine doing the work, so it must return quickly.

type ProgressStage added in v1.2.0

type ProgressStage int

ProgressStage names the part of the work a Progress report describes.

const (
	ProgressEmbed ProgressStage = iota + 1
	ProgressScan
	ProgressAssemble
)

Stages reported by an Emitter (ProgressEmbed) and a Catcher (ProgressScan, ProgressAssemble).

type Result

type Result struct {
	Payload  Payload
	FrameIdx int64 // ordinal of the frame within this Listen session (0-based)
	Partial  bool  // only the start of a spanned payload arrived; see WithPartial
	Total    int   // declared Data length of a Partial payload
}

Result is emitted by Catcher whenever a stego frame decrypts and authenticates successfully.

type Scope added in v1.1.0

type Scope int

Scope says what a stage looks for.

const (
	// ScopeMist stages look for Mist's own embedding and are calibrated.
	ScopeMist Scope = iota
	// ScopeAny stages look for data any tool may have hidden, and report a
	// Suspicion with its reasons rather than a probability.
	ScopeAny
)

type Source added in v0.2.0

type Source struct {
	Codec      string // libav's display name for the source codec
	Container  string // demuxer's name for the source container
	SampleRate int
	Channels   int
	Bitrate    int64 // bits per second as the source reports it, 0 if it does not
	Duration   time.Duration
	Lossless   bool // whether libav's codec descriptor marks the source lossless
}

Source describes the carrier Embed would decode, straight from what the installed libav reports about it — nothing here is Mist's own analysis.

func Probe added in v1.1.0

func Probe(source string) (Source, error)

Probe reports what libav says about source without decoding it — the fields ffprobe shows for its audio stream. It fails for a file with no audio stream this FFmpeg build can decode.

type Stage added in v1.1.0

type Stage struct {
	Name          string
	Scope         Scope
	Status        StageStatus
	Score         float64
	LR            float64
	AUC           float64
	Note          string
	Suspicion     Suspicion
	Findings      []string
	Informational bool
}

Stage is one step of an Analysis. For a ScopeMist stage, LR is the likelihood ratio against the calibration, above 1 favouring a message, and 0 when the stage is not evidence; AUC is how well the stage separated clean from stego files in the calibration run. A ScopeAny stage instead has a Suspicion and the Findings behind it. An Informational stage describes the file for context and is never evidence either way.

func (Stage) Decisive added in v1.1.0

func (s Stage) Decisive() bool

Decisive reports whether a stage's likelihood ratio is at least 2 to 1 in either direction, enough to count as a finding.

type StageStatus added in v1.1.0

type StageStatus int

StageStatus says whether a stage of an Analysis ran.

const (
	// StageRan means the stage scored the file.
	StageRan StageStatus = iota
	// StageSkipped means the stage could run here but did not, because a
	// tool or a reference it needs is missing.
	StageSkipped
	// StageNotRun means the stage needs what a single-file analysis does
	// not have: a key, or the original carrier.
	StageNotRun
)

type Suspicion added in v1.1.0

type Suspicion int

Suspicion is how strongly an uncalibrated check points at hidden data.

const (
	// SuspicionNone means nothing out of the ordinary.
	SuspicionNone Suspicion = iota
	// SuspicionLow is unusual but common in honest files.
	SuspicionLow
	// SuspicionMedium is rare in honest files.
	SuspicionMedium
	// SuspicionHigh is data no player reads, stored where encoders write none.
	SuspicionHigh
)

func (Suspicion) String added in v1.1.0

func (s Suspicion) String() string

String names the level for a report.

Directories

Path Synopsis
cmd
mist command
Command mist hides a text message or a file inside an audio file and recovers it again with the matching private key.
Command mist hides a text message or a file inside an audio file and recovers it again with the matching private key.
Package example contains Godoc examples for github.com/iSerganov/mist.
Package example contains Godoc examples for github.com/iSerganov/mist.
internal
av
Package av is the cgo boundary to libavformat, libavcodec, libavutil and libswresample.
Package av is the cgo boundary to libavformat, libavcodec, libavutil and libswresample.
codec
Package codec is the boundary between PCM and a compressed bitstream.
Package codec is the boundary between PCM and a compressed bitstream.
codec/vorbis
Package vorbis is the Phase 1 codec: PCM ↔ Ogg Vorbis via package av, plus a bitstream residue parser used by stego.
Package vorbis is the Phase 1 codec: PCM ↔ Ogg Vorbis via package av, plus a bitstream residue parser used by stego.
crypto
Package crypto is the hybrid box: X25519, HKDF, ChaCha20-Poly1305, Ed25519.
Package crypto is the hybrid box: X25519, HKDF, ChaCha20-Poly1305, Ed25519.
dsp
Package dsp holds signal-processing primitives shared by the analysis packages.
Package dsp holds signal-processing primitives shared by the analysis packages.
forensics
Package forensics looks for data any steganography tool may have hidden in an audio file outside its samples: bytes past the end of the audio, chunks and tags no player reads, padding that is not empty, files appended to cover art, and the MP3 frame fields known to carry covert bits.
Package forensics looks for data any steganography tool may have hidden in an audio file outside its samples: bytes past the end of the audio, chunks and tags no player reads, padding that is not empty, files appended to cover art, and the MP3 frame fields known to carry covert bits.
frame
Package frame models fixed-duration stego frames.
Package frame models fixed-duration stego frames.
quality
Package quality measures how far a processed signal has drifted from its reference: signal-to-distortion ratios on planar float PCM, and perceptual scores from external tools.
Package quality measures how far a processed signal has drifted from its reference: signal-to-distortion ratios on planar float PCM, and perceptual scores from external tools.
steganalysis
Package steganalysis scores streams of quantized values for traces of LSB embedding and measures how well those scores separate two populations.
Package steganalysis scores streams of quantized values for traces of LSB embedding and measures how well those scores separate two populations.
stego
Package stego embeds and extracts bit strings in quantized Vorbis residues.
Package stego embeds and extracts bit strings in quantized Vorbis residues.
trace
Package trace reads what an audio file shows without looking at its samples: container identity, length, how it ends, and the page or STREAMINFO facts a metadata warden can read.
Package trace reads what an audio file shows without looking at its samples: container identity, length, how it ends, and the page or STREAMINFO facts a metadata warden can read.
wire
Package wire is the inner payload framing and the outer hybrid-box envelope.
Package wire is the inner payload framing and the outer hybrid-box envelope.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL