Tantangan Kontrak API pada Perangkat IoT Mandiri

Mikrokontroler berbasis ESP8266, ESP32, atau RP2040 memiliki keterbatasan RAM dan komputasi kriptografi asimetris. Di sisi lain, transmisi data sensor berulang kali melalui HTTP memboroskan daya dan kuota modul seluler atau Wi-Fi. Kontrak API telemetri IoT DIY harus menyelesaikan tiga persoalan utama: autentikasi tanpa state session yang berat, efisiensi muatan payload, serta proteksi dari jaringan tidak stabil yang memicu duplikasi data.

Spesifikasi Kontrak Header dan Autentikasi HMAC-SHA256

Penggunaan token dinamis seperti JWT memerlukan parser kompleks di firmware. Pendekatan preshared key (PSK) statis rentan sniffing jika tidak diproteksi. Solusi optimal adalah HMAC-SHA256 signature auth yang ditransmisikan melalui HTTP header.

Klien menyusun string tanda tangan dari gabungan metadata permintaan sebelum melakukan hashing. Format kanonikal string yang di-hash:

CanonicalString = Method + "\n" + RequestPath + "\n" + Timestamp + "\n" + MessageUUID + "\n" + PayloadHash
  • X-Device-ID: Identifier unik perangkat (misal: MAC address atau UUID perangkat).
  • X-Timestamp: Epoch time (detik) saat paket dibuat.
  • X-Message-UUID: Identifier unik paket untuk kontrol idempoten.
  • X-Signature: Hex digest hasil HMAC-SHA256 dari canonical string menggunakan shared secret perangkat.

Mitigasi Replay Attack dengan Sliding Window

Server membandingkan X-Timestamp dengan jam server internal. Jika |ServerTime - RequestTime| > Treshold (contoh: 300 detik), permintaan ditolak dengan kode status 401 Unauthorized. Mekanisme ini mempersempit celah intersepsi paket.

Optimasi Payload: Skema Batch Ingestion Ringkas

Mengirimkan satu metrik per request HTTP menghasilkan rasio overhead header-ke-data yang tinggi. Payload batch disusun ringkas menggunakan key pendek guna menghemat alokasi buffer RAM mikrokontroler.

{
  "v": 1,
  "d": [
    {"t": 1711929600, "m": "temp", "v": 28.4},
    {"t": 1711929600, "m": "hum", "v": 65.2},
    {"t": 1711929660, "m": "temp", "v": 28.6}
  ]
}

Keterangan field:

  • v: Schema version untuk backward compatibility.
  • d: Array datapoint sensor.
  • t: Timestamp perekaman metrik (delta-friendly).
  • m: Metrik identifier (string ringkas atau integer mapping).
  • v: Nilai pembacaan (float/integer).

Idempotency untuk Menangani Koneksi Fluktuatif

Perangkat IoT di lingkungan tepi sering mengalami timeout saat menunggu respon HTTP, meski data sudah tersimpan di database. Klien IoT wajib melakukan retry dengan menyertakan X-Message-UUID yang sama. Backend memanfaatkan Redis untuk atomic lock dan caching respon:

  1. Periksa keberadaan X-Message-UUID di Redis.
  2. Jika sudah ada dan status PROCESSED, kembalikan HTTP 200 dengan cached response body tanpa menulis ulang ke database.
  3. Jika key belum ada, set key dengan status PROCESSING dan TTL (misal: 86400 detik), lalu lanjutkan pemrosesan batch.

Implementasi Handler Backend (Node.js / Express)

Contoh middleware verifikasi HMAC dan handler batching dengan proteksi idempoten:

const crypto = require('crypto');
const express = require('express');
const app = express();

app.use(express.raw({ type: 'application/json' }));

const DEVICE_SECRETS = {
  'esp32-node-01': 'a8f5f167f44f4964e6c998dee827110c'
};

function verifyHmac(req, res, next) {
  const deviceId = req.headers['x-device-id'];
  const timestamp = parseInt(req.headers['x-timestamp'], 10);
  const msgUuid = req.headers['x-message-uuid'];
  const signature = req.headers['x-signature'];
  const rawBody = req.body.toString('utf-8');

  if (!deviceId || !timestamp || !msgUuid || !signature) {
    return res.status(400).json({ error: 'Missing security headers' });
  }

  const secret = DEVICE_SECRETS[deviceId];
  if (!secret) {
    return res.status(401).json({ error: 'Unknown device' });
  }

  // 1. Validasi sliding window (toleransi 5 menit)
  const now = Math.floor(Date.now() / 1000);
  if (Math.abs(now - timestamp) > 300) {
    return res.status(401).json({ error: 'Timestamp out of valid window' });
  }

  // 2. Hash payload
  const payloadHash = crypto.createHash('sha256').update(rawBody).digest('hex');

  // 3. Rekonstruksi Canonical String
  const canonicalString = `${req.method}\n${req.path}\n${timestamp}\n${msgUuid}\n${payloadHash}`;
  const expectedSignature = crypto
    .createHmac('sha256', secret)
    .update(canonicalString)
    .digest('hex');

  // 4. Constant-time comparison untuk mencegah timing attacks
  const isValid = crypto.timingSafeEqual(
    Buffer.from(signature, 'hex'),
    Buffer.from(expectedSignature, 'hex')
  );

  if (!isValid) {
    return res.status(401).json({ error: 'Invalid HMAC signature' });
  }

  req.parsedData = JSON.parse(rawBody);
  next();
}

app.post('/api/v1/telemetry', verifyHmac, async (req, res) => {
  const messageUuid = req.headers['x-message-uuid'];
  const { d: dataPoints } = req.parsedData;

  // Simulasi validasi idempoten: ganti dengan Redis SETNX di produksi
  // ponytail: in-memory cache skip redis cluster, replace when scaling horizontal
  if (!Array.isArray(dataPoints) || dataPoints.length === 0) {
    return res.status(422).json({ error: 'Invalid data array' });
  }

  // Batch insert ke time-series database (TimescaleDB / InfluxDB)
  // await db.insertMetrics(dataPoints);

  return res.status(200).json({
    status: 'success',
    ingested: dataPoints.length,
    msg_id: messageUuid
  });
});

app.listen(3000, () => console.log('Telemetry server running on port 3000'));

Trade-offs dan Tips Debugging

  • Sinkronisasi Waktu: Mikrokontroler membutuhkan modul RTC atau akses NTP pool minimal sekali saat booting agar nilai X-Timestamp tidak melenceng.
  • Timing Safe Equal: Selalu gunakan fungsi pembanding waktu-konstan seperti crypto.timingSafeEqual() untuk validasi hash agar kebal terhadap timing attack.
  • Raw Body Parsing: Jangan melakukan hashing pada object yang telah di-parse ulang oleh JSON serializer bawaan web framework, karena urutan key dan whitespace dapat berubah dan merusak kalkulasi digest.