# Time Window Seed Bruteforce

> Recover a low-entropy PRNG seed from observed state plus a known time window. Common when a system uses Date.now() or time(NULL) as its default seed — that's ~41 bits of entropy, brute-forceable in seconds when you know within ~10s when the seed was minted. Use when porting a deterministic system that won't tell you its seed, or to validate a leaked-seed claim.

- Skill: `abedegno/time-window-seed-bruteforce` (Agent Skill, multi-file: 2 files)
- Install (CLI): `npx skillmds@latest add abedegno/time-window-seed-bruteforce`
- Raw SKILL.md: https://api.skillmd.com/api/skills/abedegno/time-window-seed-bruteforce/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: abedegno (https://skillmd.com/u/abedegno)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/abedegno/time-window-seed-bruteforce

---


# Time-Window Seed Brute Force

When a deterministic system uses `Date.now()` (JavaScript) or `time(NULL)` (C) or `time.time()` (Python) as its default PRNG seed, the seed has only ~41 bits of useful entropy — the high bits are a predictable millisecond timestamp. If you can observe a few values the PRNG produced (or any state derived from it), you can brute-force the seed in seconds.

This isn't an attack on the PRNG algorithm. xorshift32, mt19937, and similar are computationally strong once seeded. The weakness is in the **seed entropy**, not the algorithm.

## When this skill applies

- The system uses a PRNG with a low-entropy default seed (millisecond timestamp, process PID, etc.).
- You know roughly **when** the seed was minted (within seconds, ideally — but even within minutes is tractable).
- You can observe **some output** of the PRNG — initial state, first N draws, anything reproducible.
- You have a **port** of the exact PRNG algorithm (see [`find-production-sourcemap`](../find-production-sourcemap/SKILL.md) — the algorithm is often right there in the source).

If the seed is shipped to the client (e.g. in a `prepare_game` message), you don't need to brute-force — but knowing the technique is the fallback when it isn't.

## The math

A 64-bit JavaScript timestamp is `Date.now()`, which is "milliseconds since 1970-01-01". Today's values are around `1.78e12`, which is around `2^40.7`. That's ~41 bits.

For any specific seed event:

- If you know it's "today, within the last 24 hours" → 86,400,000 candidate ms values → ~27 bits.
- If you know it's "within the last hour" → 3,600,000 candidates → ~22 bits.
- If you know it's "within the last 10 seconds" → 10,000 candidates → ~14 bits.
- If you triggered the event and timed it yourself → ~1000 candidates → ~10 bits.

Per-candidate verification is "seed the PRNG, run the system's initialisation logic, compare against observed state". Each verification is ~microseconds. Brute-forcing 10,000 candidates takes ~10ms.

## The procedure

### Step 1: Port the PRNG bit-exactly

You need a Python (or whatever) implementation that produces the **same output** as the original given the same seed. The most common case is porting a JavaScript PRNG.

Standard JS PRNG implementations you'll encounter:

- **xorshift32** — three lines, trivial to port.
- **mulberry32** — four lines, popular for game state.
- **sfc32** — small fast counter, four state words.
- **xxHash32 finaliser** combined with one of the above.
- **mt19937** — Mersenne Twister, often imported from a library; port via `numpy.random.MT19937` for verification.

Port carefully — JS integer semantics differ from Python's. See [`references/js-prng-port-traps.md`](references/js-prng-port-traps.md).

### Step 2: Validate the port against captured output

You need at least one (seed, output) pair from the original to confirm your port is correct. Get this either:

- From a captured session where the seed is visible on the wire and you can also see the first few PRNG-derived values.
- By logging the original (if you have a debug build).
- By comparing predicted vs observed initial state for a known seed.

```python
def test_prng_port_against_known_seed():
    """If the original system says seed=12345 produces sequence [a, b, c, ...],
    my port must produce the same sequence."""
    prng = PortedPRNG(seed=12345)
    out = [prng.next_uint32() for _ in range(20)]
    assert out == [0xC18E0DDD, 0x4DE82C7D, ...], "port diverges from original"
```

Without this validation, brute-force will find seeds that produce *your port's* output for a given session, not the original's.

### Step 3: Brute-force the window

```python
def recover_seed(t_start_ms, t_end_ms, observed_initial_state, init_fn):
    """
    Try every millisecond candidate in [t_start_ms, t_end_ms]. For each:
    seed the PRNG, run the system's initialisation, compare against
    observed_initial_state. Return the first match.

    Args:
        t_start_ms, t_end_ms: bounds of the candidate window (ms-epoch).
        observed_initial_state: the state derived from the unknown seed.
        init_fn: function (prng) -> state, mimicking the original's init logic.

    Returns:
        The recovered seed, or None.
    """
    for candidate in range(t_start_ms, t_end_ms + 1):
        prng = PortedPRNG(seed=candidate)
        predicted = init_fn(prng)
        if predicted == observed_initial_state:
            return candidate
    return None
```

For an 8x8 grid of colours, `init_fn` typically draws 64 values mod-7 plus some re-roll logic to avoid immediate matches. Reproduce **that exact init logic** in your port; otherwise you'll never match.

### Step 4: Verify on independent state

A 64-cell match could be coincidence (very unlikely, but worth ruling out). Confirm by drawing more state from the seeded PRNG and comparing against more observed values:

```python
def find_seed_with_verification(t_start, t_end, primary_observation, secondary_observations, init_fn, secondary_fn):
    candidate = recover_seed(t_start, t_end, primary_observation, init_fn)
    if candidate is None:
        return None
    # Re-seed and verify secondary
    prng = PortedPRNG(seed=candidate)
    init_fn(prng)  # advance through init
    for observation in secondary_observations:
        if secondary_fn(prng) != observation:
            # Match was a coincidence
            return None
    return candidate
```

For 64-cell match plus a handful of secondary draws, false-positive probability is essentially zero.

## Narrowing the window

The smaller the window, the faster. Sources of timing info:

- **You triggered the event.** Capture `time.time() * 1000` immediately before triggering; you know within ~100ms when the seed was minted.
- **A wire timestamp.** Some servers stamp messages with epoch ms. If a message close to seed creation has one, that bounds the window.
- **The client logs a `Date.now()` value.** Inspect the JS bundle ([`find-production-sourcemap`](../find-production-sourcemap/SKILL.md)) for client-side timestamps that get sent to the server.
- **An HTTP `Date:` header on a related response.** Second-resolution; tightens the window to 1,000 ms.

If you have no timing info, you can still brute-force a "today" window (86M candidates, ~minutes) or "this hour" (3.6M, seconds).

## When this skill isn't enough

- **High-entropy seeds.** If the system uses `crypto.getRandomValues()` or seeds from `/dev/urandom`, brute-force is infeasible. You need either to find a way to observe the seed directly, or to give up on knowing it.
- **Per-session re-seeding from cryptographic source.** Even worse — each session has independent strong entropy.
- **Sealed sub-state.** Some games re-seed sub-systems with cryptographic values mid-session. Even if the primary seed is recoverable, downstream state may not be.

If brute-force isn't tractable, look harder for a leak point — does the seed appear in any client-side log, any HTTP header, any state that's later echoed back?

## Common failure modes

| Symptom | Likely cause |
|---|---|
| No candidate in the window matches | PRNG port is wrong; init logic doesn't match exactly; or the window doesn't actually contain the seed. |
| Multiple candidates match the initial state | Initial state has low entropy (e.g. only 16 distinct colours of 64 cells); add secondary verification. |
| Match works in dev, fails in prod | Different PRNG, or different init logic between dev and prod. |
| Match for one session, fails for another | The system has multiple seed sources, or you're brute-forcing the wrong one (e.g. the session-level seed when the relevant one is the player-level seed). |

## Defensive perspective

The mitigation, if you operate a system like this: don't seed your PRNG from a millisecond timestamp. Use a cryptographically secure source. `crypto.getRandomValues(new Uint32Array(1))[0]` in JavaScript, `secrets.randbits(32)` in Python.

If you must use a timestamp seed (compatibility, reproducibility), the seed is **public information** — design the rest of the system to not depend on the seed being secret. If state derived from the seed is sensitive (e.g. determines a game outcome), the seed itself is sensitive and shouldn't have ~41 bits of entropy.

## References

- [`references/js-prng-port-traps.md`](references/js-prng-port-traps.md) — JavaScript integer semantics traps when porting PRNGs.

## Pairs with

- [`find-production-sourcemap`](../find-production-sourcemap/SKILL.md) — the original PRNG source is usually right there in the recovered TS.
- [`bit-exact-sim-validation`](../bit-exact-sim-validation/SKILL.md) — your PRNG port should be in a system you also bit-exactly validated.
- [`closed-loop-live-demo`](../closed-loop-live-demo/SKILL.md) — the recovered seed is the input to closed-loop prediction.

