Encoding, hashing and encryption are three different things
A string that looks scrambled is not a string that is protected. Three separate operations produce unreadable output, and confusing them is where real security bugs come from.
| Reversible? | Needs a key? | What it is for | |
|---|---|---|---|
| Encoding | Yes, by anyone | No | Getting bytes through a channel that cannot carry them raw |
| Hashing | No | No | Fingerprints, integrity checks, lookup keys |
| Encryption | Yes, with the key | Yes | Secrecy |
Encoding is a change of alphabet. Base64, percent encoding, hex and HTML entities write the same information differently so that a parser downstream does not choke on it. No secret is involved, so there is nothing to keep. A Caesar shift belongs here too: the Caesar Cipher tool brute forces all 25 shifts at once, a fair summary of how much a fixed substitution protects. Hashing maps any input to a fixed length digest and cannot be run backwards. Encryption is the only one of the three that provides confidentiality, and it always involves a key. If you cannot point at the key, nothing is being encrypted.
Base64 and what it costs
Base64 splits three bytes (24 bits) into four groups of six and writes each group as one character from a 64 character alphabet. Four characters per three bytes means the output is a third larger. That matters most when embedding files: the File to Base64 tool produces a data URL, so a 300 KB image lands in your HTML at roughly 400 KB, re-downloaded with every page.
When the input is not a multiple of three bytes, the final group is padded with =:
"abc" -> YWJj (3 bytes, no padding)
"ab" -> YWI= (2 bytes, one =)
"a" -> YQ== (1 byte, two =)
The standard alphabet ends in + and /, a problem in a URL, because + decodes as a space in form data and / is a path separator. So a second alphabet exists: Base64url swaps + for - and / for _, and usually drops the padding. That is why a token copied out of a URL sometimes fails in a decoder set to the standard alphabet. The Base64 Encoder handles both, and handles Unicode correctly, which naive implementations often do not: text must be UTF-8 encoded before the Base64 step.
None of this is a security measure. A value a product calls an "encrypted" identifier in a URL is very often Base64 of a plain integer that anyone can read, change and re-encode in seconds. The same goes for the payload of a JSON Web Token: the JWT Decoder reads it without a key, because there is nothing to unlock.
Percent encoding, and which function to use
Percent encoding replaces a byte with % and two hex digits. It works on bytes rather than characters, so non-ASCII text is UTF-8 encoded first: é becomes %C3%A9.
Unreserved characters (A-Z, a-z, 0-9, -, ., _, ~) never need encoding. Reserved characters (: / ? # [ ] @ ! $ & ' ( ) * + , ; =) are the delimiters that give a URL its structure, and whether one needs encoding depends on whether it is being used as a delimiter or as data. That is the difference between the two JavaScript functions:
encodeURI("https://ex.com/s?q=cats & dogs")
// https://ex.com/s?q=cats%20&%20dogs the & still splits the query
encodeURIComponent("cats & dogs")
// cats%20%26%20dogs safe as a single value
encodeURI is for a whole URL you want to make legal, so it leaves delimiters intact. encodeURIComponent is for one value going into a path segment or query parameter, so it encodes them. Use the second for anything you are inserting, and note that it still leaves !, ', (, ) and * alone.
Then there is the plus sign. Form bodies sent as application/x-www-form-urlencoded encode a space as +, and the convention leaked into query strings, so most servers decode + in a query as a space. A literal plus must be sent as %2B, which is why alex+billing@example.com so often arrives as alex billing@example.com with the tag destroyed. In a path segment, by contrast, + is just a plus and a space is %20. The URL Encoder makes that visible when a value survives one round trip and breaks on the next.
Escaping depends on where the value lands
There is no single "escaped for the web" form of a string. The context decides.
| Context | Correct treatment |
|---|---|
| HTML text | Entities for &, < and > |
| Quoted attribute | Entities for & and for whichever quote wraps it, and always quote the attribute |
Inside <script> | JavaScript string escaping, not entities |
URL in href or src | Percent encode the value, attribute escape it, then check the scheme |
| CSS value | CSS escaping, and never build url() from user input |
The script case catches people out. Script contents are not entity decoded, so an entity written there stays literal, and the HTML parser closes the block on the sequence </script wherever it appears, quoted string or not:
<script>var s = "</script>";</script> <!-- block ends early -->
<script>var s = "<\/script>";</script> <!-- correct -->
Escaping alone is not enough for URLs either. A value starting javascript: executes when clicked however carefully it was encoded, so validate the scheme against an allowed list. The HTML Entities tool covers the text and attribute cases and decodes entities back, the usual need when an API has double escaped something into &amp;.
Hashing: which algorithm, and for what
| Algorithm | Digest | Status |
|---|---|---|
| MD5 | 128 bit | Broken. Collisions are trivial |
| SHA-1 | 160 bit | Broken. Chosen prefix collisions are practical and affordable |
| SHA-256, SHA-512 | 256, 512 bit | Fine for integrity and signatures |
| bcrypt, scrypt, Argon2 | varies | Passwords only |
MD5 and SHA-1 fail at collision resistance, meaning an attacker can construct two different inputs with the same digest. Anywhere a hash stands in for a document they are unusable: signatures, certificates, content addressing, deduplicating anything an attacker supplies. Neither is broken for preimages, so an old MD5 checksum still catches accidental corruption, but neither belongs in new work. Use SHA-256, which the Hash Generator computes for files as well as text.
Passwords are a different problem, and a fast hash is the wrong tool precisely because it is fast. Commodity hardware does billions of SHA-256 operations per second, so a leaked table of SHA-256 password hashes is a leaked table of passwords. bcrypt, scrypt and Argon2id are deliberately slow and tunable, with a work factor (and for the latter two a memory cost) you raise as hardware improves, and each stores a unique random salt in its output.
A hash is also not a signature. To prove a message came from someone holding a key, use HMAC-SHA-256 rather than hashing the secret and the message glued together, which is vulnerable to length extension.
What a hash cannot do
A hash cannot be reversed by calculation. It can be reversed by search whenever the set of possible inputs is small enough to enumerate: hash every four digit PIN and you have all ten thousand digests instantly, and the same goes for every postcode, phone number or address in a leaked list. Hashing an identifier does not anonymise it.
Salting, a unique random value stored with each record and mixed into the input, does not make one target harder to guess, but it forces an attacker to attack every record separately and makes precomputed tables worthless. For low entropy values, a pepper (a secret key held outside the database) is the part that genuinely helps.
Recognising an unknown string
| Shape | Tell | Example |
|---|---|---|
| Hex | Only 0-9a-f, even length. 32 chars is MD5, 40 is SHA-1, 64 is SHA-256 | 5d41402abc4b2a76b9719d911017c592 |
| Base64 | Mixed case with + and /, length a multiple of 4, may end = or == | SGVsbG8sIHdvcmxkIQ== |
| Base64url | The same with - and _, usually unpadded | eyJzdWIiOiIxMjM0NSJ9 |
| Percent encoded | % followed by two hex digits | a%20b%26c |
| JWT | Three Base64url chunks separated by full stops, starting eyJ | eyJhbGciOi... |
| bcrypt | Exactly 60 characters, starting $2a$, $2b$ or $2y$ and a cost | $2b$12$... |
| Argon2 | $argon2id$v=19$m=...,t=...,p=...$salt$hash |
eyJ is worth memorising: it is Base64 of {", so any chunk starting that way is an encoded JSON object, token or not.
Decoding successfully is not proof the guess was right, because Base64 will turn any input into bytes. Check the result is plausible text, or starts with a known file signature: Base64 beginning iVBORw0KGgo is a PNG, /9j/ a JPEG, JVBERi0 a PDF and UEsDB a zip. If the bytes look like nothing, run the output through the Text to Binary tool to read the codes directly, or check whether the value was percent encoded before it was Base64 encoded, a common double wrapping.