Skip to content
1Claw Academy
Curriculum/Working with Secrets2 minIntermediate · Lesson 6 of 10

Lab: predict a token you did not see

Lab

Generate secrets the way a lot of code does, then recover one from the clock and predict the next one.

The rotation lesson said to prefer server-side generation over producing a value yourself. One reason is shell history. This lab is about the other one, which is that generating a secret is easy to do with the wrong tool and the result looks identical in review.

  1. 1

    Write two token generators. They differ by one module, and in a code review they look the same.

    bash
    cat > /tmp/rnd.py <<'EOF'
    import random, secrets, time, string
    
    ALPHABET = string.ascii_letters + string.digits
    token_bad  = lambda n=16: "".join(random.choice(ALPHABET) for _ in range(n))
    token_good = lambda n=16: "".join(secrets.choice(ALPHABET) for _ in range(n))
    
    now = int(time.time())
    random.seed(now)                    # a service that seeds from the clock
    issued = token_bad()
    print("issued to the user:", issued)
    
    print("\n=== attacker knows roughly when it was issued ===")
    for guess in range(now - 3, now + 4):
        random.seed(guess)
        if token_bad() == issued:
            print(f"  recovered with seed {guess}")
            break
    
    print("\n=== and the NEXT token the service will issue ===")
    random.seed(now); _ = token_bad()
    print("  predicted:", token_bad())
    random.seed(now); _ = token_bad()
    print("  actual:   ", token_bad())
    
    print("\n=== the same attack against secrets ===")
    tok = token_good()
    found = any((random.seed(g), token_good())[1] == tok for g in range(now - 3, now + 4))
    print("  recovered by guessing the clock:", found)
    EOF
    python3 /tmp/rnd.py
  2. 2

    Your values will differ, and the outcome will not.

    text
    issued to the user: KfLP3M4kFfaz32nC
    
    === attacker knows roughly when it was issued ===
      recovered with seed 1788750944
    
    === and the NEXT token the service will issue ===
      predicted: ZXsrorQl5ly29QA0
      actual:    ZXsrorQl5ly29QA0
    
    === the same attack against secrets ===
      recovered by guessing the clock: False
  3. 3

    Clean up.

    bash
    rm /tmp/rnd.py

Two separate failures happened there, and the second is worse than the first.

  • The issued token was recovered by guessing the clock. An attacker who knows roughly when an account was created has a search space of seconds rather than of 62 to the sixteenth.
  • The next token was predicted exactly. random is a Mersenne Twister, which is a deterministic sequence: knowing the state gives you every future output, not just the current one.
  • secrets was immune to both, because it draws from the operating system's cryptographic source and has no reproducible state to recover.
Watch out

The generator that failed is the default one people reach for, and both functions are one line, similar length, and look equally reasonable next to each other in a diff. This is not a bug you find by reading code carefully; you find it by knowing which module to use.

Tip

The rule is short enough to memorise: for anything an attacker benefits from guessing, use secrets in Python, crypto.randomBytes in Node, and crypto/rand in Go. Never math/rand, random, or Math.random.

Better still, do not generate it at all. Server-side rotation produces the value inside the vault with a cryptographic source, and it never passes through your shell, your clipboard or your terminal history on the way in.

Check your understanding

3 questions
1

Why was the next token predictable, not just the current one?

2

What made the clock-seeded version searchable?

3

Why is secrets immune to the same attack?