Cryptography
Passwords and Hashing with Argon2id
Why fast hashes fail for passwords, what salts and work factors do, and how to hash and verify with Argon2id in Python using argon2-cffi.
Separate encoding, encryption and hashing with Base64, SHA-256 and OpenSSL labs. Learn why Base64 is not protection and hashes are one-way.
Encoding, encryption and hashing all turn data into something else, which is why people mix them up. They have different goals. Encoding changes how data is written down, so anyone can reverse it. Encryption hides data from people who do not hold a key, and you can reverse it with that key. Hashing produces a short fingerprint that you cannot turn back into the original input. If you remember one sentence: Base64 is not encryption, and a hash hides nothing about short inputs.
This article separates the three ideas, then asks you to run each one in a terminal on the same text. You will compare the results yourself.
You need a terminal with base64, sha256sum and openssl. On Windows 11 the commands below run in Git Bash, which ships with Git for Windows. A PowerShell version of the hashing step is included. You do not need prior cryptography knowledge, but reading the Caesar cipher article first helps with the idea of a key.
Every command in the lab is safe to run on your own machine. Use a test file, not a real secret.
The three operations answer three different questions.
| Operation | Question it answers | Needs a key? | Can be reversed? |
|---|---|---|---|
| Encoding | How do I represent these bytes as text or in a format another system expects? | No | Yes, by anyone |
| Encryption | Who can read this data? | Yes | Yes, by holders of the right key |
| Hashing | Did this data change, or do two values match? | No | No, by design |
Keep this table in mind during the lab. The rest of the article explains each row.
Encoding rewrites data in another form so that a system can carry it. Base64 is the classic example. It takes bytes and writes them using 64 printable characters: A to Z, a to z, 0 to 9, + and /, with = used for padding. RFC 4648 defines the format. Email attachments, data URLs and many API payloads use it because the transport expects text.
Base64 has no secret. The mapping is public and fixed. Any tool that knows the rule can turn the output back into the input. That is the purpose of encoding: interoperability, not secrecy.
Encryption takes readable data, called plaintext, and transforms it with an algorithm and a key into ciphertext. Someone without the key should learn nothing useful from the ciphertext. Someone with the key can decrypt it.
There are two families. Symmetric encryption uses the same secret key for encrypting and decrypting. AES is the common choice. Asymmetric encryption uses a public key and a private key. This article uses symmetric encryption, because the OpenSSL command in the lab does too.
Good encryption depends on more than the algorithm. You must choose a safe mode, generate keys with a good random source, and protect the key. The OpenSSL command in the lab uses a password and PBKDF2 to derive the key. OpenSSL's help text says PBKDF2 uses 10,000 iterations by default unless you change it with -iter.
A cryptographic hash function takes input of any length and returns a fixed-length output. SHA-256 is specified in FIPS 180-4, and its output is always 256 bits, written as 64 hexadecimal characters. Two properties matter for beginners:
Hashing is one-way. Nothing in the output tells you the original text. You can check whether a candidate input produces a known hash. You cannot recover the input from the hash alone, except by guessing and testing.
Note that a hash is a fingerprint, not a lock. Anyone can compute the SHA-256 of a public file. A hash proves that two copies match. It does not hide the file.
Most real security mistakes in this area come from using the wrong tool for the job.
Learning the difference early saves you from these traps.
Open Git Bash in an empty folder. Encode a short phrase, then decode the result.
echo -n 'hello lab' | base64The output I got on this machine with OpenSSL 3.2.4 and Git Bash:
aGVsbG8gbGFiNow decode that output:
echo -n 'aGVsbG8gbGFi' | base64 -dOutput:
hello labTwo things to notice. First, no key was involved. Second, echo -n matters. Without -n, echo adds a newline, and the Base64 output changes. Run echo 'hello lab' | base64 and compare: I got aGVsbG8gbGFiCg==. Base64 turns 9 bytes into 12 characters here, so the output is longer than the input.
Ask yourself: could someone without any secret reverse this? Yes. That is the test that separates encoding from encryption.
Run the same phrase through SHA-256:
echo -n 'hello lab' | sha256sumOutput:
8fc6baba79a0d60b37dee612ccc1ec096ef52dba8cb0a24a6451e8bfb0e4d761 *-Now add one exclamation mark and run it again:
echo -n 'hello lab!' | sha256sumOutput:
36902fb7fdea33458ad09dfeb52118dc47f99a8ca2b9001080366d55511de915 *-The two hashes share no visible pattern. One character changed the whole output. Both are 64 hexadecimal characters, which is 256 bits.
The - at the end means the input came from standard input, not a file. The * before it is the binary-mode marker that Git Bash's sha256sum prints. Other systems may print two spaces instead, and the hash is the same. The hash of a file works the same way.
Create a small file and hash it:
echo 'secret note' > note.txt
sha256sum note.txtOn my machine, with Unix line endings in the file, the output was:
b5f891a5e064fec661025d0b0ec2de157487a6c2e5cc46dc0971fee56f86c645 *note.txtChange the file, even a single character, and hash it again. The value will differ. That is how integrity checks work. The check tells you that the file changed. It does not tell you what changed, and it does not stop an attacker from changing both the file and the published hash.
If you prefer PowerShell, Windows has a built-in command:
Get-FileHash note.txt -Algorithm SHA256Check the output for the Hash line, which shows 64 hexadecimal characters. The hash depends on the exact bytes in the file. PowerShell's Set-Content writes Windows line endings (CRLF), which differ from the LF ending in the Bash example above. On my machine, a file written this way gave this hash:
406A5646AE75CA75D032F306ABD33085DC9F656E3109BB962AEAB335EEC9ED85The same bytes hashed with sha256sum in Git Bash gave the same value in lowercase. If your hash differs from the Bash example but matches the PowerShell result for your own file, the line endings are the reason.
Create a note and encrypt it with AES-256-CBC. OpenSSL will ask you for a password. Choose one you will remember and use only for this lab.
echo 'secret note' > note.txt
openssl enc -aes-256-cbc -pbkdf2 -salt -in note.txt -out note.encOpenSSL prompts for the password, and the encrypted file is written to note.enc. The -salt option adds a random salt, so encrypting the same file twice gives different output. An encrypted file made with -salt begins with the ASCII text Salted__. I checked this on my machine. The rest of the file is binary and unreadable.
Now decrypt it:
openssl enc -d -aes-256-cbc -pbkdf2 -in note.encEnter the same password. Output:
secret noteDecryption only works with the right password. I tested with a wrong one. OpenSSL printed bad decrypt and exited with an error.
Next, change one byte of the encrypted file and try to decrypt it. I flipped one bit inside the ciphertext block of a copy named tampered.enc. Decryption failed with bad decrypt, and the command exited with status 1.
This failure is a lucky accident of CBC padding. Other changes can produce garbage output without an error. The lesson is that plain encryption does not give you a reliable signal that the ciphertext was modified. Systems that need both secrecy and integrity use authenticated encryption, such as AES-GCM, or add a message authentication code. That is an advanced topic, but it explains why an encrypted file is not automatically a trusted file.
Run the same text through each operation and compare:
| Operation | Command | Output on hello lab (observed) |
|---|---|---|
| Encoding | echo -n 'hello lab' | base64 |
aGVsbG8gbGFi |
| Hashing | echo -n 'hello lab' | sha256sum |
8fc6baba...e4d761 |
| Encryption | openssl enc ... -in note.txt |
Binary data, needs the password to read |
Ask what each output lets you do:
hello lab.hello lab, but not recover the input from the hash alone.secret note only with the password.Calling Base64 "encryption" or "obfuscation" for secrets. Base64 hides nothing. Anyone who sees a Base64 string can decode it. Store secrets in a secrets manager or an encrypted store, not in encoded form.
Using a hash as a lock. A hash is one-way, but short or common inputs can be guessed. Hash hello lab and an attacker with a list of common phrases recovers it without breaking SHA-256. This is why password hashing needs salts and slow algorithms.
Using plain SHA-256 for passwords. SHA-256 is fast by design, which suits file checksums and is a poor fit for password storage. Use a password hashing function built for the job.
Trusting a hash from the same place as the file. A checksum on the same compromised page proves nothing. Get the expected hash through a separate, trusted channel, or verify a digital signature.
Forgetting that encryption needs key management. The lab uses a password typed into a prompt. Real systems must store, rotate and protect keys. Losing the key means losing the data.
Writing your own cryptography. Use established libraries and protocols. The lab commands are for learning, not a template for production systems.
Repeat the three labs with a different phrase, then with a file of a few kilobytes. Notice that the Base64 output grows by about a third, the hash stays 64 characters, and the encrypted file grows only by the header and padding.
Next, read about password hashing. The problem there is not hashing as such, but the cost of guessing. The next article in this series, How Password Hashing Works with Argon2id, explains salts, work factors and why slow hashes matter. If you want a refresher on the cipher side first, read Caesar Cipher in Python. It shows a key and an algorithm in a form you can test.
When you study real protocols, look for three things in each design: what it encodes, what it encrypts, and what it hashes. Most confusion disappears once you ask which of the three jobs each part is doing.
Cryptography
Why fast hashes fail for passwords, what salts and work factors do, and how to hash and verify with Argon2id in Python using argon2-cffi.
Cryptography
Public and private keys from zero: what a signature proves, why the private key stays secret, and an Ed25519 lab where a changed file fails.
Cryptography
Cryptography from zero: encryption, keys, hashes, signatures, and why a Caesar cipher teaches ideas but protects nothing.