<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: CheBit</title>
    <description>The latest articles on DEV Community by CheBit (@chebit).</description>
    <link>https://dev.to/chebit</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4182246%2Fa066bb43-e822-47ad-b83c-3f630e138156.png</url>
      <title>DEV Community: CheBit</title>
      <link>https://dev.to/chebit</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/chebit"/>
    <language>en</language>
    <item>
      <title>URL encoding, explained by the bugs it causes</title>
      <dc:creator>CheBit</dc:creator>
      <pubDate>Thu, 22 Oct 2026 14:43:59 +0000</pubDate>
      <link>https://dev.to/chebit/url-encoding-explained-by-the-bugs-it-causes-4o80</link>
      <guid>https://dev.to/chebit/url-encoding-explained-by-the-bugs-it-causes-4o80</guid>
      <description>&lt;p&gt;A link that works when you click it can still break when you paste it somewhere else.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://app.example.com/login?next=https://app.example.com/dashboard?tab=billing
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That looks like a redirect to a dashboard tab. But look at the second &lt;code&gt;?&lt;/code&gt;. Is &lt;code&gt;tab=billing&lt;/code&gt; part of the &lt;code&gt;next&lt;/code&gt; value, or a second parameter on the login URL? Different parsers answer differently. One of them sends the user to the right place; another sends them to a 404.&lt;/p&gt;

&lt;p&gt;The fix is percent-encoding. And it is one of those things everyone uses and almost nobody can explain precisely, which is why the same three bugs keep showing up in production.&lt;/p&gt;

&lt;h2&gt;
  
  
  What percent-encoding actually does
&lt;/h2&gt;

&lt;p&gt;Percent-encoding works on bytes, not characters. It leaves a small set of characters alone and rewrites everything else as &lt;code&gt;%&lt;/code&gt; followed by two hexadecimal digits.&lt;/p&gt;

&lt;p&gt;The characters that never need encoding — the "unreserved" set from RFC 3986 — are:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;A-Z  a-z  0-9  -  .  _  ~
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Everything else becomes bytes, and each byte becomes &lt;code&gt;%XX&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nf"&gt;encodeURIComponent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;a b/c&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;   &lt;span class="c1"&gt;// 'a%20b%2Fc'&lt;/span&gt;
&lt;span class="nf"&gt;encodeURIComponent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;é&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;       &lt;span class="c1"&gt;// '%C3%A9'&lt;/span&gt;
&lt;span class="nf"&gt;encodeURIComponent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;日&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;      &lt;span class="c1"&gt;// '%E6%97%A5'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice that &lt;code&gt;é&lt;/code&gt; is a single character but produces two escapes. That is not a quirk of the encoding function — &lt;code&gt;é&lt;/code&gt; is two bytes in UTF-8, and percent-encoding escapes bytes. This is also why URL-encoded strings look so much longer than the text they came from.&lt;/p&gt;

&lt;h2&gt;
  
  
  The three characters behind most of the bugs
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;&amp;amp;&lt;/code&gt; separates parameters.&lt;/strong&gt; If it appears inside a value, it has to be &lt;code&gt;%26&lt;/code&gt;. Otherwise the parser starts a new parameter and the rest of your value becomes a stray key.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nf"&gt;encodeURIComponent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;a&amp;amp;b&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;     &lt;span class="c1"&gt;// 'a%26b'&lt;/span&gt;
&lt;span class="nf"&gt;encodeURIComponent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;?&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;       &lt;span class="c1"&gt;// '%3F'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;&lt;code&gt;+&lt;/code&gt; means space only in form-encoded data.&lt;/strong&gt; That convention comes from HTML form submission (&lt;code&gt;application/x-www-form-urlencoded&lt;/code&gt;), not from the URL standard itself. In a query string most servers accept both &lt;code&gt;+&lt;/code&gt; and &lt;code&gt;%20&lt;/code&gt; as a space. In a path segment, &lt;code&gt;+&lt;/code&gt; is a literal plus, not a space.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;%&lt;/code&gt; starts an escape.&lt;/strong&gt; A literal percent sign must be encoded as &lt;code&gt;%25&lt;/code&gt;. This is where double encoding begins.&lt;/p&gt;

&lt;h2&gt;
  
  
  The double-encoding trap
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nf"&gt;encodeURIComponent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;%20&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;                        &lt;span class="c1"&gt;// '%2520'&lt;/span&gt;
&lt;span class="nf"&gt;encodeURIComponent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;encodeURIComponent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;a b&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;    &lt;span class="c1"&gt;// 'a%2520b'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you encode a string that was already encoded, the &lt;code&gt;%&lt;/code&gt; becomes &lt;code&gt;%25&lt;/code&gt; and the escape stops working. The server decodes once and gets &lt;code&gt;%20&lt;/code&gt; back as literal text, never a space. Strings containing &lt;code&gt;%2520&lt;/code&gt; in a browser address bar are almost always this bug.&lt;/p&gt;

&lt;p&gt;The cause is usually two layers each assuming the other did not encode. A client encodes a value, then a library encodes the whole URL again. The rule is simple: &lt;strong&gt;encode exactly once, at the boundary where the value enters the URL.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  encodeURI vs encodeURIComponent
&lt;/h2&gt;

&lt;p&gt;JavaScript gives you two functions with confusingly similar names.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nf"&gt;encodeURI&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://example.com/a b&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;// 'https://example.com/a%20b'&lt;/span&gt;

&lt;span class="nf"&gt;encodeURIComponent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://example.com/a b&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;// 'https%3A%2F%2Fexample.com%2Fa%20b'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;encodeURI&lt;/code&gt; preserves the characters that give a URL its structure — &lt;code&gt;: / ? # &amp;amp; =&lt;/code&gt; — so it is for a complete URL you want to keep intact. &lt;code&gt;encodeURIComponent&lt;/code&gt; escapes all of those, so it is for a value you are inserting into a URL, like a search term or a redirect target.&lt;/p&gt;

&lt;p&gt;One thing that surprises people: &lt;code&gt;encodeURIComponent&lt;/code&gt; does not escape &lt;code&gt;!&lt;/code&gt;, &lt;code&gt;'&lt;/code&gt;, &lt;code&gt;(&lt;/code&gt;, &lt;code&gt;)&lt;/code&gt;, or &lt;code&gt;*&lt;/code&gt;. Those are valid sub-delimiters, so the output is still correct, but it is not the strictest possible encoding.&lt;/p&gt;

&lt;p&gt;The redirect example from the top, fixed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;target&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://app.example.com/dashboard?tab=billing&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;login&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://app.example.com/login?next=&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nf"&gt;encodeURIComponent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// https://app.example.com/login?next=https%3A%2F%2Fapp.example.com%2Fdashboard%3Ftab%3Dbilling&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now the inner &lt;code&gt;?&lt;/code&gt; cannot be mistaken for the outer one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Spaces: %20 or +
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;URLSearchParams&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;q&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;a b&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="nf"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;   &lt;span class="c1"&gt;// 'q=a+b'&lt;/span&gt;
&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;URLSearchParams&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;q&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;a+b&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="nf"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;   &lt;span class="c1"&gt;// 'q=a%2Bb'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;URLSearchParams&lt;/code&gt; follows the form convention, so it writes spaces as &lt;code&gt;+&lt;/code&gt; and escapes a literal plus as &lt;code&gt;%2B&lt;/code&gt;. That is fine for query strings, and it is a good reason to use the API instead of building query strings by hand.&lt;/p&gt;

&lt;h2&gt;
  
  
  The slash that is not a slash
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;/&lt;/code&gt; can legally be encoded as &lt;code&gt;%2F&lt;/code&gt;. That does not mean every server treats it as data. Many frameworks decode the path before routing, then see a path separator. Proxies and CDNs differ in whether they decode at all. The result is a class of confusing bugs: a URL that returns 200 locally, 404 behind a load balancer, and 403 in a WAF.&lt;/p&gt;

&lt;p&gt;If you need an arbitrary identifier inside a path segment, avoid relying on &lt;code&gt;%2F&lt;/code&gt;. Use base64url, a UUID, or an opaque ID instead. And when validating decoded input, decode once and check the result — never decode repeatedly, because that is how encoded traversal sequences slip through.&lt;/p&gt;

&lt;h2&gt;
  
  
  The short version
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Only &lt;code&gt;A-Z a-z 0-9 - . _ ~&lt;/code&gt; are unreserved. Everything else gets percent-encoded.&lt;/li&gt;
&lt;li&gt;Encoding works on UTF-8 bytes, so &lt;code&gt;é&lt;/code&gt; becomes &lt;code&gt;%C3%A9&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Use &lt;code&gt;encodeURIComponent&lt;/code&gt; for values and &lt;code&gt;encodeURI&lt;/code&gt; for whole URLs.&lt;/li&gt;
&lt;li&gt;Encode once. &lt;code&gt;%2520&lt;/code&gt; means someone encoded twice.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;+&lt;/code&gt; means space only in form-encoded query strings.&lt;/li&gt;
&lt;li&gt;Do not put &lt;code&gt;%2F&lt;/code&gt; in a path and expect a normal slash.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I put together a browser-based &lt;a href="https://usetoolvault.com/tools/url-encoder-decoder" rel="noopener noreferrer"&gt;URL encoder and decoder&lt;/a&gt; for checking these cases without sending anything to a server — useful when the URL contains a token or an internal hostname you would rather not paste into someone else's tool.&lt;/p&gt;

&lt;p&gt;If you have ever wondered why a hash cannot be "decrypted", I covered the neighbouring question in an earlier post: hashing is one-way, encoding is reversible, and encryption is the one that needs a key.&lt;/p&gt;

&lt;p&gt;I build small, privacy-first developer tools that run entirely in the browser. Most of them exist because I needed something for five minutes and every alternative wanted an upload or an account first.&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>webdev</category>
      <category>programming</category>
      <category>security</category>
    </item>
    <item>
      <title>Why your base64-encoded image got bigger</title>
      <dc:creator>CheBit</dc:creator>
      <pubDate>Sun, 18 Oct 2026 04:18:23 +0000</pubDate>
      <link>https://dev.to/chebit/why-your-base64-encoded-image-got-bigger-51jh</link>
      <guid>https://dev.to/chebit/why-your-base64-encoded-image-got-bigger-51jh</guid>
      <description>&lt;p&gt;A 40 KB icon becomes a 53 KB data URI the moment you inline it. The page still works, the browser still renders it, but the HTML got bigger for no obvious reason.&lt;/p&gt;

&lt;p&gt;That is usually the first time anyone notices Base64 is doing something, and it is also where the two most common misconceptions start: that Base64 compresses data, and that it hides it.&lt;/p&gt;

&lt;p&gt;It does neither. Base64 is a transport encoding. Once you see what it actually does, both myths fall apart.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three bytes in, four characters out
&lt;/h2&gt;

&lt;p&gt;Base64 takes the raw bytes of your input and rewrites them using only 64 characters: &lt;code&gt;A-Z&lt;/code&gt;, &lt;code&gt;a-z&lt;/code&gt;, &lt;code&gt;0-9&lt;/code&gt;, &lt;code&gt;+&lt;/code&gt; and &lt;code&gt;/&lt;/code&gt;. That is a deliberately boring alphabet — nothing in it needs escaping inside an HTML attribute, a JSON string or an email header. (&lt;code&gt;+&lt;/code&gt; and &lt;code&gt;/&lt;/code&gt; do still need encoding in a URL, which is exactly why the URL-safe variant exists; more on that below.)&lt;/p&gt;

&lt;p&gt;The arithmetic is the whole story. Each byte is 8 bits. Base64 regroups the stream into 6-bit chunks, and 6 bits can address exactly 64 characters. Three bytes (24 bits) become four characters (4 × 6 bits).&lt;/p&gt;

&lt;p&gt;Three in, four out. That is where the 33% expansion comes from — it is not a bug or a bad implementation, it is the definition.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;input&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;hello&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;                                  &lt;span class="c1"&gt;// 5 bytes&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;encoded&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;Buffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;base64&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;  &lt;span class="c1"&gt;// 8 characters&lt;/span&gt;

&lt;span class="nx"&gt;encoded&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  &lt;span class="c1"&gt;// 'aGVsbG8='&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;=&lt;/code&gt; at the end is padding. When the input length is not a multiple of three, the last group is incomplete, and &lt;code&gt;=&lt;/code&gt; fills the gap so the output length stays a multiple of four. One or two &lt;code&gt;=&lt;/code&gt;, never more.&lt;/p&gt;

&lt;h2&gt;
  
  
  Not encryption: there is no key
&lt;/h2&gt;

&lt;p&gt;Base64 has no secret. The alphabet is public, the algorithm is public, and every step is reversible by anyone holding the string. There is nothing to "decrypt" because nothing was encrypted.&lt;/p&gt;

&lt;p&gt;This matters in one place more than any other: JWTs. A JWT is three Base64url segments joined by dots, and the middle one is the payload. People regularly assume it is encrypted because it looks like noise.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjMiLCJyb2xlIjoiYWRtaW4ifQ.signature&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;

&lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;Buffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;base64url&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;span class="c1"&gt;// { sub: '123', role: 'admin' }&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No key was needed. Anyone who intercepts that token can read the payload. JWTs are signed, not encrypted — the signature proves the token was not altered, it does not hide the contents.&lt;/p&gt;

&lt;p&gt;If you need to hide something, encode it after you encrypt it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;plaintext -&amp;gt; encrypt (AES-GCM) -&amp;gt; base64 -&amp;gt; transport
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Base64 step here exists only because binary ciphertext cannot travel safely through text channels. The secrecy comes entirely from the encryption step.&lt;/p&gt;

&lt;h2&gt;
  
  
  Not compression: it makes things bigger
&lt;/h2&gt;

&lt;p&gt;The 33% expansion is measurable and predictable.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;bytes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;Buffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;alloc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100000&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;           &lt;span class="c1"&gt;// 100 KB of bytes&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;encoded&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;base64&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nx"&gt;encoded&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;   &lt;span class="c1"&gt;// 133336  (~133 KB)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Compression reduces size by finding repetition. Base64 does the opposite: it takes a dense byte stream and spreads it across a larger set of characters. It cannot reduce entropy, because it does not try.&lt;/p&gt;

&lt;p&gt;The practical consequence is that inlining assets as data URIs is a trade-off, not a free win. For a 1 KB SVG icon it can be worth it — you save an HTTP request and the overhead is negligible. For a 500 KB photo it is usually the wrong call: you pay 33% more HTML on every page load, and a large inline URI holds up the parser before anything renders.&lt;/p&gt;

&lt;p&gt;The rule of thumb: inline things under a few kilobytes, link to everything else.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where Base64 is the right tool
&lt;/h2&gt;

&lt;p&gt;It exists because some channels only accept text. Base64 turns arbitrary bytes into something those channels can carry unchanged.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Data URIs.&lt;/strong&gt; &lt;code&gt;src="data:image/png;base64,..."&lt;/code&gt; for small icons that should not cost a request.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Email attachments (MIME).&lt;/strong&gt; Binary files are Base64-encoded so they can travel through text-based mail protocols. MIME also wraps the output at 76 characters per line.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Binary payloads in JSON.&lt;/strong&gt; Many APIs accept a file as a Base64 string instead of a multipart body.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Base64url.&lt;/strong&gt; A URL-safe variant that swaps &lt;code&gt;+&lt;/code&gt; for &lt;code&gt;-&lt;/code&gt; and &lt;code&gt;/&lt;/code&gt; for &lt;code&gt;_&lt;/code&gt;, and usually drops the padding. JWTs and OAuth parameters use it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Basic auth.&lt;/strong&gt; &lt;code&gt;Authorization: Basic base64(user:password)&lt;/code&gt; — again encoded for transport, not protected.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In every one of those cases, the reason is the same: the data is safe to carry as text, not secret and not smaller.&lt;/p&gt;

&lt;h2&gt;
  
  
  The trap: btoa does not handle Unicode
&lt;/h2&gt;

&lt;p&gt;Base64 operates on bytes, not characters. That distinction bites JavaScript developers because &lt;code&gt;btoa()&lt;/code&gt; predates Unicode-aware APIs and only accepts Latin-1.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nf"&gt;btoa&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;hello&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;   &lt;span class="c1"&gt;// 'aGVsbG8='&lt;/span&gt;
&lt;span class="nf"&gt;btoa&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;héllo&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;   &lt;span class="c1"&gt;// 'aOlsbG8='  — Latin-1 bytes, not UTF-8&lt;/span&gt;
&lt;span class="nf"&gt;btoa&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;€&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;       &lt;span class="c1"&gt;// throws: InvalidCharacterError&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The second line is the dangerous one. Because &lt;code&gt;é&lt;/code&gt; sits inside Latin-1, &lt;code&gt;btoa&lt;/code&gt; does not complain. It silently encodes the single Latin-1 byte &lt;code&gt;0xE9&lt;/code&gt; instead of the two UTF-8 bytes &lt;code&gt;0xC3 0xA9&lt;/code&gt;, so you get valid Base64 that decodes to something different from what you intended. Only characters above U+00FF, like &lt;code&gt;€&lt;/code&gt; or &lt;code&gt;日本&lt;/code&gt;, actually throw.&lt;/p&gt;

&lt;p&gt;The fix is to convert the string to UTF-8 bytes first:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;toBase64&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
  &lt;span class="nf"&gt;btoa&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fromCharCode&lt;/span&gt;&lt;span class="p"&gt;(...&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;TextEncoder&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)));&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;fromBase64&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;b64&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
  &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;TextDecoder&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nb"&gt;Uint8Array&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;atob&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;b64&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;charCodeAt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nf"&gt;toBase64&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;héllo&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;          &lt;span class="c1"&gt;// 'aMOpbGxv'&lt;/span&gt;
&lt;span class="nf"&gt;fromBase64&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;aMOpbGxv&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;     &lt;span class="c1"&gt;// 'héllo'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is also why two people can encode "the same text" and get different results: UTF-8 and UTF-16 produce different bytes, and Base64 faithfully encodes whichever bytes you gave it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The short version
&lt;/h2&gt;

&lt;p&gt;Base64 is a reversible, keyless, 33%-larger representation of bytes, designed to move binary data through text-only channels. It is not compression and it is not encryption.&lt;/p&gt;

&lt;p&gt;Use it when a channel cannot carry bytes. Do not use it to shrink files, and never confuse it with protecting them.&lt;/p&gt;

&lt;p&gt;I put together a browser-based &lt;a href="https://usetoolvault.com/tools/base64-encoder-decoder" rel="noopener noreferrer"&gt;Base64 encoder and decoder&lt;/a&gt; for the everyday cases — text in, Base64 out, and back again, with the Unicode handling above taken care of. It runs locally, so nothing you paste is uploaded anywhere.&lt;/p&gt;

&lt;p&gt;If you have ever wondered whether a hash counts as encryption, I covered that question in a separate post: MD5 is a hash, it cannot be reversed, and it is not a secret in any meaningful sense.&lt;/p&gt;

&lt;p&gt;I build small, privacy-first developer tools that run entirely in the browser. Most of them exist because I needed something for five minutes and every alternative wanted an upload or an account first.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
      <category>programming</category>
      <category>security</category>
    </item>
    <item>
      <title>What MD5 actually does (and why it isn't encryption)</title>
      <dc:creator>CheBit</dc:creator>
      <pubDate>Tue, 13 Oct 2026 15:32:36 +0000</pubDate>
      <link>https://dev.to/chebit/what-md5-actually-does-and-why-it-isnt-encryption-54pg</link>
      <guid>https://dev.to/chebit/what-md5-actually-does-and-why-it-isnt-encryption-54pg</guid>
      <description>&lt;p&gt;This snippet shows up in code review all the time, and on its face it looks entirely reasonable:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;User&lt;/span&gt; &lt;span class="nf"&gt;register&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;rawPassword&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="nc"&gt;User&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;User&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
    &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setEmail&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setPassword&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;DigestUtils&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;md5Hex&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rawPassword&lt;/span&gt;&lt;span class="o"&gt;));&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;userRepository&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;save&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The comment above it said the password was "encrypted before storing". Three things in that snippet are wrong, and pulling them apart is a good way to nail down a distinction that still trips up working developers: encoding, hashing and encryption are three different operations, and only two of them are reversible.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three operations, three purposes
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Encoding&lt;/strong&gt; changes the representation of data so it can survive transport. It is reversible by design and involves no secret. Base64 is the standard example: it turns arbitrary bytes into a safe subset of ASCII so they can travel through systems that expect text. Anyone can decode it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Hashing&lt;/strong&gt; reduces input of any length to a fixed-size digest. It is one-way by design and involves no key. There is nothing to reverse, because the digest is not a compressed copy of the input. It is a summary that many different inputs could have produced.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Encryption&lt;/strong&gt; transforms data so that only a holder of the key can read it. It is reversible, and the key is the entire point.&lt;/p&gt;

&lt;p&gt;The practical rule that follows: if you need the data back, you want encryption. If you only need to check that data has not changed, you want a hash. If you need to move data through a text-only channel, you want encoding.&lt;/p&gt;

&lt;h2&gt;
  
  
  What MD5 is, concretely
&lt;/h2&gt;

&lt;p&gt;MD5 takes any input and returns 128 bits, conventionally written as 32 lowercase hex characters.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Node.js&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;crypto&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nx"&gt;crypto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createHash&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;md5&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;hello&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;digest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;hex&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;// '5d41402abc4b2a76b9719d911017c592'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Python
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;hashlib&lt;/span&gt;

&lt;span class="n"&gt;hashlib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;md5&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;b&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;hello&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;hexdigest&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="c1"&gt;# '5d41402abc4b2a76b9719d911017c592'
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s1"&gt;'hello'&lt;/span&gt; | &lt;span class="nb"&gt;md5sum&lt;/span&gt;
&lt;span class="c"&gt;# 5d41402abc4b2a76b9719d911017c592  -&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three properties matter here. It is deterministic: the same input always produces the same digest. It is fixed-length: one character and a 10 GB disk image both produce 32 hex characters. And it has a strong avalanche effect: changing one bit of input changes about half the output bits.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;crypto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createHash&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;md5&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;hello&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;digest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;hex&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;// '5d41402abc4b2a76b9719d911017c592'&lt;/span&gt;

&lt;span class="nx"&gt;crypto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createHash&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;md5&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Hello&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;digest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;hex&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;// '8b1a9953c4611296a827abf8c47804d7'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One capital letter, and the two digests share nothing. That is exactly what you want when checking whether a file changed, and exactly why you cannot use a hash as storage for something you need to read back.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why "MD5 decrypt" is a contradiction
&lt;/h2&gt;

&lt;p&gt;Search for "MD5 decrypt" and you will find dozens of sites that will happily decrypt a hash for you. None of them are reversing the function.&lt;/p&gt;

&lt;p&gt;What they are actually doing is a lookup. &lt;code&gt;5f4dcc3b5aa765d61d8327deb882cf99&lt;/code&gt; is the MD5 of &lt;code&gt;password&lt;/code&gt;, and that fact has been sitting in public tables for twenty years. These services hash a huge dictionary of candidate inputs, store the results, and hand you back whatever matched.&lt;/p&gt;

&lt;p&gt;The distinction matters because the two failure modes are different. You cannot reverse MD5. You can guess the input, and when the input is a human-chosen password, guessing is cheap. MD5 was designed to be fast, and in this context fast is a liability.&lt;/p&gt;

&lt;h2&gt;
  
  
  MD5 has been broken since 2004
&lt;/h2&gt;

&lt;p&gt;A collision is a pair of different inputs that produce the same hash. For a hash to be useful in any security-sensitive setting, collisions have to be hard to find.&lt;/p&gt;

&lt;p&gt;For MD5 they are not. Practical collision attacks were demonstrated in 2004, and by 2008 researchers had used them to forge a certificate that a certificate authority would sign, which is about as serious as a hash break gets.&lt;/p&gt;

&lt;p&gt;Practical guidance that follows:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Never hash passwords with MD5.&lt;/strong&gt; Use bcrypt, scrypt or Argon2id, all of which are deliberately slow and salted.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Never rely on MD5 for signatures or certificates.&lt;/strong&gt; Use SHA-256 or SHA-3.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Do not treat MD5 as a security boundary.&lt;/strong&gt; If an attacker has a reason to forge a value, MD5 is the wrong tool.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Don't&lt;/span&gt;
&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setPassword&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;DigestUtils&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;md5Hex&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;password&lt;/span&gt;&lt;span class="o"&gt;));&lt;/span&gt;

&lt;span class="c1"&gt;// Do&lt;/span&gt;
&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setPassword&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;BCryptPasswordEncoder&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;encode&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;password&lt;/span&gt;&lt;span class="o"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Where MD5 is still perfectly fine
&lt;/h2&gt;

&lt;p&gt;Checksums, cache keys, and detecting accidental corruption. In all three cases the threat model excludes a deliberate attacker who controls the input, and MD5 is fast and universally available.&lt;/p&gt;

&lt;p&gt;The most common real-world use is verifying a download:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# macOS&lt;/span&gt;
md5 yourfile.iso

&lt;span class="c"&gt;# Linux&lt;/span&gt;
&lt;span class="nb"&gt;md5sum &lt;/span&gt;yourfile.iso

&lt;span class="c"&gt;# Windows&lt;/span&gt;
certutil &lt;span class="nt"&gt;-hashfile&lt;/span&gt; yourfile.iso MD5
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Compare the output against the value the publisher lists. If it matches, the file arrived intact. If it does not, the download was truncated or swapped, and you should not run it.&lt;/p&gt;

&lt;p&gt;The nuance worth keeping in mind: a match proves you have the file the publisher hashed, not that the publisher is trustworthy. And because collisions are cheap, it is a weak guarantee against tampering. For anything adversarial, checksums published over HTTPS with SHA-256 are the better choice.&lt;/p&gt;

&lt;h2&gt;
  
  
  The short version
&lt;/h2&gt;

&lt;p&gt;Encoding is reversible and takes no key. Encryption is reversible and needs a key. Hashing is neither, and that is the feature, not a limitation.&lt;/p&gt;

&lt;p&gt;MD5 is a hash. It is fine for checking that a file survived a download, and unfit for anything an attacker has a reason to forge. If you have MD5 anywhere near passwords, that is a bug worth fixing this week.&lt;/p&gt;

&lt;p&gt;I put together a browser-based &lt;a href="https://usetoolvault.com/tools/md5-encryptor" rel="noopener noreferrer"&gt;MD5 hash generator&lt;/a&gt; for the legitimate uses. It computes the digest locally, so whatever you paste never leaves your machine, which is useful when the string you are hashing is a token or a key you would rather not paste into someone else's server.&lt;/p&gt;

&lt;p&gt;I build small, privacy-first developer tools that run entirely in the browser. Most of them exist because I kept wanting a tool that wouldn't ask me to upload a config file or create an account first.&lt;/p&gt;

</description>
      <category>security</category>
      <category>webdev</category>
      <category>javascript</category>
      <category>programming</category>
    </item>
  </channel>
</rss>
