Track 3 · Lesson 4 of 5
Secrets and tokens in Python
Generate tokens, hash them, compare them safely and read them from the environment, in whole programs built from blanks.
Anything a person uses as a secret has to come from a source an attacker cannot predict. Python has two modules for randomness and only one of them is safe for this. Every program below uses the safe one, and the last one puts the pieces together into a small tool.
What you'll be able to do
- Generate a token with
secretsand know why notrandom. - Work out how many characters a byte count produces.
- Hash a token so you can store the digest instead of the token.
- Compare two secrets without leaking timing.
- Read a secret from the environment instead of the source.
Step 1: Two token functions, two lengths
secrets has two ways to make a token, and the length you get back depends on which one you use.
Tap a piece to place it
One gives hex characters, two per byte. The other packs four bytes into three characters.
Turn the switch off when you want to place the pieces yourself, and the mistakes become yours to find.
Seven lines, two figures: 32 and 16. token_hex(16) is 16 bytes written as two hex characters each. token_urlsafe(12) is 12 bytes packed four into three, which rounds up to 16 characters.
The number you pass is bytes, not characters. That is the trap, and it is the one a beginner hits.
random would give you something that looks just as random and is not. It is seeded from a value an attacker can often work out, and it is built for shuffling cards rather than for making secrets.
Step 2: Hash the token, keep the digest
A server should not store the token itself. It stores a digest and compares digests later.
Tap a piece to place it
One gap is already bytes. The other shows the first eight characters of the digest.
Turn the switch off when you want to place the pieces yourself, and the mistakes become yours to find.
Seven lines. 64 is always the answer for SHA-256, because it is 32 bytes written as hex. Storing the digest means a database leak does not hand over working tokens.
Note the gap takes token, not the string in quotes. token is already bytes because of the b on line three; retyping the string without the b would raise TypeError: Strings must be encoded before hashing.
Step 3: Compare without leaking
Comparing two secrets with == stops at the first difference, so it takes slightly longer the more characters match. Over enough attempts that timing is information.
Tap a piece to place it
Both gaps use the same function. One compares two values that match, one compares values that differ by a letter.
Turn the switch off when you want to place the pieces yourself, and the mistakes become yours to find.
Eight lines, and the two answers differ by one character. compare_digest takes the same time whether the first character differs or the last one does, which is the whole point: it costs nothing and it closes a hole.
Step 4: Read the secret from the environment
A secret in your source code is a secret in your git history. Read it from the environment instead, and handle the case where it is not there.
Tap a piece to place it
One gap reads the variable. One is the value you get when it is missing. One is the message to print.
Turn the switch off when you want to place the pieces yourself, and the mistakes become yours to find.
Eight lines with a branch. os.environ.get returns None when the name is missing, which is why the test reads is None rather than == "". An empty string means the variable is set to nothing, which is a different situation from it not being set at all.
Run it here and you get no token set, because there is no such variable in this page's environment. That is the branch working, not a failure.
Step 5: A small tool that uses all of it
Now a function, a token, and two facts about it. This is the shape of a real script.
Tap a piece to place it
One gap is the length of the token. The other is the length of what the function returns for it.
Turn the switch off when you want to place the pieces yourself, and the mistakes become yours to find.
Ten lines. The token is different every time you press Run, and both printed figures stay the same, which is the useful property: the value changes, its shape does not.
len(fingerprint) without the brackets would give you an error rather than a length, because a function is not a string. That is the third piece, and it is there to be tried.
Step 6: Type one yourself
Print the length of a token_urlsafe made from 24 bytes, so you can see the rounding for yourself.
token_urlsafe(24) gives 32 characters. Print the length of the result.
import secrets print(len(secrets.token_urlsafe(24)))Runs in your browser, and nothing is sent anywhere. Python starts loading in the background as you reach this cell, about six megabytes once. After that every cell on the site runs instantly until you close the tab.
Step 7: A mistake to find
The code runs and prints something, but the number is wrong. Press Run and look at it.
The argument is bytes, and the answer is hex characters. Two per byte.
import secrets token = secrets.token_hex(16) print(len(token))Runs in your browser, and nothing is sent anywhere. Python starts loading in the background as you reach this cell, about six megabytes once. After that every cell on the site runs instantly until you close the tab.
It prints 16, not 32. Nothing raised, so the only way to notice is to know that a hex token is twice as long as the bytes it came from. That is exactly the kind of quiet mistake a test catches and a person does not.
Quick reference
| Piece | What it does |
|---|---|
secrets.token_hex(n) | n bytes as 2n hex characters |
secrets.token_urlsafe(n) | n bytes as a URL-safe string, rounded up |
random | not for secrets, ever |
hashlib.sha256(b"...") | a digest, which needs bytes |
.hexdigest() | always 64 hex characters for SHA-256 |
hmac.compare_digest(a, b) | compare without leaking timing |
os.environ.get("NAME") | None when it is not set |
if token is None: | the branch for a missing secret |
digest[:8] | a short digest for a screen |
Next steps
- Passwords and keys is the same subject with shell tools.
- Build a password generator puts this into a script you keep.
- Hashing and encoding in Python is where the bytes rule comes from.
Test yourself on this
4 questions and 1 task where you run a real command, with the reasoning explained either way.