npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

stegdoc

v6.1.0

Published

Hide files inside Office documents (XLSX/DOCX) with AES-256 encryption and steganography

Readme

stegdoc

Hide a file inside a legitimate-looking Office document

npm version License: MIT

stegdoc encodes any file into an ordinary-looking Excel or Word document. The payload is spread across realistic nginx access log entries or Hebrew incident reports, so the cover survives casual inspection. Optionally protected with AES-256-GCM.

The format engine is written in Rust. It ships as a prebuilt native addon for the CLI, and as WebAssembly for the single-file browser tool.

Features

  • Log-based steganography — the data is the logs; there is no hidden sheet and nothing extra to notice
  • AES-256-GCM encryption, password-derived keys
  • v6 by default — Argon2id key derivation and tamper-evident metadata, with --v5 for older decoders
  • Brotli compression (quality 6), skipped for formats that are already compressed
  • XLSX and DOCX covers — an access-log spreadsheet or a Hebrew RTL incident report
  • Multi-part splitting for payloads larger than one document
  • Integrity checking with SHA-256, verified on decode
  • ZIP, directory, or single-file input on decode, discovered automatically
  • v5 and v6 read support — the decoder reads both log-embed versions

Install

npm install -g stegdoc

The install pulls a prebuilt engine for the host platform (win32-x64, linux-x64, linux-arm64, darwin-x64, darwin-arm64). No compiler or Rust toolchain is required. If a platform package is missing, log-embed files cannot be decoded; the CLI says so instead of guessing.

Or run it without installing:

npx stegdoc encode myfile.pdf -p mypassword

Quick start

# Hide a file, encrypted
stegdoc encode secret.pdf -p mypassword

# Get it back
stegdoc decode access_log_20260315_1200_A1B2_part1.xlsx -p mypassword

# Inspect without decoding
stegdoc info access_log_20260315_1200_A1B2_part1.xlsx

# Check that it will decode
stegdoc verify access_log_20260315_1200_A1B2_part1.xlsx -p mypassword

Commands

encode <inputs...>

| Option | Description | Default | |--------|-------------|---------| | -o, --output-dir <dir> | Output directory | Current directory | | --bundle-name <name> | Filename recorded when several inputs are bundled | bundle.zip | | -s, --chunk-size <size> | Split size: 5MB, 25MB, 3 parts, or max | 5MB | | -f, --format <format> | Cover format: xlsx or docx | xlsx | | -p, --password <pass> | Encryption password | None (unencrypted) | | --v5 | Emit the v5 format (PBKDF2) | Off (v6) | | --v6 | Emit the v6 format (the default) | On | | --no-limit | Bypass the DOCX 1 MB limit | Off | | --force | Overwrite existing files | Prompt | | -q, --quiet | Minimal output for scripting | Off | | -y, --yes | Skip interactive prompts | Off |

A directory is zipped before encoding, and several inputs are bundled into one zip first, so a decode hands that archive back. -s max produces a single part; -s "3 parts" splits into roughly three. Bundling a folder or several inputs uses the native engine to build the archive.

stegdoc encode document.pdf -p mysecret                 # access log spreadsheet
stegdoc encode config.json -p mysecret -f docx          # Hebrew incident report
stegdoc encode large-file.zip -p mysecret -s "3 parts"  # three parts
stegdoc encode ./my-folder -p mysecret                  # zipped first
stegdoc encode a.pdf b.pdf --bundle-name pair.zip -p mysecret

decode <file>

| Option | Description | Default | |--------|-------------|---------| | -o, --output <path> | Output file or directory | Original filename | | -p, --password <pass> | Decryption password | Prompt if needed | | --force | Overwrite existing files | Prompt | | -q, --quiet | Minimal output | Off | | -y, --yes | Skip prompts, fail if a password is needed | Off |

Point decode at any one part, a directory holding the parts, or a ZIP of them; the rest are found automatically.

stegdoc decode access_log_20260315_1200_A1B2_part1.xlsx -p mysecret
stegdoc decode system_report_20260315_0800_CD42_part1.docx -p mysecret
stegdoc decode stegdoc-parts.zip -p mysecret -o ./restored

info <file>

Reads metadata without decoding: cover format and version, original filename and size, whether it is encrypted and compressed, the part count, and whether metadata is authenticated.

verify <file> [-p <pass>]

Runs the structural and password checks without writing any output. It exits non-zero if the file will not decode.

How it works

Pipeline

Input
  |
[Brotli q6]              skipped for already-compressed types
  |
[AES-256-GCM]            optional
  |
[log-embed]              payload spread across log line fields
  |
[Office wrapper]         XLSX access logs or a DOCX incident report
  |
Output part(s)

Each log row carries 114 raw bytes across six channels:

| Channel | Encoding | Bytes | |---------|----------|-------| | URL path segment | base64url | 21 | | Query param token | base64url | 21 | | Query param state | base64url | 21 | | Referer ref param | base64url | 21 | | X-Request-ID | UUID v4 | 14 | | X-Trace-ID | 32-char hex | 16 |

Metadata and encryption parameters travel in header lines marked by the /api/v1/health path, in a length-prefixed frame:

STGD06|<metaLen>|<encLen>|<kdfLen>|{metadataJson}{encryptionMeta}{kdfParams}

v5 uses the shorter STGD05|<metaLen>|<encLen>|... frame. The decoder dispatches on the marker.

Decoding uses payloadSize and dataLineCount from the metadata to truncate the payload. Filler rows are appended for realism and ignored on decode.

Covers

XLSX — a single "Access Logs" sheet of realistic nginx entries with columns for remote address, timestamp, method, request, status, bytes, referer, user-agent, X-Request-ID, and X-Trace-ID. No hidden sheets.

DOCX — a Hebrew RTL incident report: title, executive summary, timeline table, log excerpts in monospace blocks, root cause, and recommendations. The prose is generated deterministically from the payload hash, so covers do not repeat. DOCX refuses inputs over 1 MB unless --no-limit is given; use XLSX for large payloads.

Multi-part output

Large payloads are split across parts. Parts are built in parallel, each with its own IV and tag but a shared session salt, and the CLI prints progress. Decode finds the sibling parts automatically.

Filenames are decoys:

access_log_YYYYMMDD_HH00_XXXX[_partN].xlsx
system_report_YYYYMMDD_HH00_XXXX[_partN].docx

Encryption

Encryption is optional but recommended. Without -p, the payload is hidden but not encrypted.

v6 (default)

v6 makes the metadata tamper-evident and uses a memory-hard KDF. The cover, channels, chunking, and filenames are identical to v5; only the header and crypto change.

| | | |---|---| | Cipher | AES-256-GCM | | Key derivation | Argon2id, version 19, m=65536 KiB, t=3, p=1 | | Key | 256-bit | | IV | 96-bit, random per part | | Salt | 128-bit, shared per session | | Tag | 128-bit |

The KDF parameters are recorded in the file and reproduced on decode. When encrypted, the AEAD associated data is the header frame minus the encryption parameters, so changing the original filename, size, content hash, or KDF parameters breaks decryption. The frame is STGD06|<metaLen>|<encLen>|<kdfLen>|....

v5 (opt-in, --v5)

v5 uses PBKDF2 and leaves the metadata unauthenticated. It exists for decoders that predate v6, and its read compatibility is permanent.

| | | |---|---| | Cipher | AES-256-GCM | | Key derivation | PBKDF2-SHA256, 100,000 iterations | | Key | 256-bit | | IV | 96-bit, random per part | | Salt | 128-bit, shared per session | | Tag | 128-bit |

v6 requires the native engine to produce; --v5 is the only format the JavaScript fallback can emit. The decoder reads both versions without a flag.

Decode inputs

decode accepts:

  • a single encoded .xlsx/.docx,
  • a directory containing the sibling parts,
  • a ZIP holding the parts.

A ZIP is expanded, non-container entries are ignored, and the first complete part set is decoded. Only ZIP is supported; RAR and 7z would need extra decoders.

Browser tool

The same engine also runs as WebAssembly in a single self-contained HTML file, for offline use where installing anything is not an option.

npx @stegdoc/web      # writes ./stegdoc.html

Or build it from source:

pnpm install
pnpm build:web        # -> dist/stegdoc.html

Open the file in a browser. Encoding and decoding run in a Web Worker so the page stays responsive; nothing is uploaded and no network is used. The UI offers XLSX or DOCX, v6 or v5, an optional password, an optional chunk size, and Brotli compression. Several files can be selected at once and are bundled into one zip before encoding. When the browser supports the File System Access API, the page asks for an output folder and writes the documents straight into it; otherwise a single part downloads directly and a multi-part set downloads as one stegdoc-parts.zip. Input is held in memory, so the UI warns above 64 MiB and refuses past 128 MiB with a pointer to the CLI.

Security model

  • The native engine performs no filesystem writes. The CLI owns the output path, the overwrite prompt, and the write.
  • The original filename in the metadata is untrusted. It is reduced to a bare filename and cannot escape the chosen output directory.
  • Decompression output and ZIP expansion are bounded, so a crafted file fails on the cap instead of exhausting memory or disk.
  • contentHash is verified after decoding and the output is deleted on mismatch.
  • v6 binds the metadata to the ciphertext; v5 metadata is plaintext and its integrity rests on contentHash and the path rules.

The wire format is specified in spec/FORMAT.md.

Compatibility

  • The decoder auto-detects the version, so no flag is needed to read old files.
  • v6 is the default output. --v5 emits the older format; both cover XLSX and DOCX.
  • v3/v4 (base64 in a hidden sheet or paragraph, gzip) are no longer readable. The decoder reports that the format is unsupported; re-encode the file with a current version of stegdoc.
  • Log-embed (v5/v6) decoding requires the native engine. Without it, the CLI fails loudly rather than falling back to an incomplete reader.
  • Encoding and decoding are randomised, so two encodes of the same input are not byte-identical; conformance is defined on the decode direction.

Building from source

Requires Node.js >= 18 and a Rust toolchain.

pnpm install
pnpm build:native          # build the napi binding -> native/stegdoc.node

Then run the CLI directly with node src/index.js <command>.

| Command | Purpose | |---------|---------| | pnpm test | conformance against the frozen fixtures (builds native first) | | pnpm test:rust | the Rust decoder against the same fixtures | | pnpm test:interop | Rust binary encode -> CLI (napi) decode | | pnpm test:safety | output-path traversal containment | | pnpm test:contract | CLI flags, exit codes, filenames | | pnpm build:wasm | the wasm binding -> web/pkg | | pnpm build:web | the single-file browser tool -> dist/stegdoc.html | | pnpm build:web-package | stage @stegdoc/web from dist/ | | pnpm test:wasm | wasm round-trip, headless in Node | | pnpm test:browser | the built page, round-trip from file:// | | cargo test | Rust unit and property tests |

Requirements

  • Node.js >= 18 for the CLI
  • A supported platform for the prebuilt engine (see Install)

License

MIT — see LICENSE.