Skip to main content

2. Card Binary Schema

The NFC card payload is a 496-byte binary structure stored on an NTAG215 chip. It is divided into three physical regions: Buffer A, Buffer B (A/B shadow pair), and a shared Trailer / Meta block.

Upstream sources: System Design §5 Data Layout, Tech Specs §3 Card Storage Model.


Memory map

RegionByte rangeSizeDescription
Buffer A0–215216 BActive or shadow card state (determined by activePtr)
Buffer B216–431216 BActive or shadow card state (the other buffer)
Trailer / Meta432–49564 BCryptographic anchors, key version, buffer pointer

activePtr = 0 means Buffer A is the current authoritative state. activePtr = 1 means Buffer B is current.


Buffer layout (216 bytes per buffer)

Each buffer contains the following blocks in fixed order:

BlockOffsetSizeDescription
Header / Identifier016 BMagic bytes, schema version, card ID
Identity1648 BCardholder name, user ID, status
Wallet + Runtime6424 BBalance, counter, session state
Session8816 BSession open/close timestamps, terminal ID
Log region104112 BRing buffer of 7 transaction log entries × 16 B each

Header / Identifier Block (16 bytes)

FieldOffsetSizeTypeDescriptionConstraints
magic04 BbytesFixed payload identifierMust equal 0x4B4F5057 ("KOPW"); reject card if mismatch
version41 Buint8Card layout schema versionCurrent: 1; reject if unsupported version
type51 Buint8Payload product classCurrent: 0x01 (cooperative wallet)
cardId66 BbytesUnique card identifier set at issuanceImmutable after issuance; used as backend join key
reserved124 BReserved for future useMust be zeroed on write; ignored on read

Identity Block (48 bytes)

FieldOffsetSizeTypeDescriptionConstraints
name1632 BUTF-8Cardholder display name, null-paddedMax 31 meaningful bytes + null terminator
userId484 Buint32Backend user account IDMust match a registered user; set at issuance
gender521 Buint8Gender code: 0 = unspecified, 1 = male, 2 = femaleApplication-defined; not used in financial logic
status531 Buint8Card health status codeSee status codes table below
createdAt544 Buint32Issuance timestamp (UTC seconds)Immutable after issuance
reserved586 BReservedMust be zeroed on write; ignored on read

Status codes

ValueNameDescription
0ACTIVENormal operation
1BLOCKED_TAMPERCryptographic or chain integrity failure
2BLOCKED_FRAUDSuspicious behaviour detected
3BLOCKED_EXPIREDCard past its expiresAt date
4BLOCKED_ADMINManually decommissioned by operator

Full transition rules: Tech Specs §15 Status Codes & Block Rules.

Wallet + Runtime Block (24 bytes)

FieldOffsetSizeTypeDescriptionConstraints
balance644 Buint32Current balance in smallest currency unit (integer IDR)Max 4,000,000,000; effective ceiling is Rp 16,000,000 by policy
lastBalance684 Buint32Balance before most recent transactionUsed for rollback detection; must equal previous balance
counter728 Buint64Monotonically increasing write counterNever decremented; starts at 0 at issuance; anti-replay key
lastTimestamp804 Buint32Timestamp of most recent write (UTC seconds)Must not be earlier than previous lastTimestamp
state841 Buint8Card lifecycle / session stateSee session state codes below
flags853 BbitsFeature and operational flagsSee flags layout below

Session state codes

ValueNameDescription
0IDLENo active session; card is at rest
1CHECKED_INActive session opened by a gate check-in
2CHECKED_OUTSession closed by a gate check-out

Full state machine and transition rules: System Design §4 Card State Machine, Tech Specs §6 State Machine & Session Rules.

Flags layout (3 bytes / 24 bits)

BitsNameDescription
0offlineSession1 = most recent write occurred while terminal was offline
1pendingReconcile1 = one or more log entries not yet reconciled with backend
23:2reservedMust be zeroed on write; ignored on read

Session Block (16 bytes)

FieldOffsetSizeTypeDescriptionConstraints
startTime884 Buint32Session open timestamp (UTC seconds)Set on CHECKED_IN; used as chain initialisation anchor
endTime924 Buint32Session close timestamp (UTC seconds)Zero while session is open; set on CHECKED_OUT
terminalId962 Buint16ID of terminal that opened the sessionBackend-assigned terminal identifier
reserved986 BReservedMust be zeroed on write; ignored on read

Log Region (112 bytes — 7 entries × 16 bytes)

Fixed-capacity ring buffer. When full, the oldest entry is overwritten. Current write position is tracked implicitly via rootHash in the trailer.

Full log entry definition, chain formula, and integrity rules: Tech Specs §14 Transaction Log Format.

Log entry (16 bytes):

FieldOffset (within entry)SizeTypeDescriptionConstraints
deltaTime02 Buint16Seconds since session.startTimeWraps at 65,535 s (~18 h); stale session if exceeded
amount23 Buint24Transaction amount (integer IDR)0 for state-only transitions (check-in/out)
balanceAfter54 Buint32Balance after this transactionMust be consistent with prior balance and amount
flags/type91 Buint8Transaction type + flagsSee log flags table below
hash106 BbytesTruncated SHA-256 chain hashSHA256(deltaTime || amount || balanceAfter || flags || prevHash)[0..5]

Log flags (flags/type field)

BitsNameValues
3:0txType0x0 debit, 0x1 credit/top-up, 0x2 check-in, 0x3 check-out, 0xF admin
4offlineFlag1 = offline transaction
5suspectFlag1 = terminal flagged as suspicious
7:6reservedMust be zero on write

Trailer / Meta (64 bytes, offset 432)

The trailer is written last in every update cycle. It cryptographically binds the active buffer and is the only block read to determine which buffer is authoritative.

FieldOffsetSizeTypeDescriptionConstraints
expiresAt4324 Buint32Card expiry timestamp (UTC seconds)Card is BLOCKED_EXPIRED if current time > expiresAt
keyVersion4361 Buint8Key set version used to encrypt/authenticate this cardDetermines which master key is used for HMAC derivation
rootHash4376 BbytesTruncated SHA-256 of the most recent log entry hashChain head; ties log sequence to trailer HMAC
counterBind4434 Buint32Lower 32 bits of counter included in HMAC inputAnti-replay binding in the HMAC
reserved4479 BReservedMust be zeroed on write
HMAC4568 BbytesTruncated HMAC-SHA256 over payload and trailer fieldsCovers: active buffer bytes + expiresAt + keyVersion + rootHash + counterBind
activePtr4641 Buint8Active buffer pointer: 0 = Buffer A, 1 = Buffer BFlipped only after new buffer is fully written and verified
padding46531 BZero-padding to fill 64 bytes

Size summary

RegionSize
Buffer A216 B
Buffer B216 B
Trailer / Meta64 B
Total496 B

This fits within the 504-byte usable user memory of an NTAG215 (132 pages × 4 bytes, minus lock and configuration bytes).