{"_id":"@aptxyz/lsb","name":"@aptxyz/lsb","dist-tags":{"latest":"0.0.1"},"versions":{"0.0.1":{"name":"@aptxyz/lsb","version":"0.0.1","description":"LSB steganography CLI + Node.js library: hide text or binary payloads inside image least-significant bits with a 48-bit four-corner metadata header","main":"src/index.js","bin":{"lsb":"src/cli.js","lsbjs":"src/cli.js"},"type":"commonjs","scripts":{"test":"node test/roundtrip.js"},"keywords":["steganography","lsb","least-significant-bit","hide","secret","png","image","cli"],"author":{"name":"aptxyz","url":"https://github.com/aptxyz"},"license":"MIT","homepage":"https://github.com/aptxyz/lsb#readme","repository":{"type":"git","url":"git+https://github.com/aptxyz/lsb.git"},"bugs":{"url":"https://github.com/aptxyz/lsb/issues"},"dependencies":{"minimist":"^1.2.8","pako":"^3.0.1","sharp":"^0.33.0"},"engines":{"node":"^18.17.0 || ^20.3.0 || >=21.0.0"},"gitHead":"618cb4b7ff1bbc16cc4a7d496d312b94035708d4","_id":"@aptxyz/lsb@0.0.1","_nodeVersion":"24.14.0","_npmVersion":"11.9.0","dist":{"integrity":"sha512-6iR/Y6BSkKFbQFBFQkYkqEV5ENXGMWenKR2tKtzmRY0ik6DXREIfhx+ZGsLGNM5n2V08TbErHrmWg10CLM+Mlg==","shasum":"f0b5855fadd88d6f118f19e137ad924acecf5107","tarball":"https://registry.npmjs.org/@aptxyz/lsb/-/lsb-0.0.1.tgz","fileCount":5,"unpackedSize":66539,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCIGQKtj+V6kM+TANU96KxYT2ROTCwl5oOU8vg2WmUgqZHAiEA0q+Sxab7b/obc3qoxXqF5xwd360rDiKEf52UUu2Hax4="}]},"_npmUser":{"name":"aptxyz","email":"aptxyz@gmail.com"},"directories":{},"maintainers":[{"name":"aptxyz","email":"aptxyz@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/lsb_0.0.1_1788657570562_0.7261554362425078"},"_hasShrinkwrap":false}},"time":{"created":"2026-09-06T01:19:30.390Z","0.0.1":"2026-09-06T01:19:30.740Z","modified":"2026-09-06T01:19:30.948Z"},"maintainers":[{"name":"aptxyz","email":"aptxyz@gmail.com"}],"description":"LSB steganography CLI + Node.js library: hide text or binary payloads inside image least-significant bits with a 48-bit four-corner metadata header","homepage":"https://github.com/aptxyz/lsb#readme","keywords":["steganography","lsb","least-significant-bit","hide","secret","png","image","cli"],"repository":{"type":"git","url":"git+https://github.com/aptxyz/lsb.git"},"author":{"name":"aptxyz","url":"https://github.com/aptxyz"},"bugs":{"url":"https://github.com/aptxyz/lsb/issues"},"license":"MIT","readme":"# @aptxyz/lsb\n\n[![npm version](https://img.shields.io/npm/v/@aptxyz%2Flsb.svg)](https://www.npmjs.com/package/@aptxyz/lsb)\n[![npm downloads](https://img.shields.io/npm/dm/@aptxyz%2Flsb.svg)](https://www.npmjs.com/package/@aptxyz/lsb)\n[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)\n[![Node](https://img.shields.io/node/v/@aptxyz%2Flsb)](https://www.npmjs.com/package/@aptxyz/lsb)\n\nLSB (least-significant-bit) steganography tool for images — **dual-mode**: use it as a\nglobal **CLI** (`lsb` / `lsbjs`) or as a pure **Node.js library** (`require('@aptxyz/lsb')`).\n\nIt hides text or binary payloads inside the lowest bits of image channels, supports\n1–8 bits per channel, keeps the visual change imperceptible at low bit counts, and\nstores a 48-bit metadata header in the four corner pixels of the image so decoding\nnever depends on PNG text chunks.\n\nCross-platform: Windows, Linux and macOS.\n\n---\n\n## Features\n\n- Accepts common input formats: **png, jpg, bmp, gif** — output is always PNG.\n- `-a` / `forceRgba`: force-convert the image to **RGBA** before embedding.\n  Without `-a`, the original channel layout is preserved: **RGB stays RGB,\n  RGBA stays RGBA** (grayscale is promoted to RGB).\n- Configurable **1–8 LSB bits per channel**: 1 = most stealth, 8 = full-channel\n  raw storage. Encoding defaults to **3 bits per channel** when `-b` is omitted.\n- **Lowest-bits auto-selection**: `-b` is an upper bound — if the payload already\n  fits the canvas at fewer bits, the lowest such count is used (e.g. `-b 8` on a\n  tiny message embeds at 1 bit), disturbing the image as little as possible.\n  Opt out with `--exact-bits` (`autoBits: false` in the library).\n- **Auto-scaling**: if the image is too small for the payload, it is grown with\n  the smallest **aspect-preserving nearest-neighbour upscale that fits** — not\n  blindly doubled (an incompressible 791-byte payload at lsb 1 in RGB needs\n  6328 carrier channels = 2116 pixels, so a 32×32 input grows to 46×46, not 64×64).\n- Payloads up to **16 MB** (binary or UTF-8 text).\n- **zlib compression** ([pako](https://www.npmjs.com/package/pako), MIT): text-like\n  payloads are deflated before embedding when that makes them smaller — the 1.9 MB\n  西游记 shrinks to 914 KB. Already-compressed inputs (jpg, zip, ...) are stored\n  raw. A 1-byte store tag records the choice and is stripped on decode.\n- Payload bits are **scattered across the whole canvas with an even stride**,\n  not written sequentially from the start.\n- Image decoding/re-encoding via [sharp](https://sharp.pixelplumbing.com)\n  (Apache-2.0; ships unmodified prebuilt libvips binaries).\n- **Automatic output filenames on decode**: without `-o`, the payload file is\n  written under the original payload file name recorded in the PNG iTXt chunk\n  at encode time, falling back to extension detection from the payload's magic\n  bytes (.png/.jpg/.bmp/.gif/.zip/...), then to `.txt`.\n- CRC-5 integrity check on decode; magic marker validation.\n- Pure library core (no console/file I/O) + thin CLI layer.\n\n---\n\n## Install\n\nGlobal install (provides the `lsb` and `lsbjs` commands):\n\n```bash\nnpm install -g @aptxyz/lsb\n```\n\nLocal install (library use, or `npx`-style CLI access):\n\n```bash\nnpm install @aptxyz/lsb\n```\n\nTwo CLI commands are installed: `lsb` (primary) and `lsbjs` (fallback name).\nIf `lsb` conflicts with another tool on your system, use `lsbjs` instead —\nboth commands are identical.\n\n---\n\n## CLI usage\n\n```\nlsb -i image.png -e text.txt|'direct message' -b <1-8> -o out.png [-a] [--exact-bits] [-d]\n```\n\n| Flag | Meaning |\n|------|---------|\n| `-i <path>` | Input image (png, jpg, bmp, gif). |\n| `-e <file\\|text>` | Payload (\"encrypt\"): if the value exists as a file it is read as the payload (text, image, or any other file up to 16 MB), otherwise it is used directly as a UTF-8 text message. |\n| `-b <1-8>` | LSB bits per channel **upper bound**. Defaults to **3** when omitted. The lowest count that already fits the payload is used (e.g. `-b 8` embeds at 1 bit for a tiny payload); 1 = most stealth, 8 = full-channel raw storage. |\n| `-o <path>` | Output path, relative to the current directory. Encode: output PNG (default `<input-name>_lsb.png`). Decode: payload file (default `<input-name>_lsb.txt`). An existing `_lsb` suffix in the input name is reused, never doubled. |\n| `-a` | Force-rgba: convert input to RGBA. Without `-a`, original RGB/RGBA layout is preserved. |\n| `-d` | Decode mode: extract the hidden payload. Text-like payloads are printed to stdout **and** written to the payload file; binary payloads are only written to the file and reported on stderr as `decrypted to <file>`. Without `-o`, the output name is resolved automatically (see below). |\n| `--exact-bits` | Disable the automatic bit-count downgrade: embed with exactly the `-b` count even when a lower one would suffice. |\n| `--help` | Print help, including cross-platform quoting examples. |\n\n### Linux / macOS\n\nSingle-quote messages that contain spaces:\n\n```bash\nlsb -i photo.png -e 'hello world' -b 3 -o secret.png          # RGB stays RGB\nlsb -i photo.jpg -e ./payload.bin -b 2 -a -o secret-rgba.png  # forced RGBA\nlsb -i photo.png -e 'hello world'                             # -> photo_lsb.png\nlsb -i photo_lsb.png -d > extracted.txt                       # stdout + photo_lsb.txt\n```\n\n### Windows CMD\n\nUse double quotes for messages with spaces:\n\n```bat\nlsb -i photo.png -e \"hello world\" -b 3 -o secret.png\nlsb -i photo.jpg -a -e payload.bin -b 2 -o secret-rgba.png\nlsb -i secret.png -d > extracted.txt\n```\n\n### Windows PowerShell\n\n```powershell\nlsb -i photo.png -e \"hello world\" -b 3 -o secret.png\nlsb -i secret.png -d\n```\n\n### Automatic output-filename resolution (decode)\n\nWithout an explicit `-o`, the decode output file name is resolved in this\nstrict priority order:\n\n1. **`-o <path>`** — the user's explicit choice always wins; any filename\n   stored in the PNG metadata is ignored.\n2. **PNG iTXt metadata** — the original payload file name recorded at encode\n   time (e.g. `holiday photo.png`) is used as the output name. This chunk is\n   human-readable **auxiliary** metadata only; it is never trusted for payload\n   data, and **third-party image software may strip PNG text chunks**, in\n   which case the hint is silently lost.\n3. **Content detection** — the extension is inferred from the payload's magic\n   bytes (`.png`, `.jpg`, `.bmp`, `.gif`, `.zip`, `.gz`, `.pdf`), with UTF-8\n   text detected as `.txt` and anything unrecognized defaulting to `.txt`.\n   The name becomes `<input-stem>_lsb<inferred-ext>`.\n\nNames arriving from the iTXt chunk (or the CLI) are sanitized before use:\ndirectory components are stripped, control characters removed, and Windows\nreserved device names guarded — the output always stays inside the current\ndirectory. The 48-bit four-corner-pixel header remains the single source of\ntruth for the payload itself.\n\n### Command-name conflict\n\nThe primary bin command `lsb` may conflict with other npm packages that install a\nbinary of the same name. The fallback command name `lsbjs`\nis installed at the same time — use it whenever `lsb` is taken or behaves like a different tool:\n\n```bash\nlsb -i photo.png -e 'secret text' -b 3 -o secret.png\n```\n\n---\n\n## Visual examples\n\nThree worked examples, each hiding a payload in a cover image at every LSB depth 1–8.\n\n### Group: payload `西游记` → cover `godness.png`\n\n**Original cover image:**\n\n<table><tr>\n<td align=\"center\"><img src=\"demo/godness.png\" width=\"190\" alt=\"godness.png\"/></td>\n</tr></table>\n\n<table><tr>\n<td align=\"center\"><img src=\"demo/godness_西游记_lsb1.png\" width=\"190\" alt=\"lsb1\"/><br/><sub>1bit 1353x1353 2.30Mb</sub></td>\n<td align=\"center\"><img src=\"demo/godness_西游记_lsb2.png\" width=\"190\" alt=\"lsb2\"/><br/><sub>2bit 957x957 1.95Mb</sub></td>\n<td align=\"center\"><img src=\"demo/godness_西游记_lsb3.png\" width=\"190\" alt=\"lsb3\"/><br/><sub>3bit 781x781 1.60Mb</sub></td>\n<td align=\"center\"><img src=\"demo/godness_西游记_lsb4.png\" width=\"190\" alt=\"lsb4\"/><br/><sub>4bit 677x677 1.35Mb</sub></td>\n</tr></table>\n\n<table><tr>\n<td align=\"center\"><img src=\"demo/godness_西游记_lsb5.png\" width=\"190\" alt=\"lsb5\"/><br/><sub>5bit 605x605 1.15Mb</sub></td>\n<td align=\"center\"><img src=\"demo/godness_西游记_lsb6.png\" width=\"190\" alt=\"lsb6\"/><br/><sub>6bit 552x552 1.01Mb</sub></td>\n<td align=\"center\"><img src=\"demo/godness_西游记_lsb7.png\" width=\"190\" alt=\"lsb7\"/><br/><sub>7bit 512x512 951Kb</sub></td>\n<td align=\"center\"><img src=\"demo/godness_西游记_lsb8.png\" width=\"190\" alt=\"lsb8\"/><br/><sub>8bit 479x479 899Kb</sub></td>\n</tr></table>\n\nDecode any of them: `lsb -i demo/godness_西游记_lsb1.png -d`.\n\n### Group: payload `将进酒` → cover `angry.png`\n\n<table><tr>\n<td align=\"center\"><img src=\"demo/angry.png\" alt=\"original\"/><br/><sub>original 162x162</sub></td>\n<td align=\"center\"><img src=\"demo/angry_将进酒_lsb1.png\" alt=\"lsb1\"/><br/><sub>1bit 162x162 19Kb</sub></td>\n</tr></table>\n\nIt's always 1bit since the payload is so small, decode with: `lsb -i demo/angry_将进酒_lsb1.png -d`.\n\n### Group: payload `angry.png (binary)` → cover `godness.png`\n\n<table><tr>\n<td align=\"center\"><img src=\"demo/godness.png\" alt=\"original\"/><br/><sub>original 162x162</sub></td>\n<td align=\"center\"><img src=\"demo/angry.png\" alt=\"payload\"/><br/><sub>payload 162x162</sub></td>\n<td align=\"center\"><img src=\"demo/godness_angry_lsb1.png\"alt=\"lsb1\"/><br/><sub>1bit 181x181 60Kb</sub></td>\n<td align=\"center\"><img src=\"demo/godness_angry_lsb2.png\"alt=\"lsb2\"/><br/><sub>2bit 162x162 53Kb</sub></td>\n</tr></table>\n\n2bit is enough to hide the entire payload.  Decode any of them: `lsb -i demo/godness_angry_lsb1.png -d` (writes the payload back as a file).\n\n---\n\n## Library usage\n\n```js\nconst lsb = require('@aptxyz/lsb');\nconst fs = require('fs');\n\n(async () => {\n  const image = fs.readFileSync('photo.png');\n\n  // Encode (payload up to 16 MB)\n  const enc = await lsb.encodeLSB(image, Buffer.from('hello world', 'utf8'), {\n    lsbBits: 3,       // 1-8, bits per channel\n    forceRgba: false, // true -> convert to RGBA; false -> keep original channels\n    autoGrow: true    // grow the canvas minimally when the payload does not fit\n  });\n  fs.writeFileSync('secret.png', enc.data);\n  console.log(enc.storeTag, enc.storedBytes, 'of', enc.payloadBytes, 'bytes');\n\n  // Decode (inflate happens automatically when the payload was deflated)\n  const dec = await lsb.decodeLSB(fs.readFileSync('secret.png'));\n  console.log(dec.data.toString('utf8')); // \"hello world\"\n  console.log(dec.lsbBits, dec.stride, dec.payloadBytes);\n  console.log(dec.outputFilename);        // filename hint from the iTXt chunk (or null)\n})().catch((err) => {\n  console.error(err.message);\n  process.exit(1);\n});\n```\n\nThe library exports pure async functions — `encodeLSB(imageBuffer, payloadBuffer, opts)`\nand `decodeLSB(imageBuffer, opts)` — and never touches `process.argv`, `console.log`,\n`process.exit` or the file system. All terminal and file I/O belongs to the CLI layer\n(`src/cli.js`).\n\n`opts`:\n\n| Option | Type | Meaning |\n|--------|------|---------|\n| `lsbBits` | number (1–8) | Bits per channel. Required for `encodeLSB`; optional override for `decodeLSB` (otherwise read from the header). |\n| `forceRgba` | boolean | `true` converts the image to RGBA; `false` (default) preserves the original RGB/RGBA layout. |\n| `autoGrow` | boolean | `true` grows the canvas with the smallest aspect-preserving upscale that fits the payload. |\n| `storeTag` | boolean | `true` (default) deflate-compresses the payload when smaller and prefixes a 1-byte store tag; `false` stores raw (tag byte only). |\n| `autoBits` | boolean | `true` (default) downgrades `lsbBits` to the lowest count that fits the payload on the original canvas; `false` forces exactly the requested count. The result reports `lsbBits` (used), `requestedBits` and `autoBits`. |\n| `payloadFilename` | string | Original payload file name, stored in the PNG iTXt chunk as a decode-time output-filename hint. Auxiliary only; sanitized (directory components stripped) before storing. |\n| `pngMetadata` | boolean | `true` (default) writes the `lsbjs-meta` iTXt chunk into the PNG; `false` skips all PNG text metadata. |\n\n---\n\n## Metadata specification\n\nA **48-bit metadata header** is stored in the **four corner pixels**\n(top-left, top-right, bottom-left, bottom-right). These four pixels are reserved\nfor the header and never carry payload bits.\n\nPer-corner metadata encoding — fixed **12 bits per corner pixel**:\n\n- **RGB image (3 channels):** 4 bits of metadata per channel → 4 + 4 + 4 = 12 bits\n- **RGBA image (4 channels):** 3 bits of metadata per channel → 3 + 3 + 3 + 3 = 12 bits\n\nFour corner pixels × 12 bits = **48 bits total**, allocated exactly as:\n\n| Field | Bits | Content |\n|-------|------|---------|\n| Magic marker | 16 | GBK encoding of the Chinese character `密` (0xC3 0xDC) |\n| LSB bits | 3 | LSB bit count used for embedding (1–8, stored as value−1) |\n| Payload length | 24 | **Stored** size in bytes (after compression + 1-byte store tag), max **16 MB** (16,777,215 bytes) |\n| CRC-5 | 5 | Checksum over the stored bytes |\n\n### Payload scattering\n\nWhen the payload is smaller than the total available steganography capacity\n(after subtracting the 4 corner metadata pixels), payload bits are distributed\nevenly across the whole canvas with a fixed interval stride:\n\n```\nstored_bytes    = 1 (store tag) + deflate(payload) if smaller else payload\navailable_slots = width * height * channels − 4 * channels\nslots_needed    = ceil(stored_bits / lsb_bits)\nslot_k          = floor(k * available_slots / slots_needed)\n```\n\nCarrier channel-bytes are spread evenly over the whole canvas (per channel\nbyte, not per pixel), so a payload smaller than capacity still reaches every\nregion of the image instead of being packed into the top rows. Example from\nthe spec: payload 256 bits, 4080 available slots → LSB-1-bit average stride\n≈ 15; LSB-2-bit ≈ 31. The walk is recomputed on decode to visit the same\nslot sequence.\n\n### PNG metadata rule\n\nPNG supports tEXt/zTXt/iTXt text chunks, but the main payload is **never** stored\nin PNG metadata chunks — many applications strip text chunks during save/export.\nThe **four-corner-pixel header is the primary, authoritative source of truth** for\ndecoding. The encoder writes a duplicated, human-readable copy of the metadata\n(magic, lsb-bits, payload length, store tag, **original payload file name**, crc5)\ninto a PNG iTXt chunk (`lsbjs-meta`) as an auxiliary convenience — decode logic\ndeliberately **ignores iTXt chunks for payload data** and reads only the\nfour-corner-pixel header. The filename field is used purely as an output-name\nhint (see priority rules above) and may be absent when third-party software\nstrips text chunks.\n\n### Decode flow\n\n1. Read the four corner pixels.\n2. Detect the channel layout (RGB/RGBA) and reconstruct the 48-bit header\n   (RGB: 4 bits per channel; RGBA: 3 bits per channel).\n3. Parse the header: validate the magic marker `密` (GBK 16-bit), read the\n   lsb-bit mode, stored byte length and CRC-5.\n4. Extract the scattered payload bits using the stored stride and verify CRC-5.\n5. Strip the 1-byte store tag; inflate (pako) when it is `0x01`.\n   A tag byte of `0x00` (or a payload without a recognized store tag) is returned as-is.\n\n### Capacity\n\nRequired image dimensions are calculated from the channel count (RGB = 3,\nRGBA = 4), with the four corner metadata pixels reserved:\n\n```\ncapacity_bits = (width * height * channels − 4 * channels) * lsb_bits\n```\n\nIf the payload does not fit, the image is grown with the smallest\n**aspect-preserving nearest-neighbour upscale** that provides enough capacity\n(not blindly doubled); the library behaves the same way when called with\n`autoGrow: true`.\n\n---\n\n## Development\n\n```bash\ngit clone https://github.com/aptxyz/lsb.git lsb\ncd lsb\nnpm install\n\n# Run the round-trip self test\nnpm test\n\n# Link the package globally so `lsb` / `lsbjs` are on PATH during development\nnpm link\n\n# ... test the CLI ...\nlsb -i sample.png -e 'dev test' -b 3 -o dev-out.png\nlsb -i dev-out.png -d\n\n# Remove the global links when done\nnpm unlink -g @aptxyz/lsb\n```\n\n---\n\n## Known issues\n\n- **Bin command name conflict:** the primary `lsb` command can collide with other\n  npm packages installing a binary of the same name. Use the fallback `lsbjs`\n  command in that case.\n- **Windows global PATH:** after `npm install -g @aptxyz/lsb`, if `lsb` is not found in\n  CMD/PowerShell, make sure `%APPDATA%\\npm` is on your `PATH` (the npm global bin\n  directory). Re-open the terminal after installing, or run\n  `npm config get prefix` to locate the folder and add it manually.\n- **PNG text chunks are fragile:** third-party software (editors, social platforms,\n  screenshot tools) may strip PNG iTXt/tEXt chunks when re-saving. `lsb` never\n  relies on those chunks for decoding — the payload always survives. The stored\n  payload filename (an iTXt convenience) is lost with them, and automatic\n  output-filename resolution then falls back to extension detection (`.txt`\n  default); pass `-o` to pin the output name.\n- **Resize/crop destroys hidden data:** scaling, cropping, re-compressing or\n  re-encoding the stego image alters pixels and will destroy the hidden payload\n  (decode fails on magic/CRC checks by design).\n- **RGB output compatibility:** this tool preserves RGB (no-alpha) output by\n  default; a handful of older or specialized tools are picky about PNGs without an\n  alpha channel. If a downstream tool refuses to open an RGB PNG, embed with `-a`\n  to produce RGBA output instead.\n- **Lossy formats destroy data:** the stego container must be PNG. Re-saving the\n  stego image as JPEG (or any lossy format) wipes the least-significant bits.\n\n---\n\n## License\n\nMIT\n","readmeFilename":"README.md","_rev":"1-0f09c450123424f83a5989aa23edfc1f"}