Encoding, escaping and hashing

Base64 is not encryption, btoa is not UTF-8, encodeURI is not encodeURIComponent, and SHA-256 is not a password hash. Here is what each of these actually does, and the specific mistakes that follow from assuming otherwise.

Encoding is not encryption, and it is not compression

Encoding changes how data is written down. Encryption changes who can read it. Compression changes how much space it takes. These are three different jobs, and base64 only does the first one — badly, if you were hoping for either of the other two.

Base64 takes three bytes at a time and rewrites them as four characters drawn from a 64-symbol alphabet. Four characters carrying three bytes means the output is always about 33% larger than the input, plus padding. It exists because a great deal of infrastructure — email headers, HTTP headers, JSON string values, URLs, XML attributes — was designed for text and mangles or rejects arbitrary bytes. Base64 is the adapter that lets you push bytes through a text-shaped pipe.

Anyone can reverse it instantly, without a key, because there is no key. If you base64 a password, you have published the password in a mildly inconvenient format. This matters because base64 output looks scrambled to a human eye, which is exactly the property that makes people trust it for things it cannot do.

Why btoa() breaks, and the two different ways it breaks

The browser gives you btoa() and atob(), and they are older than the modern text APIs. btoa is defined over "binary strings": strings in which every code unit is a single byte, from 0 to 255. Text is not that.

The first failure is loud. Call btoa("世界") and you get an InvalidCharacterError, because U+4E16 does not fit in a byte. Loud failures are the good kind — you notice them immediately and go looking for a fix.

The second failure is silent, and it is the one that reaches production. The character é is U+00E9, which does fit in a byte. So btoa("café") returns happily, encoding é as the single byte 0xE9. But é in UTF-8 is two bytes, 0xC3 0xA9. The base64 you just produced decodes, in every other system on earth, to something that is not your text. You will find out weeks later when a name in a database has turned into a replacement character.

The fix is to stop treating text as bytes and convert it explicitly. TextEncoder produces the UTF-8 bytes; encode those. TextDecoder turns bytes back into text, and constructing it with { fatal: true } makes it throw on invalid sequences instead of quietly substituting U+FFFD, so a decode that cannot possibly be right fails rather than returning plausible-looking nonsense. That is the pipeline this toolkit uses, which is why emoji, combining marks and right-to-left scripts all round-trip exactly.

  1. Convert text to bytes with TextEncoder — never index into the string.
  2. Encode the bytes to base64.
  3. To reverse: decode base64 to bytes, then decode the bytes as UTF-8 with fatal: true.
  4. If the UTF-8 step fails, the payload is binary, not text. Show it as hex rather than pretending.

base64 versus base64url, and the padding question

Standard base64 uses + and / as its last two symbols. Both are meaningful in URLs: + can be read as an encoded space in query strings, and / is a path separator. So RFC 4648 defines a second alphabet, base64url, which substitutes - and _ instead. JWTs use it, as do most token formats and many APIs.

Padding is the other variable. Standard base64 pads with = so the output length is always a multiple of four. base64url usually drops the padding, because = is itself an awkward character in a URL and the length can be recovered arithmetically. A decoder that insists on padding will reject perfectly valid JWT segments.

Practical advice: your decoder should accept both alphabets and tolerate missing padding, because you rarely control what you are handed. Your encoder should be explicit about which it emits, because the recipient probably does care. The base64 utility here does exactly that — it accepts anything reasonable and lets you choose precisely what it produces.

encodeURI and encodeURIComponent: the difference in one sentence

Both percent-encode using UTF-8. They differ only in which characters they leave alone, and that difference is the whole story: encodeURIComponent escapes the reserved delimiters, encodeURI does not.

The reserved delimiters are the characters that give a URL its structure: : / ? # [ ] @ ! $ & ' ( ) * + , ; =. encodeURI assumes you handed it a URL that is already correctly structured and must stay that way, so it preserves them — it will not turn https:// into https%3A%2F%2F. encodeURIComponent assumes you handed it one piece that is going to be dropped into a slot, so it escapes them, ensuring the piece cannot break out of its slot.

The bug this produces is completely mechanical. Take a search value of a&b=c. Encode it with encodeURI and append it as ?q=a&b=c, and you have silently created two parameters: q is now just "a", and a stray b=c has appeared. Encode it with encodeURIComponent and you get ?q=a%26b%3Dc, one parameter, correct value. The same class of bug lets a crafted value inject parameters into a URL your code builds — which is why "use the component form for values" is a security rule, not just a correctness one.

Form encoding is a third rule that looks like the second. application/x-www-form-urlencoded writes a space as + rather than %20. If you decode a form body with plain decodeURIComponent, every plus sign in the data becomes a space. Every search box that has ever mangled "C++" into "C " is this bug.

HTML entities, and why decoding them with innerHTML is a bad habit

Escaping for HTML is narrow and well understood: & becomes &amp;, < becomes &lt;, > becomes &gt;, and inside attribute values " and ' need escaping too. Five characters. Escaping more than that — turning every accented letter into a named entity — was a workaround for the days of uncertain character encodings, and is now optional styling rather than safety.

Decoding is where the bad habit lives. The one-line trick that appears in every answer is to assign the string to a detached element’s innerHTML and read back its textContent. It works, and it is a poor idea. You have handed untrusted input to the HTML parser, which builds real DOM nodes from it. An <img src=x onerror=...> in that string becomes an actual image element with an actual error handler attached; if that subtree is ever inserted into the document, it runs. It also silently destroys your data: tags in the input vanish instead of round-tripping, because the parser interpreted them as markup rather than text.

Decoding entities properly needs no parser at all: match the reference, look the name up in a table, or do the arithmetic for a numeric reference. That is a few dozen lines, it cannot execute anything, and it round-trips faithfully. This toolkit does it that way, which is why pasting a script tag into the entity decoder shows you a script tag.

Choosing a hash, and the three questions that decide it

A cryptographic hash turns any input into a fixed-length digest, such that finding two inputs with the same digest should be infeasible. That property is what lets a digest stand in for the data — in a signature, an integrity check, or a content address.

First question: are you protecting against accident or against an adversary? A checksum guarding against a corrupted download only needs to catch random flips; CRC32 is fine. A digest that an attacker could benefit from colliding needs a hash that is still standing. That distinction is why SHA-1 is not simply "old".

SHA-1 is broken, concretely. In 2017 the SHAttered work produced two different PDF files with the same SHA-1 digest. In 2020, "SHA-1 is a Shambles" demonstrated a chosen-prefix collision — the stronger and far more dangerous variant, because it lets an attacker collide two meaningfully different documents rather than two carefully constructed blobs. If a system’s security rests on SHA-1 collision resistance, that security is gone. SHA-1 remains in this toolkit because git object ids and a long tail of legacy API signatures still use it, and you need to be able to reproduce those values. Reproducing a value is not the same as relying on it.

Second question: is the input a password? If so, none of these are the answer. SHA-256 is designed to be fast, and fast is precisely wrong for passwords: it means an attacker with your database can try billions of guesses per second. Passwords need a deliberately slow, memory-hard function with a per-user salt — Argon2id, scrypt, or bcrypt. This is not a nuance; using SHA-256 for passwords is the single most common serious hashing mistake.

Third question: do you need a keyed digest? If you are authenticating a message rather than fingerprinting it, you want HMAC, not a bare hash. Concatenating a secret and hashing it is a classic own-goal against length-extension attacks; HMAC exists because that construction is harder to get right than it looks.

For everything else — fingerprinting a file, a content address, an integrity attribute — SHA-256 is the sensible default, and SHA-512 is often faster on 64-bit hardware while giving a wider digest.

Why the hashes here come from the browser

The digests in this toolkit are computed by SubtleCrypto, the browser’s own Web Crypto implementation, not by JavaScript shipped from this site. That is a deliberate choice: the browser’s implementation is audited, maintained, and usually running as optimised native code. A hand-written SHA-256 in a page bundle is more code to trust for no benefit.

It has one visible consequence. Web Crypto is only exposed in a secure context, meaning https:// or localhost. Open this page over plain HTTP on a LAN address and crypto.subtle will be undefined, so the hash utility will tell you so plainly rather than failing silently or substituting something weaker.

The same reasoning drives the UUID generator. crypto.randomUUID() is also secure-context-only, so where it is unavailable the toolkit falls back to crypto.getRandomValues() — which is still the same cryptographically secure source — and sets the version and variant bits itself. What it will never do is fall back to Math.random(). That is a fast non-cryptographic PRNG whose internal state can be recovered from a short run of its outputs, and identifiers have an unfortunate habit of being promoted into session keys and password-reset links. If no secure source exists, this tool generates nothing and says why.

What happens to what you paste

  • Every conversion, hash, decode and diff runs in your browser tab. No input is uploaded, logged or stored on a server, because there is no server involved once the page has loaded.
  • Hashes come from the browser’s own Web Crypto implementation, and UUIDs from its cryptographically secure random generator. Neither involves a network call.
  • Nothing you type is written to local storage or a cookie. Reloading the page discards it; closing the tab discards it.
  • There is no analytics script, no advertising script and no third-party request of any kind. You can confirm all of this in your browser’s network panel — the page makes no requests after it loads.
  • That said: a JWT or an API key is a live credential. The safe habit is never to paste one into a web page you did not write, however trustworthy its claims — including this one.

Questions

Is base64 a way to hide data?

No. It is a reversible text representation with no key, decodable by anyone in a fraction of a second. It makes data survive text-only channels; it does not make it secret. Anything genuinely sensitive needs encryption, and the encrypted result is often then base64-encoded for transport — which is the source of the confusion.

Why is my base64 longer than the input?

Because four output characters carry three input bytes, so the output is roughly 4/3 the size, plus up to two padding characters. That is inherent to the format. If size matters, compress before encoding — never after, because base64 output compresses poorly.

Which URL encoding function should I use?

Use encodeURIComponent for any single piece you are inserting into a URL: a query value, a path segment, a fragment. Use encodeURI only when you have a whole, already-structured URL that merely contains spaces or non-ASCII. If you are building a query string, prefer URLSearchParams, which applies the correct rule for you and handles the space-as-plus difference.

Why does my decoder throw "URI malformed"?

Because a % in the input is not followed by two hexadecimal digits. Usually the text contains a literal percent sign — "50% off" — that was never encoded. A literal percent must be written as %25. The URL utility here reports the exact position of the offending escape instead of just refusing.

Can I use SHA-256 to store passwords?

No. SHA-256 is fast by design, which means an attacker who steals your database can test billions of candidate passwords per second on commodity hardware. Passwords need a slow, memory-hard, salted function: Argon2id, scrypt, or bcrypt. This is the most common serious mistake in this area.

Why is SHA-1 still here if it is broken?

Because you still need to reproduce SHA-1 values that already exist: git object ids, old TLS certificate fingerprints, legacy API request signatures. Being able to compute a value for interoperability is different from relying on it for security. Every place SHA-1 appears in this toolkit is labelled accordingly.

Why do two tools give different hashes for the same text?

Almost always a difference in the bytes, not the algorithm. The usual culprits are a trailing newline (a file ends with one; a text box may not), a different text encoding, or CRLF versus LF line endings. This tool hashes the UTF-8 bytes of exactly what you typed and shows you the byte count, which usually makes the discrepancy obvious.

Limitations

  • The named HTML entity table covers the practical subset — markup-critical characters, typography, currency, arrows, maths, Greek and Latin-1 — not all 2,231 HTML5 named references. Unrecognised names are reported and left exactly as written rather than guessed at.
  • Entity decoding requires the terminating semicolon. HTML5 tolerates a handful of legacy references without one, but decoding them correctly depends on the surrounding markup context, which a standalone text tool does not have.
  • Hashing and UUID generation need a secure context (https:// or localhost) because Web Crypto is not exposed otherwise. The tool reports this rather than substituting a weaker implementation.
  • Only SHA-1, SHA-256, SHA-384 and SHA-512 are available, because those are what SubtleCrypto implements. MD5 is absent by choice as well as by necessity.
  • There is no HMAC, no key derivation and no encryption here. Those need key management, which is not something a page you found on the internet should be handling.
  • Everything is limited by your device’s memory, since it all runs in one browser tab. Inputs are capped — a few megabytes per utility — and the tool refuses oversized work rather than freezing.

Last reviewed 2026-09-13.