The previous module ended with a list of defences you have already seen working — TLS, the 120-second signed URLs, the HMAC on the payment gateway webhook, DKIM, passkeys, the RS256 signature on the JWT, backup encryption, the constant-time comparison of the TOTP code — and with one observation: they all rest on the same foundation, and so far we have treated it as a black box. This lesson opens the box. You will not come out of it able to write ciphers: you will come out knowing what one asks of cryptography, what one can genuinely ask of it, the precise vocabulary used to talk about it, and why — this is the idea that runs through the whole module — the hard part is almost never the algorithm. It matters because most real cryptographic failures in SMEs do not consist of breaking AES, but of using the wrong primitive for the wrong problem, confusing encoding with encryption, or generating a key from a source of randomness that was not one.

Contents

  1. What problem cryptography actually solves
  2. Precise vocabulary: encoding, encrypting, hashing and obfuscating are not the same thing
  3. History as a conceptual lever: Caesar, Vigenère and Enigma
  4. Kerckhoffs's principle and why security lives in the key
  5. Attacker models and types of cryptographic attack
  6. What "secure" means today: security bits and key sizes
  7. Cryptographic randomness: the invisible foundation
  8. The map of module 3
  9. The cryptography Nimbus already uses without having explained it

  1. What problem cryptography actually solves

Cryptography is the set of mathematical techniques that allow you to protect information against an adversary who has access to the channel, to the medium, or to both. That last phrase is the crux and is worth reading slowly: cryptography does not assume a secure channel or a safe disk. It assumes the opposite. It takes for granted that the attacker sees the data, and still makes that data useless to them.

It is the direct application of the assume breach principle from 01-03. A clinical note belonging to a physiotherapy clinic that is a Nimbus customer travels over café Wi-Fi, is stored on a cloud provider's disks and is copied to backup buckets. At none of those three points do we control the physical medium. Cryptography turns "let nobody touch the medium" — impossible — into "let touching the medium be worthless" — achievable.

Which property each primitive provides

01-01 defined the CIA triad and its complements: authenticity, non-repudiation and traceability. Each of those properties has one specific cryptographic primitive that supplies it. This table is the mental map of the entire module:

Property you want Cryptographic primitive Concrete example Studied in
Confidentiality: nobody else reads it Encryption (symmetric or asymmetric) AES-GCM over a Nimbus attachment 03-02, 03-03
Integrity: detect that it has changed Hash function (against error) and MAC (against an attacker) SHA-256 of a backup; HMAC of a webhook 03-04
Authenticity: know who produced it MAC (between two parties sharing a key) or digital signature (against everybody) The gateway HMAC; the JWT signature 03-03, 03-04
Non-repudiation: the sender cannot deny it Digital signature exclusively Ed25519 over a booking receipt 03-03
Freshness: it is not an old message replayed Nonce, timestamp, counter The AES-GCM nonce; the TOTP 30 s window 03-02, 03-05
Key agreement: share a secret without having met Key exchange ECDHE in the TLS handshake 03-03, 03-05

Three important readings of this table:

  • Encryption alone does not give integrity. It is the most widespread conceptual error. A ciphertext produced by an old mode can be altered by an attacker in a controlled way without knowing the key. That is why today we use authenticated encryption, which joins both properties in a single operation (03-02).
  • A MAC and a signature are not interchangeable. Both give authenticity and integrity, but a MAC uses a shared key: if Nimbus and the gateway share the key, Nimbus could have fabricated that message, so it does not serve as proof against a third party. A signature, with a private key, does (03-03).
  • Freshness is given by none of the above. A message that was encrypted, intact and authentic three months ago is still encrypted, intact and authentic today. Replaying it is a replay attack, and it is countered with nonces and time windows, not with more encryption.

And what cryptography does not solve

Just as important as the above. Cryptography does not:

  • Protect against somebody who holds the key legitimately. If an attacker steals Iván's credentials, the API decrypts for them entirely correctly. That is an identity problem (02-05) and an authorisation problem, not a cryptography problem.
  • Hide metadata. TLS hides the content of the request, not the fact that your laptop is talking to nimbusreservas.example at 03:14, nor the approximate size of the response.
  • Give availability. Ransomware uses perfectly correct cryptography against you. Encrypting does not stop anyone deleting.
  • Replace access control. We will come back to this in 03-07: encrypting a database whose API exposes an IDOR does not fix the IDOR.
  • Fix itself. A badly stored key turns AES-256 into decoration. That is the subject of 03-06.

  1. Precise vocabulary: encoding, encrypting, hashing and obfuscating are not the same thing

The whole module depends on using five words properly. Let us start with the basics:

Term Definition
Plaintext The original readable data. It does not have to be text: a PDF, an image or a database row is plaintext too
Ciphertext The result of applying encryption. It must be indistinguishable from random data
Algorithm (cipher) The mathematical procedure. Public, studied, standardised
Key The secret that parameterises the algorithm. The only thing the attacker must not know
Encrypt / decrypt The two inverse operations of the algorithm using the key
Cryptanalysis The study of how to break a scheme without the key

And now the distinction that is most often got wrong in professional practice:

Operation Reversible? Needs a key? What it is for Example
Encode Yes, by anybody No Represent data in another alphabet in order to transport it Base64, URL encoding, UTF-8, hexadecimal
Encrypt Yes, only with the key Yes Confidentiality AES-GCM, ChaCha20-Poly1305
Hash No (it is one-way) No (HMAC does) Integrity, fingerprint, password storage SHA-256, Argon2id
Obfuscate Yes, with effort No Make casual reading harder. It is not security Minifying JavaScript, reversing a string

Encoding protects absolutely nothing. Base64 turns up constantly in tokens, in HTTP headers and in configuration files, and with depressing regularity somebody concludes that it is "encrypted". Let us prove it is not:

import base64

# A fictitious piece of sensitive Nimbus data
data = "patient=Ana Ruiz; session=knee rehabilitation; clinic=CL-014"

# ENCODE: converts to an alphabet safe for URLs and headers
encoded = base64.b64encode(data.encode("utf-8")).decode("ascii")
print("Encoded:", encoded)

# DECODE: anybody, with no secret at all, reverses it
recovered = base64.b64decode(encoded).decode("utf-8")
print("Recovered:", recovered)

Output:

Encoded: cGF0aWVudD1BbmEgUnVpejsgc2Vzc2lvbj1rbmVlIHJlaGFiaWxpdGF0aW9uOyBjbGluaWM9Q0wtMDE0
Recovered: patient=Ana Ruiz; session=knee rehabilitation; clinic=CL-014

Let us go through the code line by line, because the detail matters:

  • data.encode("utf-8") converts the Python text into bytes. Base64 operates on bytes, not on characters.
  • base64.b64encode(...) produces the representation in the A-Z a-z 0-9 + / alphabet, with = padding at the end. No key was involved. The result depends solely on the input.
  • base64.b64decode(...) reverses the operation. No secret is passed to it because there is none.

The operational conclusion: if you see a value ending in == in a log, a ticket or a URL, it is not protected; it is transported. And if it contains personal data, that log is a leak (this connects directly with the "what to log and what not to log" rule from 02-04).

Pocket test to tell them apart. Ask yourself: can I undo this without any secret? If the answer is yes, it is encoding or obfuscation, and it adds no security. If I need a secret, it is encryption. If it cannot be undone at all, it is a hash.


  1. History as a conceptual lever: Caesar, Vigenère and Enigma

The history of cryptography is not studied here for erudition, but because each historical failure left behind a lesson that still holds word for word.

Scheme Period Idea Why it fell Permanent lesson
Caesar cipher Rome Shift each letter N positions There are only 25 possible keys The key space must be unreachable
Monoalphabetic substitution Middle Ages Each letter is swapped for a fixed other one Frequency analysis destroys it The ciphertext must not preserve the structure of the original
Vigenère 16th century Substitution with a repeating key Repeating the key creates detectable patterns Never reuse key material (a rule that will reappear with nonces)
Enigma WWII Mechanical rotors, daily key Procedural mistakes, predictable messages and captured machines You attack the implementation and the procedure, not the mathematics

Breaking a Caesar cipher in four lines

This example exists purely so that you can see with your own eyes what "small key space" means.

Explicit warning. The Caesar cipher has no security value whatsoever. Never use it to protect anything, not even as an "additional layer". It appears here solely as a conceptual illustration, just like a drawing of a 1900 lock in a locksmithing manual.

def caesar(text: str, shift: int) -> str:
    """Shifts the letters of the English alphabet N positions. Teaching only."""
    output = []
    for c in text:
        if c.isalpha():
            base = ord("A") if c.isupper() else ord("a")
            # (ord(c) - base) gives position 0..25; we add and wrap back to 0..25 with %26
            output.append(chr((ord(c) - base + shift) % 26 + base))
        else:
            output.append(c)          # spaces, digits and punctuation pass through intact
    return "".join(output)

intercepted = "Errnlqj pryhg wr hljkw r'forfn"

# BRUTE FORCE: we walk through EVERY possible key. There are 25.
for key in range(1, 26):
    print(f"key={key:2d} -> {caesar(intercepted, -key)}")

Extract from the output:

key= 1 -> Dqqmkpi oqxgf vq gkijv q'enqem
key= 2 -> Cppljoh npwfe up fjhiu p'dmpdl
key= 3 -> Booking moved to eight o'clock
key= 4 -> Annjhmf lnudc sn dhfgs n'bknbj

What you need to take from this:

  • No mathematics was required. The loop tries all 25 keys in microseconds and a human spots the right one at a glance.
  • The % 26 operation is what makes the alphabet circular: after z comes a again.
  • Decrypting is encrypting with the negative shift: caesar(text, -key).
  • A modern computer would try on the order of a billion keys per second against such a simple scheme. With 25 keys, no defence is possible. With 2^128, as we will see in section 6, brute force ceases to exist as an option.

The permanent lesson of Caesar, of Vigenère and of Enigma boils down to three sentences that hold in 2026: the key space has to be astronomical; key material is never reused; and the attacker attacks the weakest part, which is almost always the human procedure, not the algebra.


  1. Kerckhoffs's principle and why security lives in the key

In 01-03 you met Kerckhoffs's principle as one of the fifteen secure design principles. Here is where it acquires its full meaning:

A cryptographic system must be secure even if everything about it, except the key, is public knowledge.

Put another way: the algorithm is public; the secret is the key, and only the key. The opposite — trusting that nobody knows the algorithm — is called security through obscurity and is one of the worst possible decisions in cryptography, for four concrete reasons:

Reason Explanation
Design secrecy does not hold It leaks through reverse engineering, through an employee who leaves, through a repository, through a downloadable binary
You cannot rotate an algorithm If a key leaks, you generate another in a second. If your proprietary algorithm leaks, you have to rewrite the system
Nobody has reviewed it AES has been attacked by thousands of cryptographers for over two decades. Your algorithm has been looked at by two people on your team, in a hurry
It prevents interoperability and auditing Neither the customer nor the auditor can verify anything

From this comes the central teaching rule of this module, which you will read in all seven lessons:

Never implement cryptography by hand. Not the algorithm, not the mode, not the padding, not the tag comparison. Use established, maintained and audited libraries — in Python, cryptography, plus the hashlib, hmac and secrets modules from the standard library. Your professional job is not to invent primitives: it is to choose the right one, configure it properly and manage its keys. All three are frequently done badly, and all three are enough on their own to ruin a system.

This is not a style recommendation. Industrial-grade cryptography demands defences against side channels, padding errors, multiplications that take different times depending on the bits, and faulty random number generators. An implementation "that gives the right answer" may be leaking the key through its execution time without any test detecting it.


  1. Attacker models and types of cryptographic attack

To say that something is secure you have to say against which attacker. Cryptography formalises this in attacker models, distinguished by what the adversary is able to do:

Model The attacker can... Real example in the Nimbus world
Ciphertext only See ciphertexts Somebody captures traffic on the guest Wi-Fi
Known plaintext See (plaintext, ciphertext) pairs they did not choose They know that every response starts with {"bookings":[
Chosen plaintext Make the system encrypt whatever they want They create a booking with tailored text and observe the resulting ciphertext
Chosen ciphertext Make the system decrypt whatever they want and observe the reaction They send manipulated tokens and tell a "format error" apart from a "signature error"
Active attacker (MITM) On top of that, modify, reorder and replay messages A rogue access point in a café, as in 02-02

The modern standard is to demand security against the strongest model. A current algorithm such as AES-GCM is considered secure even against an attacker who chooses plaintexts at will; if a scheme only withstands the weak model, it is discarded.

Families of attack

Family What it consists of Defence
Brute force Try every key A key space of 128 bits or more (section 6)
Cryptanalysis Exploit mathematical weaknesses in the algorithm Use only standardised, current primitives
Dictionary / precomputed tables Try common passwords or already-computed hashes Unique salt and slow functions (03-04)
Side channel Measure time, power consumption, cache or noise to deduce the key Constant-time operations; use libraries that already do this
Attack on the implementation Exploit a flaw in the code, not in the algorithm: badly validated padding, repeated nonce, comparison with == Review, high-level libraries, authenticated encryption
Attack on the environment Steal the key from the disk, the repository, memory or the person Key management (03-06)

Let us pause on the timing attack, because it is counter-intuitive. If your code compares a verification code character by character and returns False as soon as it finds a difference, it takes longer when more characters are correct. An attacker who measures thousands of attempts rebuilds the secret character by character, reducing an impossible problem to a trivial one. That is why hmac.compare_digest exists, which you will see in detail in 03-04, and why comparing secrets with == is forbidden throughout this course.

The idea to take away from this section. Almost no real cryptographic incident is due to somebody breaking the algebra. It is due to badly stored keys, repeated nonces, obsolete protocol versions, disabled validations and naive comparisons. The attacker attacks the implementer, not the algorithm.


  1. What "secure" means today: security bits and key sizes

A scheme has n bits of security when the best known attack requires on the order of 2^n operations. That is not the same as the key size: RSA with a 2048-bit key offers roughly 112 bits of security, because factoring is easier than trying every key.

Security bits Typical equivalents Status in 2026
56 DES Broken. Broken in hours with cheap hardware
~63–80 SHA-1 (real collision resistance) Broken. Collisions demonstrated publicly
112 3DES, RSA-2048, DH-2048 Legacy minimum. Being retired
128 AES-128, RSA-3072, ECC P-256, SHA-256 The current standard. Adequate for all general use
192 AES-192, ECC P-384 High security, long-lived data
256 AES-256, ECC P-521, SHA-512 Practical maximum. Margin for the very long term

Why 2^128 is unreachable

Big numbers mean nothing until they are made concrete. Suppose an imaginary machine capable of trying a billion keys per second (10^9), and suppose we put together a billion such machines (10^18 keys per second, far beyond the entire computing capacity of the planet):

Possible keys with 128 bits : 2^128  ~= 3.4 x 10^38
Assumed speed               : 10^18 keys per second
Seconds required            : 3.4 x 10^20
Years required              : ~ 1.1 x 10^13  (eleven trillion years)

Eleven trillion years against the ~13.8 billion years of the universe's age. And every additional bit doubles that number: 256 bits is not "twice as secure" as 128, it is 2^128 times more.

The practical conclusion is liberating: key size is not the problem. If you use AES-128 or AES-256, brute force against the key is ruled out forever with classical technology. When you read that "an encryption scheme has been broken", it almost always means one of these five things:

  1. The key was derived from a weak password (03-04).
  2. The key was stored where the attacker got to (03-06).
  3. An obsolete mode or algorithm was used (ECB, RC4, MD5).
  4. A nonce was repeated or key material was reused (03-02).
  5. The implementation was broken: side channel, disabled validation, padding error.

And AES-256 versus AES-128? Choose 256 when the data must remain confidential for decades or when a sector standard requires it; the performance cost is small. For Nimbus's general use, either one is beyond the reach of any adversary.


  1. Cryptographic randomness: the invisible foundation

All of cryptography rests on being able to generate values that the attacker cannot predict: keys, nonces, salts, session identifiers, password recovery tokens. If those values are guessable, the algorithm is irrelevant: the attacker breaks nothing, they simply regenerate your secret.

Python has two generators and choosing the wrong one is a genuine security flaw:

Module Type Predictable Correct use
random Statistical PRNG (Mersenne Twister) Yes. With ~624 outputs its state can be reconstructed and everything else predicted Simulations, games, sampling, tests
secrets CSPRNG, fed by the operating system No, by design Anything that is a secret
import random
import secrets

# --- WRONG: never use random for security material ---
bad_token = "".join(random.choice("0123456789abcdef") for _ in range(32))
print("WRONG (predictable):", bad_token)

# --- CORRECT ---
good_token = secrets.token_urlsafe(32)      # ~256 bits of entropy, URL-safe
key_bytes  = secrets.token_bytes(32)        # 32 bytes = a 256-bit key
sms_code   = secrets.randbelow(1_000_000)   # uniform integer in [0, 999999]

print("CORRECT token :", good_token)
print("CORRECT key   :", key_bytes.hex())
print("CORRECT code  :", f"{sms_code:06d}")

Output (different on every run, by definition):

WRONG (predictable): 8f3a1c0b74e2d95a6b0f4c8137ae52d9
CORRECT token : 9tHqR2vXbN0sLpKcYfWm3Zj7aQe1UoIdT5gRnVxB4yA
CORRECT key   : 4c1b9f0a7d6e5382bb44c0f19a7e2d33518c6b09f2a4d7e1c3059b8a6f4e2d10
CORRECT code  : 048217

An explanation of each line:

  • random.choice uses the Mersenne Twister, an excellent generator for statistics and catastrophic for security: it is deterministic and its internal state can be reconstructed by observing enough outputs. If Nimbus generated its "I have forgotten my password" tokens with it, an attacker who requested a few tokens for their own account could predict Sara's.
  • secrets.token_urlsafe(32) asks the operating system's secure generator for 32 bytes and encodes them in URL-safe Base64. Careful: the 32 are bytes of entropy, not output characters.
  • secrets.token_bytes(32) returns raw bytes: it is the correct way to generate a 256-bit symmetric key from scratch.
  • secrets.randbelow(1_000_000) gives a uniform integer without the bias that a % 1000000 over a large random number would introduce. The :06d formatting preserves the leading zeros: 048217 is a valid code.

Historical case. In 2013 it was discovered that a flaw in the random number generator of certain Android applications caused transaction signing keys to repeat. With two signatures sharing that value, the private key was exposed by elementary algebra. The algorithm was correct and well implemented. The randomness was not, and that was enough.

Operational rule for Nimbus, with no exceptions: any value the attacker must not guess is generated with secrets (or with the platform equivalent: crypto.getRandomValues in the browser, crypto/rand in Go). Never with random, never with the clock, never with a counter, never with a version 1 UUID.


  1. The map of module 3

Before we get into the algorithms, this is the route and the piece each lesson contributes:

flowchart TB
    A["03-01 FUNDAMENTALS\nVocabulary, Kerckhoffs,\nsecurity bits, randomness"]
    A --> B["03-02 SYMMETRIC\nOne shared key.\nAES-GCM, modes, nonces.\nProblem: distributing the key"]
    A --> C["03-04 HASH AND MAC\nSHA-256, HMAC,\nArgon2id and passwords"]
    B --> D["03-03 ASYMMETRIC\nKey pair. RSA, ECC,\nDiffie-Hellman, digital signature"]
    C --> D
    D --> E["03-05 PROTOCOLS\nTLS, SSH, VPN.\nCombining primitives properly"]
    E --> F["03-06 KEYS AND PKI\nLife cycle, certificates,\nCA. The hard problem"]
    F --> G["03-07 APPLICATIONS\nEncryption at rest, fields,\nbackups, artefact signing"]

The logic of the sequence: symmetric cryptography solves confidentiality but leaves key distribution open; asymmetric cryptography solves distribution but is slow, which forces you to combine them; hashes and MACs provide the integrity that encryption does not give; protocols are the correct way to assemble all of that; PKI answers "whose public key is this?"; and the last lesson applies it end to end to the Nimbus system.


  1. The cryptography Nimbus already uses without having explained it

The lesson closes with an honest inventory: over two modules we have propped defences up on cryptography without explaining it. This table is the debt that module 3 comes to pay.

Element already seen Where it appeared Which primitive lies underneath Explained in
HTTPS/TLS in the SPA and the API The whole course ECDHE exchange + certificate + AES-GCM 03-02, 03-03, 03-05
JWT signed with RS256 and validated with JWKS 02-05 Asymmetric RSA signature over a hash 03-03, 03-07
120 s signed URLs for the attachments bucket (A-05) 01-04, 02-04 HMAC over method, path and expiry 03-04, 03-07
Webhook HMAC from the payment gateway 02-02, 02-04 HMAC-SHA256 with a shared key 03-04
Six-digit TOTP for MFA 02-05 HMAC + a 30 s time counter 03-04, 03-05
Passkeys / FIDO2 02-05 A key pair per site; signature over a challenge 03-03
Password hashes in the credentials table 02-05 Slow derivation with salt (Argon2id) 03-04
DKIM on outbound e-mail 02-03 Asymmetric signature of the headers 03-03
Encrypted, immutable 3-2-1-1-0 backups 02-04 Symmetric encryption + key management 03-02, 03-07
Code signing (the SolarWinds lesson) 02-06 Asymmetric signature over the artefact 03-03, 03-07
The .env secret that enabled the escalation 02-06 None: that is precisely where management failed 03-06

The last row is the most eloquent. In the Nimbus ransomware case, no cryptographic primitive failed: what failed was that a key was stored where it should not have been. That is the thesis that will govern 03-06 and that is worth internalising from today: the mathematics holds; management is what breaks.


Common Mistakes and Tips

Conceptual mistakes

  1. Calling something Base64-encoded "encrypted". This is mistake number one. Base64 and hexadecimal are transport, not protection. Apply the pocket test from section 2.
  2. Believing that encryption provides integrity. It does not, unless you use authenticated encryption. An attacker can alter the ciphertext in a controlled way in old modes.
  3. Confusing hashing with encryption. "We encrypt the passwords" is an incorrect and dangerous sentence: if they could be decrypted, the attacker who steals the key would have them all. Passwords are derived with a slow, one-way function (03-04).
  4. Thinking a secret algorithm is more secure. Kerckhoffs says exactly the opposite and a century of experience confirms it.
  5. Obsessing over key size. Debating AES-128 versus AES-256 while the key lives in a .env file in the repository is optimising the part that does not fail.
  6. Assuming cryptography replaces access control. They are different layers. The IDOR from 01-01 happened over an impeccable TLS connection.

Implementation mistakes

  1. Using random for security material. Tokens, salts, nonces, keys: always secrets.
  2. Comparing secrets with ==. It opens a timing side channel. Use hmac.compare_digest.
  3. Writing your own primitive "because it is a simple XOR". No.
  4. Copying the first snippet a search engine returns. A good part of the cryptographic code circulating on forums is from 2011 and uses ECB, MD5 or fixed IVs.

Practical tips

  • Before choosing an algorithm, write in one sentence which property you need: confidentiality, integrity, authenticity, non-repudiation or freshness? The table in section 1 practically hands you the primitive.
  • Also write down who you are protecting against. Encrypting a server's disk does not defend against a compromised API, and knowing that stops you buying false reassurance.
  • Keep a team rule: cryptographic code is always reviewed by two people, even when the change looks trivial.
  • Consult current sources for parameters and sizes (NIST SP 800-57, the CCN-CERT and ENISA guidance, the OWASP roadmap). Specific values expire; principles do not.

Exercises

Exercise 1 — Classifying operations

For each real Nimbus situation, state: (a) which operation is being applied — encoding, encrypting, hashing or obfuscating; (b) which security property it actually provides; (c) whether it is adequate for the stated purpose and, if not, what should be done instead.

  1. The session token the SPA stores contains the user's e-mail address in Base64, "so that it is not visible at a glance".
  2. Rubén sends a customer a password-protected ZIP file and sends them the password by the same e-mail.
  3. Iván stores the result of sha256(password) in the database.
  4. A clinic's attachment is uploaded to the A-05 bucket encrypted with AES-GCM and the key lives in a secrets manager.
  5. Lucía publishes a .sha256 file with the archive's fingerprint alongside the nightly backup.
  6. The SPA's JavaScript is minified with single-letter variable names, "so that nobody understands the pricing logic".

Exercise 2 — Auditing a token generator

This is the real code Nimbus uses for password recovery links:

import random, time, base64

def recovery_token(email: str) -> str:
    random.seed(int(time.time()))                      # (1)
    n = random.randint(100000, 999999)                 # (2)
    raw = f"{email}:{n}"
    return base64.b64encode(raw.encode()).decode()     # (3)

You are asked to: (a) identify the three problems marked and explain the concrete attack each one enables; (b) estimate how many attempts an attacker would need if they know Sara's e-mail address and the approximate minute in which she requested recovery; (c) rewrite the function correctly, explaining each decision; (d) state which other two measures, unrelated to token generation, this flow should have.

Exercise 3 — Choosing the primitive

For each Nimbus requirement, say which property is needed, which primitive provides it and in which lesson of the module it is studied. No code required.

  1. That the external consultancy (A-19) cannot read the content of the backups it holds.
  2. That Nimbus can prove to a customer, months later, that it was that customer who cancelled a booking.
  3. That the API detects whether a payment gateway webhook was fabricated by a third party.
  4. That an attacker who records the mobile app's encrypted traffic today cannot read it if in two years' time they steal the server's private key.
  5. That an attachment downloaded from the bucket cannot be reused tomorrow with the same URL.
  6. That, if the credentials table dump is stolen, the passwords cannot be recovered within a useful timeframe.

Solutions

Exercise 1

# Operation What it actually provides Adequate?
1 Encoding Nothing. Anybody reverses it No. If the token has to hide data, we encrypt; but the right answer is for the token to be an opaque random identifier (secrets) with no personal data inside
2 Encryption (the ZIP), but with the key over the same channel Almost nothing: whoever intercepts the e-mail has both things No. The key must travel over a different channel (telephone, with verification as in 02-03), or use a link with an expiry
3 Hashing, but with a fast function and no salt Very little: rainbow tables and GPUs destroy it No. Argon2id with a unique salt per user (03-04)
4 Encryption with AEAD Confidentiality and integrity of the attachment Yes. It is the correct pattern (03-02, 03-07)
5 Hashing Integrity against errors (corruption, incomplete transfer) Partially. It does not protect against an attacker, who would recompute the .sha256. That needs a signature or an HMAC (03-04)
6 Obfuscation Nothing against an analyst with ten minutes to spare No. Logic that must stay secret lives on the server, not in the browser

Exercise 2

(a) The three problems:

  1. random.seed(int(time.time())) — the generator is seeded with the clock in seconds. The seed space for a whole day is 86,400 values. An attacker who knows the day reproduces every possible token.
  2. random.randint(100000, 999999) — two flaws: it uses random (predictable) and there are only 900,000 possible values, about 20 bits. It is trivial brute force even without knowing the seed.
  3. base64.b64encode(...) — the token is not protected: it contains the e-mail address in the clear for anybody to read and, on top of that, reveals the internal format, which makes fabricating candidates easier.

(b) Estimate. If the attacker knows the e-mail address and the approximate minute, they have ~60 candidate seeds. Each seed deterministically fixes the first output of randint, so it yields ~60 candidate tokens. With automated retries, they succeed in seconds. There is not even any need to explore the 900,000: seeding by time destroys all the entropy.

(c) Correct version:

import secrets
import hashlib
from datetime import datetime, timedelta, timezone

def create_recovery_token(user_id: int, repo) -> str:
    # 1. 32 bytes from a CRYPTOGRAPHIC generator -> ~256 bits, impossible to guess
    token = secrets.token_urlsafe(32)

    # 2. The DB does NOT store the token, but its hash: if the table leaks,
    #    the pending links are unusable. SHA-256 is enough here because
    #    the token already has high entropy (it is not a human password).
    fingerprint = hashlib.sha256(token.encode()).hexdigest()

    # 3. Short expiry and single use
    repo.save(
        user_id=user_id,
        fingerprint=fingerprint,
        expires_at=datetime.now(timezone.utc) + timedelta(minutes=15),
        used=False,
    )
    # 4. The plaintext token is only sent by e-mail; it is never written to logs
    return token

Decisions: sufficient entropy from a secure source; the token contains no data (it is opaque, it does not reveal the e-mail address); it is stored hashed; it expires in 15 minutes; it is single use.

(d) Two other measures: (1) an identical response whether or not the account exists — "if the e-mail address is registered, you will receive a link" — so that user enumeration is not possible; (2) rate limiting per e-mail address and per IP, plus logging of the event for alerting (02-04). A desirable third one: invalidate all active sessions once the change is completed.

Exercise 3

# Property Primitive Lesson
1 Confidentiality Authenticated symmetric encryption (AES-GCM) with a key the consultancy does not hold 03-02, 03-07
2 Non-repudiation (+ authenticity and integrity) Digital signature with the customer's key; a MAC would not do 03-03
3 Authenticity and integrity between two parties with a shared key HMAC-SHA256 03-04
4 Forward secrecy Ephemeral ECDHE exchange 03-03, 03-05
5 Freshness / expiry HMAC over the path plus a signed expiry stamp 03-04, 03-07
6 One-wayness and computational cost Argon2id with a unique salt per user 03-04

Conclusion

You have opened the black box. You now have the map that was missing: each security property has its primitive — confidentiality goes with encryption, integrity with the hash and the MAC, authenticity and non-repudiation with the signature, freshness with the nonce, and key agreement with key exchange — and you also know what cryptography does not do: it does not protect against whoever holds the key, it does not hide metadata, it does not give availability and it does not replace access control. You can tell encoding, encrypting, hashing and obfuscating apart precisely, and you have a pocket test so you never confuse them again: can I undo it without any secret? You have seen in Base64 that encoding protects nothing, and in a Caesar cipher broken in 25 attempts what an insufficient key space means.

From history you take away three lessons that remain intact: the key space must be astronomical, key material is never reused — it will return with nonces in the next lesson — and the attacker attacks the procedure and the implementation, not the algebra. On that basis you have recovered Kerckhoffs: the algorithm is public and the secret is only the key, from which comes the rule that governs the entire module — never implement cryptography by hand. You know how to place an adversary in their model, how to tell a brute force attack from a side channel one, and why 2^128 puts brute force outside the universe, which moves the real problem to the same five causes as always: weak passwords, badly stored keys, obsolete algorithms, repeated nonces and defective implementations. And you have internalised the invisible foundation: secrets, never random, because a bad source of randomness breaks any algorithm, however perfect.

Finally, you have put numbers on the debt: eleven Nimbus elements that have been working for two modules thanks to cryptography that was never explained, from the webhook HMAC to the 120-second signed URLs, and a final row that sums up the module's thesis: in the ransomware of 02-06 no primitive failed, what failed was where a key lived.

In Symmetric Cryptography (03-02) we start with the oldest and most widely used primitive: a single shared key that encrypts and decrypts. You will see why AES is the standard, what a mode of operation is and why ECB is forbidden, what role the nonce plays and why repeating it is catastrophic, and how a Nimbus attachment is correctly encrypted with AES-GCM by binding it to its tenant_id. And you will finish with the problem that symmetric cryptography cannot solve on its own — how to get the key to the other end — which is exactly what motivates the following lesson.

Fundamentals of Information Security Course

Module 1: Introduction to Information Security

Module 2: Cybersecurity

Module 3: Cryptography

Module 4: Risk Management and Protection Measures

Module 5: Security Tools and Techniques

Module 6: Best Practices and Regulations

Module 7: Final Project

© Copyright 2026. All rights reserved