crypto1

package
v0.167.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: May 11, 2026 License: AGPL-3.0 Imports: 4 Imported by: 0

Documentation

Overview

Package crypto1 is the pure-Go implementation of the Crypto1 stream cipher used by MIFARE Classic and some HID iCLASS legacy systems.

Algorithm overview

Crypto1 is a 48-bit LFSR-based stream cipher. State is a 48-bit register; a non-linear output filter f(x) produces one keystream bit per clock. The filter is built from boolean functions f_a, f_b, f_c at specific tap positions (see filterOutput below).

References (clean-room reimplementation from the public papers; do NOT copy mfoc/mfcuk/proxmark3 C source):

  • Garcia, de Koning Gans, Muijrers, van Rossum, Verdult, Schreur, Jacobs. "Dismantling MIFARE Classic." ESORICS 2008.
  • Courtois, Nohl, O'Neil. "Algebraic Attacks on the CRYPTO1 Stream Cipher in MiFare Classic and Oyster Cards." IACR 2008.

Thread safety

A Cipher is not safe for concurrent use. Create one per goroutine, or serialise access externally.

API surface

  • Init(key uint64) — load the 48-bit sector key into LFSR positions 0..47.
  • Crypt(input uint32) uint32 — clock LFSR 32 times, XOR keystream with input. NO external feedback. Symmetric.
  • EncCrypt(input, nr uint32) — like Crypt, but mixes input bits into LFSR feedback (used during the reader-nonce phase of MIFARE auth).
  • CryptFeedback(in uint32) — feeds external bits into LFSR feedback while running keystream (used during the tag-nonce nT phase of MIFARE auth).
  • Prng(from uint32, n int) — advance the tag PRNG (16-bit LFSR pair, x^16 + x^14 + x^13 + x^11 + 1) n cycles.

Bit ordering

All inputs and outputs are LSB-first. The 48-bit LFSR has bit 0 as the freshest shift-in position; new bits enter at bit 47 each clock. Inputs to Crypt/EncCrypt/CryptFeedback are processed bit-by-bit, LSB first, for `len(input)*8` clocks; with input as uint32 we run 32 clocks.

mfcuk.go — pure-Go offline darkside key recovery.

This implements the offline portion of the mfcuk (MIFARE Classic darkside attack) from Garcia et al. ESORICS 2008 and Courtois 2009. The live-NFC phase (driving malformed authentication frames at a real card) is handled by the Proxmark3 / libnfc integration layer; this package handles the cryptographic core.

Background

The MIFARE Classic ISO14443A framing includes a parity bit for each byte. During authentication, the reader sends {NR}Ks (the reader nonce encrypted with the cipher keystream). The card checks the parity of each received encrypted byte; if a parity bit is wrong, the card sends a 4-bit NACK response instead of the normal {AT}Ks.

The NACK is sent encrypted: the card XORs the constant NACK value (0x5) with the next 4 keystream bits. An attacker who observes (NT, NR, enc_NACK) can compute the 4-bit keystream at that position:

ks_nack[0..3] = enc_NACK XOR 0x5

These 4 bits constrain the 48-bit key. Collecting many (NR_i, Parity_i) pairs — each with a different first-byte of NR — gives enough constraints to reconstruct the key via exhaustive search over the surviving candidates.

Key-stream position

With the cipher initialised as:

Init(key) → CryptFeedback(uid ^ NT) [32 bits] → 8 EncCrypt clocks with NR[0..7]

the NACK 4-bit keystream occupies cipher-clock positions 8..11 of the EncCrypt phase (after the first byte of NR has been absorbed with feedback). The EncCrypt feedback mixes the PLAIN NR bits into the LFSR (LSB-first: bits 0..7 of the NR uint32 = the first 8 cipher clocks), so the cipher state at the NACK position depends on both the key and the low byte of NR.

Search strategy

For each candidate key K:

  1. Simulate Init(K) + CryptFeedback(uid ^ NT) → base state S.
  2. For each observation (NR_i, P_i): a. Copy S, clock 8 EncCrypt bits using NR_i[0..7] as feedback. b. Collect 4 more keystream bits (NACK window). c. Require those 4 bits == P_i XOR 0x5.
  3. A key that passes all observations is returned.

Worst-case complexity is O(2^48 * N) where N is the number of pairs. For keys fitting in 16 bits (the closed-loop test regime) and N=256 pairs the search is sub-millisecond. For full 48-bit keys, callers use RecoverDarksideWithRange with a restricted high-bit range.

The expected survivor count after N observations is 2^48 / 16^N. With N=256 pairs the key is fully over-constrained, but in practice the first failing pair eliminates ~94% of candidates, making the search fast even without algebraic pre-filtering.

Degeneracy note

For certain (uid, nt) combinations a small number of distinct keys (typically 2) produce identical 4-bit NACK keystreams for all 256 NR low-byte values. This is an inherent property of the 4-bit NACK position constraint and is not a bug. Such degeneracies can be resolved by collecting observations at a different NACK byte position (i.e. using a NR that forces the parity error at byte 1 or 2 rather than byte 0). The current implementation uses only the byte-0 NACK position; operator-facing documentation should note this limitation for live capture workflows.

SPDX-License-Identifier: AGPL-3.0-or-later

mfoc.go — pure-Go offline nested-authentication key recovery.

v0.6 status:

  • mfoc/mfcuk handlers: REAL — offline nested-authentication key recovery backed by internal/crypto1.RecoverNestedWithRange. Operator provides pre-captured nested-auth nonces (at least 2 NestedAttempts per NestedCapture). For live-NFC attacks requiring a real reader, federate mplogas/pm3-mcp.

This implements the offline portion of the classical mfoc (MIFARE Classic nested attack) from Garcia et al. ESORICS 2008. The live-NFC phase (driving a real reader) is handled by the Flipper / Proxmark3 integration layer; this package handles the cryptographic core that can run without any hardware.

Background

MIFARE Classic's "nested authentication" feature lets a reader authenticate to a second sector without going through a full anticollision cycle. In a nested auth the card sends the new sector's nT encrypted under the cipher state left over from the previous (known-sector) authentication, rather than as a plain random value. This encrypted nT leaks information: an attacker who knows the cipher state at the end of the previous auth can decrypt the nT for the new sector, which yields a valid (uid, nt, nr, ar) tuple that can be fed directly to the mfkey32 rollback in mfkey32.go.

Data capture model

Each NestedAttempt captures one complete nested authentication sequence. The attacker re-authenticates to the known sector before each nested attempt, so the cipher is reset to a fresh known-state for every attempt. Concretely, each attempt has:

  • KnownNT, KnownNR: nonces of the known-sector re-authentication for this attempt (the tag issues a fresh nT each time, and the reader chooses a fresh nR — both are captured by the sniffer).
  • NTEnc: the encrypted nT the card sent for the target sector in the nested authentication (on-wire value).
  • NR, AR: the reader nonce sent to the target sector, and the card's encrypted aR response, both captured by the sniffer.

RecoverNested decrypts each NTEnc to obtain a plain (nt, nr, ar) tuple, then feeds the first two tuples into the mfkey32 rollback to recover the target sector key.

Package crypto1 — Garcia §4 filter-selectivity optimised key recovery.

Algorithm overview — Garcia et al. ESORICS 2008 §4

The Crypto1 filter f() reads ONLY the 20 odd-indexed LFSR bits at positions {9,11,13,...,47}. This structural property enables a two-phase attack that exploits the LFSR's odd/even decomposition.

The 48-bit state is decomposed into:

oddState  — 24 bits at positions 1,3,5,...,47  (oddState[k] = state[2k+1])
evenState — 24 bits at positions 0,2,4,...,46  (evenState[k] = state[2k])
fullState = interleave(oddState, evenState)

Key structural facts:

  1. filterOutput at t=0 reads ONLY oddState bits 4..23. Therefore ks2[0] = filterOdd(oddState) exactly — no approximation.

  2. After one Crypt-mode clock from interleave(0,even), the filter reads evenState bits 5..23 plus the even-part feedback bit. Therefore filterEven(even) approximates ks2[1] from the even sub-state.

Two-phase attack

Phase 1 — oddState enumeration with pred16EvenFromOdd filter:

Enumerate 2^24 oddState candidates X. For each, simulate state
interleave(X, 0) forward and record the 16 even-time keystream bits
(t=0,2,4,...,30). Compare against the captured ks2's even-indexed bits.
Expected ~256 survivors from 2^24 when the approximation aligns.

Note: the comparison is probabilistic because the evenState contributes
to the actual even-time bits through feedback (entering at LFSR bit-47).
The first bit (t=0) is exact; subsequent bits are correlated at ~50%.
When pred16EvenFromOdd(oddState_real) happens to equal ks2_even
(probability ~2^-15), the fast path finds the key in O(2^32).

Phase 2 — evenState enumeration per survivor:

For each phase-1 survivor X, enumerate 2^24 evenState candidates Y.
Use ks8Full(X,Y) as an 8-bit pre-check to eliminate ~255/256 wrong
Y values before computing the full 32-bit ks2. For each full match,
roll back to candidate key K and verify against the second capture.

Correctness guarantee

RecoverFast always falls back to RecoverWithRange(0, 1<<32) if the phase-1+phase-2 path does not find the key. This guarantees correctness for all 48-bit keys at the cost of O(2^48) worst-case fallback work. The fallback terminates quickly for small keys (O(2^N) for N-bit keys).

References

  • Garcia, de Koning Gans, Muijrers, van Rossum, Verdult, Schreur, Jacobs. "Dismantling MIFARE Classic." ESORICS 2008.
  • equipter/mfkey32v2 — bit-packing convention reference (clean-room reimplementation; no code was copied from that project).

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func AuthEncrypt added in v0.9.0

func AuthEncrypt(key uint64, uid uint32, cap AuthCapture) (nrEnc, arEnc uint32)

AuthEncrypt simulates the reader side of one MIFARE Classic authentication exchange and returns the encrypted (on-wire) values.

Cipher sequence (Garcia et al. §3.3, reader perspective):

  1. c.Init(key) — seed LFSR with the 48-bit sector key
  2. c.CryptFeedback(uid ^ cap.NT) — advance LFSR through the nT phase
  3. c.EncCrypt(cap.NR, 0) — encrypt nR; plain nR feeds LFSR
  4. ks2 = c.Crypt(0) — aR-phase keystream

Returned values:

nrEnc = {nR}Ks = cap.NR XOR ks1  (sent on wire)
arEnc = {aR}Ks = prng(cap.NT,64) XOR ks2  (sent on wire)

func Prng

func Prng(from uint32, n int) uint32

Prng advances the MIFARE Classic tag PRNG forward n cycles from the 32-bit seed `from`. The hardware PRNG is two 16-bit LFSRs in lockstep (polynomial x^16 + x^14 + x^13 + x^11 + 1 per half) whose outputs are concatenated into the 32-bit nT value.

Per-cycle: the high half advances (its low bit shifts out, new bit computed from taps); the low half is the high half's previous value. This is the equivalent of mfoc's `prng_successor` with byte-swapped representation.

func Recover added in v0.9.0

func Recover(uid, nt0, nr0, ar0, nt1, nr1, ar1 uint32) (uint64, error)

Recover reconstructs the 48-bit MIFARE Classic sector key from two captured authentication exchanges (mfkey32v2 algorithm).

Parameters:

uid           — card UID (4 bytes, big-endian)
nt0, nr0, ar0 — first-attempt:
                 nt0 = tag nonce (plain, received from tag)
                 nr0 = reader nonce (plain — the attacker chose it)
                 ar0 = {aR}Ks encrypted reader auth response on wire
nt1, nr1, ar1 — second-attempt (same card, same sector, different nonces)

Returns the 48-bit key (low 48 bits of uint64) and an error if no candidate key matches both nonce-pairs.

Algorithm: for each 48-bit candidate key K, simulate the auth exchange and compare the produced aR keystream against the constraint derived from the captured {aR} values. Both captures must agree on the same K.

From each capture: ks2 = {aR} XOR prng(nT, 64). The key K is correct when AuthEncrypt(K, uid, cap0) produces ks2_0 AND AuthEncrypt(K, uid, cap1) produces ks2_1.

Performance: this implementation is O(2^48) in the worst case for arbitrary 48-bit keys and may take hours. For keys in a smaller search space (e.g. vendor-default keys or the low 24 bits unknown), supply keyHi via the keyspace argument (a range of hi-bit prefixes). If keyspace is nil, all 2^48 keys are searched — clearly document expected runtime in the caller.

TODO(v0.6): implement the O(2^24) partial-state enumeration described in Garcia et al. ESORICS 2008 §4 ("filter-selectivity" technique). That approach enumerates 2^24 odd-bit candidates of the mid-auth LFSR state and uses the filter structure to derive remaining bits, reducing the search to a feasible runtime for arbitrary 48-bit keys.

func RecoverDarkside added in v0.9.0

func RecoverDarkside(c DarksideCapture) (uint64, error)

RecoverDarkside recovers the 48-bit MIFARE Classic key from a DarksideCapture. It searches keys where bits 47..16 are zero (16-bit key space, sub-millisecond). For larger key spaces, call RecoverDarksideWithRange.

func RecoverDarksideWithRange added in v0.9.0

func RecoverDarksideWithRange(ctx context.Context, c DarksideCapture, loHi, hiHi uint64) (uint64, error)

RecoverDarksideWithRange is RecoverDarkside with an explicit high-32-bit search range [loHi, hiHi). See RecoverWithRange for range semantics.

ctx is checked once per hi32 outer-loop iteration; cancellation causes an early return of ctx.Err() so the goroutine running this function terminates promptly when the deadline fires rather than leaking until the range is done.

func RecoverFast added in v0.9.0

func RecoverFast(uid, nt0, nr0, ar0, nt1, nr1, ar1 uint32) (uint64, error)

RecoverFast recovers the 48-bit MIFARE Classic sector key from two captured authentication exchanges using the Garcia et al. ESORICS 2008 §4 filter-selectivity optimisation.

Algorithm: two-phase state-space search using the (oddState, evenState) decomposition:

  1. Phase 1: enumerate 2^24 oddState candidates. For each, compute pred16EvenFromOdd and compare to the captured ks2's even bits. ~256 survivors expected when the approximation aligns (probabilistic; see package overview). Budget: ≤1024 survivors before falling back.

  2. Phase 2: for each survivor, enumerate 2^24 evenState candidates. 8-bit pre-check (ks8Full) followed by full 32-bit ks2 verification. Roll back to candidate key and verify against second capture.

  3. Fallback: RecoverWithRange(0, 1<<32) for guaranteed correctness when the fast path misses. Terminates in O(2^N) for N-bit keys.

RecoverFast always returns the correct key or an error. It is equivalent to Recover for correctness; the fast path provides better-than-O(2^48) expected performance for most key sizes.

Parameters:

uid           — card UID (4 bytes)
nt0, nr0, ar0 — first capture: tag nonce, reader nonce, {aR}Ks
nt1, nr1, ar1 — second capture (same card, different nonces)

func RecoverFastTimeout added in v0.9.0

func RecoverFastTimeout(ctx context.Context, uid, nt0, nr0, ar0, nt1, nr1, ar1 uint32) (uint64, error)

RecoverFastTimeout is RecoverFast with a context deadline. The context is checked approximately every 64K iterations to bound cancellation latency to a few milliseconds.

Returns context.Canceled or context.DeadlineExceeded (wrapped) if the context is done before a key is found.

func RecoverNested added in v0.9.0

func RecoverNested(c NestedCapture) (uint64, error)

RecoverNested recovers the 48-bit target-sector key from a NestedCapture.

Algorithm:

  1. For each NestedAttempt, reconstruct the cipher state at the point where the card encrypts the nested nT: a. Init cipher with KnownKey. b. CryptFeedback(uid ^ KnownNT) — advance through the known-sector nT phase (32 clocks, nT mixes into LFSR). c. EncCrypt(KnownNR, 0) — advance through the known-sector nR phase (32 clocks, plain nR feeds LFSR). d. Crypt(0) — advance through the known-sector aR phase (32 clocks, no external feedback); cipher is now at "end of known auth". e. Crypt(0) — produce the 32-bit keystream used to encrypt the nested nT; XOR with NTEnc → plain NT for the target sector.

  2. Collect the (NT, NR, AR) tuples from the first two attempts.

  3. Call RecoverWithRange on those two tuples, restricting the hi32 search to 0..loHi (determined by the search range argument, defaulting to 1 for a 16-bit search that finishes in <100 ms for typical test vectors).

The function returns the recovered 48-bit key (low 48 bits of uint64) or an error if no matching key was found in the default 16-bit search range.

For production use with unknown keys, call RecoverNestedWithRange with a larger range (up to 1<<32 for full 48-bit coverage).

func RecoverNestedWithRange added in v0.9.0

func RecoverNestedWithRange(ctx context.Context, c NestedCapture, loHi, hiHi uint64) (uint64, error)

RecoverNestedWithRange is RecoverNested with an explicit high-32-bit search range [loHi, hiHi). See RecoverWithRange for range semantics.

ctx is forwarded to RecoverWithRange so that deadline cancellation propagates into the inner key-search loop; the goroutine running this function will terminate promptly when ctx is cancelled rather than leaking until the search range is exhausted.

func RecoverWithRange added in v0.9.0

func RecoverWithRange(ctx context.Context, uid, nt0, nr0, ar0, nt1, nr1, ar1 uint32, loHi, hiHi uint64) (uint64, error)

RecoverWithRange is Recover with an explicit search range over the high 32 bits of the candidate key (bits 47..16). loHi and hiHi are the inclusive-start and exclusive-end of the hi32 range. The full 48-bit key is formed as (hi32 << 16) | lo16 for all lo16 in 0..65535.

Use loHi=0, hiHi=1 to search only keys with bits 47..16 = 0 (i.e. keys fitting in 16 bits), which completes in ~70 ms. Use loHi=0, hiHi=1<<32 to exhaustively search all 2^48 keys (hours).

ctx is checked once per hi32 iteration; cancellation causes an early return of ctx.Err() so the caller's goroutine does not leak.

func SynthesizeDarksideParity added in v0.9.0

func SynthesizeDarksideParity(key uint64, uid, nt, nr uint32) uint8

SynthesizeDarksideParity is the exported form of darksideSynthesizeParity. It computes the 4-bit encrypted NACK nibble (Parity field in DarksidePair) that a MIFARE Classic card would return when the operator sends the given NR with a deliberate byte-0 parity error. Used to construct synthetic DarksidePairs for testing and for verifying a recovered key against new observations.

Equivalent to: observe on wire when Init(key); CryptFeedback(uid^nt); 8 EncCrypt clocks with NR low byte; then the card sends NACK (0x5) encrypted with the next 4 keystream bits.

Types

type AuthCapture added in v0.9.0

type AuthCapture struct {
	NT uint32 // Tag nonce (sent in plain)
	NR uint32 // Reader nonce (plain value — attacker knows it)
	AR uint32 // Encrypted reader auth response {aR}Ks sent on wire
}

AuthCapture represents one MIFARE Classic authentication exchange as captured by a passive sniffer or an attacker-controlled reader.

type Cipher

type Cipher struct {
	// contains filtered or unexported fields
}

Cipher is a 48-bit Crypto1 LFSR. Zero value is invalid; call Init before any Crypt / EncCrypt / CryptFeedback. Reinit is allowed and expected between MIFARE authentication exchanges.

func New

func New() *Cipher

New returns a fresh Cipher. Equivalent to `var c Cipher`; provided for symmetry with the wider PromptZero `New()` constructor convention.

func (*Cipher) Crypt

func (c *Cipher) Crypt(input uint32) uint32

Crypt clocks the LFSR 32 times producing 32 keystream bits, XORs them with the input bits (LSB-first), and returns the ciphertext. No external feedback — the LFSR purely advances under its own recurrence. Symmetric: applying twice with the same key recovers the plaintext.

func (*Cipher) CryptFeedback added in v0.9.0

func (c *Cipher) CryptFeedback(in uint32) uint32

CryptFeedback runs the cipher exactly like Crypt but feeds each input bit (LSB-first) into the LFSR's high position alongside the natural feedback. Used during the MIFARE Classic auth nT phase, where the tag nonce mixes into the cipher state. Returns the keystream produced (caller almost always discards it; the side effect on state is the point).

func (*Cipher) EncCrypt

func (c *Cipher) EncCrypt(input, nr uint32) uint32

EncCrypt is Crypt plus reader-nonce feedback: each input bit XOR'd with the corresponding nr bit is fed into the LFSR's high position. Used during the MIFARE Classic auth nR phase, where the plain reader nonce mixes into the cipher state.

Note: the canonical convention in Garcia et al. has the input plain bit (not nr) feed back into the LFSR. We follow that — `nr` is retained as the second argument so callers wishing to model the "nR plain feed" can pass the same value as input. The single-arg case (callers passing nr=0) reduces to vanilla feed-input-into-LFSR.

func (*Cipher) Init

func (c *Cipher) Init(key uint64)

Init seeds the LFSR from the low 48 bits of key. The high 16 bits of key are ignored.

Bit-spread: key bit i lands in LFSR position i for i in 0..47, so `Init(key); ks := c.Crypt(0)` produces a deterministic keystream derived purely from key.

type DarksideCapture added in v0.9.0

type DarksideCapture struct {
	// UID is the card UID (4 bytes, big-endian word).
	UID uint32

	// NT is the tag nonce received from the card in plaintext at the start
	// of the authentication exchange.  All NRArs pairs in this capture share
	// the same NT (i.e. all were collected during the same PRNG "lock" window
	// where the card keeps emitting the same nT).
	NT uint32

	// NRArs is the set of (NR, enc_NACK) observations.  At least 1 pair is
	// required; in practice 8+ pairs are recommended for reliable key
	// recovery.  Each pair's NR should have a distinct low byte (NR & 0xFF)
	// so that each observation exercises a different EncCrypt feedback path,
	// giving independent keystream constraints.  256 pairs with all distinct
	// low bytes provide unique recovery in the 16-bit key space for most
	// (uid, nt) combinations (see degeneracy note in package doc).
	NRArs []DarksidePair
}

DarksideCapture is the full set of observed data needed for one offline darkside recovery.

type DarksidePair added in v0.9.0

type DarksidePair struct {
	// NR is the plain reader nonce the attacker sent.  Only the low byte
	// (bits 0..7, the first 8 cipher clocks in LSB-first processing) influences
	// the cipher state at the NACK position; the remaining bytes are present
	// for completeness.
	NR uint32

	// Parity is the 4-bit encrypted NACK nibble observed on the wire.  The
	// underlying constraint is: 4 keystream bits == Parity XOR darksideNACK.
	// Only the low 4 bits of Parity are examined.
	Parity uint8
}

DarksidePair is one malformed-authentication observation.

type NestedAttempt added in v0.9.0

type NestedAttempt struct {
	// KnownNT is the plain tag nonce from the known-sector re-authentication
	// that precedes this nested attempt.
	KnownNT uint32

	// KnownNR is the plain reader nonce the reader sent to the known sector.
	KnownNR uint32

	// NTEnc is the encrypted (nested) tag nonce for the target sector, as
	// observed on the wire.
	NTEnc uint32

	// NR is the plain reader nonce sent to the target sector.
	NR uint32

	// AR is the tag's encrypted aR response to the target-sector auth
	// challenge, as observed on the wire.
	AR uint32
}

NestedAttempt captures one nested authentication round:

  • The known-sector authentication nonces for this particular re-auth (KnownNT, KnownNR) — they change every attempt because the tag's PRNG advances and the reader picks a fresh nR.
  • The nested target-sector nT as it appeared on the wire (NTEnc) — encrypted under the known-sector cipher state.
  • The reader nonce sent to the target sector (NR) and the card's aR response (AR) — both captured from the wire. These are the same (uid, nt, nr, ar) tuple shape that mfkey32_recover expects once NTEnc has been decrypted.

type NestedCapture added in v0.9.0

type NestedCapture struct {
	// UID is the card UID (4 bytes, big-endian word).
	UID uint32

	// KnownKey is the 48-bit sector key that is already known to the attacker
	// (low 48 bits of uint64).  The known sector must have been authenticated
	// at least once before each nested attempt.
	KnownKey uint64

	// Attempts contains the sniffed data from each nested authentication
	// attempt.  At least 2 attempts are required for the mfkey32 intersection.
	// There is no benefit in passing more than 2; additional attempts are
	// ignored.
	Attempts []NestedAttempt
}

NestedCapture is the full set of sniffed data needed for one offline nested attack. It groups the known-sector credentials and the nested-auth attempts against the target sector.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL