Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 41 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,20 @@ graph TB

## **Web Server Endpoints**

following Wifi connection via the captive portal, device swaps its Mac Address for an API Key and Friendly ID from the server (which get saved on device).
### GET /api/time

Returns current server timestamp for Ed25519 authentication.

```curl
GET /api/time

response example:
{ "timestamp_ms": 1706380800000 }
```

### GET /api/setup

Following WiFi connection via the captive portal, device swaps its Mac Address for an API Key and Friendly ID from the server (which get saved on device).

```curl
GET /api/setup
Expand All @@ -109,13 +122,25 @@ headers = {
}

response example (success):
{ "status": 200, "api_key": "2r--SahjsAKCFksVcped2Q", "friendly_id": "917F0B", "image_url": "https://trmnl.com/images/setup/setup-logo.bmp", "filename": "empty_state" }

{
"status": 200,
"api_key": "2r--SahjsAKCFksVcped2Q",
"friendly_id": "917F0B",
"image_url": "https://usetrmnl.com/images/setup/setup-logo.bmp",
"filename": "empty_state",
"auth_mode": "ed25519"
}

# auth_mode can be "api_key" (default) or "ed25519"

response example (fail, device with this Mac Address not found)
{ "status" => 404, "api_key" => nil, "friendly_id" => nil, "image_url" => nil, "filename" => nil }
{ "status": 404, "api_key": null, "friendly_id": null, "image_url": null, "filename": null }
```

assuming the Setup endpoint responded successfully, future requests are made solely for image / display content:
### GET /api/display

Assuming the Setup endpoint responded successfully, future requests are made solely for image / display content:

```curl
GET /api/display
Expand All @@ -126,7 +151,11 @@ headers = {
'Refresh-Rate' => '1800',
'Battery-Voltage' => '4.1',
'FW-Version' => '2.1.3',
'RSSI' => '-69'
'RSSI' => '-69',
# When auth_mode is "ed25519", these additional headers are sent:
'X-Public-Key' => '3b6a27bc...64 hex chars', # Ed25519 public key (device identity)
'X-Signature' => 'a1b2c3d4...128 hex chars', # Ed25519 signature
'X-Timestamp' => '1706380800000' # Timestamp used in signature
}

response example (success, device found with this access token):
Expand Down Expand Up @@ -176,6 +205,13 @@ POST /api/log
# example request tbd
```

## **Authentication**

The server controls the authentication mode via the `auth_mode` field in the `/api/setup` response:

- **`api_key`** (default) — device sends `Access-Token` header. Backward compatible.
- **`ed25519`** — device generates an Ed25519 keypair on first boot (stored in NVS) and signs each request with `timestamp_ms || public_key`. See the `X-Public-Key`, `X-Signature`, and `X-Timestamp` headers above. Factory reset regenerates the keypair.

## **Power consumption**

A bit of background first. The ESP32-C3 inside the TRMNL OG is one of Espressif's newer, more efficient microcontrollers. For battery powered applications, it's designed to be put to sleep to conserve power when your project doesn't need it to be active. There are two sleep modes - light and deep. Deep sleep conserves the most power, but at the cost of losing the contents of the main memory. The lowest possible power consumption is about 4uA @ 3V with a timed wakeup, but TRMNL needs to be able to wake up with a button press. Keeping the GPIO active during deep sleep (to detect the button press) uses about 100uA on average (see power profile below). This means that a 2500mAh battery could theoretically keep the TRMNL powered in this state for approximately 25,000 hours.
Expand Down
7 changes: 7 additions & 0 deletions include/api-client/submit_log.h
Original file line number Diff line number Diff line change
@@ -1,10 +1,17 @@
#pragma once

#include <WString.h>

// Forward declaration for auth support
struct DeviceIdentity;

struct LogApiInput
{
String api_key;
const char *log_buffer;
// Ed25519 authentication (optional)
String authMode; // "api_key" or "ed25519"
const DeviceIdentity *identity; // Device identity for Ed25519 auth (can be nullptr)
};

bool submitLogToApi(LogApiInput &input, const char *api_url);
23 changes: 23 additions & 0 deletions include/api-client/time.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
#pragma once

#include <Arduino.h>

/**
* Result from fetching server time
*/
struct TimeApiResult {
bool success;
uint64_t timestamp_ms; // Server timestamp in milliseconds
String error;
};

/**
* Fetch current time from server
*
* Makes a GET request to /api/time and parses the JSON response.
* Used to get a timestamp for Ed25519 authentication signatures.
*
* @param baseUrl Base URL of the API server
* @return TimeApiResult with success status and timestamp or error message
*/
TimeApiResult fetchServerTime(const String &baseUrl);
7 changes: 7 additions & 0 deletions include/config.h
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,13 @@
#define PREFERENCES_CONNECT_API_RETRY_COUNT "retry_count"
#define PREFERENCES_CONNECT_WIFI_RETRY_COUNT "wifi_retry"

// Ed25519 authentication keys
#define PREFERENCES_ED25519_PUBLIC_KEY "ed_pub"
#define PREFERENCES_ED25519_PRIVATE_KEY "ed_priv"
#define PREFERENCES_AUTH_MODE "auth_mode"
#define PREFERENCES_AUTH_MODE_DEFAULT "api_key"
#define PREFERENCES_AUTH_MODE_ED25519 "ed25519"

#define WIFI_CONNECTION_RSSI (-100)

#define DISPLAY_BMP_IMAGE_SIZE 48062 // in bytes - 62 bytes - header; 48000 bytes - bitmap (480*800 1bpp) / 8
Expand Down
4 changes: 4 additions & 0 deletions include/preferences_persistence.h
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@ class PreferencesPersistence : public Persistence

size_t writeBool(const char *key, const bool value) override;

size_t readBytes(const char *key, uint8_t *buffer, size_t maxLen) override;

size_t writeBytes(const char *key, const uint8_t *buffer, size_t len) override;

bool clear() override;

bool remove(const char *key) override;
Expand Down
7 changes: 7 additions & 0 deletions lib/trmnl/include/api_types.h
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ struct ApiSetupResponse
String friendly_id;
String image_url;
String message;
String auth_mode; // "api_key" or "ed25519"
};

enum class ApiDisplayOutcome
Expand All @@ -45,6 +46,9 @@ struct ApiDisplayResponse
String action;
};

// Forward declaration for auth support
struct DeviceIdentity;

struct ApiDisplayInputs
{
String baseUrl;
Expand All @@ -59,6 +63,9 @@ struct ApiDisplayInputs
int displayWidth;
int displayHeight;
SPECIAL_FUNCTION specialFunction;
// Ed25519 authentication (optional)
String authMode; // "api_key" or "ed25519"
const DeviceIdentity *identity; // Device identity for Ed25519 auth (can be nullptr)
};

typedef struct
Expand Down
42 changes: 42 additions & 0 deletions lib/trmnl/include/auth_signature.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
#pragma once

#include <Arduino.h>
#include <stdint.h>
#include <device_identity.h>
#include <ed25519.h>

/**
* Authentication signature for Ed25519 challenge-response
*/
struct AuthSignature {
uint8_t signature[ED25519_SIGNATURE_SIZE]; // 64 bytes
uint64_t timestamp_ms;
bool valid;
};

/**
* Compute authentication signature for API request
*
* Signs the message: timestamp_ms (8 bytes big-endian) || public_key (32 bytes)
*
* @param identity Device identity with keypair
* @param timestamp_ms Server timestamp in milliseconds
* @return AuthSignature with signature and validity flag
*/
AuthSignature computeAuthSignature(const DeviceIdentity &identity, uint64_t timestamp_ms);

/**
* Convert signature to hex string for use in HTTP headers
*
* @param sig Auth signature to convert
* @return 128-character hex string of the 64-byte signature
*/
String signatureToHex(const AuthSignature &sig);

/**
* Convert timestamp to string for use in HTTP headers
*
* @param timestamp_ms Timestamp in milliseconds
* @return String representation of the timestamp
*/
String timestampToString(uint64_t timestamp_ms);
44 changes: 44 additions & 0 deletions lib/trmnl/include/device_identity.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
#pragma once

#include <Arduino.h>
#include <stdint.h>
#include <persistence_interface.h>
#include <ed25519.h>

/**
* Device identity containing Ed25519 keypair
*/
struct DeviceIdentity {
uint8_t publicKey[ED25519_PUBLIC_KEY_SIZE]; // 32 bytes
uint8_t privateKey[ED25519_PRIVATE_KEY_SIZE]; // 64 bytes
bool initialized;
};

/**
* Initialize device identity by loading from storage or generating new keypair
*
* If keys exist in NVS, loads them into the identity struct.
* If keys don't exist, generates a new Ed25519 keypair and stores it.
*
* @param storage Persistence interface for NVS access
* @param identity Output struct to populate with keys
* @return true if identity was initialized (either loaded or generated)
*/
bool initDeviceIdentity(Persistence &storage, DeviceIdentity &identity);

/**
* Clear device identity from storage
*
* Called during factory reset. Keys will be regenerated on next boot.
*
* @param storage Persistence interface for NVS access
*/
void clearDeviceIdentity(Persistence &storage);

/**
* Convert public key to hex string for use in HTTP headers
*
* @param identity Device identity containing the public key
* @return 64-character hex string of the 32-byte public key
*/
String publicKeyToHex(const DeviceIdentity &identity);
44 changes: 44 additions & 0 deletions lib/trmnl/include/ed25519.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
#pragma once

#include <stdint.h>
#include <stddef.h>

#define ED25519_PUBLIC_KEY_SIZE 32
#define ED25519_PRIVATE_KEY_SIZE 64
#define ED25519_SIGNATURE_SIZE 64

/**
* Generate a new Ed25519 keypair
*
* @param public_key Output buffer for 32-byte public key
* @param private_key Output buffer for 64-byte private key
* @return true on success, false on failure
*/
bool ed25519_generate_keypair(uint8_t public_key[ED25519_PUBLIC_KEY_SIZE],
uint8_t private_key[ED25519_PRIVATE_KEY_SIZE]);

/**
* Sign a message using Ed25519
*
* @param signature Output buffer for 64-byte signature
* @param msg Message to sign
* @param len Length of message
* @param private_key 64-byte private key
* @return true on success, false on failure
*/
bool ed25519_sign(uint8_t signature[ED25519_SIGNATURE_SIZE],
const uint8_t *msg, size_t len,
const uint8_t private_key[ED25519_PRIVATE_KEY_SIZE]);

/**
* Verify an Ed25519 signature
*
* @param signature 64-byte signature to verify
* @param msg Message that was signed
* @param len Length of message
* @param public_key 32-byte public key
* @return true if signature is valid, false otherwise
*/
bool ed25519_verify(const uint8_t signature[ED25519_SIGNATURE_SIZE],
const uint8_t *msg, size_t len,
const uint8_t public_key[ED25519_PUBLIC_KEY_SIZE]);
10 changes: 10 additions & 0 deletions lib/trmnl/include/ed25519_config.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
#pragma once

// NVS keys for Ed25519 authentication
// These are duplicated here for lib/trmnl access
// Main definitions are in include/config.h
#define PREFERENCES_ED25519_PUBLIC_KEY "ed_pub"
#define PREFERENCES_ED25519_PRIVATE_KEY "ed_priv"
#define PREFERENCES_AUTH_MODE "auth_mode"
#define PREFERENCES_AUTH_MODE_DEFAULT "api_key"
#define PREFERENCES_AUTH_MODE_ED25519 "ed25519"
17 changes: 17 additions & 0 deletions lib/trmnl/include/hex_utils.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
#pragma once

#include <Arduino.h>
#include <stdint.h>
#include <stddef.h>

inline String bytesToHex(const uint8_t *bytes, size_t len)
{
static const char hexChars[] = "0123456789abcdef";
String hex;
hex.reserve(len * 2);
for (size_t i = 0; i < len; i++) {
hex += hexChars[(bytes[i] >> 4) & 0x0F];
hex += hexChars[bytes[i] & 0x0F];
}
return hex;
}
4 changes: 4 additions & 0 deletions lib/trmnl/include/persistence_interface.h
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@ class Persistence

virtual size_t writeBool(const char *key, const bool value) = 0;

virtual size_t readBytes(const char *key, uint8_t *buffer, size_t maxLen) = 0;

virtual size_t writeBytes(const char *key, const uint8_t *buffer, size_t len) = 0;

virtual bool clear() = 0;

virtual bool remove(const char *key) = 0;
Expand Down
Loading