How GPG private key hashes are extracted
A technical walkthrough of how Hash Extractor parses OpenPGP secret key packets to produce a crackable $gpg$ hash for hashcat and John the Ripper.
Hash Extractor parses GnuPG and OpenPGP secret keys entirely in the browser and emits the $gpg$ hash that hashcat and John the Ripper understand. This post walks through exactly how that parsing works, from ASCII-armored text to the final hash string.
Detection and de-armoring
GnuPG keys come in two forms: binary packets and ASCII-armored text. ASCII-armored keys begin with -----BEGIN PGP PRIVATE KEY-----. De-armoring strips the header/footer lines, drops armor headers (lines containing :), removes the trailing CRC-24 checksum line (starting with =), and base64-decodes the body into raw packet bytes.
Binary keys are detected by inspecting the first byte. OpenPGP requires bit 7 to be set on every packet header. Bits 5–2 of an old-format header, or bits 5–0 of a new-format header (where bit 6 is set), carry the packet tag. Tags 5 and 7 identify secret key and secret subkey packets respectively.
OpenPGP packet framing
The OpenPGP packet format (RFC 4880) has two header styles.
Old format (bit 6 of byte 0 is 0): bits 5–2 are the tag; bits 1–0 are the length type — 00 means the next 1 byte is the length, 01 means 2 bytes (big-endian), 10 means 4 bytes.
New format (bit 6 of byte 0 is 1): bits 5–0 are the tag. The length follows using a variable-length encoding: a first byte below 192 is a literal 1-byte length; 192–223 pairs with a second byte to encode lengths from 192 to 8383; a first byte of 255 is followed by a 4-byte big-endian length.
The parser walks packet boundaries in sequence, handing off tag 5 and tag 7 content to the secret-key parser. GNU-dummy stubs (S2K type 101) contain no secret material and are skipped.
Public key MPIs and the key bit-size
Inside a secret key packet, the first fields are the public key:
- 1 byte version (supported: 2, 3, or 4)
- 4 bytes creation timestamp
- versions 2/3 also carry a 2-byte validity period
- 1 byte public-key algorithm
Multi-precision integers (MPIs) follow. Each MPI is a 2-byte big-endian bit count followed by ceil(bits / 8) bytes of value. The bit count of the first MPI is used as the key's reported size:
| Algorithm | MPIs consumed | Reported bits |
|---|---|---|
| RSA (1, 2, 3) | n, e | bit length of n |
| ElGamal (16, 20) | p, g, y | bit length of p |
| DSA (17) | p, q, g, y | bit length of p |
ECC algorithms are not supported and produce an unsupported-format error.
The S2K usage byte and cipher selection
Immediately after the public MPIs comes the S2K usage byte.
0— unencrypted; the key has no passphrase and no hash is produced.254or255— the passphrase is protected using a full S2K specifier (described below).- Any other value — legacy encryption: the byte value is treated directly as the symmetric cipher algorithm, with a simple MD5-based S2K and no salt.
For usage 254 or 255, two more fields appear before the S2K type: the symmetric cipher algorithm byte and the S2K type byte.
S2K types: simple, salted, and iterated
Type 0 — Simple S2K: hash algorithm byte only; no salt, no iteration count.
Type 1 — Salted S2K: hash algorithm byte followed by 8 bytes of salt.
Type 3 — Iterated and salted S2K: hash algorithm byte, 8 bytes of salt, and a 1-byte coded iteration count c. The actual count is decoded as:
count = (16 + (c & 0x0f)) << ((c >> 4) + 6)
For example, the coded byte 0x60 (decimal 96) yields (16 + 0) << (6 + 6) = 65536. The coded byte 0xfe used in the reference test yields (16 + 14) << (15 + 6) = 65011712.
Type 101 is the GNU-dummy stub — there is no secret material on the card and the packet is skipped.
The IV
After the S2K fields, the IV is read. Its length is determined by the symmetric cipher algorithm:
| Algorithms | Block size / IV length |
|---|---|
| IDEA (1), 3DES (2), CAST5 (3), Blowfish (4) | 8 bytes |
| AES-128/192/256 (7/8/9), Twofish (10), Camellia-128/192/256 (11/12/13) | 16 bytes |
All remaining bytes in the packet body are the encrypted secret-key material.
The $gpg$ hash format
The hash is assembled as:
$gpg$*<algo>*<datalen>*<bits>*<data>*<spec>*<usage>*<hashAlgo>*<cipherAlgo>*<ivlen>*<iv>*<count>*<salt>
| Field | Meaning |
|---|---|
algo | Public-key algorithm byte |
datalen | Byte length of encrypted secret material |
bits | Key bit-size from the first public MPI |
data | Encrypted secret material, hex-encoded |
spec | S2K type (0 = simple, 1 = salted, 3 = iterated) |
usage | S2K usage byte (254, 255, or legacy cipher number) |
hashAlgo | Hash algorithm byte from S2K specifier |
cipherAlgo | Symmetric cipher algorithm byte |
ivlen | IV length in bytes |
iv | IV, hex-encoded |
count | Decoded iteration count (0 for types 0 and 1) |
salt | 8-byte salt, hex-encoded (all zeros for type 0) |
Real example
The test vector in the parser is an RSA-2048 key protected with passphrase pw, using AES-256 (cipher algo 9), SHA-1 (hash algo 2), iterated S2K (type 3), usage byte 254, and a 16-byte IV. The hash begins and ends with these fixed substrings:
$gpg$*1*668*2048*fc6274d025ad4ed7f3d0bed131e314be...*3*254*2*9*16*0cd461bdc6d938c02bb9120b11f65195*65011712*8a4513a8b563e724
Breaking that down against the format:
1— RSA668— 668 bytes of encrypted secret material2048— 2048-bit key3— iterated S2K254— usage byte (full S2K specifier)2— SHA-19— AES-25616— 16-byte IV0cd461bdc6d938c02bb9120b11f65195— the IV65011712— iteration count decoded from coded byte0xfe8a4513a8b563e724— 8-byte salt
Cracking with hashcat and John the Ripper
hashcat mode -m 17010 targets OpenPGP S2K secret keys. The -m 16700 family covers symmetric-only OpenPGP, while 17010 and 17020 cover the secret-key S2K variants produced here.
echo '$gpg$*1*668*2048*fc6274d025ad4ed7f3d0bed131e314be...' > hash.txt
hashcat -m 17010 hash.txt /usr/share/wordlists/rockyou.txt
With John the Ripper:
john --format=gpg hash.txt --wordlist=/usr/share/wordlists/rockyou.txt
Client-side extraction, no upload
Hash Extractor compiles the parser to WebAssembly and runs it entirely in your browser. The private key bytes are read locally, parsed locally, and the $gpg$ hash is assembled locally. Nothing is uploaded to a server. The tool mirrors what gpg2john does on the command line, except it works directly in the browser without installing any software.