Zaragoza and Aragon bus & tram transport card reverse engineered spec, encoder & decoder implementations for Avanza and Lazo public transit passes
  • Java 50%
  • Rust 31.5%
  • TypeScript 18.3%
  • JavaScript 0.2%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
2026-09-02 01:35:29 +02:00
.forgejo/workflows Add reproducibility to Java lib 2026-08-27 13:59:55 +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 Fix JavaScript lib docs 2026-08-29 16:47:04 +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 Update Android app link 2026-09-02 01:35:29 +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 7 bytes
Sectors 16 40 (sectors 0-31 with 4 blocks, 32-39 with 16 blocks)
Blocks 64 256

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). The access conditions of the data blocks are the same on both.

Avanza blocks

Sector Block Description Template Access Conditions
0 0 [00-03] RFID's UID
[04] BCC (checksum byte)
[05] SAK (88 for MIFARE Classic 1K)
[15] last two digits of the manufacturing year (20 for 2020, 25 for 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 02..,,..,,..,,..,,..,,..,,..,,.. No restrictions
6 Appears to always be 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
Always 000000000000000A000000000000000A on unlimited personal cards
..,,..,,..,,..02..,,..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 unlimited cards 00000000000000000000000000000000 Only Key B can write
14 Copy of block 13 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 Copy of block 17 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 02..,,..,,..,,..,,..,,..,,..,,.. No restrictions
29 See block 28 02..,,..,,..,,..,,..,,..,,..,,.. No restrictions
30 See block 28 02..,,..,,..,,..,,..,,..,,..,,.. No restrictions
31 7th sector's trailer block 04000C0F0903..,,..,,0B02070A0409 Trailer
8 32 See block 28 02..,,..,,..,,..,,..,,..,,..,,.. No restrictions
33 See block 28 02..,,..,,..,,..,,..,,..,,..,,.. No restrictions
34 Expiration date, encoding unknown; always 00000000FFFFFFFF0000000000FF00FF on top up cards 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).

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

Sectors 38 and 39 still have the factory default keys and access conditions (FF0780).

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
..,,..,,..,,..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 Appears to always be empty 00000000000000000000000000000000 No restrictions
5 Latest transaction log entry 0D..,,..,,..,,..,,..,,..,,..,,.. No restrictions
6 Appears to always be 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.

Data structures

These are the same on both cards. Where a field differs on personal unlimited 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 and BP on Avanza cards, CT on Lazo cards
  • [02-04] Card number, one decimal digit per nibble
  • [05-14] zero
  • [15] XOR of all previous bytes

See implementation for JS, Rust, Java.

Card type

Block 1 says which product a card is:

  • [00-02] Card type
  • [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 expiring Avanza card MIFARE Classic 1K 0A9775000000000000000000000000
Balance top-up Lazo card MIFARE Classic 4K 0D371F000000000000000000000000

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 unlimited 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] First byte of the card type
  • [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. A journey with amount 0000 is a free transfer. 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. The ring buses only ever run 01. 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. On personal unlimited cards it is always 000000000000000A000000000000000A; 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] First byte of the card type
  • [08] 01 when the current journey was free, 00 when paid
  • [09-10] Line and direction of the current journey
  • [11-12] 0000
  • [13] 63 after a paid journey, 62 after a free transfer
  • [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:

    ty ?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 ty fr ln dr 0000 tr 00 xr
    ty = card type byte      cp = consecutive payments     fr = free flag
   amt = amount              ln dr = line, direction       tr = 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] Unknown
  • [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

Blocks 13 and 14 (a copy) for the product of block 12, blocks 17 and 18 for the product of block 16. Still work in progress.

  • [00-01] Start date
  • [02-03] End date
  • [04-05] Unknown, appears to always be 0000
  • [06-09] Unknown
  • [10-11] Date of the last usage, [12] hour, [13] minute, [14] second
  • [15] XOR of all previous bytes

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. On Avanza balance cards it's incrementing by one per validation. On a Lazo card it's 01. On a personal Avanza card off the Avanza network it's 00.

Route ids 152 and 251

None of the three is in Avanza's feed, whose ids stop at 210, and their stop ids are not in the urban or the tram space.

152 and 251 appear only on personal cards and 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.

Cercanías route IDs gathered so far: 169.

Stop ids

The urban stop id in [05-06] is scoped to the route rather than shared across the network: routes with no stop in common still use the same ids. Within one route it 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

The values 1, 3, 4 and 5 appear on unrelated routes and may be placeholders.

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 top up cards. On personal cards it's 0600030A1204 followed by zeros: the product ids of blocks 12 and 16 next to their sector numbers.

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 and may only log usage on one network.

Notes

  • A top up can carry 21 in the sequence byte instead of 0 to 4
  • Before a ring has wrapped, an unused archive slot can hold 00000000000000000000000000000004 instead of all zeroes

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