A fully client-side browser tool for:
- 🔐 Encrypting messages with AES-GCM 256 (key derived via PBKDF2-SHA256 with 310,000 iterations)
- 🖼️ Hiding the encrypted message inside a PNG image using LSB steganography on the R, G and B channels
- 🔗 Building short encrypted links carrying the payload in the URL fragment (
#m=...) - 🔓 Recovering the message from an image or an encrypted link with a password
- 📁 Downloading the encrypted payload as a file (
.secret.txt) for messages that are too large for a link - 📱 Right-to-left (RTL), mobile-friendly design with built-in dark / light theme support
No data is ever sent to a server. The message, password and image are processed exclusively on your device. No analytics, tracking or external services are used.
- ✅ Five complete tabs: hide in image, recover from image, create link, open link, about / security notes
- ✅ Drag & drop image upload with live preview
- ✅ Image capacity meter and live estimate of the size required for the current message
- ✅ Secure random password generator powered by Web Crypto
- ✅ Client-side password strength meter with helpful hints
- ✅ PBKDF2 with 310,000 iterations, fresh random salt and IV for every message
- ✅ JPEG warning to flag images that may have been re-compressed
- ✅ QR Code for the encrypted link (rendered locally with
qrcode.react, no external service) - ✅ Graceful fallback when the payload is too large for a URL — a downloadable encrypted payload file is offered instead
- ✅ Hash cleanup from the URL after a successful decryption
- ✅ Toast notifications for success / error feedback (no ugly
alert()) - ✅ No React Router, no Redux, no heavy UI kit
- ✅ TypeScript strict, fully type-safe
- ✅ RTL layout with system font stack (no external font CDN)
- React 18 + TypeScript (strict)
- Vite 5 for dev server and production build
- Tailwind CSS 3 for styling
- Web Crypto API for cryptography (no JS crypto library)
- qrcode.react (installed locally via npm, no external API)
- Zero heavy dependencies, no UI kit, no state manager
npm install
npm run devThen open http://localhost:5173 in your browser.
Important: The Web Crypto API only works on HTTPS or localhost. Opening the page over plain HTTP will disable all cryptographic features.
npm run buildThe output is written to the dist/ folder and is fully static.
npm run previewnpm run typecheck- Push the repository to GitHub / GitLab / Bitbucket.
- In Vercel click Add New → Project and import the repository.
- Vercel will auto-detect everything:
- Framework Preset: Vite
- Build Command:
npm run build - Output Directory:
dist
- Click Deploy.
No environment variables are required. No serverless functions are used. The entire project is served as a static site from the Vercel CDN.
A vercel.json is included in the project root for SPA fallback. It is safe
to keep; remove it only if you know you do not need it.
Because the build output is just static files, you can deploy the dist/
folder to any static host:
- Netlify:
Build command: npm run build|Publish directory: dist - GitHub Pages: via GitHub Actions or
gh-pages - Cloudflare Pages:
Build command: npm run build|Output: dist
⚠️ The encrypted link is NOT truly one-time-use. Anyone who has both the link and the password can open the message. The link can also leak via browser history, server logs, or URL-scanning tools.
- ✅ Always send the password through a separate channel (for example via an end-to-end encrypted messenger or a phone call)
- ✅ Send the PNG as the original file, not pasted into a chat
- ❌ Do not post the image on social networks — most of them re-compress uploaded images, which destroys the hidden message
- ❌ Do not use JPEG — JPEG uses lossy compression
- ✅ Use a strong password (at least 8 characters; 12+ with mixed case, digits and symbols is much better)
- ❌ Do not rely on this tool for high-stakes or corporate use — it is intended for educational, personal and casual use
- HTTPS or localhost is required for the Web Crypto API to be available
- The URL payload is capped at about 2 KB of base64url. For larger messages, use the PNG image output or download the payload file
- Older browsers may not support the Web Crypto API or some modern APIs
- If you lose the original image, the hidden message is gone forever
secret-image-react/
├── index.html
├── package.json
├── tsconfig.json
├── vite.config.ts
├── tailwind.config.js
├── postcss.config.js
├── vercel.json
├── README.md
└── src/
├── main.tsx
├── App.tsx
├── index.css
├── types/
│ └── index.ts
├── constants/
│ └── index.ts
├── lib/
│ ├── crypto.ts # AES-GCM + PBKDF2
│ ├── base64url.ts # base64url helpers
│ ├── stego.ts # LSB embedding / extraction
│ ├── payload.ts # link & filename builders
│ ├── utils.ts # download, formatBytes, etc.
│ └── validators.ts # input validation
├── hooks/
│ ├── useToast.tsx
│ ├── useTheme.ts
│ ├── useClipboard.ts
│ ├── usePasswordStrength.ts
│ ├── useDragAndDrop.ts
│ ├── useImageData.ts
│ └── useHashPayload.ts
└── components/
├── layout/
│ ├── Container.tsx
│ ├── Header.tsx
│ ├── Tabs.tsx
│ └── Footer.tsx
├── ui/
│ ├── Button.tsx
│ ├── Card.tsx
│ ├── Input.tsx
│ ├── TextArea.tsx
│ ├── PasswordInput.tsx
│ ├── PasswordStrength.tsx
│ ├── DropZone.tsx
│ ├── Alert.tsx
│ ├── Toast.tsx
│ ├── Spinner.tsx
│ ├── CopyButton.tsx
│ ├── QRBox.tsx
│ ├── EmptyState.tsx
│ ├── Modal.tsx
│ └── CapacityMeter.tsx
└── tabs/
├── EncodeImageTab.tsx
├── DecodeImageTab.tsx
├── CreateLinkTab.tsx
├── OpenLinkTab.tsx
└── AboutTab.tsx
stream = 4 bytes payloadLength (big-endian) | payload
payload = 4 bytes magic "HMSG"
| 1 byte version (1)
| 16 bytes salt
| 12 bytes iv
| 4 bytes ciphertextLength (big-endian)
| ciphertext (AES-GCM output, includes the 16-byte auth tag)
- All lengths are stored in big-endian byte order
- PBKDF2-SHA256 with 310,000 iterations
- AES-GCM with a 256-bit key
- A fresh random salt and IV are generated for every message
This project is released for free use in personal, educational and commercial projects. No user data is ever collected.