Cryptography Basics for Testers
Cryptography is the set of mathematical tools that protect data: hashing produces a fixed-length fingerprint that cannot be reversed, encryption hides data so that only the holder of a key can read it, and an HMAC proves that a message came from someone who holds a shared secret and was not changed on the way. Encoding such as Base64 is not a protection tool at all, although it looks like one: it only changes the representation of the data, and anyone can reverse it.
Example: a sign-up form stores the password password123 as cGFzc3dvcmQxMjM=. The value looks "encrypted", but it is plain Base64 and any decoder turns it back in a second. The correct value would be the output of a slow, salted password hash such as Argon2id, which cannot be turned back into the password. A tester who can tell the two apart finds a defect that every functional test misses.
Why it matters
A product can lose every password, card number or session token while every feature "works": the values only need to be stored in a reversible encoding or with a fast, unsalted hash, or a forged webhook only needs to be accepted because nobody verified its signature. The OWASP Top 10 groups these cases under A04:2025 Cryptographic Failures, and functional testing does not find them, because the user's own journey succeeds.
A tester does not need the mathematics. A tester needs three things: to recognise the kind of value in front of them from its shape, to know which tool fits which purpose (encoding for transport, hashing for integrity and passwords, HMAC for proving the origin, encryption for confidentiality), and to ask the right questions: where is the key, who verifies, and what happens when it leaks?
Encoding, hashing and encryption are three different things
Encoding changes how data is represented, by a public rule with no secret in it. Base64 (RFC 4648) turns every 3 bytes into 4 characters from a 64-character alphabet and, when needed, pads the end with =; hex writes every byte as two digits. Its purpose is to carry bytes safely inside text, a URL or an HTTP header, as the Basic Authorization header in the lessons on HTTP headers and on authentication and authorization did. Whoever has the output has the input.
Hashing turns any input into a fixed-length fingerprint: 64 hex characters for SHA-256, 40 for SHA-1, 32 for MD5. The same input always gives the same hash, the input cannot be recovered from the hash, and changing one character changes the whole hash. Systems use it to check that a file is intact and to compare data without storing it. MD5 and SHA-1 must not be used where security depends on them: practical ways to create two different inputs with the same hash (a collision) have been published for both.
Encryption hides data so that it can only be recovered with a key. Symmetric encryption (AES, for example) uses the same key to encrypt and decrypt; asymmetric encryption (RSA and elliptic curves) uses a key pair, where what the public key encrypts only the private key can decrypt. TLS uses both: asymmetric to identify the server and agree keys, symmetric for the data itself. The question that separates the three is: can it be reversed, and who holds what is needed to reverse it?
EncodingBase64 · RFC 4648
Changes how bytes are written, by a public rule with no secret
cGFzc3dvcmQxMjM=
- Needs a key:
- No
- Reversible:
- Yes, by anyone
- Protects:
- Nothing: transport only
- Typical place:
- Basic header, JWT parts, data URLs
- The tester asks:
- Is it being mistaken for protection?
HashingSHA-256 · FIPS 180-4
A fixed-length fingerprint of the input; the same input always gives the same hash
ef92b778bafe771e…
- Needs a key:
- No
- Reversible:
- No
- Protects:
- Integrity: any change shows
- Typical place:
- File checksums, commit ids, fingerprints
- The tester asks:
- Is it MD5 or SHA-1? Is it used for passwords without salt and work factor?
HMACRFC 2104
A hash computed with a shared secret key
2b46560cfc07e738…
- Needs a key:
- Yes, shared by both sides
- Reversible:
- No
- Protects:
- Integrity and origin
- Typical place:
- Webhook signatures, HS256 JWTs, signed cookies
- The tester asks:
- Are a wrong and a missing signature rejected? Is replay handled?
EncryptionAES · RSA · TLS
Hides the data; the ciphertext differs on every run and only the key recovers it
- Needs a key:
- Yes: one shared key, or a public and private pair
- Reversible:
- Yes, with the key
- Protects:
- Confidentiality
- Typical place:
- TLS, data at rest, encrypted backups
- The tester asks:
- Who holds the key? Which TLS versions? How is the key rotated?
The values are the real Base64, SHA-256 and HMAC-SHA256 outputs for the input password123, with the test key test-secret; the two digests are shortened.
Text version
Input: password123
Encoding Base64 cGFzc3dvcmQxMjM=
anyone can decode it: no key, no secret
Hashing SHA-256 ef92b778bafe771e89245b89ecbc08a44a4e166c06659911881f383d4473e94f
one way, fixed length, same input -> same output
MD5 (legacy) 482c811da5d5b4bc6d497ffa98491e38
32 hex characters; not for anything security-related
HMAC HMAC-SHA256 2b46560cfc07e738b1b40765b282308026cdd5b9d2aae58be8a347b1293a82e7
key: test-secret; only a key holder can compute or check it
Encryption AES-256-GCM <ciphertext + nonce>, different on every run
reversible only with the keyTry it: Base64 Decoder Try it: SHA-256 Generator Try it: MD5 Generator
HMAC and signatures: a hash with a key proves the origin
A hash alone detects a change but does not prove the origin: whoever edits a message can compute its new hash too. HMAC (RFC 2104) solves this by mixing a secret key into the calculation: HMAC-SHA256(key, message). Only a holder of the key can produce or check the right value, so the value proves two things at once: the message was not changed, and it came from someone who holds the secret. HMAC does not hide the message; it is not encryption.
You will meet it in webhook signatures (a header such as X-Signature-256), in JWTs signed with HS256 (which is HMAC-SHA256) and in signed links and cookies. A digital signature (RS256 or ES256, for example) does the same job with asymmetric keys: the private key signs and the public key verifies, so the verifier needs no secret. In both cases decoding a token is one thing and verifying its signature is another, as the JWT guide explains.
- Same body, correct signature: accepted. Change one character of the body and keep the signature: must be rejected.
- A signature computed with the wrong secret, or no signature header at all: both must be rejected. A server that accepts the request without the header is not verifying.
- Replaying an old, valid request: HMAC alone does not prevent it. Ask whether a timestamp is part of the signed material and whether old ones are rejected.
- Ask the developer how the two values are compared: the usual recommendation is a constant-time comparison, which takes the same time however many characters match, so response times reveal nothing about a guess. You cannot see it from outside.
Partner -> your server
POST /webhooks/payments HTTP/1.1
Host: shop.example.com
Content-Type: application/json
X-Signature-256: sha256=ff40b419851351c2ed7966278bc1d026ecbac717fbbbfebfb068715b3860c4d7
{"event":"order.paid","orderId":"ORD-1042","amount":"249.00"}
Check on your server: HMAC-SHA256(secret, raw body) == the value after "sha256="?
yes -> process the event no -> reject it (for example with 401)
Secret for this example: whsec_test_key. Change one byte of the body, or the secret,
and the computed value no longer matches.Password hashing is a special case
Passwords are weak inputs: short, predictable and reused. A fast hash such as SHA-256 is therefore not enough, because an attacker who obtains the database can compute the hashes of billions of common passwords and compare. The answer has two parts: a random salt that differs per user, so two users who chose the same password have different stored values and precomputed tables are useless; and a function designed to be deliberately slow, and in Argon2id and scrypt also memory-hungry: a delay one login can afford and a billion guesses cannot.
The OWASP Password Storage cheat sheet recommends Argon2id first, then scrypt, bcrypt for legacy systems, and PBKDF2 where FIPS-140 compliance (a US government standard for cryptographic modules) is required. These algorithms generate the salt themselves and store it inside the value, next to the algorithm name and its parameters; the salt is not a secret, its only job is to make every stored value unique. So you can recognise these values by their shape, as in the example.
- Create two accounts with the same password: the stored values must differ (a salt per user).
- Forgot password: a reset link must arrive, never the current password. A system that can send it stores it in a reversible form.
- With bcrypt, remember that it ignores everything after 72 bytes: set an 80-character password, then log in with the last character changed. If login succeeds, ask about the length limit.
- Look at the value, not the name: a column called
password_hashcan hold Base64 or MD5.
Two users, both chose password123
Unsalted SHA-256 (defect): identical values, and easy to look up in precomputed tables
user A ef92b778bafe771e89245b89ecbc08a44a4e166c06659911881f383d4473e94f
user B ef92b778bafe771e89245b89ecbc08a44a4e166c06659911881f383d4473e94f
Argon2id (accepted): algorithm, parameters, random salt and hash; different per user
user A $argon2id$v=19$m=19456,t=2,p=1$<salt A>$<hash A>
user B $argon2id$v=19$m=19456,t=2,p=1$<salt B>$<hash B>
bcrypt (accepted): $2b$<cost>$<22-character salt><31-character hash>, 60 charactersTLS and keys: where the tools meet the system
The lesson on HTTP and HTTPS explained the handshake, certificates and the chain of trust. Seen from this lesson, TLS (RFC 8446 for version 1.3) is the most common place where these tools work together: a certificate and a digital signature to identify the server, asymmetric cryptography to agree keys, hashes and HMAC to derive those keys and confirm the handshake, then symmetric encryption with an integrity check on every byte that passes. Every password, token and card number sent over plain HTTP travels without any of these protections.
- Accepted versions: TLS 1.2 and 1.3 only; older versions and all of SSL are deprecated. Check this on every environment, not only production. The API Endpoint Tester reports the TLS and HSTS of a public endpoint; listing every version a server still accepts needs a TLS scanner, such as
nmap --script ssl-enum-ciphers -p 443 <host>, run only on hosts you have permission to test. - Every entry point: the API, the web app, the mobile app, the webhook your server sends to a partner and the internal calls between services, not only the home page.
- Test clients verify certificates the way real users do; switching verification off "for now" is a defect in the test setup itself.
- Keys and secrets: a key embedded in a front-end bundle or a mobile app is effectively public; a secret produced with
Math.random()is not random enough; and ask how a key is rotated when it leaks. - What does "encrypted at rest" mean here? Ask who holds the key: if the application server can decrypt everything, so can anyone who compromises it.
Worked example: reviewing a payment and webhook feature
You are testing a new feature: a customer creates an account, saves a card, and the payment partner notifies your server of each payment through a webhook. During the design and data review you notice four values:
users.passwordholdscGFzc3dvcmQxMjM=: Base64, so a plain password for anyone who reads the database. Defect; attach the decoded value from the test environment, never a real user's password.cards.pan_maskedshows**** 4242, but the/profileresponse carries acardRefin Base64 that decodes to the full card number. A sensitive data exposure defect whose root cause is mistaking encoding for encryption.- The webhook carries
X-Signature-256, yet the same request without the header is accepted with 200. The server is not verifying; anyone who knows the URL can announce "payment received". Defect. - A TLS check on
api-stagingshows that TLS 1.0 is accepted. A finding for the platform team, with a question: is production configured the same way?
Common mistakes
- Treating Base64, hex or URL encoding as protection: "the card number is encoded" means it is readable.
- Writing "encrypted" in a bug report or a requirement when "hashed" is meant, or asking developers to "decrypt" a hash: the wrong word leads to the wrong fix.
- Accepting "passwords are hashed" without asking which algorithm and whether there is a salt; SHA-256 alone is a defect for password storage.
- Decoding a JWT or a webhook, checking its content and trusting it without verifying the signature, or testing only the happy path of a signature check and never sending a wrong signature.
- Judging a value by its look: a random-looking string of 64 characters may be the unsalted SHA-256 of a password, or an encoded, predictable identifier.
- Trying to break the algorithms themselves, or trying on systems you do not own: a tester checks that the right tool is used in the right way, on systems they have written permission to test.
The QA perspective
In practice this lesson appears at three moments: during the requirements and design review of any feature that stores or transmits a secret (a password, a token, a card, a partner key), where you ask "what exactly protects this value, who holds the key, and what happens if it leaks?"; during an API or webhook integration, where you test signature verification with wrong requests and not only right ones; and before any release that changes authentication or infrastructure, when TLS is checked again. The counterparts are the developers for storage algorithms, the platform team for certificates and keys, and the security team where there is one.
In the report, record the shape of the value you saw (length, alphabet, prefix), where it was (column, field, header), why it does not fit its purpose and the test that proves it. Never put a real secret or a real user's password into a report: use test accounts and mask everything else. The right name for the tool and the algorithm sends the developer straight to the fix.
Practice exercise
Classify the values and decide what to test
While testing the staging environment of a shop application you saw the five values below in a database export and in requests. For each one decide: is it encoding, hashing, HMAC, encryption or a TLS setting? Does it fit its purpose? What is the next test or question?
Use the Base64 Decoder, the MD5 and SHA-256 generators and the Webhook Signature Analyzer from the Mutqan tools where they help, and work with test data only.
1. users.password cGFzc3dvcmQxMjM=
2. legacy_users.pwd 482c811da5d5b4bc6d497ffa98491e38
3. users.password_hash $2b$12$<22-character salt><31-character hash>
4. incoming webhook header X-Signature-256: sha256=<64 hex characters>
5. api-staging.example.com TLS check: accepts TLS 1.0, 1.2 and 1.3- Look at the shape: length, alphabet, prefix and an
=at the end. - Decide: can it be reversed, and does it need a key?
- Compare that with the purpose it is used for.
- Write the test or the question that proves or rules out the defect.
Show the model answer
Value 1: Base64 (ends with =, decodes to password123). The password is stored in a reversible form, which is as good as plain text. Defect; the test is decoding it yourself, and the report names the column and states that anyone who reads the database has every password.
Value 2: 32 hex characters, the length of an MD5 hash. Hashing password123 (the password decoded from value 1) with the MD5 Generator gives exactly this value: unsalted MD5. Defect: a fast, deprecated function without a salt, so every user who chose the same password has the same value. The length alone only suggests MD5; the match with a known password proves it.
Value 3: bcrypt with cost 12, acceptable (OWASP asks for a work factor of 10 or more). Tests: two accounts with the same password give different values; forgot password sends a link, not a password; an 80-character password must not log in with its last character changed, otherwise ask about the 72-byte limit.
Value 4: 64 hex characters after sha256= fit HMAC-SHA256, but also a plain SHA-256 of the body; the shape cannot tell them apart. Compute the SHA-256 of a captured raw body: if it equals the value, there is no key and anyone can forge it (defect). If the Webhook Signature Analyzer confirms HMAC-SHA256 with the test secret, the design is right; then test that a body changed by one character, a wrong secret and a missing header are all rejected, and ask whether a timestamp stops an old valid request sent again.
Value 5: a TLS setting, not an algorithm; accepting TLS 1.0 is a finding for the platform team: only 1.2 and 1.3 should be allowed. Ask whether production has the same setting, and complete the certificate and HTTP-to-HTTPS redirect checks from the lesson on HTTP and HTTPS.
Frequently asked questions
Is hashing a kind of encryption?
No. Encryption is reversible with the key and its purpose is confidentiality; hashing is one way, has no key and its purpose is integrity and comparison. Mixing the two up in a requirement or a report leads to the wrong design, so use the precise word.
Our passwords are hashed with SHA-256. Is that enough?
No. SHA-256 is very fast, and without a salt it gives the same value to every user who chose the same password. Password storage needs a dedicated slow, salted algorithm: Argon2id first, or scrypt or bcrypt, or PBKDF2 where FIPS compliance is required.
Can I verify a webhook signature or a JWT without the secret?
Not when it is an HMAC: you can decode and read the content, but verifying an HMAC needs the shared secret. RS256 and ES256 signatures need no secret, only the public key, which can be shared openly. A test environment should have its own test secret, never the production one.
Do I need to understand the mathematics behind these algorithms?
No. A tester needs to recognise the tool from the shape of a value, to know which tool fits which purpose, and to ask about the key, the verification and the rotation. The security of the algorithms themselves is a specialist subject, and you never try to break it on systems you do not own.
References
- OWASP Cheat Sheet Series: Cryptographic StorageWhich algorithms and key lengths to use for data at rest, and how to manage keys and randomness.
- OWASP Cheat Sheet Series: Password StorageHow to store passwords: Argon2id, scrypt or bcrypt with salts — never MD5, SHA-1 or unsalted hashes.
- RFC 2104: HMAC: Keyed-Hashing for Message AuthenticationThe HMAC construction: how a secret key and a hash function produce a message authentication code.
- RFC 8446: The Transport Layer Security (TLS) Protocol Version 1.3The TLS 1.3 handshake and record protocol that protect HTTPS traffic.
Practise with Mutqan tools
Generate SHA-256 hashes of text — the standard for integrity checks and signatures.
Generate MD5 hashes of text in your browser (for checksums and legacy comparisons).
Generate HMAC signatures (SHA-256/384/512/SHA-1) for webhook and API request signing.
Verify webhook signatures (HMAC-SHA256/SHA1, Stripe, GitHub, Shopify, Slack styles) with your secret — locally.
Decode Base64 or Base64url strings to text, with automatic padding repair.
Quickly check that an API endpoint is reachable: status, response time, size, content type and TLS.
Go deeper
Your progress on this topic
Not started
Mark the topic complete when you have studied it and done the exercise.
Sign in to track your progress
Mark topics complete, continue where you left off and unlock specialisation exams.