wbpdv is a steganography command-line tool for concealing any file type via a WebP image.

Demo Image - KEY: YT2m-hZFa-TFqa-3kC9-6amo-qyvh
There is also a Web edition, which you can use immediately, as a convenient alternative to downloading and compiling the CLI source code.
Unlike the common steganography method of concealing data within the pixels of a cover image (LSB), wbpdv embeds files within Chunks of a WebP image, such as iCCP (Mastodon/Tumblr), EXIF & XMP (Bluesky).
| Conceal Mode | Storage Method | Compatible Platforms |
|---|---|---|
| (no option) | iCCP Profile Chunk | Mastodon, Tumblr |
| -b | EXIF / XMP Chunks | Bluesky only |
You can conceal any file type up to 1GiB, although compatible sites (listed below) have their own much smaller size limits and other requirements.
Your data file is compressed with libdeflate/zlib, then encrypted with XChaCha20-Poly1305 (libsodium secretstream) under a 256-bit encryption key derived by Argon2id from a randomly generated 128-bit recovery KEY, and finally embedded in the cover image.
The KEY is displayed once, at the end of conceal, printed as 24 Base58 characters (letters and digits) in six groups of four (XXXX-XXXX-XXXX-XXXX-XXXX-XXXX) and is not saved by the program. Without it, the concealed file cannot be recovered. Its last two characters form a checksum, so most typing mistakes are reported straight away.
Images created before wbpdv 4.0 used a numeric recovery PIN instead. They remain recoverable with that PIN (leading zeros are ignored, as before): recover reads the image's version and prompts for a KEY or PIN to match.
$ sudo apt update
$ sudo apt install g++ cmake ninja-build util-linux libsodium-dev zlib1g-dev libdeflate-dev libwebp-dev
$ chmod +x compile_wbpdv.sh
$ ./compile_wbpdv.sh
$ sudo cp wbpdv /usr/bin
$ wbpdv
Usage: wbpdv conceal [-b] <cover_image> <secret_file>
wbpdv recover <cover_image>
wbpdv --info
$ wbpdv conceal your_cover_image.webp your_secret_file.doc
Saved "file-embedded" WEBP image: wbpdv_129462.webp (143029 bytes).
Recovery KEY: [***1vSe-caX6-dEbg-dhT6-q3vp-dizz***]
Important: Keep your KEY safe, so that you can extract the hidden file.
Complete!
$ wbpdv recover wbpdv_129462.webp
KEY: ****-****-****-****-****-****
Extracted hidden file: your_secret_file.doc (6165 bytes).
Complete! Please check your file.
Posting size limit measured by the combined size of the cover image + your compressed hidden data file (payload):
● Mastodon (16MiB)
● Tumblr (9MiB)
● Bluesky (2,000,000 bytes | ~1.9MiB)
wbpdv mode arguments:
conceal - Compresses, encrypts and embeds your secret data file within a WebP cover image.
recover - Decrypts, uncompresses and extracts the concealed data file from a WebP cover image (recovery KEY/PIN required).
The recover prompt says which secret the image takes: KEY: for an image made with wbpdv 4.0 or later, PIN: for an older one. Error messages follow the same rule.
The KEY printed by conceal is the only copy. If it cannot be written out (for example, output redirected to a full disk or a closed pipe), conceal deletes the image it just saved and exits with an error, as that image could never be recovered.
When the KEY/PIN is typed at a terminal, each character is masked. If recover cannot turn off the terminal's echo, it refuses to read the KEY/PIN rather than show it on screen. A KEY/PIN piped in from another program is still accepted.
wbpdv conceal mode platform option:
"-b" To create compatible "file-embedded" WEBP images for posting on the Bluesky platform, you must use the -b option with conceal mode.
$ wbpdv conceal -b my_image.webp hidden.docThese images are only compatible for posting on Bluesky. Your embedded data file will be removed if posted on a different platform.
You are also required to use the Python script create_bsky_post.py (found in the repo src/bsky folder) to post the image to Bluesky. It will not work if you post images to Bluesky via the browser site or mobile app.
To use the script, you will need to create an app password from your Bluesky account.
See the create_bsky_post.py script in the src/bsky folder for some basic usage examples.
wbpdv_cli_txt.mp4
wbpdv_web_jpg.mp4
wbpdv_cli_bsky_jpg.mp4
wbpdv_cli_mastodon_mp3.mp4
wbpdv_cli_tumblr_jpg.mp4
This section is for anyone writing a compatible implementation. Everything below is fixed by wbpdv 4.0.
The KEY is 16 bytes from a cryptographic random number generator (never all zero). It is written as:
-
Body — the 16 bytes read as one 128-bit big-endian integer, written in Base58 as exactly 22 digits, most significant first, padded on the left with
1(the zero digit). Alphabet (Bitcoin's):123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz. -
Check — two more characters:
digest = BLAKE2b-256(ASCII("wbpdv key check v1") || key_bytes) (18-byte label, no NUL; unkeyed) value = big_endian_u32(digest[0..4]) mod 3364 check = alphabet[value / 58] || alphabet[value mod 58] -
Display — the 24 characters in six groups of four, joined with
-.
When a KEY is entered, spaces, tabs and - are ignored and letters are case-sensitive. A numeric entry is an old (pre-4.0) PIN: leading zeros are ignored, then 1–20 digits remain (at most 18446744073709551615, never 0).
| Key bytes (hex) | KEY |
|---|---|
00000000000000000000000000000001 |
1111-1111-1111-1111-1111-12NH |
00000000000000000000000000000f2c |
1111-1111-1111-1111-1112-9y11 (check value 0) |
0000000000000000000000000000057a |
1111-1111-1111-1111-1111-RBzz (check value 3363) |
000102030405060708090a0b0c0d0e0f |
112d-rXXU-ifSr-RnXL-GbXg-8Egn |
80000000000000000000000000000000 |
GokL-Usho-3eiV-vNYN-d1wg-fyVk |
ffffffffffffffffffffffffffffffff |
YcVf-xkQb-6JRz-qk5k-F2tN-LvN3 |
Encryption key — Argon2id (libsodium crypto_pwhash, ALG_ARGON2ID13, opslimit 2, memlimit 64 MiB, both recorded in the image) with the 16 raw key bytes as the password and the random salt stored in the image (metadata version KDF4, which has KDF3's layout). Old images (KDF2 and KDF3) hash the PIN's decimal digits instead.
-
libsodium — cryptographic random generation, Argon2id key derivation and XChaCha20-Poly1305 secret streams. Dynamically linked as a system library.
License: ISC License
Copyright (c) 2013–2026 Frank Denis.
-
zlib — Streaming zlib compression and decompression. Dynamically linked as a system library.
License: zlib License
Copyright (C) 1995–2026 Jean-loup Gailly and Mark Adler.
-
libdeflate — Fast whole-buffer zlib-format compression. Dynamically linked as a system library.
License: MIT
Copyright 2016 Eric Biggers.
Copyright 2024 Google LLC.
-
libwebp — WebP validation, decoding, and encoding. Dynamically linked as a system library.
License: BSD 3-Clause, with an additional patent grant.
Copyright (c) 2010, Google Inc. All rights reserved.
-
Compact ICC Profiles — A modified sRGB-v2-micro.icc profile is embedded in the standard WebP template.
License: CC0 1.0 Universal.
-
Bryan Newbold / ATProto Hacker Cookbook — create_bsky_post.py — Basis for the forked Bluesky posting helper (src/bsky/create_bsky_post.py). For reference see the Cookbook copy
License: CC0 1.0 Universal.
-
Requests — HTTP and Bluesky API requests.
License: Apache 2.0 / NOTICE
Copyright 2019 Kenneth Reitz.
-
Beautiful Soup 4 — HTML and Open Graph metadata parsing.
License: MIT
Copyright (c) Leonard Richardson.
-
Pillow — Image validation, dimensions, and aspect-ratio handling.
License: MIT-CMU
PIL copyright © 1997–2011 Secret Labs AB and © 1995–2011 Fredrik Lundh and contributors.
Pillow copyright © 2010 Jeffrey “Alex” Clark and contributors.