# CJaraTech image steganography, binary format v1 This document specifies the deterministic payload framing implemented by `dist/projects/steg-core.js`. The format is an image carrier format, not a general-purpose steganography detector. Decoder support is limited to this format and version. ## Carrier The carrier is an RGBA pixel buffer obtained from a decoded image, traversed in row-major order. Use only pixels whose alpha channel is exactly 255. Skip every other pixel entirely; neither its RGB values nor alpha are modified. This rule avoids the lossy premultiplied-alpha conversions that can otherwise corrupt data when a Canvas PNG is decoded again. For each eligible pixel, visit the R, G, then B channel. Store one payload bit in each channel's least significant bit. Bits within each payload byte are visited most significant first (bit 7 to bit 0). Channels become `(channel & 254) | bit`. Alpha is unchanged. Pixels and channels after the payload ends are unchanged. Total embedding capacity, **including** the header, is `floor(fullyOpaquePixels * 3 / 8)` bytes. A normal fully opaque photograph of W × H pixels therefore has `floor(W * H * 3 / 8)` bytes. The compressed file size of the cover does not determine capacity. The carrier must be exported losslessly as PNG; screenshots, recompression, resizing, editing, or color conversion may destroy the embedded bits. ## Fixed 64-byte header All integer fields are unsigned, big-endian. Offsets and lengths are bytes. | Offset | Length | Contents | | --- | --- | --- | | 0 | 8 | ASCII magic `CJSTEG01` (hex `43 4a 53 54 45 47 30 31`) | | 8 | 1 | Version: 1 | | 9 | 1 | Security flag: 0 = passwordless, 1 = encrypted; other values rejected | | 10 | 1 | Embedding method: 1 = one LSB in opaque-pixel RGB channels | | 11 | 1 | Reserved, must be zero | | 12 | 2 | Header length, must be 64 | | 14 | 2 | Reserved, must be zero | | 16 | 4 | Body length, excludes header | | 20 | 4 | Plaintext record length | | 24 | 4 | PBKDF2 iteration count: 0 for passwordless, exactly 600000 for encrypted | | 28 | 16 | Random PBKDF2 salt when encrypted; zero when passwordless | | 44 | 12 | Random AES-GCM nonce when encrypted; zero when passwordless | | 56 | 8 | Reserved, all zero | Total payload length is 64 plus body length. Before allocating the body or deriving a key, the decoder validates the header, version, flags, reserved values, lengths, fixed KDF work factor, hard memory limits, and image capacity. ## Plaintext record The record consists of: 1. Four-byte unsigned big-endian UTF-8 metadata length. 2. UTF-8 JSON metadata with exactly three keys: `mime`, `name`, and `byteLength`. 3. Original secret file bytes, unchanged when resizing/compression was not selected. Encoder metadata uses this key order: `{"mime":"image/png","name":"secret.png","byteLength":123}`. Supported MIME types are `image/png`, `image/jpeg`, and `image/webp`. `name` is a sanitized download filename: path separators, controls, and filesystem-reserved characters become underscores; leading dots and surrounding whitespace are removed; names are capped at 120 Unicode code points before a matching extension is added if needed. An empty name falls back to `hidden-image` with the matching extension. Metadata must be between 1 and 1024 UTF-8 bytes. Secret data must be between 1 byte and 8 MiB, and its byte count must exactly match `byteLength`. The decoder verifies the expected image signature. Full image decoding and dimension validation are performed by the image/UI layer. ## Passwordless body `body = plaintextRecord || SHA256(header || plaintextRecord)` The body length must equal plaintext record length plus 32. SHA-256 detects accidental corruption; it is **not** a secret MAC or a signature. Anyone able to change the file can read and rewrite a passwordless payload and recompute its digest. This mode provides concealment, not cryptographic confidentiality or sender authenticity. ## Password-protected body - Password encoding: UTF-8, exact string; no trimming or Unicode normalization. - KDF: PBKDF2-HMAC-SHA256 with exactly 600000 iterations and the header's random 16-byte salt. - Derived key: 256-bit, non-extractable Web Crypto AES-GCM key. - Encryption: AES-256-GCM, 12-byte random header nonce, 128-bit authentication tag. - Additional authenticated data: the complete fixed 64-byte header. - Plaintext: the complete record, including metadata. - Body: Web Crypto's returned ciphertext followed by its 16-byte tag. The body length must equal plaintext record length plus 16. Fresh salt and nonce are generated with `crypto.getRandomValues()` for every encode operation. The password must be nonempty and no longer than 1024 JavaScript UTF-16 code units. The interface recommends a strong unique password. Wrong-password and authenticated-payload corruption errors are deliberately indistinguishable. A lost password cannot be recovered by the site. Encrypted filenames and MIME metadata are inside the ciphertext. Public framing still reveals use of this format, its version, the security flag, payload size, salt, nonce, and KDF parameters. This is sequential LSB embedding, not an undetectable or statistically resistant embedding scheme. It does not authenticate the carrier's visible image or establish a sender's identity. ## Resource limits and cleanup - At most 12,000,000 RGBA pixels (48,000,000 pixel-buffer bytes). - At most 8 MiB original secret bytes, further constrained by actual cover capacity. - At most 1024 metadata bytes. - At most 8 MiB + 1028 plaintext record bytes. - No attacker-selected KDF algorithm or iteration count. - The decoder never allocates based only on unchecked embedded lengths. - Async RGB traversals yield at bounded intervals; the worker runs the same engine. - Plaintext record, working payload, hash-input, and password-byte arrays are zeroed when practical. JavaScript strings, `CryptoKey` objects, browser image caches, and garbage collection cannot be reliably wiped; the UI must release references and object URLs and terminate workers on reset/navigation. ## JavaScript and worker API The browser global is `StegCore`; Node tests use `module.exports`. - `capacity(pixels)` → number of available embedding bytes, including framing. - `estimate({byteLength,mime,name,encrypted}, available?)` → exact `payloadBytes`, `metadataBytes`, `fileBytes`, `headerBytes`, `encrypted`; with `available`, also `capacityBytes` and `fits`. - `await analyze(pixels, progress?, options?)` → validated header information. This does not prove body integrity until decode runs. - `await encode(pixels, secretBytes, options, progress?)` → `{pixels,info}`. **Mutates the supplied pixel array in place.** Pass a separate buffer if retaining the original cover. - `await decode(pixels, password?, progress?, options?)` → `{bytes,mime,name,info}`; returned file bytes are a separate owned copy. `options` can contain `isCancelled()` for a main-thread fallback. This function must not be cloned into a Worker. Progress callbacks receive numbers from 0 to 1 and must not contain secret values. Worker input: `{id,op,pixels,secretBytes?,options?,password?}` where op is `capacity`, `encode`, `analyze`, or `decode`. Capacity returns `{capacityBytes}`. The worker emits `{ready:true}` after its engine and message listener are ready, allowing a safe fallback before buffers are transferred. Progress replies are `{id,progress}`. Final replies are `{id,result}` or `{id,error}`. Pixel buffers and recovered file buffers are transferred back when returned. The UI should terminate its worker after use and on cancellation.