Encoding, hashing en encryptie zijn drie verschillende dingen

Hoe encoding, hashing en encryptie van elkaar verschillen, waar Base64, percent-encoding en HTML-escaping werkelijk voor dienen, en hoe je een onbekende string op het oog herkent.

Een string die door elkaar gehusseld lijkt, is geen string die beschermd is. Drie verschillende bewerkingen leveren onleesbare uitvoer op, en ze door elkaar halen is waar echte beveiligingsfouten vandaan komen.

Omkeerbaar?Sleutel nodig?Waarvoor het dient
EncodingJa, door iedereenNeeBytes door een kanaal krijgen dat ze niet ruw kan vervoeren
HashingNeeNeeVingerafdrukken, integriteitscontroles, zoeksleutels
EncryptieJa, met de sleutelJaGeheimhouding

Encoding is een wisseling van alfabet. Base64, percent-encoding, hexadecimaal en HTML-entiteiten schrijven dezelfde informatie anders op, zodat een parser verderop er niet in stikt. Er komt geen geheim aan te pas, dus er valt niets te bewaren. Een Caesar-verschuiving hoort hier ook thuis: de Caesar Cipher probeert alle 25 verschuivingen in één keer uit, een eerlijke samenvatting van hoeveel bescherming een vaste substitutie biedt. Hashing beeldt elke invoer af op een digest van vaste lengte en kan niet achterstevoren gedraaid worden. Encryptie is de enige van de drie die vertrouwelijkheid biedt, en er is altijd een sleutel bij betrokken. Als je niet naar de sleutel kunt wijzen, wordt er niets versleuteld.

Base64 en wat het kost

Base64 splitst drie bytes (24 bits) in vier groepen van zes en schrijft elke groep als één teken uit een alfabet van 64 tekens. Vier tekens per drie bytes betekent dat de uitvoer een derde groter is. Dat telt het zwaarst bij het insluiten van bestanden: de File to Base64 tool produceert een data-URL, dus een afbeelding van 300 KB belandt met ongeveer 400 KB in je HTML, en wordt bij elke paginaweergave opnieuw gedownload.

Als de invoer geen veelvoud van drie bytes is, wordt de laatste groep aangevuld met =:

"abc"  ->  YWJj      (3 bytes, no padding)
"ab"   ->  YWI=      (2 bytes, one =)
"a"    ->  YQ==      (1 byte, two =)

Het standaardalfabet eindigt op + en /, een probleem in een URL, want + decodeert in formulierdata als een spatie en / is een padscheidingsteken. Daarom bestaat er een tweede alfabet: Base64url vervangt + door - en / door _, en laat de aanvulling meestal weg. Daarom mislukt een token dat uit een URL is gekopieerd soms in een decoder die op het standaardalfabet staat. De Base64 Encoder kan met beide overweg, en gaat correct met Unicode om, wat naïeve implementaties vaak niet doen: tekst moet als UTF-8 gecodeerd worden voor de Base64-stap.

Niets hiervan is een beveiligingsmaatregel. Een waarde die een product een "versleutelde" identifier in een URL noemt, is heel vaak Base64 van een gewoon geheel getal dat iedereen in seconden kan lezen, veranderen en opnieuw coderen. Hetzelfde geldt voor de payload van een JSON Web Token: de JWT Decoder leest hem zonder sleutel, want er valt niets te ontsluiten.

Percent-encoding, en welke functie je moet gebruiken

Percent-encoding vervangt een byte door % en twee hexcijfers. Het werkt op bytes en niet op tekens, dus niet-ASCII tekst wordt eerst als UTF-8 gecodeerd: é wordt %C3%A9.

Niet-gereserveerde tekens (A-Z, a-z, 0-9, -, ., _, ~) hoeven nooit gecodeerd te worden. Gereserveerde tekens (: / ? # [ ] @ ! $ & ' ( ) * + , ; =) zijn de scheidingstekens die een URL zijn structuur geven, en of er een gecodeerd moet worden hangt ervan af of het als scheidingsteken of als data gebruikt wordt. Dat is het verschil tussen de twee JavaScript-functies:

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 bedoeld voor een hele URL die je geldig wilt maken, dus het laat scheidingstekens intact. encodeURIComponent is bedoeld voor één waarde die in een padsegment of queryparameter terechtkomt, dus het codeert ze wel. Gebruik de tweede voor alles wat je invoegt, en let erop dat die !, ', (, ) en * nog steeds met rust laat.

Dan is er nog het plusteken. Formulierdata die als application/x-www-form-urlencoded verstuurd wordt, codeert een spatie als +, en die conventie lekte door naar query strings, dus de meeste servers decoderen + in een query als een spatie. Een letterlijke plus moet als %2B verstuurd worden, en daarom komt alex+billing@example.com zo vaak aan als alex billing@example.com met de tag vernield. In een padsegment daarentegen is + gewoon een plus en is een spatie %20. De URL Encoder maakt dat zichtbaar wanneer een waarde de ene ronde overleeft en bij de volgende breekt.

Escapen hangt af van waar de waarde belandt

Er bestaat geen enkele vorm van een string die "voor het web ge-escapet" is. De context beslist.

ContextJuiste behandeling
HTML-tekstEntiteiten voor &, < en >
Attribuut tussen aanhalingstekensEntiteiten voor & en voor het aanhalingsteken dat het omsluit, en zet het attribuut altijd tussen aanhalingstekens
Binnen <script>JavaScript-stringescaping, geen entiteiten
URL in href of srcPercent-encode de waarde, escape hem voor het attribuut, en controleer daarna het scheme
CSS-waardeCSS-escaping, en bouw url() nooit uit invoer van een gebruiker

Het script-geval verrast mensen. De inhoud van een script wordt niet op entiteiten gedecodeerd, dus een entiteit die daar staat blijft letterlijk, en de HTML-parser sluit het blok bij de reeks </script waar die ook staat, tussen aanhalingstekens of niet:

<script>var s = "</script>";</script>   <!-- block ends early -->
<script>var s = "<\/script>";</script>  <!-- correct -->

Voor URL's is escapen alleen ook niet genoeg. Een waarde die met javascript: begint voert code uit zodra erop geklikt wordt, hoe zorgvuldig hij ook gecodeerd is, dus valideer het scheme tegen een lijst met toegestane waarden. De HTML Entities tool dekt zowel het tekst- als het attribuutgeval en decodeert entiteiten ook terug, wat je meestal nodig hebt wanneer een API iets dubbel heeft ge-escapet tot &amp;amp;.

Hashing: welk algoritme, en waarvoor

AlgoritmeDigestStatus
MD5128 bitGebroken. Botsingen zijn triviaal
SHA-1160 bitGebroken. Chosen-prefix-botsingen zijn praktisch en betaalbaar
SHA-256, SHA-512256, 512 bitPrima voor integriteit en handtekeningen
bcrypt, scrypt, Argon2wisselendAlleen wachtwoorden

MD5 en SHA-1 falen op botsingsbestendigheid, wat betekent dat een aanvaller twee verschillende invoerwaarden met dezelfde digest kan construeren. Overal waar een hash voor een document staat zijn ze onbruikbaar: handtekeningen, certificaten, content addressing, en het ontdubbelen van alles wat een aanvaller aanlevert. Geen van beide is gebroken voor preimages, dus een oude MD5-checksum vangt nog steeds toevallige beschadiging op, maar geen van beide hoort in nieuw werk. Gebruik SHA-256, die de Hash Generator ook voor bestanden berekent en niet alleen voor tekst.

Wachtwoorden zijn een ander probleem, en een snelle hash is precies daarom het verkeerde gereedschap. Gangbare hardware doet miljarden SHA-256-bewerkingen per seconde, dus een gelekte tabel met SHA-256-wachtwoordhashes is een gelekte tabel met wachtwoorden. bcrypt, scrypt en Argon2id zijn bewust traag en instelbaar, met een werkfactor (en bij de laatste twee ook een geheugenkost) die je verhoogt naarmate de hardware sneller wordt, en elk slaat een unieke willekeurige salt op in zijn uitvoer.

Een hash is ook geen handtekening. Om te bewijzen dat een bericht van iemand met een sleutel komt, gebruik je HMAC-SHA-256 in plaats van het geheim en het bericht aan elkaar te plakken en te hashen, want dat is kwetsbaar voor length extension.

Wat een hash niet kan

Een hash kan niet door rekenen omgekeerd worden. Hij kan wel door zoeken omgekeerd worden wanneer de verzameling mogelijke invoerwaarden klein genoeg is om op te sommen: hash elke pincode van vier cijfers en je hebt alle tienduizend digests meteen, en hetzelfde geldt voor elke postcode, elk telefoonnummer of elk adres in een gelekte lijst. Een identifier hashen anonimiseert hem niet.

Salting, een unieke willekeurige waarde die bij elk record wordt opgeslagen en in de invoer wordt gemengd, maakt één doelwit niet moeilijker te raden, maar dwingt een aanvaller wel om elk record apart aan te vallen en maakt vooraf berekende tabellen waardeloos. Bij waarden met weinig entropie is een pepper (een geheime sleutel die buiten de database wordt bewaard) het deel dat echt helpt.

Een onbekende string herkennen

VormKenmerkVoorbeeld
HexAlleen 0-9a-f, even lengte. 32 tekens is MD5, 40 is SHA-1, 64 is SHA-2565d41402abc4b2a76b9719d911017c592
Base64Hoofd- en kleine letters door elkaar met + en /, lengte een veelvoud van 4, kan op = of == eindigenSGVsbG8sIHdvcmxkIQ==
Base64urlHetzelfde met - en _, meestal zonder aanvullingeyJzdWIiOiIxMjM0NSJ9
Percent-encoded% gevolgd door twee hexcijfersa%20b%26c
JWTDrie Base64url-stukken gescheiden door punten, beginnend met eyJeyJhbGciOi...
bcryptPrecies 60 tekens, beginnend met $2a$, $2b$ of $2y$ en een kostenfactor$2b$12$...
Argon2$argon2id$v=19$m=...,t=...,p=...$salt$hash

eyJ is het onthouden waard: het is Base64 van {", dus elk stuk dat zo begint is een gecodeerd JSON-object, token of niet.

Dat het decoderen lukt is geen bewijs dat de gok klopte, want Base64 maakt van elke invoer bytes. Controleer of het resultaat plausibele tekst is, of dat het met een bekende bestandssignatuur begint: Base64 die met iVBORw0KGgo begint is een PNG, /9j/ een JPEG, JVBERi0 een PDF en UEsDB een zip. Als de bytes nergens op lijken, haal de uitvoer dan door de Text to Binary tool om de codes rechtstreeks te lezen, of ga na of de waarde percent-encoded was voordat hij Base64-gecodeerd werd, een veelvoorkomende dubbele verpakking.