Zaragoza and Aragon bus & tram transport card reverse engineered spec, encoder & decoder implementations for Avanza and Lazo public transit passes
  • Java 49.9%
  • Rust 31.2%
  • TypeScript 18.7%
  • JavaScript 0.2%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
Viktor Shchelochkov 48302b235c
All checks were successful
Publish (NPM) / publish (release) Successful in 9s
Publish (JSR) / publish (release) Successful in 13s
Publish (Maven) / publish (release) Successful in 1m0s
ci-orchestrator / provision running on cpx42 in fsn1
Publish (crates.io) / publish (release) Successful in 23s
Decode 4-byte UID Lazo cards, add card type 0D375F and a journey summary slot in Java lib
2026-10-04 14:50:15 +02:00
.forgejo/workflows Name runners by image, floor and provider 2026-09-17 10:47:25 +02:00
.github Add build workflows 2026-02-28 17:57:05 +01:00
.vscode Fix vscode Even Better TOML schema lint on Cargo.toml 2026-08-26 16:34:46 +02:00
docs Add spreadsheet.avif 2026-02-28 17:57:05 +01:00
lib Decode 4-byte UID Lazo cards, add card type 0D375F and a journey summary slot in Java lib 2026-10-04 14:50:15 +02:00
.gitignore Add Java/Kotlin lib 2026-08-27 10:57:56 +02:00
LEGAL.md Add LEGAL.md and disclaimer 2026-02-28 17:57:07 +01:00
LICENSE Add balance operations 2026-02-28 17:57:05 +01:00
README.md Document 4-byte UID Lazo cards, card type 0D375F and the Lazo access bytes 2026-10-04 14:50:15 +02:00

Zaragoza & Aragon Transport Card Spec

NPM Version JSR Version Crates.io Version Maven Central Version

See also: Zaragoza Tarjeta Bus Android App

Important

⚠️ Aviso legal: Este repositorio es un proyecto de investigación de seguridad independiente.
No está destinado a cometer fraude ni a facilitar el uso indebido del transporte público.
No se proporcionará asistencia para ningún uso ilícito.
Consulta LEGAL.md para el descargo de responsabilidad completo.

Zaragoza and Aragon Avanza/Lazo bus & tram public transport card full up-to-date specification, reverse engineered from multiple MIFARE Classic card dumps. This project intends to be a security research paper, publicly available for anyone for free and is not qualified as a guide. There are no step-by-step instructions on how to commit fraud.

Getting started

Zaragoza and Aragon public transport uses Avanza and Lazo cards, both of which are MIFARE Classic, meaning they're vulnerable to the Crypto-1 attack. The keys can be dumped with a Proxmark, but since they're static and well-known, you can use the ones listed for each card below.

Every sector has two keys (Key A and Key B) that control access to its blocks, and the last block of every sector is its trailer, holding both keys and the access conditions.

Avanza Tarjeta Bus Lazo card
Chip MIFARE Classic 1K MIFARE Classic 4K
SAK 88 18
UID 4 bytes 4 or 7 bytes
Sectors 16 40 (sectors 0-31 with 4 blocks, 32-39 with 16 blocks)
Blocks 64 256

A 4-byte UID is followed by a BCC, the XOR of its four bytes; a 7-byte UID has none, so its SAK and ATQA sit two bytes later. Check [07] first: a 7-byte UID can hold a SAK value in [05] by chance.

What is stored inside the blocks is the same on both cards and is documented once under data structures. Block 1 tells the products apart, see card type. Everything that is still unresolved is collected under unconfirmed.

Avanza Tarjeta Bus

Avanza keys

Sectors Key A Key B
0-8 04000C0F0903 0B02070A0409
9-15 (unused) on top up cards A0A1A2A3A4A5 B0B1B2B3B4B5
9-15 (unused) on personal cards 04000C0F0903 0B02070A0409

On top up cards Key B can rewrite the keys and access conditions of every sector (trailer access bits 011); on personal cards the trailers are locked (110), so no key can change a sector key or an access bit again. The data blocks keep the same access conditions on both, so the lock does not freeze the card's contents.

Sectors 9-15 of a top up card are not in factory condition: they carry the NXP sample keys with access bytes 7F0788 and user byte 00, not FFFFFFFFFFFF / FF0780 / 69.

Each trailer's three access bytes are fixed, and differ per sector and card kind:

Sectors Access bytes (top up) Access bytes (personal)
0 2C378D 24BF0D
1 7E1788 769F08
2 4C378B 44BF0B
3-6 787788 70FF08
7 7F0788 778F08
8 3B478C 33CF0C
9-15 7F0788 778F08

The user byte is 00 on every Avanza trailer.

Avanza blocks

Sector Block Description Template Access Conditions
0 0 [00-03] RFID's UID
[04] BCC, the XOR of the four UID bytes
[05] SAK (88 for MIFARE Classic 1K)
[06-07] ATQA (0400)
[11] 20 and [15] the last two digits of the manufacturing year: read together they spell it in BCD, 20 25 = 2025
..,,..,,..880400C8,,0020000000.. Read-only
1 Card type ..,,..000000000000000000000000,, Only Key B can write
2 Card ID 42,,..,,..00000000000000000000.. Read-only
3 0th sector's trailer block 04000C0F0903..,,..,,0B02070A0409 Trailer
1 4 Empty on top up cards, see block 4
[15] XOR of all previous bytes
00000000000000000000000000000000 Only Key B can write
5 Latest transaction log entry ..,,..,,..,,..,,..,,..,,..,,..,, No restrictions
6 Always empty 00000000000000000000000000000000 No restrictions
7 1st sector's trailer block 04000C0F0903..,,..,,0B02070A0409 Trailer
2 8 Balance ..,,0000..,,FFFF..,,000002FD02FD Value block
9 Always has the same value as block 8 ..,,0000..,,FFFF..,,000002FD02FD Value block
10 Journey summary, empty on a new card
Not maintained on personal cards: all zero, or 000000000000000A000000000000000A
..,,..,,..,,..,,..,,..0000..00.. No restrictions
11 2nd sector's trailer block 04000C0F0903..,,..,,0B02070A0409 Trailer
3 12 Subscription metadata 00000000000000000000000000000000 Only Key B can write
13 Subscription on personal cards 00000000000000000000000000000000 Only Key B can write
14 Mirror of block 13, not always written; do not rely on the copy 00000000000000000000000000000000 Only Key B can write
15 3rd sector's trailer block 04000C0F0903..,,..,,0B02070A0409 Trailer
4 16 Subscription metadata of a second product 00000000000000000000000000000000 Only Key B can write
17 Subscription of the second product 00000000000000000000000000000000 Only Key B can write
18 Mirror of block 17, see block 14 00000000000000000000000000000000 Only Key B can write
19 4th sector's trailer block 04000C0F0903..,,..,,0B02070A0409 Trailer
5 20 Empty 00000000000000000000000000000000 Only Key B can write
21 Empty 00000000000000000000000000000000 Only Key B can write
22 Empty 00000000000000000000000000000000 Only Key B can write
23 5th sector's trailer blocks 04000C0F0903..,,..,,0B02070A0409 Trailer
6 24 Empty on most cards, see block 24 00000000000000000000000000000000 Only Key B can write
25 Empty 00000000000000000000000000000000 Only Key B can write
26 Empty 00000000000000000000000000000000 Only Key B can write
27 6th sector's trailer block 04000C0F0903..,,..,,0B02070A0409 Trailer
7 28 Transaction logs; value of block 5 right before overwriting it, archived here when its sequence counter is 0 and in blocks 29, 30, 32 and 33 for counters 1 to 4 ..,,..,,..,,..,,..,,..,,..,,..,, No restrictions
29 See block 28 ..,,..,,..,,..,,..,,..,,..,,..,, No restrictions
30 See block 28 ..,,..,,..,,..,,..,,..,,..,,..,, No restrictions
31 7th sector's trailer block 04000C0F0903..,,..,,0B02070A0409 Trailer
8 32 See block 28 ..,,..,,..,,..,,..,,..,,..,,..,, No restrictions
33 See block 28 ..,,..,,..,,..,,..,,..,,..,,..,, No restrictions
34 Expiration date, a guess; encoding unknown. Usually an empty value block, see below 00000000FFFFFFFF0000000000FF00FF Value block
35 8th sector's trailer block 04000C0F0903..,,..,,0B02070A0409 Trailer

Blocks 8, 9 and 34 are value blocks: a 32-bit integer, its bitwise complement, then the integer again, with value block access conditions (Key A can read, decrement, restore and transfer, Key B can also write and increment).

Block 34 usually holds 00000000FFFFFFFF0000000000FF00FF, an empty value block with the address bytes 00FF00FF. A personal card can instead hold a value with the address bytes 22DD22DD, such as 37810005C87EFFFA3781000522DD22DD, where bytes [00-01] 3781 read as a date give 2027-12-01. The field is unidentified and the expiration date is a guess from that reading.

The chip write-protects block 0; its access bits say 110, the value block condition, rather than the 010 of a read-only block. That shows only on a magic card, where block 0 is writable.

Lazo card

Lazo keys

Sectors Blocks Key A Key B
0-31 0-127 4E303D402F20 243372407C2E
32 128-143 216F5B212A7A 44202E476E5B
33 144-159 5148755C3427 3C4520753758
34 160-175 Unknown 206F7C4C4F36
35 176-191 5246612E7C4B Unknown
36 192-207 354B39454861 567D734C403C
37 208-223 455D732C385F 2426217B3B3B
38-39 (unused) 224-255 FFFFFFFFFFFF FFFFFFFFFFFF

Each trailer's three access bytes are fixed per sector:

Sectors Access bytes
0 7B4788
1-2, 7-9 7F0788
3-6 7E1788
10, 15 787788
11-14 0F00FF
16-31 08778F
32-37 787788
38-39 FF0780

The user byte is 69 on sectors 0-31 and 38-39, 00 on 32-37. Key B can rewrite every sector's keys and access conditions (trailer access bits 011) except 38 and 39, which still have the factory default keys and access conditions.

Lazo blocks

Sector Block Description Template Access Conditions
0 0 [00-06] RFID's UID (7 bytes, so no BCC)
[07] SAK (18 for MIFARE Classic 4K)
[08-09] ATQA (0200)
[10-15] manufacturer data; [15] is most likely the last two digits of the manufacturing year
4-byte UID: [00-03] UID, [04] BCC, [05] SAK, [06-07] ATQA
..,,..,,..,,..1802008100000023.. Read-only
1 Card type ..,,..000000000000000000000000,, No restrictions
2 Card ID 4354..,,..00000000000000000000.. Only Key B can write
3 0th sector's trailer block 4E303D402F20..,,..,,243372407C2E Trailer
1 4 Empty on most cards, see block 4 00000000000000000000000000000000 No restrictions
5 Latest transaction log entry 0D..,,..,,..,,..,,..,,..,,..,,.. No restrictions
6 Always empty 00000000000000000000000000000000 No restrictions
7 1st sector's trailer block 4E303D402F20..,,..,,243372407C2E Trailer
2 8 Balance ..,,0000..,,FFFF..,,000002FD02FD No restrictions
9 Always has the same value as block 8 ..,,0000..,,FFFF..,,000002FD02FD No restrictions
10 Journey summary, empty on a new card ..,,..,,..,,..0D..,,..0000..00.. No restrictions
11 2nd sector's trailer block 4E303D402F20..,,..,,243372407C2E Trailer
3-6 12, 16, 20, 24 Empty on top up cards 00000000000000000000000000000000 Only Key B can write
13-14, 17-18, 21-22, 25-26 Empty on top up cards 00000000000000000000000000000000 No restrictions
15, 19, 23, 27 Trailer blocks of sectors 3-6 4E303D402F20..,,..,,243372407C2E Trailer
7 28-30 Transaction log archive slots for sequence counters 0, 1 and 2 0D..,,..,,..,,..,,..,,..,,..,,.. No restrictions
31 7th sector's trailer block 4E303D402F20..,,..,,243372407C2E Trailer
8 32-33 Transaction log archive slots for sequence counters 3 and 4 0D..,,..,,..,,..,,..,,..,,..,,.. No restrictions
34 Always empty 00000000000000000000000000000000 No restrictions
35 8th sector's trailer block 4E303D402F20..,,..,,243372407C2E Trailer
9 36-38 Empty 00000000000000000000000000000000 No restrictions
10, 15 40-42, 60-62 Empty 00000000000000000000000000000000 Only Key B can write
11-14 data blocks 44-46, 48-50, 52-54, 56-58 Empty 00000000000000000000000000000000 Only Key B can read and write
16-31 data blocks 64-126 Empty 00000000000000000000000000000000 Value block
32-37 data blocks 128-222 Empty 00000000000000000000000000000000 Only Key B can write
38-39 data blocks 224-254 Empty, never personalized 00000000000000000000000000000000 No restrictions

The trailer blocks of sectors 9-39 (39, 43, ..., 127, then 143, 159, 175, 191, 207, 223, 239 and 255) hold the keys from the table above.

Blocks 8 and 9 use the value block format (integer, complement, integer) but their sector is unrestricted, so both keys can write them directly. Only the empty sectors 16-31 carry real value block access conditions.

The chip write-protects block 0 here too, though its access bits say 000, no restriction at all.

Data structures

These are the same on both cards. Where a field differs on personal cards, the field list says so.

Date

Two bytes, read as 16 bits:

  • 7 bits: year, 2000-based
  • 4 bits: month (1-12)
  • 5 bits: day (1-31)

34 4E = 0011010 0010 01110 = 2026-02-14.

See implementation for JS, Rust, Java.

Card ID

  • [00-01] ASCII prefix: BE on Avanza top up cards, BP on Avanza personal cards, CT on Lazo cards
  • [02-04] Card number, one decimal digit per nibble
  • [05-14] zero
  • [15] XOR of all previous bytes

The prefix identifies a personal Avanza card without reading block 1.

The id is the number printed on the card.

See implementation for JS, Rust, Java.

Card type

Block 1 says which product a card is:

  • [00] Kind of card: 02 and 0D on the top up cards, 0A on the personal ones. Whether it is a field of its own is unconfirmed, so read the three bytes as one card type
  • [01-02] The rest of the card type, opaque
  • [03-14] zero
  • [15] XOR of all previous bytes
Card type Chip Block 1 value
Balance top-up Avanza card MIFARE Classic 1K 02699F000000000000000000000000
Personal Avanza card MIFARE Classic 1K 0A9775000000000000000000000000
Personal Avanza card, second profile MIFARE Classic 1K 0A98DA000000000000000000000000
Balance top-up Lazo card MIFARE Classic 4K 0D371F000000000000000000000000
Balance top-up Lazo card, second value MIFARE Classic 4K 0D375F000000000000000000000000

Block 1 identifies the product, not the card. What separates the two personal values is unknown, and so is what separates the two Lazo values; cards carrying 0A98DA are printed "Abono de transporte".

Top up cards pay for each journey out of a balance; personal cards use a subscription and never change balance.

See implementation for JS, Rust, Java.

Balance

Blocks 8 and 9, identical, in the MIFARE value block format. €1.00 = 1000 units.

  • [00-03] units, little-endian
  • [04-07] bitwise complement of [00-03]
  • [08-11] [00-03] again
  • [12-15] address bytes, always 02FD02FD

€5.00 is 8813000077ECFFFF8813000002FD02FD. Personal cards always hold zero: 00000000FFFFFFFF0000000002FD02FD.

See implementation for JS, Rust, Java.

Transaction log

Six 16-byte records: block 5 holds the newest, blocks 28, 29, 30, 32 and 33 the five before it. When a new transaction is written, the previous block 5 is copied into the archive slot selected by its sequence byte [15] (0 to block 28, 1 to 29, 2 to 30, 3 to 32, 4 to 33), so the counter wraps every five transactions. Block 34 is never part of the ring.

  • [00] The product that paid: the subscription metadata product id on a personal card, the first byte of the card type on a top up card, which has no products
  • [01] 00 on top up cards; 01 or 02 on personal cards, matching bit 15 of [05-06]
  • [02-03] Amount, big-endian, in balance units; 0000 when free of charge, always 0000 on personal cards
  • [04] Consecutive payments of this card at one terminal, counting from 1; 00 on top ups and on a check-out
  • [05-06] Stop. Bit 15 set: an urban bus stop, the other 15 bits an internal stop id. Bit 15 clear: the tram, where the value is the stop number × 100 (0514 = 1300 = Plaza España), or another operator
  • [07] Route id of the operator's GTFS feed: numbered buses have their number, Ci1 to Ci4 are 11 to 14, N1 to N7 are 111 to 117, the tram is 210
  • [08] 01 or 02: a journey and its direction; 08: a top up
  • [09] See byte 09
  • [10-11] Date
  • [12] Hour, [13] minute, [14] second, plain binary
  • [15] Sequence counter, 0 to 4

A journey subtracts the amount from the balance, a top up adds it. On a top up card a journey with amount 0000 is a free transfer; on a personal card every journey carries 0000 and none is a transfer. Byte [01] tells them apart: 00 on a top up card, non-zero on a personal one. The operator grants one per paid ride, to a different line, within 60 minutes for the urban Tarjeta BUS and 75 when a CTAZ card enters Zaragoza; bus and tram count as one network. A top up carries the point of sale id in [05-06] and zeros in [04], [07] and [09], unless it was made on board, where it carries the line and stop like a journey. Top ups do not touch block 10, and the balance a new card is sold with leaves no record.

Byte [08] is the operator's GTFS direction_id plus one, so it picks one of the two headsigns the feed gives that route: 01 is direction_id 0 and 02 is direction_id 1. Ci1 and Ci2 carry 02 as well as 01, so either the ring routes are not one-way in the feed or the 11 to 14 mapping needs revisiting. The tram directions are still unconfirmed, probably 01 runs south to Mago de Oz and 02 north to Avenida de la Academia.

See implementation for JS, Rust, Java.

Journey summary (block 10)

Rewritten on every journey, untouched by top ups, so it stays all zero on a card that has only ever been topped up. Personal cards do not maintain it: it stays all zero, or holds a product id in [07] with every other byte zero. The layout below is the top up card one.

  • [00-01] Line and direction of the previous journey, 0000 on the first; consecutive payments at one terminal count as one journey
  • [02-03] Date and [04-05] hour and minute of the last journey that was charged; on a free transfer these still point at the paid ride it belongs to
  • [06] Consecutive payments counter of the current journey, the same as byte [04] of block 5
  • [07] The same product id as byte [00] of a transaction
  • [08] 01 when the current journey was free, 00 when paid
  • [09] Line of the current journey
  • [10] Direction of the current journey, except on 0D375F, see block 10 on a 0D375F card
  • [11-12] 0000
  • [13] 63 after a paid journey, 62 after a free transfer, except on 0D375F
  • [14] 00
  • [15] XOR of all previous bytes

A free transfer onto the tram 33 minutes after a paid ride on bus 31, block 5 above block 10:

    pi ?1 amt  cp stop ln dr ?9 date HH MM SS sq
 5: 0D 00 0000 01 05DC D2 02 01 3518 16 2C 20 00
10: 1F 01 3518 16 0B 01 0D 01 D2 02 0000 62 00 91
    pl pd date HH MM cp pi fr ln dr 0000 pm 00 xr
    pi = product id          cp = consecutive payments     fr = free flag
   amt = amount              ln dr = line, direction       pm = 63 paid / 62 free
  stop = stop id             pl pd = previous line, direction
  date = date                HH MM SS = time               sq = sequence
    ?1 = byte 01             ?9 = byte 09                  xr = XOR

See implementation for JS, Rust, Java.

Subscription metadata

Blocks 12 and 16 on personal cards, one product per sector. Still work in progress.

  • [00] Product id, the code a transaction carries in byte [00] and block 4 lists. It is not a duration: one id covers passes of different lengths
  • [01] Unknown, appears to always be 01
  • [02-03] Purchase date
  • [04-07] Unknown, appears to always be 00210000
  • [08-09] Validity in days
  • [10-14] Unknown, appears to always be 0021000000
  • [15] XOR of all previous bytes

See implementation for JS, Rust, Java.

Subscription

Block 13 for the product of block 12, block 17 for the product of block 16. Blocks 14 and 18 mirror them, but not always, so read 13 and 17 and ignore the mirror. Still work in progress.

  • [00-01] Start date
  • [02-03] End date, the start date plus the metadata's validity in days minus one. The pass stays valid through the whole of that day
  • [04-05] Unknown, appears to always be 0000
  • [06-09] Unknown, and zero except on a year long pass
  • [10-11] Date of the last usage, [12] hour, [13] minute, [14] second. Only a journey whose stop has bit 15 clear moves it, the tram and other operators, so a pass used on urban buses alone keeps it all zero
  • [15] XOR of all previous bytes

The metadata purchase date and the start date are independent: a pass can be bought days before it starts, and two passes on one card need not cover consecutive days.

See implementation for JS, Rust, Java.

Unconfirmed

Theories and open questions.

Byte 09 of a transaction

On buses it's most likely which trip of the vehicle's daily duty this is, counting from 1. Avanza's GTFS names every trip <service>__<N>, where N is the trip's chronological position within one vehicle's day, and the byte matches the N of the trip in progress. It never exceeds the longest duty on its route, which a count of the day's trips would.

It's incremented through the day, stays same across consecutive payments and across an on-board top up and the ride paid in the same second, repeats at the same hour on different days, and differs widely between routes at one hour. It is neither the stop's position along the route nor the passenger's order at the stop.

On the tram it is something else: it increments by one per validation on an Avanza balance card and is 01 on a Lazo card. Off the tram a Lazo card's value varies by route; route 150 always writes 01. On a personal Avanza card off the Avanza network it's 00.

Route ids 150, 152 and 251

None is in Avanza's feed, whose ids stop at 210, and their stop ids are in neither the urban nor the tram space. 152 and 251 appear only on personal cards. 150 charges 810 units where the urban fare is 550, at stops 19, 58, 64 and 3016. All three are unidentified.

Cercanias

Cercanías is the Renfe commuter rail that runs under the city. It charges its own fare, and a journey is a check-in and a check-out where the second validation costs nothing.

Byte [04] is 00. Byte [06] looks like the station's position along the line, counting from the far terminal stop. Byte [05] is 00 on the check-in and 23 on the check-out.

The only known Cercanías route id is 169.

Stop ids

The urban stop id in [05-06] looks like one network-wide location space rather than one scoped to the route: the ids of a single route span a range far wider than its stop count, and one id can appear on two routes. Within one route the id means a location rather than a platform, the same id appearing in both directions. No public identifier has been found. Neither the PA number on the stop nor the GTFS stop_id, nor the rank of the stop in any ordering of the pole data, nor a position along the route.

Line 05-06 bytes 09 byte Pole Name
22 81AF 12 676 P. María Agustín 37
35 804C 11 471 Fueros de Aragón 15
31 81DB 0B 3071 Av. de Madrid / Aljafería
35 8099 09 707 Plaza Aragón 1
31 807E 08 147 Av. Francisco de Goya 83
22 81C8 0D 434 Duquesa Villahermosa 3
30 802E 0F 430 Doctor Iranzo N.º 61
40 805D 25 633 P. de la Constitución 16
Ci4 808F 0C 3030 Av. de San José 7
22 81B8 03
22 819E 04
22 80CF 06
30 805D 05
30 806F 0A
Ci3 80AE 03
Ci3 81DB 09
44 80BE 13

The values 1, 3, 4 and 5 appear on unrelated routes and may be placeholders. A70F is 9999 with the urban bit set, a sentinel a validator writes when it has no stop configured.

Block 10 on a 0D375F card

Bytes [00-09] and the XOR follow the documented layout, but [10] holds 3D where the direction belongs and [13] holds 01 where 63 or 62 belongs. Either [13] carries the direction on this product, or [10] packs it in its low two bits, as 3D ends in 01. A journey in direction 02 would tell them apart.

Block 24

Empty on most top up cards. On the others it's 0200, something shaped like a date at [02-03] repeated at [10-11], zeros, 01 at [14] and an XOR of all previous bytes at [15].

Block 4

Empty on Avanza top up cards, so a non-zero block 4 marks a personal Avanza card. A Lazo top up card can hold 10 in [12], meaning unknown. On a personal card it is a list of fixed 3-byte records packed from byte 0, one per product: [product id][unknown byte][sector]. A single product gives 061203, two give 060003 then 0A1204. The middle byte is 12 on a product still valid and 00 on an expired one, which is a guess: only those two values are known.

Subscription blocks

Personal cards have one product per sector in sectors 3 and 4, each with its own metadata and subscription blocks. The last usage field does not move on every journey, see subscription.

Notes

  • A top up can carry 21 in the sequence byte instead of 0 to 4. It goes into block 33 and does not advance the counter
  • Before a ring has wrapped, an unused archive slot can hold 00000000000000000000000000000004 instead of all zeroes
  • A personal card has four product slots: sectors 3, 4, 5 and 6 carry the same access bytes 70FF08 and operator keys, with blocks 12, 16, 20 and 24 as their heads. Only sectors 3 and 4 are ever occupied
  • Blocks 20, 21, 22, 25 and 26 are always zero, as is every data block of sectors 9 to 15

Implementations

JavaScript, TypeScript

lib/javascript is the spec implementation library in JavaScript/TypeScript. Zero dependencies, compatible with Node.js >= 25, Bun, Deno and 2025 browsers, MIT License. The package is published on npm as zgz-transport and on JSR as @hloth/zgz-transport.

Start from src/index.ts.

Rust

lib/rust is the spec implementation library in Rust. Zero dependencies, no_std with alloc, MIT License. The crate is published on crates.io as zgz-transport.

Start from src/lib.rs.

Java, Kotlin

lib/java is the spec implementation library in Java. Zero dependencies, usable from Java >= 17, Kotlin and Android API >= 26, MIT License. The library is published on Maven Central and on self-hosted Maven repository at git.hloth.dev as dev.hloth:zgz-transport.

Start from src/main/java/dev/hloth/zgztransport/Card.java.

Contributing

If you'd like to contribute to the project's development, consider the following resources:

The spreadsheet with publicly disclosed dumps and highlights:

Spreadsheet

Acknowledgements

Huge thanks to li0ard for help with decoding RFIDs and dates!

License

MIT

Donate

hloth.dev/donate