Free, client-side privacy and developer toolbox — 53 tools, zero server uploads.
A comprehensive single-page web app with 53 cryptographic, encoding, parsing, and developer utilities that run entirely in your browser. PGP key generation, key inspection, revocation certificates, QR public-key sharing, QR image decoding (hand-rolled, Reed-Solomon corrected), TLS / X.509 certificate parsing, OpenSSH public key parsing, PEM ↔ DER conversion, generic ASN.1 / DER (X.690) decoding, CSR (PKCS#10) decoding with self-signature verification, JWK ↔ PEM key conversion, BIP-0039 mnemonic generation and validation, file and text encryption / decryption, password-only (symmetric) file encryption, image steganography, age file encryption (X25519 recipients or scrypt passphrase, interoperable with the age CLI), digital signing and verification, HMAC (hex + Base64), JWT inspection and verification (HS / RS / ES / EdDSA, PEM / JWK / JWKS), password-based key derivation (PBKDF2 / scrypt / Argon2id — RFC 8018 / 7914 / 9106), TOTP / HOTP 2FA (RFC 6238 / 4226), password generation, offline passphrase-strength estimation, file-type magic-byte identification, ASCII armor conversion, Shamir's Secret Sharing, EXIF metadata stripping, hashing and checksums (SHA-1 / 256 / 384 / 512, SHA3-256 / 512, SHAKE128 / 256, BLAKE2b, BLAKE3), Base64 / Base32 / Base58 / Base58Check / hex encoding, number base conversion (binary / octal / decimal / hex / any base 2–36), UUID v4 / v5 / v7 and ULID generation, Unix timestamp conversion (epoch ↔ ISO ↔ IANA timezone ↔ ISO week / day-of-year), URL parsing, line-level and unified diff, CSV ↔ JSON ↔ TSV conversion, regex testing, cron decoding, color conversion (HEX / RGB / HSL / OKLCH / CMYK + CSS named colors) with WCAG contrast checking, JSON / YAML / XML formatting and CBOR (RFC 8949) decoding, and CIDR / IPv4 + IPv6 subnet calculation. Searchable command palette (⌘K / Ctrl+K), full keyboard navigation, five UI languages. No server uploads, no analytics, no CDN, no tracking.
Inspired by Kevin Qiu
- Generate PGP Keys — Create ECC (Curve25519) or RSA 3072 / 4096 key pairs with customizable expiry. Optional regulator presets (BSI TR-02102-1, ANSSI RGS B1, NIST SP 800-57, CNSA 2.0, Privacy Guides) auto-fill the algorithm + key size from each regulator's published recommendation and link out to the source document.
- Key Info — Inspect any PGP public key: fingerprint, user IDs, algorithm, key size, creation and expiration dates.
- Revocation Certificate — Generate a pre-signed certificate to retire a compromised key.
- QR Share — Encode a PGP public key or encrypted message as one or more scannable QR codes (multi-QR
EAL-QR/v1/{n}/{total}/for long inputs). - QR Decoder — The inverse: recover the text payload from a QR image (PNG / JPEG / WebP), entirely in-browser — never uploaded. A hand-rolled decoder (no new vendored dependency): Otsu binarisation → finder-based module-grid sampling → BCH-corrected format info → zig-zag codeword walk → block de-interleaving → Reed-Solomon error correction over GF(256), so a code with some damaged modules still decodes. Byte (UTF-8), numeric, and alphanumeric modes; QR versions 1–40; all four EC levels; all eight masks. Scoped to flat, axis-aligned images (screenshots, generated codes, flat scans); reports a clear message when an image is too skewed. Verified offline against the page's own generator across versions 1–18, every EC level and mask, and injected-error cases.
- TLS Certificate Parser — Hand-rolled ASN.1 / DER decoder for X.509 certificates: subject, issuer, SAN, validity, public key algorithm, SHA-1 + SHA-256 fingerprints. Also decodes PKCS#7 / CMS (RFC 5652) SignedData — certs-only bundles (
.p7b/.p7c) and CAdES-BES signatures: it surfaces every embedded certificate plus each signer's digest algorithm, signature algorithm, and signed signing-time. It ingests a PAdES-signed PDF directly (load the file): it pulls each signature dictionary's/ContentsCMS blob out of the PDF in-tab and decodes it the same way. And it decodes a pasted XAdES / XML-DSig signature: XAdES level (B-B / B-T / B-LT / B-LTA), signature + canonicalization methods, reference digests, signing time, and embedded certificates. (This is the §2.5 eIDAS signature inspector — CAdES + PAdES + XAdES, the three eIDAS signature families. No network, no cryptographic-verification or revocation checking — it reports structure + signer identity, not a validity verdict.) - SSH Key Parser — OpenSSH RFC 4253 wire-format parser: key type (rsa / ed25519 / ecdsa-sha2-nistp{256,384,521}), bit size,
SHA256:<base64>fingerprint, comment. - PEM ↔ DER — Round-trip between PEM (Base64 with header/footer) and DER (raw binary as hex).
- ASN.1 / DER Decoder — A generic ITU-T X.690 tag-length-value tree viewer for any DER blob (hex / Base64 / PEM): SEQUENCE, SET, INTEGER, OBJECT IDENTIFIER, the string types, and context-specific tags decode into an indented, typed tree, with OIDs and small integers rendered in full. Complements the specialized X.509 / CMS / CBOR parsers with a raw view for anything they don't cover — a private-key file, an OCSP response, a timestamp token — without an online decoder. Long-form lengths, multi-byte tags, and nesting are handled; malformed input is reported, never thrown.
- CSR (PKCS#10) Decoder — Decode a Certificate Signing Request (PEM or DER, RFC 2986): subject distinguished name, public-key algorithm, requested Subject Alternative Names, and a real self-signature verification. Reuses the hand-rolled X.509 ASN.1 reader; the signed data is the exact DER of
CertificationRequestInfosliced from the original bytes (never re-encoded), so the hash matches byte-for-byte. RSA, ECDSA (with DER → rawr‖sconversion), and Ed25519 signatures verify via Web Crypto. Completes the certificate lifecycle alongside the cert parser. - JWK ↔ PEM Converter — Convert a public JSON Web Key to SPKI PEM and back (RFC 7517 / 7518 / 8037). OKP (Ed25519 / X25519) is hand-rolled both directions — the SPKI wrapper is a fixed 12-byte prefix plus the 32-byte key, so it needs no Web Crypto and works regardless of browser Ed25519 support — while EC (P-256/384/521) and RSA go through Web Crypto import/export. PEM → JWK reads the algorithm OID from the SPKI structure to pick the key type. Bridges the JOSE world (which the JWT verifier speaks) to PEM-consuming TLS / OpenSSL tooling, fully offline.
- BIP-0039 Mnemonic — Generate cryptographically random 12 / 15 / 18 / 21 / 24-word seed phrases (BIP-0039) or validate the checksum of any existing mnemonic. Computes the BIP-0039 PBKDF2-HMAC-SHA512 seed locally with optional passphrase.
- Encrypt Files — Encrypt files with one or more PGP public keys.
- Decrypt Files — Decrypt PGP-encrypted files with your private key and passphrase.
- Text Messages — Encrypt or decrypt PGP text blocks for email, chat, or notes.
- Password Encrypt — Encrypt a file with a passphrase only (symmetric, no PGP key required).
- Steganography — Hide an encrypted payload inside a PNG using LSB encoding. Optional password XORs the payload (wrong password fails magic-byte validation, no garbage output).
- age Encryption — Encrypt and decrypt files with age (the modern PGP successor): to one or more X25519 recipients (
age1…) or a scrypt passphrase, and decrypt with anAGE-SECRET-KEY-1…identity or passphrase. Generate keypairs. ASCII-armored output, byte-compatible with theageCLI andrage. Hand-rolled ChaCha20-Poly1305 / scrypt / bech32 (pure JS); X25519 / HKDF / HMAC via Web Crypto. Interop-verified against the age CLI 1.3.1.
- Sign — Produce detached, attached, or cleartext PGP signatures for files or text using your private key.
- Verify — Verify any PGP signature against a public key.
- HMAC — Sign and verify with HMAC-SHA-1 / 256 / 384 / 512; MAC rendered in both hex and Base64 (RFC 2104). Constant-time comparison on verify; key buffer zeroized after use.
- JWT / credential Inspector — Decode the three Base64-URL parts; verify HS256/384/512 (shared secret), RS256/384/512 (PEM SPKI public key), ES256/384/512 (PEM SPKI ECDSA key), or EdDSA / Ed25519 (RFC 8037). The verification-key field also accepts a JWKS (
{"keys":[…]}) or bare JWK (RFC 7517) — the key matching the token'skidheader is selected and surfaced, then verified across all four JOSE key types (OKP / RSA / EC / oct). Expiry indicator from theexpclaim. Doubles as the EUDI-wallet credential decoder: it decodes SD-JWT (selective-disclosure JWT — disclosures,_sddigest matching, KB-JWT verification,sd_hash/aud/noncechecks) and ISO 18013-5 mdoc (paste the CBOR as hex / base64url — surfaces docType, per-namespace data elements, and the COSE_Sign1 MSO: digest algorithm, validity window, value-digest count, device key, and the x5chain issuer certificate). Decode-only; no issuer-trust verification.
- Strong Password Generator — Cryptographically random passwords with customizable charset and length.
- Passphrase Strength Estimator — Order-of-magnitude entropy and offline crack-time for a candidate passphrase, scored 0–4, entirely offline with no breach-database lookup (unlike HIBP-style checkers, the passphrase never leaves the device). Charset entropy (
bits ≈ length × log2(alphabet)) is treated as an upper bound; a common-password blocklist and repeat/sequence checks pull weak inputs down. Reports the score, estimated bits, crack time at ~10¹⁰ guesses/s, character classes, and any penalty notes. - File Type Identifier — Identify a format from its leading magic bytes — PNG, JPEG, GIF, PDF, ZIP (and OOXML/ODF/JAR), gzip, bzip2, xz, zstd, 7z, RAR, tar, ELF, Mach-O, PE, Java class, WebAssembly, SQLite, Ogg, FLAC, MP3, and RIFF containers (WebP / WAV / AVI). Drop a file (only the leading 64 bytes are read via the File API — never uploaded) or paste hex; reports the format name and MIME type.
- ASCII Armor Converter — Round-trip between PGP binary and ASCII-armored encodings.
- Shamir Secret Sharing — Split a secret into N shares where any K reconstruct it; share format
EAL-SSS/v1/{K}-of-{N}/{rawShare}. A Verify shares step validates each pasted share before combine (format, hex-only body, library component parse), names a corrupted/mistyped share by index, cross-checks K-of-N consistency, and reports whether ≥ K valid shares are present — Shamir reconstruction over GF(256) otherwise silently yields a wrong secret on a bad share. - EXIF Eraser — Strip GPS, camera serial numbers, timestamps from JPEG / PNG / WebP via canvas re-encode.
- Hash & Checksum — SHA-1 / 256 / 384 / 512 (Web Crypto), SHA3-256 / SHA3-512 (FIPS 202, hand-rolled Keccak), BLAKE2b-512 (RFC 7693, hand-rolled), and BLAKE3-256 (hand-rolled, full Merkle-tree mode) over text or files, with constant-time hash comparison. None of SHA-3 or the BLAKE digests are in Web Crypto, so all are hand-rolled inline with no vendored dependency; each is verified against its official test vectors and cross-checked byte-for-byte against the audited
@noble/hashesbuild (and Node's native SHA-3) across every rate / chunk / block boundary. An Advanced panel surfaces the full extendable surface: BLAKE2b with 1–64-byte output and an optional key (keyed MAC, RFC 7693 §3.2); BLAKE3 in XOF (arbitrary 1–1024-byte output), keyed (32-byte key), or derive-key (KDF with a context string) mode; and SHAKE128 / SHAKE256 (FIPS 202 XOFs) with variable output length. - Base64 / 32 / 58 / Hex Encoder — Cross-convert text ↔ hex ↔ Base64 (standard + URL-safe) ↔ Base32 ↔ Base58 (Bitcoin alphabet) ↔ Base58Check (version‖payload‖4-byte SHA-256d checksum, as used by Bitcoin addresses / WIF — decode verifies and strips the checksum, encode appends a fresh one).
- UUID / ULID Generator — UUID v4 (random), UUID v7 (RFC 9562 time-ordered), UUID v5 (RFC 9562 §5.5 deterministic namespace + name, SHA-1), ULID. Bulk generate up to 1000 per click (v5 is deterministic → single value). Verified against the canonical
v5(NS_DNS, "example.com")vector. - Unix Timestamp Converter — Auto-detect epoch seconds / milliseconds / ISO 8601; render in UTC, local, relative ("5 minutes ago"), an explicit IANA timezone (
Intl.DateTimeFormat, Local + UTC + eight common zones), ISO 8601 week (YYYY-Www), and day-of-year. - URL Parser — Parse via the built-in
URLAPI; surface protocol/host/port/path/search/hash/origin and a query-parameter table. Percent-encode and decode utilities. - TOTP / 2FA — RFC 6238 time-based generator and RFC 4226 HOTP counter-based mode (Appendix D vector: counter 0 →
755224); acceptsotpauth://totp/...URIs or bare Base32 secrets. SHA-1/256/512, configurable digits (6–10) and period (15–120). Secret zeroed on Stop. - Diff — Line-level LCS comparison with optional ignore-whitespace and ignore-case toggles, plus a unified-diff export (
--- / +++ / @@hunks, 3 lines of context) that every patch tool consumes. - CSV ↔ JSON ↔ TSV — RFC 4180-style parser (quoted fields, escaped
"", CRLF/LF). Auto-detect delimiter, "first row is header" toggle. - Regex Tester — Live match highlighting, numbered + named capture groups, replacement preview. Built with
createElement/textContent(noinnerHTML). - Cron Decoder — Parse 5-field cron (
*,*/n, ranges, lists, named months/dows); show description and next 5 fire times in local timezone. - Color Converter — HEX ↔ RGB ↔ HSL ↔ OKLCH (hand-rolled Oklab matrices) ↔ CMYK, and accepts the 148 CSS Color Module Level 4 named colors (
rebeccapurple → #663399). WCAG AA / AAA contrast checker with live preview swatch. - JSON / YAML / XML / CBOR Formatter — Pretty-print, minify, and cross-convert JSON ↔ YAML; pretty-print or minify XML standalone; and decode CBOR (RFC 8949) from hex or base64 to JSON / YAML — a hand-rolled decoder covering all eight major types, indefinite lengths, bignums, and semantic tags. It is credential-aware: tag 24 (encoded CBOR data item) is decoded recursively, and well-known tags are named (
$tagName) — so a pasted COSE_Sign1 / CWT / ISO 18013-5 mdoc (EUDI wallet) decodes to a fully readable, self-labelled tree, with the embedded MobileSecurityObject expanded inline. Byte strings show as{$bytes}, tags as{$tag, $tagName, $value}. Auto-detect input format. (TOML omitted — no small browser-ready parser exists without a build step.) - CIDR / Subnet Calculator — IPv4 and IPv6. IPv4: pure 32-bit unsigned arithmetic — network, broadcast, netmask (with binary view), wildcard, first/last usable host, total + usable counts, address class, RFC 1918 / loopback / link-local tags; handles
/0,/31(RFC 3021), and/32correctly. IPv6 (RFC 4291 / RFC 5952): a colon in the address routes to a 128-bitBigIntpath reporting the compressed + expanded network, the last address, the2^(128−prefix)host count, and address-type tags (loopback, link-localfe80::/10, ULAfc00::/7, multicastff00::/8, documentation2001:db8::/32, global-unicast2000::/3). - PBKDF2 / scrypt / Argon2id Key Derivation — a KDF selector over three functions sharing one password/salt/key-length form: PBKDF2 (RFC 8018, Web Crypto; configurable iterations 1–10,000,000 and PRF SHA-1 / 256 / 384 / 512), scrypt (RFC 7914; configurable N / r / p — reuses the page's hand-rolled scrypt, verified against the RFC 7914 §12 vector), and Argon2id (RFC 9106; configurable memory / time / parallelism — reuses the same engine as the dedicated Argon2 tool). Salt as text or hex, key length 1–512 bytes, hex + Base64 output, and wall-clock derivation time so you can size cost to a target.
- Argon2 Key Derivation — RFC 9106 memory-hard password hashing / KDF (Argon2id, Argon2i, Argon2d), the Password Hashing Competition winner recommended by BSI / ANSSI / OWASP. Configurable memory cost, time cost, parallelism, salt, and output length, with hex + PHC-string (
$argon2id$v=19$m=…,t=…,p=…$…) output and wall-clock time. Hand-rolled pure JavaScript on the page's BLAKE2b (no WASM, no vendored dependency, no CSP change), with a flat-Uint32Arraymemory matrix; verified against the RFC 9106 test vectors and@noble/hashes. Recommended presets (OWASP / RFC 9106 / libsodium) one click away. - Number Base Converter — Convert integers between binary, octal, decimal, hexadecimal, and any base 2–36 simultaneously. Auto-detects
0x/0b/0oprefixes; uses BigInt internally so values aren't capped at 53-bit JS Number precision; preserves negatives sign-and-magnitude. - IBAN Validator / Generator — ISO 13616 MOD-97 checksum and per-country structure for ~80 IBAN jurisdictions. Splits out bank / branch / account where the registry defines them. Generator side produces a syntactically valid test IBAN (
crypto.getRandomValuesBBAN, computed check digits) for fixtures and integration tests — clearly labeled "test data only." - BIC / SWIFT Code Checker — ISO 9362 format check for 8- and 11-character BICs. Decodes bank / country / location / branch, cross-checks the country segment against the IBAN country table, flags test BICs (trailing
0), passive participants (trailing1), and primary branches (XXX). - GS1 / EAN / GTIN Barcode — Mod-10 weighted checksum for EAN-8, UPC-A, EAN-13 / GTIN-13, ITF-14 / GTIN-14, and SSCC. Looks up the issuing region from an inline GS1 prefix registry (~130 ranges covering every assigned prefix).
- EU VAT Number Validator — Format check for all 27 EU member states + GB, NO, CH, XI (Northern Ireland). Computes the published checksum where one exists (ISO 7064 MOD 11,10 for DE/HR; Luhn for IT/SE; mod-97 for BE; mod-89 for LU; mod-11 weighted for many others). ES support is limited to the CIF (corporate) subset by design; individual NIF/NIE inputs are explicitly rejected with a pointer to the spec. No VIES round-trip — fully offline.
- Ed25519 Sign / Verify — Generate Ed25519 keypairs and produce or verify signatures via native Web Crypto (RFC 8032). Keys round-trip as the 32-byte seed; signatures as 64-byte hex. Same primitive used by SSH ed25519 keys, age, Signal, Noise, and sigstore. Detects lack of browser support (needs Safari 17 / Chrome 113 / Firefox 130) and shows an explicit banner rather than failing silently.
- X25519 Key Agreement — Generate X25519 keypairs and derive a shared secret (ECDH, RFC 7748) via native Web Crypto, with optional HKDF-SHA-256 / HKDF-SHA-512 post-processing and a user-supplied "info" context. Underlies WireGuard, age, Signal, Noise, X3DH, TLS 1.3.
- PEPPOL / UBL + Factur-X / ZUGFeRD Invoice Inspector — Decode OASIS UBL 2.1
<Invoice>and<CreditNote>documents (PEPPOL BIS Billing 3.0, EN 16931) entirely in the browser, plus UN/CEFACT Cross Industry Invoice (CII) — the XML syntax inside Factur-X / ZUGFeRD / Order-X hybrid PDFs (paste the standalone CII XML or load the hybrid PDF directly — the embedded invoice is extracted in-tab; the same EN 16931 model, thersm:/ram:vocabulary, rendered by a sister walker). Renders the header (customization / profile / invoice number / dates / currency), supplier and customer, totals (sum of line amounts, tax-exclusive, tax-inclusive, payable), line items (quantity, unit price, line amount), and per-rate VAT breakdown. Beyond rendering it runs 100+ EN 16931 / PEPPOL conformance gates (UBL syntax; a parallel CII-native gate family has started — mandatory-header presence, grand-total arithmetic, party-name presence, line presence, currency-code and document-type-code validity, the line-sum (BR-CO-10), tax-basis (BR-CO-13), payable (BR-CO-16) and VAT-breakdown-sum (BR-CO-14) invariants that complete the CII header monetary chain end to end, the per-rate VAT cross-product (BR-CO-17) and category-code (UNCL 5305) validation of the breakdown it rolls up from, seller/buyer country presence (BR-09 / BR-11) and ISO 3166-1 validity, the per-line net cross-product (BR-CO-04) and due-date ≥ issue-date ordering, the specification-identifier (BR-01) presence and tax-scheme type-code (UNCL 5153 "VAT") validity, per-line item-name (BR-25) / billed-quantity unit-code (BT-130) / net-price (BR-26) / quantity (BT-129) presence, plus Factur-X / ZUGFeRD profile identification and seller-VAT (BT-31) validity, twenty-four gates so far — see the CII-native gates cheat sheet below) — mandatory-field presence (BR-*), arithmetic invariants (BR-CO-10 line totals, BR-CO-13/15 tax consistency, BR-CO-14 VAT-breakdown sum, BR-CO-17 per-rate cross-product, BR-CO-16 payable reconciliation), 2-decimal caps (BR-DEC), code-list membership (UNCL 1001 / 4461, ISO 4217, PEPPOL EAS), and IBAN MOD-97 — each catching documents that are XML-valid but rail-rejected by an access point. Namespace-blindDOMParserwalk — same code handles every minor UBL 2.1 revision. Standalone CII XML, Factur-X / ZUGFeRD profile identification, and hybrid PDF/A-3 attachment extraction all ship — load a Factur-X / ZUGFeRD PDF and the embedded invoice XML is inflated in-tab via the nativeDecompressionStream(no PDF library, no network). - SEPA ISO 20022 XML Inspector — Decode
pain.001(credit-transfer initiation),pain.008(direct-debit initiation), andcamt.053(bank-to-customer statement) entirely in the browser. Surfaces the group header, per-payment-info blocks, and per-transaction detail, then runs 110+ EPC SEPA Rulebook conformance gates: IBAN MOD-97 + per-country structure, BIC ISO 9362 format and IBAN↔BIC country parity, EPC character-set restrictions across every free-text field, Rulebook length caps (AdrLine 70 / TwnNm 35 / PstCd 16), mandatory-element presence down to the per-transactionFinInstnIdrouting identifier, EUR-only currency,NbOfTxs/CtrlSumarithmetic invariants, strictly-positive amounts and the €999,999,999.99 per-instruction amount cap, and EndToEndId uniqueness — each catching files that pass XSD validation but get bounced by EBA STEP2 / STET / RT1. Namespace-blind walk; no-ops cleanly on message types a given gate does not apply to. - eIDAS Trust List (LOTL / TSL) Viewer — Parse an EU List of Trusted Lists or a national Trusted Service List (ETSI TS 119 612) and browse per-country trust service providers, their services, and current status (granted / withdrawn / supervision-in-cessation), entirely offline. Paste the XML; no network round-trip to the LOTL endpoint.
The interface is fully translated into:
| Language | Code | Status |
|---|---|---|
| English | en |
Source of truth |
| French | fr |
Drafted carefully (recommend native review) |
| Simplified Chinese | zh-CN |
Machine-quality (requires native review) |
| German | de |
Machine-quality (requires native review) |
| Hindi | hi |
Machine-quality (requires native review) |
Language is auto-detected from navigator.language on first visit, persisted in localStorage, and switchable from the picker in the top bar. All translations are vendored statically — no fetch, CSP unchanged.
Pre-rendered SEO variants. /fr/, /zh/, /de/, /hi/ are generated as static HTML by scripts/build-i18n-variants.js. Each variant carries a localized <meta name="description">, OpenGraph / Twitter title + description, and a per-variant <link rel="canonical"> / og:url / twitter:url. Social-share locale is localized too: each variant sets og:locale to its own locale (fr_FR, zh_CN, de_DE, hi_IN) and lists the other four — including en_US — as og:locale:alternate, so a shared /fr/ link previews as French rather than English. The source index.html declares <link rel="alternate" hreflang> for all five locales plus x-default. Long compound words in localized headings (e.g. German Dateiverschlüsselung) wrap rather than overflow on narrow viewports via overflow-wrap: break-word.
Native-review tooling. scripts/export-strings-csv.js produces i18n/strings.csv (key, en, fr, zh-CN, de, hi, plus per-locale status columns) for native reviewers. scripts/import-strings-csv.js round-trips edits back into the inline STRINGS literal in index.html. The en column is treated as source of truth and is not overwritten.
- 100% Client-Side - All operations happen in your browser
- No Server Uploads - Your files and keys never leave your device
- Open Source - Audit the code yourself
- Works Offline - Download and use without internet connection. A minimal service worker (
sw.js) precaches the shell on first visit so subsequent loads work offline straight from the cache.
The whole application is one HTML file plus four vendored libraries. There is no server, no API, no build step at deploy time, and — enforced by CSP — no outbound network call. Every byte you type is processed in the page's own JavaScript heap and never crosses the trust boundary.
YOUR DEVICE (the entire trust boundary)
┌─────────────────────────────────────────────────────────────┐
│ Browser tab │
│ ┌───────────────────────────────────────────────────────┐ │
│ │ index.html (one origin, one document) │ │
│ │ │ │
│ │ user input ─▶ tool logic ─▶ Web Crypto / vendored │ │
│ │ ▲ │ libs (local) │ │
│ │ │ ▼ │ │
│ │ keyboard rendered output (createElement / │ │
│ │ / paste / textContent — never innerHTML) │ │
│ │ file picker │ │
│ │ │ │
│ │ localStorage: theme, language, lastTool ONLY │ │
│ │ (never keys, secrets, plaintext, or inputs) │ │
│ └───────────────────────────────────────────────────────┘ │
│ ╳ connect-src 'none' ╳ form-action 'self' │
│ ╳ no fetch / XHR / WebSocket / EventSource / Beacon │
└─────────────────────────────────────────────────────────────┘
╳ NOTHING crosses this line at runtime ╳
─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
network (used once to load the page, then never again)
The only time the network is touched is the initial GET that loads the page and its four scripts; after that the service worker serves the shell from cache and connect-src 'none' makes a runtime exfiltration call physically impossible. Load it once, pull your network cable, and every tool still works.
index.html
├─ <meta CSP> connect-src 'none'; frame-ancestors 'none'; …
├─ inline <style> CSS logical properties (RTL-ready), no external sheet
├─ STRINGS {} 2,124 keys × 5 locales — i18n table, vendored inline
├─ tool registry 53 tools → {id, group, route, render()}
│ ├─ Keys (12) OpenPGP.js · hand-rolled ASN.1/DER reader · CSR (PKCS#10)
│ │ self-sig verify · JWK↔PEM · QR encode + hand-rolled
│ │ QR decode (Reed-Solomon/GF256) · SSH wire format
│ ├─ Encrypt (6) OpenPGP.js · age (X25519/scrypt) · LSB stego
│ ├─ Sign (4) OpenPGP.js · Web Crypto HMAC / JWT (JWKS + EdDSA) verify
│ └─ Utilities (31) Web Crypto · BigInt · hand-rolled codecs/parsers
│ (BLAKE2b/3, SHA-3/Keccak, Argon2, scrypt) · offline
│ passphrase-strength + file-type magic-number aids
│ + IBAN/BIC/VAT/GS1, SEPA & UBL/CII conformance engines
├─ router hashchange → render(route); deep-linkable #/group/tool
└─ clearSensitiveFields() beforeunload → wipe every secret-bearing field
vendored: openpgp.min.js · qrcode.js · secrets.min.js · js-yaml.min.js
(each pinned by SHA-384, cross-checked on every commit)
paste plaintext + public key
│
▼
render() reads DOM values ──▶ openpgp.encrypt({message, encryptionKeys})
│ │ (runs in this tab's JS heap)
│ ▼
│ armored ciphertext (string)
▼ │
textContent = ciphertext ◀───────────┘
│
▼
beforeunload ─▶ clearSensitiveFields() zeroizes the plaintext + key inputs
No step in that chain has a network egress point — there is nowhere for the plaintext or key to go except back into the same DOM.
Encryptalotta was designed from the ground up with security as the primary concern. This application implements defense-in-depth with multiple layers of protection.
Your data never leaves your device. This isn't just a promise - it's cryptographically enforced:
- Content Security Policy (CSP) with
connect-src 'none'- The browser physically cannot make outbound network requests - No external API calls - All cryptographic operations happen locally
- No analytics or tracking - Zero telemetry of any kind
- Works completely offline - Download and use without any internet connection
Built on OpenPGP.js v6.3.0, a well-audited cryptographic library:
| Algorithm | Type | Security Level |
|---|---|---|
| ECC Curve25519 (Default) | Elliptic Curve | High - Modern standard |
| RSA 3072-bit | Traditional | High |
| RSA 4096-bit | Traditional | Very High |
Why ECC Curve25519 is the default:
- Designed by Daniel J. Bernstein, a renowned cryptographer
- Resistant to timing attacks by design
- Smaller keys with equivalent security to RSA 3072
- Faster key generation and encryption/decryption operations
No CDN dependencies. No npm packages. No build process.
All third-party JavaScript is vendored directly in the repository with SHA-384 integrity hashes recorded in HTML comments next to each <script> tag. The manifest below lets you verify that what you download from this repo matches what the maintainer published, and that what runs in your browser matches the official upstream library release.
| File | Library | Version | License | Source | SHA-384 (base64) |
|---|---|---|---|---|---|
openpgp.min.js |
OpenPGP.js | 6.3.0 | LGPL-3.0 | https://github.com/openpgpjs/openpgpjs | Z5tStPoeClmuLPd1gAwdTCU53WcAkUjBNw7mEEvrQumVuNqEl52A9Nx5IhFdKE/c |
qrcode.js |
qrcode-generator | 2.0.4 | MIT | https://github.com/kazuhikoarase/qrcode-generator | e9EFD6BGC90bkW9aDV5xbbBfzwN7G8YImHao2lfLVKV/hPB0E0go+H3I64h7oHtA |
secrets.min.js |
secrets.js-grempe | 2.0.0 | MIT | https://github.com/grempe/secrets.js | xfBMbh8fdSIrQ9XbZARwZ5z/Eh9zC7gsgG5vSE331lZSjgXQob1KxM4m7vEdH0e0 |
js-yaml.min.js |
js-yaml | 4.1.1 | MIT | https://github.com/nodeca/js-yaml | ZeqCzuWczURac3RacSufGD7oSbzeaX7xxnnOr3PTcYTLx4Av0qBj0kBq7AeCtHLA |
Each hash is also recorded inline as an HTML comment beside the corresponding <script> tag in index.html, so a reader auditing the page source sees the same integrity claim.
Download the published distribution from the upstream source listed in the table above, hash it the same way (see Verifying Integrity below), and compare. Hashes match → byte-for-byte identical to the official release.
A non-match means either:
- You downloaded a different version (check version numbers).
- The repo's vendored copy was modified (audit the diff).
- The upstream release was modified (rare, but possible — cross-check against another mirror).
The W3C Subresource Integrity (SRI) attribute requires the script to be served cross-origin or with specific CORS settings. Since these files are served same-origin from the static site, SRI provides no extra benefit. The SHA-384 hashes here serve the same auditing purpose — they let any reader confirm the bytes haven't changed.
If you fork this repo onto a different domain and want to add SRI, replace each <script src="..."> with the integrity-pinned form:
<script src="./openpgp.min.js"
integrity="sha384-Z5tStPoeClmuLPd1gAwdTCU53WcAkUjBNw7mEEvrQumVuNqEl52A9Nx5IhFdKE/c"
crossorigin="anonymous"></script>- Single-file application - No complex dependency chains that could be compromised
- No build tools - What you see in the repository is exactly what runs in your browser
- Translations vendored statically - Inline
STRINGSobject, no JSON fetch, CSPconnect-src 'none'unchanged
This eliminates entire categories of supply chain attacks that have affected other security tools.
Sensitive data is cleared from memory after use:
- Automatic passphrase clearing — Passphrase fields are wiped after decryption operations.
- Page unload protection — On
beforeunload,clearSensitiveFields()wipes every passphrase, private-key, shared-secret, BIP-0039 mnemonic, and PBKDF2 password input across all 53 tools, plus the readonly output panes that render decrypted plaintext, derived keys, BIP-0039 seeds, Shamir secrets, and steganographic payloads. Public keys, signed messages, and signature-verification statuses are left alone (they aren't secrets and clearing them would erase audit context). - JavaScript variable clearing — Sensitive
Uint8Arraybuffers (PBKDF2 password bytes, generated PGP private keys) are zeroized via.fill(0)/secureWipe()after use. (Note: this is best-effort — JS engines may have already retained internal copies for GC, andStringvalues are immutable so we cannot overwrite them in place.)
The application enforces a strict CSP that prevents common web attacks:
default-src 'self';
script-src 'self' 'unsafe-inline';
style-src 'self' 'unsafe-inline';
img-src 'self' data: blob:;
connect-src 'none';
form-action 'self';
base-uri 'self';
frame-ancestors 'none';
upgrade-insecure-requests;
What this means:
connect-src 'none'— No network requests allowed (data exfiltration impossible). Audited viascripts/audit-release.js: zerofetch,XMLHttpRequest,WebSocket,EventSource, orsendBeaconcall sites in the codebase.frame-ancestors 'none'— Cannot be embedded in iframes (prevents clickjacking).form-action 'self'— Forms cannot submit to external servers.base-uri 'self'— Prevents base tag injection attacks.img-src 'self' data: blob:—blob:is required so the Steganography and EXIF Eraser tools can render user-selected images viaURL.createObjectURL(file). Blob URLs cannot be created from network responses without first making a network request, whichconnect-src 'none'forbids; every blob URL in this app traceably originates from a local file or a canvastoBlob()call, and is revoked after use.
When deployed to Cloudflare Pages (or any server respecting the _headers file):
| Header | Value | Purpose |
|---|---|---|
Strict-Transport-Security |
max-age=31536000; includeSubDomains; preload |
Force HTTPS for 1 year |
X-Content-Type-Options |
nosniff |
Prevent MIME-type sniffing |
X-Frame-Options |
DENY |
Block iframe embedding |
X-XSS-Protection |
1; mode=block |
Legacy XSS protection |
Referrer-Policy |
no-referrer |
Don't leak URLs |
Cross-Origin-Opener-Policy |
same-origin |
Isolate browsing context |
Cross-Origin-Embedder-Policy |
require-corp |
Prevent cross-origin leaks |
Cross-Origin-Resource-Policy |
same-origin |
Block cross-origin reads |
Cache-Control |
no-store, no-cache, must-revalidate |
Prevent caching sensitive pages |
Browser features that could be abused are explicitly disabled. The current policy denies every capability except clipboard-write=(self) (so per-tool Copy buttons work) — clipboard-read is denied so no script can silently steal what's on your clipboard.
Denied: accelerometer, autoplay, camera, clipboard-read, cross-origin-isolated, display-capture, encrypted-media, fullscreen, geolocation, gyroscope, keyboard-map, magnetometer, microphone, midi, payment, picture-in-picture, publickey-credentials-get, screen-wake-lock, sync-xhr, usb, web-share, xr-spatial-tracking.
Allowed: clipboard-write=(self) only.
Private keys are protected with enforced passphrase requirements:
- Minimum 12 characters
- Must include uppercase letter
- Must include lowercase letter
- Must include number
- Must include special character
- Passphrase confirmation required
- Warning system for weak passphrases (can be overridden if needed)
- Password fields use
type="password"to hide input autocomplete="off"prevents browser password saving for sensitive fields- No sensitive data stored in localStorage, sessionStorage, or cookies
We are upfront about the limits of what a static web page can promise. We do not claim "100% secure" or "no attack surface." That's marketing, not engineering. A static page that handles cryptographic secrets has a real attack surface, and you should understand it before trusting it with anything that matters.
- Your browser and OS are part of the trust boundary. Same-origin JavaScript injected by a malicious browser extension can read secrets from the DOM and from memory. Web Crypto does not protect you from a compromised browser environment. Use a clean browser profile, or no extensions at all, for high-value operations.
- The TLS path is part of the trust boundary. When you load this page from
encryptalotta.com, you are trusting Cloudflare's TLS and our DNS. If you want to eliminate that trust assumption, clone the repo, verify the SHA-384 hashes against the Manifest, and openindex.htmlfrom disk — it works fully offline. - CSS-injected styling tricks are a thing. A compromised stylesheet (we use inline styles only, but stylesheet injection at the proxy/extension layer is still possible) could in principle reveal user input via timing or
:has()selectors. The strict CSP makes this hard, not impossible. - OpenPGP.js has had CVEs in the past. Pinning the library to a specific version means we don't auto-pull upstream fixes. We commit to a manual update cadence (see below); please check that you're running the latest tagged release.
- Cryptographic correctness depends on libraries written by other people. OpenPGP.js, qrcode-generator, and secrets.js-grempe are all third-party dependencies. We trust them based on their audit history, code reviewability, and active maintenance — but that is a trust boundary, not an absence of one.
- Steganography hides existence, not content. Don't market it (or rely on it) as protection against state-level adversaries. Sophisticated steganalysis (chi-squared LSB tests, RS-analysis) detects LSB modification with high confidence. Encrypt the payload first; the stego layer is for casual concealment only.
If your threat model includes any of: a determined nation-state, a targeted attacker who controls your network or your endpoint, a browser-extension-level adversary, or "this must never leak under any circumstances" — use a desktop tool on an air-gapped machine. This project is for everyday client-side cryptography, not for protecting state secrets.
Vendored libraries are pinned by SHA-384 hash, so we don't auto-pull upstream fixes. To compensate:
- Quarterly review — at least once a quarter, check for new releases of OpenPGP.js, qrcode-generator, secrets.js-grempe, and js-yaml. Compare the diff against the pinned version, vendor the new minified file, update the SHA-384 hash in two places (HTML comment beside the
<script>tag, and the Manifest table above), and re-runnode scripts/audit-release.js. - Immediate response on advisory — if a CVE or security advisory is published for any vendored library, treat the bump as a P0: vendor the patch within 48 hours, push a release, and note the CVE in the commit message.
- Pre-commit hash check —
scripts/git-hooks/pre-commit(a versioned hook in this repo) re-hashes every vendored library on every commit and refuses to land changes if the on-disk bytes don't match the recorded hashes. Install on a fresh clone with:ln -sf ../../scripts/git-hooks/pre-commit .git/hooks/pre-commit. The hook simply runsscripts/audit-release.js, which gates all 14 mechanically-checkable invariants (innerHTML hygiene, STRINGS parity, i18n key resolution, vendored SHA-384 cross-check, CSP integrity, no outbound vectors, page weight, locale-variant sync, robots.txt + sitemap.xml + JSON-LD presence, and Phase-7 SEO prose key coverage). - Server-side CI gate —
.github/workflows/audit.ymlre-runs that samescripts/audit-release.json every push and pull request tomain(and on demand viaworkflow_dispatch), so the invariants above are enforced in GitHub Actions even when a contributor bypasses the local hook (git commit --no-verify) or pushes from a clone where it was never installed. The audit uses only Node built-ins (nonpm install, no browser, no network), so the job is fast and deterministic. The Playwright end-to-end suite (tests/, ~782 serial cases,workers: 1) and the responsive-overflow sweep (tests/responsive-check.mjs) stay a local / manual gate by design — run them withcd tests && npm ci && npx playwright test. - Automated quarterly reminder —
.github/workflows/dep-check.ymlrunsscripts/check-dependency-updates.json the 1st of each quarter (Jan / Apr / Jul / Oct). The script reads the Manifest, queries each upstream's GitHub Releases API, and exits non-zero if any pinned version is behind upstream. The workflow then opens a tracking issue. The shipped site never makes a network call — this runs in GitHub Actions only. The reminder is informational; the actual vendoring + re-hashing remains manual (steps below).
The interface is a home grid + command palette, designed to stay minimal on desktop and mobile alike:
- Home (
#/) — a single screen showing every tool as a card grouped by category. The full list of tools is rendered in the DOM on first paint, so search engines see all of them. - Command palette (
⌘K/Ctrl+K, or/) — opens a centered modal with a fuzzy-search input. Type a few letters, use ↑/↓ to move, Enter to open, Esc to close. Mobile: tap the search box in the top bar. - Top bar — logo (returns home), search box, language picker. That's it. No nested tabs, no hamburger.
- Breadcrumb — when inside a tool, a thin "← All tools" button + a "Group / Current tool" trail at the top of the page. One click back to home.
- Deep links — every tool still has its own URL (e.g.
#/keys/qr,#/utilities/exif), so you can bookmark or share a specific tool. - Keyboard-first —
⌘K/Ctrl+Kto search, arrow keys to navigate, Enter to open, Esc to close. No need for a mouse. - Last-used persistence —
localStorage.lastToolremembers which tool you were last on, so reopening the page returns you there (or to home on first visit).
All 53 tools are organized into four groups. Every tool has a deep-link route.
| Tool | Route | What it does |
|---|---|---|
| Generate Keys | #/keys/generate |
Create new PGP key pairs (ECC Curve25519 or RSA 3072 / 4096) |
| Key Info | #/keys/key-info |
Inspect any PGP key's fingerprint, user ID, algorithm, expiry |
| Revoke Key | #/keys/revoke |
Create a revocation certificate for a compromised key |
| QR Share | #/keys/qr |
Encode a public key or message into a QR (multi-QR for long inputs) |
| QR Decoder | #/keys/qr-decode |
Decode a QR image back to text (hand-rolled, Reed-Solomon corrected, offline) |
| TLS Certificate Parser | #/keys/tls-cert |
Decode an X.509 cert, a PKCS#7 / CMS / CAdES signature, a PAdES-signed PDF, or a XAdES / XML-DSig signature (the eIDAS signature inspector) |
| SSH Key Parser | #/keys/ssh-key |
Inspect an OpenSSH public key: type, bits, SHA-256 fingerprint |
| PEM ↔ DER | #/keys/pem-der |
Convert between PEM (Base64) and DER (binary) encodings |
| ASN.1 / DER Decoder | #/keys/asn1 |
Decode any DER blob (hex / Base64 / PEM) into a typed TLV tree (ITU-T X.690) |
| CSR Decoder | #/keys/csr |
Decode a PKCS#10 CSR (RFC 2986): subject, key algorithm, SANs, self-signature check |
| JWK ↔ PEM | #/keys/jwk |
Convert a public JWK ↔ SPKI PEM (RFC 7517 / 8037 — OKP / EC / RSA) |
| BIP-0039 Mnemonic | #/keys/bip39 |
Generate or validate BIP-0039 mnemonic seed phrases (12 / 15 / 18 / 21 / 24 words) |
| Tool | Route | What it does |
|---|---|---|
| Encrypt Files | #/crypt/encrypt |
Encrypt files with one or more PGP public keys |
| Decrypt Files | #/crypt/decrypt |
Decrypt files with your private key + passphrase |
| Text Messages | #/crypt/text-crypto |
Encrypt / decrypt PGP text for email, chat, or notes |
| Password Encrypt | #/crypt/password-encrypt |
Encrypt a file with a passphrase only (no PGP key needed) |
| Steganography | #/crypt/stego |
Hide a payload inside a PNG using LSB encoding |
| age Encryption | #/crypt/age |
age v1 encrypt / decrypt (X25519 or scrypt) + keygen, ASCII armor |
| Tool | Route | What it does |
|---|---|---|
| Sign | #/signing/sign |
Digitally sign messages or files (detached, attached, cleartext) |
| Verify | #/signing/verify |
Verify a PGP signature against a public key |
| HMAC | #/signing/hmac |
HMAC-SHA-1/256/384/512 sign and verify (constant-time compare) |
| JWT / credential Inspector | #/signing/jwt |
Decode + verify HS / RS / ES / EdDSA JWTs (PEM, JWK, or JWKS-by-kid); decode SD-JWT and ISO 18013-5 mdoc (EUDI wallet) |
| Tool | Route | What it does |
|---|---|---|
| Passwords | #/utilities/passwords |
Generate cryptographically strong random passwords |
| Passphrase Strength | #/utilities/strength |
Offline entropy + crack-time estimate, scored 0–4 (no breach-DB lookup) |
| Armor | #/utilities/armor |
Convert between PGP binary and ASCII-armored encodings |
| Shamir Split | #/utilities/shamir |
Split a secret into N shares; any K reconstruct |
| EXIF Eraser | #/utilities/exif |
Strip GPS / metadata from JPEG, PNG, WebP images |
| Hash & Checksum | #/utilities/hash |
SHA-1/256/384/512 + SHA3-256/512 + BLAKE2b-512 + BLAKE3-256; advanced: keyed MAC, variable length, BLAKE3 XOF / keyed / derive-key, SHAKE128/256 |
| Encode | #/utilities/encode |
Cross-convert Base64 / Base32 / Base58 / Base58Check / Hex / text |
| UUID / ULID | #/utilities/uuid |
Bulk-generate UUID v4, UUID v7, deterministic v5 (namespace), or ULID |
| Unix Timestamp | #/utilities/timestamp |
Convert epoch ↔ ISO 8601 ↔ UTC ↔ local ↔ relative ↔ IANA timezone ↔ ISO week / day-of-year |
| URL Parser | #/utilities/url |
Parse + percent-encode / decode URLs and queries |
| TOTP / 2FA | #/utilities/totp |
RFC 6238 TOTP + RFC 4226 HOTP code generator (otpauth:// URI or bare secret) |
| Diff | #/utilities/diff |
Line-level LCS diff (whitespace / case toggles) + unified-diff (@@ hunk) export |
| CSV / JSON / TSV | #/utilities/csv |
Convert tabular data with RFC 4180 quoting |
| Regex Tester | #/utilities/regex |
Live match highlight + capture groups + replace |
| Cron Decoder | #/utilities/cron |
Decode cron expression + show next 5 run times |
| Color Converter | #/utilities/color |
HEX / RGB / HSL / OKLCH / CMYK + CSS named colors + WCAG contrast checker |
| Format / Convert | #/utilities/format |
Pretty / minify / convert JSON, YAML, XML; decode CBOR (hex / base64) → JSON |
| CIDR / Subnet | #/utilities/cidr |
Decode IPv4 + IPv6 CIDR: network, broadcast / last address, host range, host count, tags |
| PBKDF2 / scrypt / Argon2id | #/utilities/pbkdf2 |
Derive a key from a password via PBKDF2 (RFC 8018), scrypt (RFC 7914), or Argon2id (RFC 9106) |
| Argon2 | #/utilities/argon2 |
Argon2id/i/d memory-hard KDF + password hash (RFC 9106), PHC output |
| Number Base | #/utilities/base |
Convert integers between binary, octal, decimal, hex, any base 2–36 |
| File Type | #/utilities/filetype |
Identify a format from its magic bytes (offline; reads only the leading bytes) |
| IBAN | #/utilities/iban |
ISO 13616 MOD-97 validator + test-data generator (per-country structure) |
| BIC / SWIFT | #/utilities/bic |
ISO 9362 format check + bank / country / location / branch decoder |
| GS1 Barcode | #/utilities/gs1 |
Mod-10 checksum for EAN-8/-13, UPC-A, GTIN-14, SSCC + GS1 prefix lookup |
| EU VAT | #/utilities/vat |
Format + checksum check for EU / GB / NO / CH / XI VAT numbers (offline) |
| Ed25519 | #/utilities/ed25519 |
Generate / sign / verify with native Ed25519 (RFC 8032) via Web Crypto |
| X25519 | #/utilities/x25519 |
ECDH key agreement (RFC 7748) with optional HKDF-SHA-256 / SHA-512 |
| PEPPOL / Factur-X Invoice | #/utilities/ubl |
OASIS UBL 2.1 Invoice / CreditNote (PEPPOL BIS Billing 3.0, EN 16931) inspector + 100+ conformance gates; also renders UN/CEFACT CII (Factur-X / ZUGFeRD) XML |
| SEPA XML | #/utilities/sepa |
ISO 20022 pain.001 / pain.008 / camt.053 inspector + 110+ EPC Rulebook conformance gates |
| eIDAS Trust List | #/utilities/lotl |
ETSI TS 119 612 LOTL / TSL viewer — per-country trust service providers, services, status |
- Pure client-side QR rendering via vendored qrcode-generator. The canvas is drawn locally; the only blob URL produced is for the download anchor (not visible to
img-src), so CSP is unchanged. - For inputs over 1,200 bytes, the input is chunked into multi-QR sequences with the prefix
EAL-QR/v1/{n}/{total}/. Capacity is enforced as 1,200 bytes × 16 chunks = 19,200 bytes maximum. - Chunking uses UTF-8 byte length (
TextEncoder) and split-codepoint-safe decoding (TextDecoder({stream: true})).
The inverse of QR Share, and the one deferred new-tool the project chose to hand-roll rather than vendor — keeping the dependency count at four. The decode pipeline (ISO/IEC 18004), all in the page's own JS heap, no upload:
uploaded image (PNG/JPEG/WebP)
│ <canvas> getImageData (img-src 'self' data: blob: already allows this)
▼
grayscale → Otsu threshold → 1-bit module grid
│ finder-pattern run (1:1:3:1:1) gives module size → sample N×N matrix
▼
format info (15 bits, BCH-corrected) → EC level + data mask
│ unmask · zig-zag codeword walk · de-interleave RS blocks
▼
Reed-Solomon decode over GF(256) ← corrects damaged modules
│ (Berlekamp-Massey locator · Chien search · Forney magnitudes)
▼
segment decode (byte / numeric / alphanumeric) → text payload
- Scoped to flat, axis-aligned images (screenshots, generated codes, flat scans); no rotation/perspective correction — a skewed photo gets a clear "image may be rotated or skewed" message rather than a wrong guess.
- Versions 1–40, all four EC levels, all eight masks. Verified offline against the page's own generator (versions 1–18, every level/mask, multiple scales, injected single-module errors) before shipping; regression-locked in tests/specs/18-newtools-vectors.spec.js.
- Decoding an image that encodes a private key or address keeps that image on your device — unlike a phone camera app or a website scanner.
- Threat model: casual concealment, not nation-state adversaries. Sophisticated steganalysis (chi-squared LSB tests, RS-analysis) can detect LSB modification with high confidence. This tool is for situations where the existence of the data should not be obvious to a casual observer.
- Always encrypt the payload first. Use the Text Messages tab to produce a PGP block, then hide that block here. Steganography hides existence, not content.
- Format: 4-byte magic
EAL1+ 4-byte big-endian length + payload bytes encoded into LSBs of R/G/B channels (alpha untouched, transparency preserved). - Optional password XORs the payload with a
SHA-256(password || counter_be32)keystream. A wrong password produces a magic-byte mismatch in 99.9%+ of cases, surfacing a "no payload found" error rather than garbage output. - PNG only. JPEG output is impossible — re-encoding would destroy the LSB data. Input format is enforced via
accept="image/png".
A full, hand-rolled implementation of the age v1 format (age-encryption.org/v1) — Filippo Valsorda's modern file-encryption tool. Encrypt to X25519 recipients or a scrypt passphrase, decrypt with a secret-key identity or passphrase, and generate keypairs. Output is the standard ASCII armor, byte-compatible with the age CLI and rage.
Wire format (decrypted, before armor):
age-encryption.org/v1 ← intro line
-> X25519 <base64(ephemeral_share)> ← one stanza per recipient
<base64(ChaCha20Poly1305(wrap_key, fileKey))> (or: -> scrypt <salt> <log2N>)
--- <base64(HMAC-SHA256(hdr_key, header))> ← header MAC (authenticates everything above)
‖ 16-byte payload nonce ‖ STREAM ciphertext… ← binary payload follows the header
Key derivation cheat sheet:
| Step | Construction |
|---|---|
| File key | 16 random bytes (crypto.getRandomValues) |
| X25519 wrap key | HKDF-SHA256(ikm = X25519(eph, recipient), salt = eph_share ‖ recipient_pub, info = "age-encryption.org/v1/X25519") → 32 B |
| scrypt wrap key | scrypt(passphrase, salt = "age-encryption.org/v1/scrypt" ‖ rand16, N = 2^logN, r = 8, p = 1) → 32 B |
| Header MAC key | HKDF-SHA256(ikm = fileKey, salt = "", info = "header") → HMAC-SHA256 key |
| Payload key | HKDF-SHA256(ikm = fileKey, salt = nonce16, info = "payload") → 32 B |
| Payload | ChaCha20-Poly1305 STREAM: 64 KiB chunks, nonce = counter[11 B big-endian] ‖ last_flag[1 B] |
| Recipient / identity encoding | bech32 (BIP-173): age1… (lowercase HRP age) / AGE-SECRET-KEY-1… (uppercase HRP age-secret-key-) |
Crypto provenance. ChaCha20, Poly1305 (the audited TweetNaCl limb arithmetic), the ChaCha20-Poly1305 AEAD (RFC 8439), scrypt (RFC 7914 — Salsa20/8 + BlockMix + ROMix), and bech32 are hand-rolled in pure JavaScript. X25519, HKDF-SHA-256, HMAC-SHA-256, and scrypt's inner/outer PBKDF2 rounds use Web Crypto. The decrypt-side HKDF needs the recipient's own public key as salt; it is recovered from the pasted secret scalar via the basepoint trick X25519(scalar, u=9) rather than being stored alongside the identity.
Design decisions.
- Hand-rolled, not vendored. The age v1 format is ~600 lines; implementing it inline avoids a new vendored
.jsfile, keeps the SHA-384 supply-chain manifest unchanged, and adds nothing to the CSP. The same "ship pure-JS first" choice made for Argon2. - No low-order-point footgun. Web Crypto's X25519
deriveBitsthrows on an all-zero shared secret, so the age spec's low-order-point check is enforced by the platform. - scrypt work factor is selectable (log₂ N: 12 / 14 / 15 default / 16 / 18). The CLI's default of 18 (256 MiB) is decryptable here but takes a few seconds in pure JS; lower factors are offered for interactive use. Decryption honours whatever factor the file carries (capped at 2³⁰ to bound a malicious header).
- Passphrase = sole recipient. Per the age spec, a scrypt stanza may not be mixed with X25519 stanzas; the tool enforces this on encrypt.
Interop is verified, not assumed. The test suite decrypts static armored fixtures produced by age CLI 1.3.1 (one X25519, one scrypt) entirely in-browser, and the development harness round-trips both directions (CLI→tool and tool→CLI, including multi-chunk ≥150 KiB and empty inputs) plus RFC 8439 / RFC 7914 known-answer vectors and a cross-check against the platform's native ChaCha20-Poly1305 and scrypt.
Threat-model notes. Everything runs locally; secret keys, passphrases, and decrypted output are wiped on page unload (clearSensitiveFields()). age provides confidentiality and integrity but, like the format itself, no sender authentication — a recipient cannot prove who encrypted a file (use the Sign / Verify tools if you need that). Anyone holding an AGE-SECRET-KEY-1… can decrypt every file sent to its age1… recipient; treat it like a private key.
- Built on the well-audited secrets.js-grempe library, initialized with a 256-bit security parameter.
- Share format:
EAL-SSS/v1/{K}-of-{N}/{rawShare}so the threshold metadata is visible at-a-glance. The combine handler accepts shares with or without this prefix. - Each share leaks the length of the secret. If length is sensitive, pad your secret to a fixed size before splitting.
- Shares are independent of each other — no single share (or any K−1 shares) reveals any information about the secret. Distribute them through separate channels.
- Pure canvas re-encode, no external library. Stripping is achieved by drawing the image to a
<canvas>and callingtoBlob()— the browser's image encoder produces a metadata-free output. - Pre-strip detection (informational only) parses JPEG APP1 segments for IFD0 + GPS-IFD tag counts, and PNG
tEXt/iTXt/zTXt/eXIfchunks. The strip happens regardless of the detection result. - Re-encoding recompresses the image. For a JPEG with sensitive metadata that you want removed without recompression, use a desktop tool. Default quality is 92, adjustable 50–100.
- A BIP-0039 mnemonic is a wallet master key. Anyone holding the words can derive every private key in every account derived from it, on every chain. Treat the words like cash, not like an email password.
- Entropy is generated locally via
crypto.getRandomValues(the browser's Web Crypto CSPRNG). The official BIP-0039 English wordlist (2048 words) is vendored inline; no network fetch. - The seed is computed via
PBKDF2-HMAC-SHA512(mnemonic, "mnemonic" + passphrase, 2048 iterations, 64 bytes)per the BIP-0039 spec, using Web Crypto. - The mnemonic textarea and passphrase field are both included in
clearSensitiveFields()and wiped on page unload — but a tab snapshot, browser extension, or screen-recording could capture them in the meantime. For high-value wallets, generate the mnemonic on an air-gapped machine. - Validation is offline-only (checksum + seed derivation). It does not check whether the mnemonic controls funds on any chain.
- Web Crypto's
SubtleCrypto.deriveBitsruns the PBKDF2 loop locally; the password buffer is zeroized after the derive call. - Iteration count and PRF choice directly govern brute-force cost. OWASP 2023 guidance: ≥600,000 iterations for PBKDF2-HMAC-SHA-256, ≥1,300,000 for SHA-1. The default 100,000 is conservative for test vectors, not for protecting a real password — bump it before shipping a derived key into production storage.
- Optional regulator presets (BSI TR-02102-1, ANSSI RGS B1, NIST SP 800-132 / OWASP minimum, Privacy Guides) auto-fill
iterations+ PRF from each authority's published recommendation and link out to the source document. Manually editing iterations or PRF resets the dropdown to "Custom" so the UI never claims a regulator's recommendation when the user has diverged from it. - Output length is capped at 512 bytes to avoid pathological inputs; that's well above any standard symmetric-key size.
- PBKDF2 is not the strongest password KDF. For new designs, prefer Argon2id (memory-hard) — now shipped as its own tool (
#/utilities/argon2). PBKDF2 is kept because it is the lowest-common-denominator that ships with Web Crypto and matches RFC 8018, the JWE/PKCS#5 ecosystem, and most existing test vectors; Argon2id resists GPU/ASIC cracking far better but, in pure JavaScript without WASM, is slower (≈1–1.5 s at the OWASP default vs. PBKDF2's native Web Crypto speed).
- Pure JavaScript BigInt — no library — so values aren't capped at 53-bit JS Number precision and there's no
Number.MAX_SAFE_INTEGERcorner case. - Negatives are stored sign-and-magnitude:
-0xff→-255decimal →-11111111binary. Two's-complement views are not produced; if you need them for a fixed-width register, mask explicitly with the appropriate2^n - 1. - Underscores in input are stripped as digit-grouping separators (matches Python / Rust literals). Leading
0x/0b/0oare recognized when "Auto" is selected.
- Pure HTML, CSS, and JavaScript — no frameworks, no build step at deploy time.
- OpenPGP.js v6.3.0 — PGP key generation, encryption, decryption, signatures (vendored locally).
- qrcode-generator v2.0.4 — QR rendering for the QR Share tool (vendored locally).
- secrets.js-grempe v2.0.0 — Shamir's Secret Sharing math (vendored locally).
- js-yaml v4.1.1 — YAML parsing and emitting for the Format / Convert tool (vendored locally).
- Web Crypto API (
SubtleCrypto.digest/sign/verify/importKey) — hashing, HMAC, JWT verification, TOTP, steganography keystream, certificate fingerprints. - Hand-rolled, no library: ASN.1 / DER decoding (TLS Certificate Parser), OpenSSH wire format (SSH Key Parser), Base32 / Base58 codecs, OKLCH colour matrices, cron parser, CIDR arithmetic, line-level LCS diff, RFC 4180 CSV parser, regex highlighting, percent-encoding helpers — kept hand-rolled rather than vendored to keep total page weight under 0.32 MB gzipped.
- Single-file deployment (
index.html) plus four vendored libraries — every script pinned with a SHA-384 hash that is cross-checked against the README manifest and the on-disk bytes byscripts/audit-release.json every commit.
Every tool maps to a published standard so its output is checkable against an authoritative source. No proprietary formats.
| Domain | Standard / RFC | Tool(s) |
|---|---|---|
| PGP / OpenPGP | RFC 9580 (formerly 4880) | Generate, Key Info, Revoke, Encrypt, Decrypt, Text, Sign, Verify, Armor |
| Symmetric KDF | RFC 8018 (PBKDF2), RFC 7914 (scrypt), RFC 9106 (Argon2id), BIP-0039 | PBKDF2 / scrypt / Argon2id, BIP-0039 |
| Password hashing | RFC 9106 (Argon2id/i/d); vectors RFC 9106 §5 | Argon2 |
| Signatures (EdDSA) | RFC 8032 (Ed25519) | Ed25519 |
| Key agreement (ECDH) | RFC 7748 (X25519), RFC 5869 (HKDF) | X25519 |
| HMAC | RFC 2104, FIPS 198-1; vectors RFC 4231 | HMAC |
| Hashing | FIPS 180-4 (SHA-1/2), FIPS 202 (SHA-3 / SHAKE), RFC 7693 (BLAKE2b), BLAKE3 spec | Hash & Checksum |
| JWT / JOSE | RFC 7519, 7515, 7518; RFC 7517 (JWK / JWKS), RFC 8037 (EdDSA); RFC 8725 (BCP) | JWT Inspector |
| EUDI credentials | SD-JWT (IETF draft), ISO 18013-5 mdoc | JWT / credential Inspector |
| TOTP / HOTP | RFC 6238, RFC 4226 | TOTP / 2FA |
| X.509 / ASN.1 | RFC 5280, X.690 (DER) | TLS Certificate Parser |
| ASN.1 / DER (generic) | ITU-T X.690 (BER/DER TLV) | ASN.1 / DER Decoder |
| CSR / PKCS#10 | RFC 2986 (CertificationRequest), self-signature verify | CSR Decoder |
| JWK ↔ PEM | RFC 7517 (JWK), RFC 7518 (JWA), RFC 8037 (OKP/EdDSA) | JWK ↔ PEM Converter |
| File signatures | magic-number / file-format signatures | File Type Identifier |
| Passphrase strength | Shannon entropy (NIST SP 800-63B guidance) | Passphrase Strength Estimator |
| PKCS#7 / CMS | RFC 5652 (SignedData), CAdES-BES (ETSI EN 319 122) | TLS Certificate Parser |
| PAdES (PDF signatures) | ETSI EN 319 142, ISO 32000 signature dictionary | TLS Certificate Parser |
| XAdES / XML-DSig | ETSI EN 319 132, XML Signature (RFC 3275) | TLS Certificate Parser |
| CBOR | RFC 8949 (Concise Binary Object Representation) | Format / Convert |
| COSE / CWT / mdoc | RFC 9052 (COSE), RFC 8392 (CWT), ISO 18013-5 (mdoc) — tag-aware CBOR decode | Format / Convert |
| SSH wire format | RFC 4253, RFC 4716 | SSH Key Parser |
| UUID / ULID | RFC 9562 (UUID v4 / v5 §5.5 / v7) | UUID / ULID |
| Base encodings | RFC 4648 (Base64/32), Base58 + Base58Check (Bitcoin, SHA-256d checksum) | Encode |
| QR codes | ISO/IEC 18004 (generate + decode, Reed-Solomon over GF(256)) | QR Share, QR Decoder |
| CSV | RFC 4180 | CSV ↔ JSON ↔ TSV |
| CIDR / subnetting | RFC 4632, RFC 3021 (/31), RFC 1918 (IPv4); RFC 4291 / RFC 5952 (IPv6) | CIDR / Subnet |
| Color | WCAG 2.1 contrast, OKLCH (Oklab), CSS Color Module Level 4 named colors, CMYK | Color Converter |
| IBAN | ISO 13616, MOD-97-10 (ISO 7064) | IBAN |
| BIC | ISO 9362 | BIC / SWIFT |
| GS1 barcodes | GS1 General Specs (mod-10) | GS1 / EAN / GTIN |
| EU VAT | per-MS algorithms (ISO 7064 MOD 11,10; Luhn; mod-97) | EU VAT |
| SEPA payments | ISO 20022 (pain.001 / pain.008 / camt.053) + EPC SCT/SDD Rulebooks | SEPA XML |
| E-invoicing | OASIS UBL 2.1, EN 16931, PEPPOL BIS Billing 3.0, UN/CEFACT CII (Factur-X / ZUGFeRD) | PEPPOL / UBL |
| Trust lists | ETSI TS 119 612, eIDAS LOTL / TSL | eIDAS Trust List |
| Shamir | Shamir (1979) over GF(256), 256-bit param | Shamir Split |
The SEPA and UBL inspectors share one idea: schema-valid is not rail-valid. A pain.001 that passes the ISO 20022 XSD, or a UBL invoice that passes the OASIS schema, can still be bounced by a clearing system (EBA STEP2 / STET / RT1) or a PEPPOL access point for a Rulebook reason the XSD does not encode. Each gate below catches one such reject, emits a single ✓ (clean) or ✗ (offending value cited) row, and no-ops on documents it does not apply to.
paste XML
│
▼
DOMParser ──▶ namespace-blind walk (localName only)
│ one code path for every pain.001.001.03 … .09,
│ camt.053, pain.008, UBL 2.1.x, PEPPOL BIS 3.0
▼
detect message/document type ──▶ render header + lines
│
▼
run every applicable gate (each independent, order-free)
├─ structural / checksum ─ IBAN MOD-97, BIC ISO 9362, IBAN↔BIC country parity
├─ character-set & length ─ EPC charset; AdrLine 70 / TwnNm 35 / PstCd 16 / Nm 70
├─ presence (wrapper→leaf) ─ element down to FinInstnId routing identifier / account Id
├─ code-list membership ─ EUR-only, LclInstrm CORE/B2B/COR1, UNCL 4461/5305/5153, ISO 3166/4217
├─ arithmetic invariants ─ NbOfTxs, CtrlSum 2-dec, amount cap €999,999,999.99 │ BR-CO-10/13/14/15/16
├─ cardinality / forbidden ─ single Strd per RmtInf; IntrmyAgt / ChrgsAcct / SvcLvl·Prtry forbidden
└─ uniqueness ─ EndToEndId
▼
✓ / ✗ rows (no network — verdicts computed entirely in-tab)
| Family | SEPA (ISO 20022 / EPC, 111 gates) | UBL (EN 16931 / PEPPOL, 118 gates) |
|---|---|---|
| Structural / checksum | IBAN MOD-97 + per-country BBAN structure; BIC ISO 9362; IBAN↔BIC country parity | IBAN MOD-97 on Payee/Payer account |
| Character-set & length | EPC restricted charset on every free-text field; AdrLine 70 / TwnNm 35 / PstCd 16 / party Nm 70 caps | — (UTF-8 per EN 16931) |
| Presence (wrapper → leaf) | <PmtInf> → <Dbtr>/<Cdtr> → <DbtrAgt>/<CdtrAgt> → <FinInstnId> routing-id content; account <Id> → IBAN content |
BR-* root, line, and party mandatory-field presence down to PayeeFinancialAccount/PaymentMandate/EndpointID@schemeID; document-level AllowanceCharge Amount (BR-31/36) + ChargeIndicator + reason (BR-33/38) + VAT category (BR-32/37) + TaxScheme (BR-CO-08); line-level AllowanceCharge Amount (BR-41/43) + ChargeIndicator + reason (BR-42/44) |
| Code-list membership | EUR-only <Ccy>; LclInstrm/Cd CORE/B2B/COR1; SeqTp; CtryOfRes ISO 3166 |
UNCL 1001 type, UNCL 4461 means, UNCL 5305/5153 VAT codes; ISO 4217 currency; PEPPOL EAS schemeID |
| Arithmetic | NbOfTxs/CtrlSum non-empty + 2-decimal cap; per-tx 2-decimal precision; strictly-positive amounts; amount cap €999,999,999.99 |
BR-CO-10 line totals, BR-CO-13/15 tax consistency, BR-CO-14 breakdown sum, BR-CO-17 per-rate cross-product, BR-CO-16 payable; BR-DEC 2-decimal caps |
| Cardinality / forbidden | single <Strd> per <RmtInf>; IntrmyAgt / ChrgsAcct / SvcLvl·Prtry forbidden |
at-least-one <InvoiceLine>; conditional gates scoped by payment-means code |
| Uniqueness | EndToEndId across the file |
— |
Each gate ships as a four-part unit — spec entry (SPEC-INTERNATIONAL.md §2.4 / §2.7) + implementation + i18n strings × 5 locales + a Playwright confirm/flag pair — so the catalogue grows without regressions. The full per-draft history (drafts 16 → 282) lives in the spec.
UBL and CII carry the same EN 16931 semantic model in two different vocabularies — UBL's cbc:/cac: versus UN/CEFACT's rsm:/ram:. The ~118 ublCheck* gates are written against UBL element names and never fire on a CII document, so a parallel ciiCheck* family walks the rsm:/ram: tree. Each CII gate is the syntactic mirror of a UBL gate it cites, reuses the shared validators (ublParseCents, ISO_3166_1_ALPHA2, ISO_4217_ACTIVE, UBL_VAT_CATS, ublCiiDate), and no-ops on documents it does not apply to. The family currently stands at 24 gates (drafts 259 → 282).
The header monetary chain reconciles end to end — each arrow is one gate proving the next figure is derivable from the previous, with a ±1-cent tolerance per EN 16931's rounding policy:
Σ per-line LineTotalAmount (BT-131) ← line cross-product 273: each = NetPrice × BilledQty
│ line-sum 265 (BR-CO-10)
▼
header LineTotalAmount (BT-106)
│ tax-basis 266 (BR-CO-13): − AllowanceTotal (BT-107) + ChargeTotal (BT-108)
▼
TaxBasisTotalAmount (BT-109) ──┐
│ │ Σ per-category CalculatedAmount (BT-117)
│ ▼ ▲ VAT-breakdown sum 268 (BR-CO-14)
│ header TaxTotalAmount (BT-110) ▲ per-rate cross-product 269 (BR-CO-17):
│ grand-total 260 (BR-CO-15): + TaxTotal each CalculatedAmount = BasisAmount × Rate
▼
GrandTotalAmount (BT-112)
│ payable 267 (BR-CO-16): − TotalPrepaid (BT-113) + Rounding (BT-114)
▼
DuePayableAmount (BT-115)
| CII gate (draft) | EN 16931 | ram: anchor |
UBL analogue |
|---|---|---|---|
| Header presence (259) | BR-01/02/03/05 | ExchangedDocument/ID · IssueDateTime · TypeCode · InvoiceCurrencyCode |
root mandatory-field presence |
| Grand-total arithmetic (260) | BR-CO-15 | …HeaderMonetarySummation/GrandTotalAmount |
ublCheckTotals |
| Party names (261) | BR-06/07 | Seller/BuyerTradeParty/Name |
ublCheckSellerName/…BuyerName |
| Line presence + anchor (262) | BR-16 / BT-131 | IncludedSupplyChainTradeLineItem · …/LineTotalAmount |
ublCheckLinePresence |
| Currency ISO 4217 (263) | BT-5 | InvoiceCurrencyCode |
ublCheckDocCurrencyCode |
| Document type UNCL 1001 (264) | BT-3 | ExchangedDocument/TypeCode |
ublCheckDocumentTypeCode |
| Line-sum (265) | BR-CO-10 | Σ line LineTotalAmount = header BT-106 |
ublCheckTotals inv. 1 |
| Tax-basis (266) | BR-CO-13 | TaxBasisTotalAmount = BT-106 − BT-107 + BT-108 |
ublCheckTotals |
| Payable (267) | BR-CO-16 | DuePayableAmount = BT-112 − BT-113 + BT-114 |
ublCheckPayable |
| VAT-breakdown sum (268) | BR-CO-14 | Σ ApplicableTradeTax/CalculatedAmount = BT-110 |
ublCheckTaxTotalBreakdownSum |
| VAT per-rate cross-product (269) | BR-CO-17 | CalculatedAmount = BasisAmount × RateApplicablePercent |
ublCheckTaxCrossProduct |
| VAT category code (270) | UNCL 5305 | ApplicableTradeTax/CategoryCode |
ublCheckVatCategories |
| Seller/buyer country presence (271) | BR-09/11 | …TradeParty/PostalTradeAddress/CountryID |
ublCheck{Seller,Buyer}PostalAddressCountry |
| Country-code ISO 3166-1 (272) | BT-40/55 | PostalTradeAddress/CountryID |
ublCheckCountryCode |
| Line net cross-product (273) | BR-CO-04 | LineTotalAmount = NetPriceProductTradePrice/ChargeAmount × BilledQuantity |
ublCheckLineAmount |
| Due-date ordering (274) | BT-9 ≥ BT-2 | SpecifiedTradePaymentTerms/DueDateDateTime |
ublCheckDates |
| Specification id (275) | BR-01 / BT-24 | GuidelineSpecifiedDocumentContextParameter/ID |
ublCheckCustomizationIdPresence |
| Tax-scheme type code (276) | UNCL 5153 | ApplicableTradeTax/TypeCode = VAT |
ublCheckTaxScheme |
| Line item name (277) | BR-25 / BT-153 | SpecifiedTradeProduct/Name |
ublCheckItemName |
| Billed-qty unit code (278) | BT-130 | BilledQuantity/@unitCode |
ublCheckQuantityUnitCode |
| Line net-price presence (279) | BR-26 / BT-146 | NetPriceProductTradePrice/ChargeAmount |
ublCheckPriceAmountPresence |
| Line quantity presence (280) | BT-129 | SpecifiedLineTradeDelivery/BilledQuantity |
ublCheckLineQuantity |
| Profile identification (281) | BT-24 → profile | GuidelineSpecifiedDocumentContextParameter/ID URN |
CII-specific (Factur-X / ZUGFeRD profile) |
| Seller VAT validity (282) | BT-31 | SpecifiedTaxRegistration/ID schemeID="VA" |
ublCheckSellerVat |
Factur-X / ZUGFeRD profile identification (draft 281) decodes the BT-24 CustomizationID URN into the conformance profile the document declares — the profile sets which data is mandatory and which gates apply. ZUGFeRD 2.x is the German co-publication of Factur-X and shares the same URN family:
| Profile (BT-24 CustomizationID URN) | Scope | Typical use |
|---|---|---|
MINIMUM — urn:factur-x.eu:1p0:minimum |
header totals only, no lines | booking aid (not a legal invoice in FR/DE) |
BASIC WL — …:basicwl |
header + totals, no line detail | "without lines" |
BASIC — urn:cen.eu:en16931:2017#compliant#urn:factur-x.eu:1p0:basic |
EN 16931 subset incl. lines | core B2B |
EN 16931 (COMFORT) — urn:cen.eu:en16931:2017 |
full EN 16931 semantic model | the European standard |
EXTENDED — …#conformant#urn:factur-x.eu:1p0:extended |
EN 16931 + UN/CEFACT extensions | complex / multi-party flows |
XRechnung — …#compliant#urn:…:xrechnung_3.0 |
EN 16931 + German CIUS | German B2G (Chorus-Pro equivalent) |
Hybrid PDF/A-3 attachment extraction (draft 283) completes the CII slice: load a Factur-X / ZUGFeRD PDF and the inspector pulls the embedded invoice XML straight out of it — no server, no PDF library, no page-weight hit. Rather than parse the full PDF object graph and cross-reference tables, it walks every stream … endstream block, inflates each with the browser-native DecompressionStream (the same zlib used by PDF's FlateDecode), and keeps the first one whose bytes decode to a <rsm:CrossIndustryInvoice> or UBL Invoice-2 / CreditNote-2 document. The invoice-root signature check is what keeps the PDF's own XMP metadata stream (also XML, but <x:xmpmeta>) from being mistaken for the invoice; uncompressed streams are sniffed for a leading < so binary image / font streams are skipped without a decode attempt.
choose Factur-X / ZUGFeRD PDF
│ file.arrayBuffer() (never leaves the tab)
▼
%PDF magic check ──▶ scan stream … endstream blocks
│ │ per block:
│ ├─ DecompressionStream('deflate') ─ FlateDecode (zlib)
│ ├─ DecompressionStream('deflate-raw') ─ headerless deflate
│ └─ raw bytes (only if it starts with '<')
▼
first block decoding to <rsm:CrossIndustryInvoice> / UBL Invoice-2 ──▶ run the 24 CII gates
This was the last deferred CII slice; the inspector now ingests both the standalone XML and the hybrid PDF. (Per-draft detail in SPEC-INTERNATIONAL.md §2.7.)
The non-obvious engineering choices, and why they were made:
- Single HTML file, no build step. What is in the repository is byte-for-byte what runs in your browser — there is no transpiler, bundler, or minifier between source and shipped artifact to audit. The cost is a large source file; the benefit is that a reviewer can read exactly what executes.
- Zero network by construction, not by policy.
connect-src 'none'plus anaudit-release.jsgate that greps forfetch/XHR/WebSocket/EventSource/sendBeaconcall sites means "no exfiltration" is a property the browser enforces and CI verifies, not a promise in a privacy policy. - Hand-rolled parsers over vendoring for ASN.1/DER, the SSH wire format, Base32/Base58, OKLCH matrices, the cron parser, CIDR arithmetic, the LCS diff, and the RFC 4180 CSV parser. Each is small, auditable, and keeps the page under the 2 MB gzipped budget — vendoring a library for each would multiply the supply-chain surface for a few hundred lines of logic.
textContent/createElementonly — neverinnerHTML. A grep-verifiable invariant (zeroinnerHTML =assignments) removes the most common XSS sink even for fully developer-controlled content.- SHA-384 pinning instead of SRI. Same-origin scripts gain nothing from SRI's CORS requirements, so the integrity claim lives as a hash recorded in three places (HTML comment, README manifest, on-disk bytes) and is cross-checked on every commit — a forger has to defeat all three.
- Conformance gates as an incremental, test-backed methodology. The SEPA and UBL inspectors are not one-shot parsers — they are growing libraries of narrow, independent validation gates (253+ and counting — 111 SEPA, 118 UBL, and a CII-native family at 24 — as of this writing), each one shipped as a spec entry + implementation + i18n strings × 5 locales + Playwright confirm/flag pair. The thesis: a document that passes XSD validation can still be rejected by a clearing system or PEPPOL access point for a Rulebook reason the schema does not encode (a name-only
FinInstnIdwith no routable BIC, a VAT breakdown whose subtotals don't sum to the header, an amount over the scheme cap). Each gate catches one such real-world reject, no-ops cleanly on documents it doesn't apply to, and is pinned by a green test before it lands. The full catalogue lives in SPEC-INTERNATIONAL.md §2.4 (SEPA) and §2.7 (UBL).
| Option | Values | Notes |
|---|---|---|
| Algorithm | ECC (Curve25519), RSA 3072, RSA 4096 | ECC recommended |
| Expiration | Never, 1, 2, or 5 years | Choose based on use case |
| Passphrase | User-defined | Strong requirements enforced |
| Size | Performance | Recommendation |
|---|---|---|
| Under 100MB | Optimal | Recommended |
| 100MB - 500MB | May be slow | Use with patience |
| Over 500MB | Risk of browser crashes | Split files first |
| Over 1GB | Not recommended | Use desktop PGP tools |
| Feature | Encryptalotta | Web-based PGP Tools | Desktop PGP |
|---|---|---|---|
| No server uploads | Yes | Often No | Yes |
| No CDN dependencies | Yes | Usually No | Yes |
| Works offline | Yes | Usually No | Yes |
| No installation | Yes | Yes | No |
| Open source | Yes | Varies | Usually Yes |
| Modern ECC default | Yes | Varies | Varies |
| Enforced CSP | Yes | Rarely | N/A |
| Digital signatures | Yes | Sometimes | Yes |
| Password encryption | Yes | Rarely | Yes |
| Key info viewer | Yes | Rarely | Yes |
| Password generator | Yes | Rarely | Sometimes |
This is a single-file application. Simply open index.html in a web browser or deploy to any static hosting service.
git clone https://github.com/clay-good/encryptalotta.git
cd encryptalotta
# Open index.html in your browser - that's it!# Regenerate /fr/, /zh/, /de/, /hi/ from the source STRINGS table.
node scripts/build-i18n-variants.js
# Run all mechanical security/quality gates. Exits non-zero on first failure.
node scripts/audit-release.jsThe audit checks: zero innerHTML = assignments, STRINGS parity across all 5 locales, every data-i18n key resolves, vendored SHA-384 hashes match across HTML comments + the README manifest + on-disk bytes, CSP connect-src 'none' on both meta and _headers, no outbound network call sites, page weight under 2 MB gzipped, locale variants (/fr/, /zh/, /de/, /hi/) in sync with source, robots.txt + sitemap.xml + JSON-LD presence, Phase-7 SEO prose key coverage for all 53 tools, and (if present) sbom.json hash consistency and encryptalotta-portable.html having no <script src=> references.
# Generate a self-contained single-file build for USB / file:// / portable use.
node scripts/build-portable.js # → encryptalotta-portable.html (~1.3 MB)
# Emit a CycloneDX 1.5 SBOM listing all vendored deps with SHA-256/384/512 hashes.
node scripts/build-sbom.js # → sbom.json
# Append a SHA-256 manifest section for the current working tree (use at tagged releases).
node scripts/build-release-manifest.js # → RELEASES.md (append-only)All three scripts are deterministic: re-running with unchanged inputs produces byte-identical output, so they're safe to wire into a release pipeline. The audit cross-checks sbom.json hashes against the on-disk vendored bytes — if you update a vendored library and forget to regenerate the SBOM, the next commit's audit will fail.
Install the versioned hook on a fresh clone so audit-release.js runs on every commit:
ln -sf ../../scripts/git-hooks/pre-commit .git/hooks/pre-commitTo verify that the vendored libraries haven't been tampered with, run:
openssl dgst -sha384 -binary openpgp.min.js | openssl base64 -A
# Z5tStPoeClmuLPd1gAwdTCU53WcAkUjBNw7mEEvrQumVuNqEl52A9Nx5IhFdKE/c
openssl dgst -sha384 -binary qrcode.js | openssl base64 -A
# e9EFD6BGC90bkW9aDV5xbbBfzwN7G8YImHao2lfLVKV/hPB0E0go+H3I64h7oHtA
openssl dgst -sha384 -binary secrets.min.js | openssl base64 -A
# xfBMbh8fdSIrQ9XbZARwZ5z/Eh9zC7gsgG5vSE331lZSjgXQob1KxM4m7vEdH0e0
openssl dgst -sha384 -binary js-yaml.min.js | openssl base64 -A
# ZeqCzuWczURac3RacSufGD7oSbzeaX7xxnnOr3PTcYTLx4Av0qBj0kBq7AeCtHLAOr all at once:
for f in openpgp.min.js qrcode.js secrets.min.js js-yaml.min.js; do
printf '%-22s %s\n' "$f" "$(openssl dgst -sha384 -binary "$f" | openssl base64 -A)"
doneCompare each output against the Manifest above.
- Clone the repository
- Deploy the entire directory to your static hosting provider
- Ensure all files are served with proper MIME types
This site is optimized for Cloudflare Pages deployment with automatic security headers:
- Fork or clone this repository
- Connect to Cloudflare Pages
- Deploy - no build command needed (static HTML)
- Security headers from
_headersfile are automatically applied
| File / dir | Purpose |
|---|---|
index.html |
Main application (single-file, self-contained, includes all UI + i18n strings + tool logic) |
openpgp.min.js |
Vendored OpenPGP.js (key generation, encryption, decryption, signatures) |
qrcode.js |
Vendored qrcode-generator (QR Share tool) |
secrets.min.js |
Vendored secrets.js-grempe (Shamir Secret Sharing tool) |
js-yaml.min.js |
Vendored js-yaml (Format / Convert tool — YAML parse / emit) |
_headers |
HTTP security headers for Cloudflare Pages |
favicon.ico, favicon-*.png |
Browser tab icons |
apple-touch-icon.png |
iOS home screen icon |
encryptalotta.png |
Logo / OpenGraph image |
site.webmanifest |
Web app manifest |
robots.txt, sitemap.xml |
Search engine directives |
fr/, zh/, de/, hi/ |
Pre-rendered SEO variants (built by scripts/build-i18n-variants.js) |
i18n/strings.csv |
Reviewer-facing CSV exported from STRINGS (built by scripts/export-strings-csv.js) |
scripts/build-i18n-variants.js |
Emits localized static HTML for each non-en locale |
scripts/export-strings-csv.js / import-strings-csv.js |
CSV round-trip for native-speaker review |
scripts/audit-release.js |
Release-gate audit: 14 mechanical security/quality checks |
scripts/build-sitemap.js |
Regenerates sitemap.xml with today's <lastmod> and the 5 locale roots |
scripts/check-dependency-updates.js |
Quarterly upstream-version checker (used by GitHub Actions only — never runs on the live site) |
scripts/git-hooks/pre-commit |
Versioned pre-commit hook running audit-release.js |
.github/workflows/audit.yml |
Push / PR / manual CI gate that re-runs audit-release.js server-side |
.github/workflows/dep-check.yml |
Quarterly GitHub Actions workflow that runs check-dependency-updates.js |
Key Backup: If you lose your private key, you cannot decrypt your files. Ever. There is no recovery mechanism.
Key Security: Never share your private key with anyone. Your public key is safe to share.
Browser Security: This tool is only as secure as your browser environment. Use an updated browser on a trusted device.
Passphrase Strength: Use a strong, unique passphrase. The built-in password generator can help create secure passphrases.
Revocation Certificates: Generate and securely store a revocation certificate immediately after creating a new key pair. This allows you to invalidate the key if it's ever compromised.
Password Encryption: When using password-only encryption, choose a strong password. There is no recovery mechanism if you forget the password.
Signature Verification: Always verify the public key fingerprint through a trusted channel before trusting signatures from that key.
Offline Use: For maximum security, download the repository and use it offline on an air-gapped machine.
This application is open source specifically so security researchers can audit it. Key areas to review:
index.html- All application logic (CSP meta tags, JavaScript cryptographic calls, memory clearing, i18n, all 53 tools)_headers- HTTP security headersopenpgp.min.js- Compare against official OpenPGP.js v6.3.0 releaseqrcode.js- Compare against official qrcode-generator v2.0.4 releasesecrets.min.js- Compare against official secrets.js-grempe v2.0.0 releasejs-yaml.min.js- Compare against official js-yaml v4.1.1 release- Manifest section above — SHA-384 manifest for all vendored libraries
A grep-friendly audit invariant: the codebase has zero innerHTML = assignments. All DOM construction goes through createElement / textContent / setAttribute, eliminating the most common XSS vector even when content is fully controlled by the developer.
Found a vulnerability? Please report it via GitHub Issues or contact the maintainer directly.
