Architecture · Module 2.1
Time Buckets¶
under construction
The first genuinely Zajil-specific module. It turns a wall-clock instant into a bucket index — the counter that address derivation feeds to HMAC. It is TOTP's counter, and nothing more: no hashing, no addresses, no clock access of its own.
History: forty years of turning secrets into codes¶
Understanding why this construction fits Zajil means understanding what problem each of its ancestors solved.
| Year | System | Idea | Limitation |
|---|---|---|---|
| 1981 | Lamport hash chains | Hash a seed n times, reveal in reverse. Each value verifies against the next; captured values reveal nothing forward. | Finite chain — runs out, needs re-provisioning. (Became S/KEY.) |
| 1986 | RSA SecurID | Shared secret + time → a number changing each minute. | Proprietary black box; vendor holds every secret (breached in 2011). |
| 2004 | OATH | Consortium formed to break that vendor lock-in with open standards. | — |
| 2005 | HOTP (RFC 4226) | Truncate(HMAC(K, counter)). Open, auditable. |
Counter is stored state on both sides — drifts, needs resync windows. |
| 2011 | TOTP (RFC 6238) | HOTP where the counter is the clock: C = ⌊(now − T0) / X⌋. |
Trades a sync problem for a clock-integrity problem. |
TOTP's entire innovation is that one substitution: delete the stored counter, read it off the clock instead. Two parties who have never communicated agree on the counter because they both look at the same clock.
Why this shape, and not something else¶
That property is not a nice-to-have for Zajil — it is the only shape that fits the problem. A client must derive a cell's address before any communication exists. You cannot negotiate a shared counter with a server you cannot yet reach; that is circular. Time is the only shared state obtainable without a channel.
So Zajil borrows TOTP's counter deliberately, not out of familiarity.
The construction¶
Three fields, all manifest tunables, define the schedule:
| Field | Meaning | v1 candidate |
|---|---|---|
duration_secs |
how long one bucket lasts | 3600 (hourly) |
epoch |
where bucket 0 begins | unsettled |
skew |
buckets accepted on each side | 1 → 3 live buckets |
Clocks drift, so a cell binds a window of indices, not one:
This is TOTP's skew tolerance (RFC 6238 recommends ±1 step). A client asks "which
bucket is it?" via index_at; a cell asks "which buckets do I accept?" via
window_at. Same schedule, asymmetric use.
What we kept and what we threw away¶
Most of TOTP is interface convention for a human squinting at a phone. Zajil keeps the cryptographic core and discards the rest.
| From TOTP | In Zajil | Why |
|---|---|---|
counter = ⌊(now − T0)/X⌋ |
kept — this module | The shared-state-without-a-channel property |
| Skew window | kept — window_at |
Clock drift is real |
| Truncation | kept, but to raw bits | No decimal, no modulo — see below |
| 6-digit decimal + modulo | dropped | Human-interface tax; no human reads an address |
| Rate limiting, single-use | dropped | The output is not a credential |
The digit machinery, and why we drop it¶
TOTP's & 0x7FFFFFFF mask exists only because Java has no unsigned integers — a
crypto standard permanently shaped by a language's type system. Its 6-digit
decimal conversion exists only so a human can read it, and going past 8 digits
introduces modulo bias (at 10 digits, 78% of codes become unreachable). None
of this applies to Zajil: no human ever reads an address, so we take raw HMAC bits
directly and the entire question of digits and bias evaporates.
The lesson worth internalising: when borrowing a construction, separate what is cryptographically necessary from what is interface convention. Most of TOTP is the latter.
Skew costs something different here¶
For TOTP, widening the acceptance window linearly multiplies attack surface — ±1 triples the guess space (10⁻⁶ → 3×10⁻⁶). That is why the RFC is conservative.
For Zajil, widening the window costs resources, not security. Each accepted bucket is another row of addresses a cell must bind. With 30 slots, skew 1 means 90 bindings; skew 2 means 150. The security impact is nil — 2⁶⁴ does not get meaningfully easier when you triple it. So window sizing is an operations question, not a security one, and the RFC's conservatism does not transfer.
The clock is an attack surface¶
Since time is the shared state, whoever controls a client's clock controls its connectivity. Push the clock far enough (spoofed, unauthenticated NTP) and the client derives addresses nobody is listening on — a total denial of service, no cryptography broken.
Two defenses:
- Planned: a root-signed time service, so spoofing a client's clock requires the root key. The security is in the signature, not in obscuring the epoch. (See open questions — an early idea to use an exotic calendar was dropped as obfuscation with zero attacker cost.)
- Interim: a monotonic floor — persist the highest time ever observed and refuse any clock that jumps backward past it. Cheap, and it converts "attacker rewound your clock" from a silent failure into a detectable one. Derivation must use wall-clock time (the shared reference), but a monotonic clock can sanity-check it.
Leap seconds
Unix time is not a true count of SI seconds — leap seconds are repeated or smeared. With hourly buckets and skew tolerance, a one-second anomaly is invisible. Worth knowing it exists; it does bite systems that assume strict second-resolution monotonicity.
The module surface¶
Pure integer arithmetic, no_std, and — critically — it never reads a clock.
Time always enters as an argument. That is what makes it deterministically
testable and keeps Layer 7 simulation possible.
| Type | Role |
|---|---|
UnixTime |
Seconds since the Unix epoch. Passed in, never read from a clock. |
BucketIndex |
The derived counter. Public and predictable — carries no secrecy. |
BucketSchedule |
duration + epoch + skew. Holds the arithmetic: index_at, start_of, window_at. |
BucketWindow |
Inclusive range of accepted indices. Clamps at 0, saturates near max — never wraps. |
MonotonicFloor |
Rejects backward clock jumps. Separate and stateful. |
See engineering › implementation plan for
where this sits in the ladder, and the module's own README.md and test suite for
the test-first build in progress.