Skip to content

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.

Published on 5 min read

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:

AlgorithmMPIs consumedReported bits
RSA (1, 2, 3)n, ebit length of n
ElGamal (16, 20)p, g, ybit length of p
DSA (17)p, q, g, ybit 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.
  • 254 or 255 — 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:

AlgorithmsBlock 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>
FieldMeaning
algoPublic-key algorithm byte
datalenByte length of encrypted secret material
bitsKey bit-size from the first public MPI
dataEncrypted secret material, hex-encoded
specS2K type (0 = simple, 1 = salted, 3 = iterated)
usageS2K usage byte (254, 255, or legacy cipher number)
hashAlgoHash algorithm byte from S2K specifier
cipherAlgoSymmetric cipher algorithm byte
ivlenIV length in bytes
ivIV, hex-encoded
countDecoded iteration count (0 for types 0 and 1)
salt8-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 — RSA
  • 668 — 668 bytes of encrypted secret material
  • 2048 — 2048-bit key
  • 3 — iterated S2K
  • 254 — usage byte (full S2K specifier)
  • 2 — SHA-1
  • 9 — AES-256
  • 16 — 16-byte IV
  • 0cd461bdc6d938c02bb9120b11f65195 — the IV
  • 65011712 — iteration count decoded from coded byte 0xfe
  • 8a4513a8b563e724 — 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.

Related articles

Why 7z is the trickiest archive to crack: LZMA-compressed headers, AES coder property encoding, and the full $7z$ hash format explained.
Inside the adb backup text header: how PBKDF2 salts, round counts, and a master-key blob become a crackable $ab$ hash.
A technical walkthrough of how Hash Extractor parses Ansible Vault files to produce a crackable $ansible$ hash for hashcat and John the Ripper.