NFC Binding¶
sim-hw generates an NFC payload on every startup, simulating the NFC tag embedded in a physical Inklet device. This payload enables tap-to-pair device binding.
NFC v2 Payload Format¶
| Parameter | Description |
|---|---|
v |
Protocol version, always 2 |
kid |
Factory master key ID |
hw |
Device hardware UUID (32 hex characters, no hyphens) |
cv |
Credential version number (incremented on revoke-and-reissue) |
p |
Base64url-encoded 128-bit truncated HMAC proof (16 bytes) |
Example:
Key Derivation and Proof¶
NFC v2 uses a per-device key scheme:
deviceKey = HKDF-SHA256(masterKey[kid], salt=hwId, info="inklet:nfc-bind:v2")
proof = Truncate128(HMAC-SHA256(deviceKey, "v={v}&kid={kid}&hw={hw}&cv={cv}"))
- The masterKey lives in the backend's Secret Manager and is never persisted, logged, or sent to any client.
- Each device's key is derived from the masterKey and that device's hwId, so a leaked device key compromises exactly one device.
- The proof binds the payload's version, keyId, hwId, and credential version together — changing any field invalidates it.
v1 format is retired
The old inklet:1:{hwId}:{signature} format (HMAC truncated to 16 hex chars under a single shared FACTORY_SECRET) is no longer accepted by the backend. sim-hw now generates NFC v2 URI payloads.
Payload File¶
sim-hw writes the NFC payload to {data-dir}/nfc-payload on startup. Read this file to simulate scanning the device's NFC tag.
cat devices/kitchen/nfc-payload
# inklet://bind?v=2&kid=k1&hw=a1b2c3d4e5f60708090a0b0c0d0e0f10&cv=1&p=AAAAAAAAAAAAAAAA
Binding Flow¶
Physical device sim-hw equivalent
──────────────── ─────────────────
NFC tag on device → nfc-payload file in data dir
User taps phone to device → User copies URI payload from file
App reads NFC tag → User pastes payload in sim-dashboard
App parses URI parameters → sim-dashboard parses URI
App calls bind/nfc API → sim-dashboard calls bind/nfc API
Backend verifies proof → Backend verifies proof (same)
Device bound to user → Device bound to user (same)
Step-by-step:
-
App reads NFC — In production, the mobile app reads the
inklet://bind?...URI from the NFC tag. In the simulator, copy the contents ofnfc-payload. -
App calls API — The app (or sim-dashboard) parses the URI and posts to
POST /api/devices/bind/nfc: -
Backend verifies — The backend derives the deviceKey for this device using HKDF-SHA256, recomputes the proof, and compares in constant time. On match, the device is bound to the authenticated user and
{"device": {...}, "status": "bound"}is returned. -
Device receives notification — The backend publishes a
boundcommand to the device's MQTT topic. sim-hw renders a binding confirmation on the display.
Security Notes¶
Key management
The masterKey is managed by the factory console via KMS/Secret Manager and must never appear in any log, response, or client-side code. In local development, sim-hw uses the NFC_MASTER_KEYS environment variable to configure test keys.
- Per-device keys: each device's proof is device-specific; a leaked key compromises exactly one device.
- Payload integrity: the proof covers
v,kid,hw, andcv— tampering with any field immediately fails verification. - Replay prevention: the factory increments
cvon revoke-and-reissue, invalidating all prior proofs for that device. - 128-bit proof: leaves no practical margin for online guessing while fitting comfortably in an NDEF URI record.
Testing NFC Binding¶
-
Start a simulated device:
-
Read the NFC payload:
-
In sim-dashboard, click Bind Device, select the NFC Payload tab, and paste the payload string.
-
Or parse the URI and call the API directly:
-
The device should show "Device bound successfully" and appear in your dashboard.