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 secrets and know why not random.
  • 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.

Hex and URL-safe tokens Checkpoint
import secrets
hex_token = secrets.(16)
url_token = secrets.(12)
print(len(hex_token))
print(len(url_token))

Tap a piece to place it

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.

Store the digest Checkpoint
import hashlib
token = b"local-dev-token"
digest = hashlib.sha256().hexdigest()
print(len(digest))
print()

Tap a piece to place it

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.

Compare two tokens Checkpoint
import hmac
a = "token-abc"
b = "token-abc"
c = "token-abd"
print("same:", )
print("different:", )

Tap a piece to place it

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.

A secret that may be missing Checkpoint
import os
token = os.environ.("LEARMUX_DEMO_TOKEN")
if token is :
print()
else:
print("a token is set")

Tap a piece to place it

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.

Token and fingerprint Checkpoint
import hashlib
import secrets
def fingerprint(token):
return hashlib.sha256(token.encode()).hexdigest()[:8]
token = secrets.token_hex(16)
print("length", )
print("fingerprint length", )

Tap a piece to place 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.

A 32 character token Checkpoint

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.

Find the mistake Find the mistake

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

PieceWhat 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
randomnot 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

Test yourself on this

4 questions and 1 task where you run a real command, with the reasoning explained either way.

Start