How Timeless Encryption Works for Devices Without a Real-Time Clock

You’re three weeks into a BLE sensor node design. The hardware team just cut the 32.768 kHz crystal from the BOM to save $0.40 and 4mm² of board space. Reasonable call for a device shipping at volume. Then you open the firmware and realize: your encryption scheme derives its nonce from wall-clock time. The RTC was the nonce source. And now it’s gone.
Most encryption documentation assumes you have a reliable clock. The Eddystone-EID spec assumes it. TOTP assumes it. Half the example code on vendor forums assumes it. But encryption doesn’t need to know what time it is. It needs a value that never repeats. A counter works. An uptime tick works. A carefully constructed monotonic integer works fine.
This article covers how to build encryption and identity rotation for BLE devices with no RTC: counter-based schemes, uptime-based schemes, hybrid approaches for EID rotation, and the specific failure modes you need to test for.
Why Encryption Asks for Time in the First Place
Symmetric encryption modes like AES-CCM and AES-CTR need a nonce (or IV) for every operation. Encrypt two different plaintexts with the same key and the same nonce, and an attacker can XOR the ciphertexts to recover content. This is a well-documented, practical attack.
A timestamp makes a convenient nonce because it’s naturally unique (time moves forward) and both sides can derive it independently (they both have clocks). But “convenient” isn’t “required.”
A nonce substitute needs exactly three properties: uniqueness (never repeat under the same key), monotonicity (always increase, so you can reject replays), and predictability by authorized peers (so the other side can decrypt or resolve your identity).
BLE’s own link-layer encryption already proves this works. The LE encryption spec (Bluetooth Core Spec v5.4, Vol 6 Part B) uses a packetCounter as the nonce for AES-CCM. No clock involved. Just a counter that increments with every PDU.
Counter-Based Encryption: The Workhorse Approach
Replace the timestamp with a monotonically incrementing integer. Every time you encrypt a packet, bump the counter by one and use the new value as your nonce.
AES-CTR mode is built for exactly this. The counter value gets encrypted with the key to produce a keystream block, which then gets XORed with your plaintext. Different counter value, different keystream, secure encryption. The counter doesn’t need to be secret. It just can’t repeat.
The question is what happens when your device reboots.
Volatile counters (a variable in RAM) reset to zero on power loss. That’s fine within a single session, as long as you pair the counter with a session identifier. But if you naively restart at zero after a reboot and reuse the same key, you’ve got nonce reuse. Catastrophic.
Persistent counters (stored in flash or NVP) survive reboots but bring write-wear concerns. Writing to flash every time you increment a counter will kill your storage long before the device’s intended lifespan.
The practical pattern is a leap-ahead counter. On each boot, read the last stored counter value, add a large leap (say, 1000), and write that new base back to flash. Then increment in RAM from there. If power dies unexpectedly mid-session, you’ve “lost” at most the leap value, but you’ve never reused a nonce.
COUNTER-BASED NONCE GENERATION
==============================
Boot #1 Boot #2 Boot #3
| | |
v v v
[NVP: 0] [NVP: 1000] [NVP: 2000]
| | |
+-- 0 +-- 1000 +-- 2000
+-- 1 +-- 1001 +-- 2001
+-- 2 +-- 1002 +-- 2002
: : :
+-- 347 +-- 1847 +-- 2499
| | |
[power loss] [power loss] [clean shutdown]
|
[NVP: 2500]
* Leap value = 1000 (safety margin per boot)
* Actual usage << leap value = no nonce collisionSize the leap value based on your expected packets-per-session. If your device typically sends 200 packets between reboots, a leap of 1000 gives you a 5x safety margin. Budget your flash write cycles accordingly: if your NVP sector tolerates 100,000 writes and you reboot once a day, that’s 273 years.
If you’re working with the Hubble device SDK, the advertising packet configuration already handles much of the BLE-level counter management. The concept here applies to your application-layer encryption on top of that.
Uptime-Based Schemes: Session-Scoped Security
Your microcontroller probably has a SysTick or a free-running timer peripheral even without an RTC crystal. That gives you a monotonic uptime tick, usually at 1ms resolution.
For session-scoped encryption (where both sides establish a shared reference point at connection time), uptime works cleanly. The BLE connection event counter already provides something similar: a value that both central and peripheral can track from the moment the connection is established.
The problems start at boundaries.
Reboot resets uptime to zero. If you combine the uptime tick with a persistent boot counter, you get a globally unique nonce:
UPTIME-BASED NONCE STRUCTURE
=============================
|<-- 16 bits -->|<-- 48 bits ----------------->|
+---------------+------------------------------+
| Boot Counter | Uptime Ticks (ms) |
+---------------+------------------------------+
Example:
Boot #3, 12,345 ms uptime
Nonce = 0x0003 | 0x000000003039
= 0x0003000000003039
* Globally unique across reboots
* Monotonic within each boot
* 48-bit tick space = ~8.9 years at 1ms resolutionTimer overflow is a real concern for long-running devices. A 32-bit millisecond counter overflows at roughly 49.7 days. If your device runs continuously for months, you need either a 64-bit counter (most Cortex-M chips can handle this in software) or an overflow handler that bumps a higher-order counter.
Drift between peers is acceptable for encryption nonces since the receiver doesn’t need to independently derive the same nonce (it’s transmitted or implied by sequence). But if you’re using uptime for synchronized identity rotation, drift becomes a real sync problem.
Identity Rotation Without a Clock: EID and RPA Strategies
Identity rotation solves a different problem than encryption nonces, but the underlying approach is similar.
BLE Resolvable Private Addresses (RPA) and Eddystone-EID both conventionally rotate on wall-clock intervals. RPA defaults to a 15-minute rotation. EID rotates on a configurable time exponent. Both assume a reliable clock.
Without one, you’ve got three options.
Counter-driven rotation: rotate your identity every N advertising events or connection cycles. A device that advertises at 1 Hz and rotates every 900 events gets roughly 15-minute rotations. Deterministic, no drift. But the rotation rate couples to activity. A device that goes quiet (say, sending one packet every 10 minutes in low-power sleep) rotates slowly, and a quiet device with a stable address is a tracking target.
Uptime-driven rotation: rotate every M milliseconds of uptime. This gives you a consistent cadence regardless of how chatty the device is. The catch: your resolving central needs to estimate the device’s uptime to try candidate identities. That’s a harder sync problem than tracking a shared clock.
Hybrid approach: rotate on whichever comes first, N events OR M uptime ticks. A sleeping device still rotates on the uptime trigger; a chatty device rotates on the event trigger. You bound your worst-case rotation interval, but provisioning gets more complex.
ROTATION STRATEGY COMPARISON
================================================================
Strategy | Trigger | Pros | Cons
----------------|-----------------|----------------|---------------
Counter-driven | Every N events | Deterministic, | Rotation rate
| | no drift | varies w/
| | | activity
----------------|-----------------|----------------|---------------
Uptime-driven | Every M ms of | Consistent | Peer must
| uptime | cadence | estimate
| | | device uptime
----------------|-----------------|----------------|---------------
Hybrid | First of N | Bounded worst | More complex
| events OR M ms | case interval | provisioning
================================================================Syncing with the resolver works like TOTP’s tolerance window, but for identity. During pairing or provisioning, share the rotation scheme parameters (leap value, event threshold, uptime interval). The central tracks the last-known counter or uptime and tries a window of candidate values when resolving; +/- 5 rotations is typical. If you’re provisioning devices through Hubble’s platform, the device registration API is where you’d attach these rotation parameters as device metadata.
The Risks: Nonce Reuse and Desynchronization
⚠️ Nonce reuse in AES-CTR/CCM is catastrophic. Two plaintexts encrypted with the same key and nonce allow an attacker to XOR the ciphertexts and recover both messages. This is a complete break of confidentiality.
How does nonce reuse happen in clockless systems? Flash write failure during a counter update. Counter rollback after an unexpected reset hits before the leap-ahead write completes. Boot counter corruption from a brown-out during NVP write.
Mitigations: the leap-ahead pattern is your first line of defense. Redundant NVP storage (write the counter to two separate flash pages and take the higher value on boot) is your second. Your third is a one-way hash ratchet, where each counter value is derived by hashing the previous one, making rollback structurally impossible. It costs CPU cycles, but it eliminates an entire class of rollback bugs.
Desynchronization happens when the peer and the device disagree on the current counter or uptime state. You’ll detect it when decryption fails or MAC verification rejects a packet. Recovery options: re-pairing (expensive, involves user interaction), a counter resync protocol (the device broadcasts its current counter in a signed but unencrypted field), or expanding the trial window (the peer tries more candidate nonces, trading CPU time for resilience).
These risks exist in clock-based systems too: NTP spoofing, a dead RTC battery, a time zone misconfiguration. Clockless systems just surface the risks earlier, which can be an advantage. You’re forced to handle them rather than assuming the clock is always right.
Implementation Checklist
Before you ship, walk through this list:
- Identify every nonce/IV source in your encryption pipeline
- Choose counter-based (persistent) or uptime-based (session) for each use case
- Implement a leap-ahead counter with NVP write-wear budgeting (calculate your flash cycle budget)
- Combine boot counter + uptime for a globally unique nonce space
- Define your identity rotation trigger: event count, uptime, or hybrid
- Provision rotation parameters to resolving peers at pairing time
- Implement desync detection (MAC failure) and a recovery path
- Test: simulate power loss at every counter increment boundary
- Test: verify the nonce never repeats across 10,000 simulated reboot cycles
If you’re building on the Hubble SDK, the timing management guide covers how the SDK handles timing constraints on devices without a reliable clock source.
Removing the Crystal, Not the Security
Cutting the RTC from your BOM doesn’t weaken your encryption. It changes which counter you lean on. The cryptographic primitives don’t care whether your nonce came from a wall clock, a SysTick timer, or a persistent integer in flash. They care that it never repeats.
Budget your flash writes. Test your power-loss boundaries. Size your leap values with margin. The math is on your side.
Hubble Network enables encrypted communication for clockless devices without adding RTC hardware or cloud-based time sync. See how it works →