Skip to content

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

inklet://bind?v=2&kid=<keyId>&hw=<hwId>&cv=<credentialVersion>&p=<proof>
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:

inklet://bind?v=2&kid=k1&hw=a1b2c3d4e5f60708090a0b0c0d0e0f10&cv=1&p=AAAAAAAAAAAAAAAA

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:

  1. App reads NFC — In production, the mobile app reads the inklet://bind?... URI from the NFC tag. In the simulator, copy the contents of nfc-payload.

  2. App calls API — The app (or sim-dashboard) parses the URI and posts to POST /api/devices/bind/nfc:

    curl -X POST https://dev.iminklet.com/api/devices/bind/nfc \
      -H "Authorization: Bearer {accessToken}" \
      -H "Content-Type: application/json" \
      -d '{
        "version": 2,
        "keyId": "k1",
        "hwId": "a1b2c3d4e5f60708090a0b0c0d0e0f10",
        "credentialVersion": 1,
        "proof": "AAAAAAAAAAAAAAAA"
      }'
    
  3. 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.

  4. Device receives notification — The backend publishes a bound command 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, and cv — tampering with any field immediately fails verification.
  • Replay prevention: the factory increments cv on 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

  1. Start a simulated device:

    python -m eink_hw --data-dir devices/kitchen
    
  2. Read the NFC payload:

    cat devices/kitchen/nfc-payload
    
  3. In sim-dashboard, click Bind Device, select the NFC Payload tab, and paste the payload string.

  4. Or parse the URI and call the API directly:

    # Assuming payload: inklet://bind?v=2&kid=k1&hw=abc...&cv=1&p=xxx
    curl -X POST http://localhost:4000/api/devices/bind/nfc \
      -H "Authorization: Bearer {your-token}" \
      -H "Content-Type: application/json" \
      -d '{"version":2,"keyId":"k1","hwId":"abc...","credentialVersion":1,"proof":"xxx"}'
    
  5. The device should show "Device bound successfully" and appear in your dashboard.