Skip to content

NFC Binding

sim-hw generates an NFC payload on every startup, simulating the NFC tag that would be embedded in a physical Inklet device. This payload enables tap-to-pair device binding.

Payload Format

inklet:1:{hw_id}:{signature}
Component Description
inklet Protocol identifier
1 Version number
{hw_id} Device hardware UUID (e.g., a1b2c3d4-5678-9012-abcd-ef0123456789)
{signature} First 16 hex characters of HMAC-SHA256(hw_id, FACTORY_SECRET)

Example:

inklet:1:a1b2c3d4-5678-9012-abcd-ef0123456789:3f7a8b2c1d9e0f4a

Signature Generation

The signature is computed using HMAC-SHA256 with the factory secret as the key and the hardware UUID as the message. Only the first 16 hex characters (8 bytes) of the full HMAC digest are used.

import hashlib
import hmac


def generate_nfc_signature(hw_id: str, factory_secret: str) -> str:
    """Generate the NFC signature for a device hardware ID.

    Args:
        hw_id: Device hardware UUID string.
        factory_secret: Hex-encoded factory secret (32 bytes = 64 hex chars).

    Returns:
        First 16 hex characters of HMAC-SHA256(hw_id, factory_secret).
    """
    secret_bytes = bytes.fromhex(factory_secret)
    mac = hmac.new(secret_bytes, hw_id.encode("utf-8"), hashlib.sha256)
    return mac.hexdigest()[:16]

Usage:

hw_id = "a1b2c3d4-5678-9012-abcd-ef0123456789"
secret = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"

signature = generate_nfc_signature(hw_id, secret)
payload = f"inklet:1:{hw_id}:{signature}"
print(payload)
# inklet:1:a1b2c3d4-5678-9012-abcd-ef0123456789:3f7a8b2c1d9e0f4a

Why Truncate to 16 Characters?

NFC tags have limited storage capacity. Truncating the HMAC to 8 bytes (16 hex characters) provides a reasonable security margin while fitting within NFC payload size constraints. The 64-bit signature space makes brute-force attacks impractical for the device binding use case.

Payload File

sim-hw writes the NFC payload to {data-dir}/nfc-payload on every startup. You can read this file to simulate scanning the device's NFC tag.

cat devices/kitchen/nfc-payload
# inklet:1:a1b2c3d4-5678-9012-abcd-ef0123456789:3f7a8b2c1d9e0f4a

Binding Flow

The full NFC binding flow works as follows:

Physical Device                        sim-hw Equivalent
────────────────                       ─────────────────
NFC tag on device          →           nfc-payload file in data-dir
User taps phone to device  →           User copies payload from file
App reads NFC payload      →           User pastes payload in sim-dashboard
App extracts hwId + sig    →           sim-dashboard parses the payload
App calls bind/nfc API     →           sim-dashboard calls bind/nfc API
Backend verifies HMAC      →           Backend verifies HMAC (same)
Device is bound to user    →           Device is bound to user (same)

Step by step:

  1. App reads NFC --- In the real world, the mobile app reads the NFC tag. In the simulator, you copy the content of the nfc-payload file.

  2. App calls the API --- The app (or sim-dashboard) sends a POST /api/devices/bind/nfc request with the extracted hwId and signature:

    curl -X POST https://auth.iminklet.com/api/devices/bind/nfc \
      -H "Authorization: Bearer {accessToken}" \
      -H "Content-Type: application/json" \
      -d '{"hwId": "a1b2c3d4-5678-9012-abcd-ef0123456789", "signature": "3f7a8b2c1d9e0f4a"}'
    
  3. Backend verifies --- The backend recomputes HMAC-SHA256(hwId, FACTORY_SECRET) and compares the first 16 hex characters with the provided signature. If they match, the device is bound to the authenticated user.

  4. Device receives notification --- The backend publishes a bound command to the device's MQTT topic. sim-hw receives this and displays a binding confirmation.

Security Considerations

Factory Secret

The FACTORY_SECRET must be identical between sim-hw and the backend. In production, this secret is burned into devices at the factory and configured in the backend's environment. Never expose the factory secret in client-side code or logs.

  • The HMAC signature prevents unauthorized binding --- you cannot bind a device without knowing the factory secret.
  • Each device has a unique hardware UUID, so signatures are device-specific.
  • The signature is deterministic: the same hw_id and FACTORY_SECRET always produce the same signature. This is intentional --- the NFC tag content never changes.

Testing NFC Binding

To test the NFC binding flow with sim-hw:

  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 the sim-dashboard, click "Bind Device" and paste the NFC payload string.

  4. Alternatively, call the API directly:

    curl -X POST http://localhost:4000/api/devices/bind/nfc \
      -H "Authorization: Bearer {your-token}" \
      -H "Content-Type: application/json" \
      -d '{"hwId": "...", "signature": "..."}'
    
  5. The device should display "Device bound successfully" and appear in your dashboard.