Specification
INTD bucket format v1
The format is open on purpose: confidentiality and integrity come from encrypting and signing the request, not from keeping the layout secret. A fixed structure and CRC32 let the server throw away junk cheaply, before anything is written to storage.
Container
| Offset | Size | Field | Value |
|---|---|---|---|
| 0 | 4 | MAGIC | 0x49 0x4E 0x54 0x44 (“INTD”) |
| 4 | 1 | VERSION | 0x01 |
| 5 | 1 | FLAGS | 0x00, reserved |
| 6 | 8 | TIMESTAMP | uint64 BE, unix ms — the base for the deltas |
| 14 | 4 | EVENTS_COUNT | uint32 BE |
| 18 | 4 | BLOB_LENGTH | uint32 BE, length of EVENTS_BLOB |
| 22 | N | EVENTS_BLOB | the events, back to back |
| 22+N | 4 | CRC32 | uint32 BE, CRC-32/IEEE of EVENTS_BLOB |
The total size is exactly 26 + N bytes; trailing bytes are an error.
Event
| Size | Field | Value |
|---|---|---|
| 1 | TYPE | 0x01 pageview, 0x02 search |
| 4 | TS_DELTA | uint32 BE, ms after TIMESTAMP |
| 2 + var | DOMAIN | uint16 length ≤ 255, UTF-8 |
| 2 + var | URL | uint16 length ≤ 2048, UTF-8 |
| 2 + var | QUERY | uint16 length ≤ 500, 0 when absent |
| 1 + var | LANG | uint8 length ≤ 20, BCP 47 |
| 1 | DEVICE_TYPE | 0x00 unknown, 0x01 desktop, 0x02 mobile, 0x03 tablet, 0x04 bot |
Strings are truncated on a UTF-8 character boundary. The server rejects a bucket when a length exceeds the limit, a string is not valid UTF-8, the event count disagrees with the header, or the CRC32 does not match.
What the server does
- The domain is recomputed from the URL (eTLD+1); the client value is not used.
- Tokens, passwords, e-mail addresses and card numbers are stripped from the URL and the query.
- Events older than 7 days, and events more than 10 minutes in the future, are dropped.
- The country is resolved from the connection IP, and the installation identifier is hashed separately for each extension.
Encryption and signing
sessionKey = random(32)
iv = random(12)
ciphertext = AES-256-GCM(sessionKey, iv, bucket)
ephemeral = X25519 — a fresh pair per bucket
shared = X25519(ephemeral.private, extension.publicKey)
derivedKey = HKDF-SHA256(IKM = shared, salt = iv, info = "intentd-bucket-v1", 32)
encryptedKey = AES-256-GCM(derivedKey, iv, sessionKey) // 48 bytes X-Signature = hex(HMAC-SHA256(secret,
"intentd-v2|bucket|" + X-Timestamp + "|" + X-API-Key + "|" + hex(SHA-256(body)))) The scheme gives no forward secrecy if the extension private key is later compromised, and does not protect against whoever holds a copy of the bundle: repackaging is countered by per-installation sessions, the extension ID check and the server-side checks.
Test vectors
// One search event: base 1758000000000, delta 7,
// domain a.io, URL https://a.io/?q=x, query x, language en, desktop
494e5444 01 00 0000019950f72c00 00000001 00000025
02 00000007 0004 612e696f 0011 68747470733a2f2f612e696f2f3f713d78 0001 78 02 656e 01
cee33ee2 HKDF-SHA256(IKM = 0x11 × 32, salt = 0x22 × 12, info = "intentd-bucket-v1", L = 32)
=> 4a359b8818fc073f6f81a3cb4acd9065302e285fbef0d35d2ff71fba107e6056