Version: 1.0 Date: April 2026 Status: Specification — Revision D
TagoTiP/S wraps TagoTiP data in a binary envelope. For the plaintext frame format (methods, variable syntax, parsing), see TagoTiP.md.
TagoTiP/S (Secure) is a binary crypto envelope that provides AEAD authenticated encryption for TagoTiP data without requiring TLS. It is designed for links where TLS is unavailable or too expensive — such as LoRa, Sigfox, NB-IoT, or raw UDP.
TagoTiP/S uses a compact headless inner frame that omits fields carried by the envelope header, saving ~40–50 bytes per message (see §4).
21 bytes header + ciphertext + auth tag
TagoTiP/S uses the following credentials for envelope construction and processing.
at + 32 hex chars (34 chars total)at prefix (UTF-8 encoded). Used as the profile identifier in the envelope header and in TagoTiP frames. Safe to display in UIs/logs.Encryption Keys MUST be unique per device within a profile. Key reuse across devices is a configuration error and may compromise nonce uniqueness. Key length (16 or 32 bytes) is determined by the cipher suite (see §3.1). Changing to a cipher suite with a different key length requires reprovisioning the Encryption Key.
Given an Authorization Token ate2bd319014b24e0a8aca9f00aea4c0d0:
at prefix: e2bd319014b24e0a8aca9f00aea4c0d00x4d 0xee 0xdd 0x7b 0xab 0x88 0x17 0xecNote: The Authorization Hash derivation is shared between TagoTiP and TagoTiP/S. See TagoTiP.md §2 for the same computation.
Given a device serial sensor-01:
sensor-010xab 0x77 0x88 0xd2 0x2e 0xb7 0x37 0x2f)Note: With an 8-byte hash (2^64 space), collisions within a single profile are virtually impossible in practice. If multiple devices nonetheless share the same Device Hash within a profile, the server SHOULD attempt decryption with each matching device's key until one succeeds or all fail. If all decryption attempts fail, the server MUST respond with
ACK|ERR|auth_failed.
The Encryption Key and Authorization Token SHOULD be provisioned during manufacturing or via a secure out-of-band channel. The protocol does not define a key exchange mechanism. The cipher suite SHOULD also be agreed upon during provisioning. The protocol does not include an in-band cipher negotiation mechanism; both sides must be configured to use the same cipher suite for a given device.
Implementations MAY derive the Encryption Key from the Authorization Token and device serial number using HMAC-SHA256 instead of provisioning a separate key. This is a convenience feature — pre-provisioned keys remain fully supported and are the default.
Construction:
derive_key(token, serial, key_len):
1. hex_part = strip "at" prefix from token (if present)
2. hmac_key = UTF-8 bytes of hex_part
3. message = UTF-8 bytes of serial
4. output = HMAC-SHA256(key=hmac_key, msg=message) // 32 bytes
5. return output[0..key_len]
key_len = 16 for AES-128 cipher suites, 32 for AES-256 / ChaCha20-Poly1305Test vector (using the spec credentials from §11.1):
Token: ate2bd319014b24e0a8aca9f00aea4c0d0
Serial: sensor-01
HMAC key (UTF-8): "e2bd319014b24e0a8aca9f00aea4c0d0"
HMAC message (UTF-8): "sensor-01"
Derived key (32 bytes):
e5 05 f0 3c c9 e9 3f db cc 38 28 44 cc a3 e1 7f
df 0b b3 13 18 58 53 95 ce aa a3 9a 5d 14 19 64
First 16 bytes (AES-128): e5 05 f0 3c c9 e9 3f db cc 38 28 44 cc a3 e1 7f
Note: When using derived keys, the Authorization Token becomes the sole secret for a given device. Implementations using key derivation MUST treat the token with the same care as an Encryption Key.
SDK implementations SHOULD provide hex_to_bytes and bytes_to_hex utility functions to simplify working with pre-provisioned keys supplied as hex strings. These are not protocol-level operations but are commonly needed by integrators.
TagoTiP/S supports multiple AEAD cipher suites. The cipher suite is encoded in bits 7–5 of the Flags byte (see §5.1).
L is the CCM length-field size parameter (in bytes). For CCM suites the nonce length is 15 − L; with L = 2 the nonce is 13 bytes and the maximum plaintext size is 2^16 − 1 bytes (64 KiB). The L column is not applicable ("—") to non-CCM cipher suites.
All implementations MUST support cipher suite 0 (AES-128-CCM). Support for additional cipher suites is OPTIONAL but RECOMMENDED for deployments with higher security requirements.
The device's Encryption Key (pre-provisioned, never transmitted) must match the key size required by the selected cipher suite.
The authentication tag is produced automatically by the AEAD cipher during encryption. Its length depends on the cipher suite: 8 bytes for CCM suites, 16 bytes for GCM and ChaCha20-Poly1305. The tag covers both the plaintext (the headless inner frame) and the Associated Authenticated Data (AAD = envelope header, bytes 0–20). There is no separate hash computation.
The protocol uses a bandwidth-efficient nonce derived from the counter and device hash fields in the envelope header. The nonce length depends on the cipher suite:
The full nonce is never transmitted.
Components:
Construction (13-byte nonce — CCM suites):
[Flags (1 byte)] [00 00 00 00] [Device Hash (4 bytes)] [Counter (4 bytes)]
Construction (12-byte nonce — GCM / ChaCha20-Poly1305 suites):
[Flags (1 byte)] [00 00 00] [Device Hash (4 bytes)] [Counter (4 bytes)]
For cipher suites with a 12-byte nonce, the zero-padding is 3 bytes instead of 4.
Transmission: The 4-byte counter and 8-byte Device Hash are sent in the envelope header. The receiver reconstructs the full nonce using the Flags byte (byte 0 of the envelope), the first 4 bytes of the Device Hash, Counter, and the nonce length from the cipher suite table.
Including the Flags byte in the nonce ensures uniqueness across message directions and cipher suites: different cipher/method combinations in the Flags byte occupy distinct nonce spaces, preventing nonce reuse even when the counter values coincide. The Device Hash component further guarantees uniqueness across devices that share the same encryption key.
TagoTiP/S does not encrypt a complete TagoTiP frame. Instead, it uses a compact headless inner frame that omits fields already carried by the envelope header:
PUSH|...)The Device Hash in the envelope header is an 8-byte routing hint derived from the serial. The full SERIAL stays inside the encrypted payload so that the actual device identity remains confidential.
0x0)SERIAL|BODYsensor-01|[temp:=32]0x1)SERIAL|[VARNAME;...]sensor-01|[temperature]0x2)SERIALsensor-01The BODY follows the same syntax as in plaintext TagoTiP (structured variables, passthrough, etc.) — only the frame-level fields (method, auth, counter) are removed.
0x3)STATUS[|DETAIL]OK|3, OK|[temperature:=32#F@1694567890000] (PULL), CMD|ota=https://example.com/v2.1.bin, or PONGThe ACK| prefix from plaintext TagoTiP is not present — the method is encoded in the Flags byte. The inner frame starts directly with the STATUS field.
The envelope is a single binary message constructed by concatenating the following components with no delimiters:
[Flags] + [Counter] + [Authorization Hash] + [Device Hash] + [Ciphertext + Auth Tag]
at prefix (UTF-8 encoded). Used by the server to identify the profile and look up the Encryption Key.Total overhead: 29–37 bytes, depending on cipher suite. Formula: 21 (header) + tag_length where tag_length is 8 bytes for CCM suites or 16 bytes for GCM / ChaCha20-Poly1305. No padding is required.
Associated Authenticated Data (AAD): The first 21 bytes of the envelope (Flags + Counter + Authorization Hash + Device Hash) are passed as AAD to the AEAD cipher. This means the header is integrity-protected but not encrypted — an attacker cannot modify any header field without causing authentication failure during decryption.
The first byte of the envelope encodes the cipher suite, protocol version, and method:
Bit 7 6 5 4 3 2 1 0
└─Cipher──┘ └Ver─┘ └Method─┘
0 = current version. 1–3 reserved for future versions.0 = PUSH, 1 = PULL, 2 = PING, 3 = ACK. 4–7 reserved.Examples:
000 00 0000x00000 00 0010x01000 00 0100x02000 00 0110x03100 00 0000x80Receivers MUST reject envelopes with an unsupported version number or unsupported cipher suite.
Note: The Flags byte value
0x41(010 00 001) corresponds to cipher 2 (AES-256-CCM), version 0, PULL. This value is reserved for framing disambiguation (see §5.4) and MUST NOT appear as a valid Flags byte. Implementations using AES-256-CCM with PULL MUST use version ≥ 1 (i.e.,010 01 001=0x49) or use a different cipher suite.
For a PUSH with serial sensor-01 and body [temp:=32], the headless inner frame is sensor-01|[temp:=32] (20 bytes):
Header (AAD) = Flags + Counter + Auth Hash + Device Hash (first 21 bytes)
00 (= cipher 0 AES-128-CCM, v0, PUSH)00 00 00 2A (= 42)4d ee dd 7b ab 88 17 ecab 77 88 d2 2e b7 37 2fTotal envelope: 1 + 4 + 8 + 8 + 20 + 8 = 49 bytes.
\n as a delimiter.On all transports, the receiver distinguishes plaintext fallback frames from TagoTiP/S envelopes by inspecting the first byte of the message: 0x41 (ASCII A) indicates a plaintext ACK frame (e.g., ACK|ERR|auth_failed); any other value indicates a TagoTiP/S envelope. The only plaintext frames that can appear on a TagoTiP/S connection are server-side ACK error fallbacks (see §9), so reserving 0x41 alone is sufficient for disambiguation.
The TagoTiP/S envelope is binary and may contain any byte value (including 0x0A). Therefore, newline (\n) MUST NOT be used as a delimiter for TagoTiP/S on stream transports.
When carrying TagoTiP/S over TCP (or any stream transport), all messages MUST be framed using a length prefix:
[Length (uint16, Big-Endian)] + [Message bytes...]
Where Length is the number of bytes in the following message (not including the length field). This applies to both TagoTiP/S envelopes and plaintext fallback frames (e.g., ACK|ERR|auth_failed).
The receiver distinguishes between envelope and plaintext by inspecting the first byte of the message content: if the first byte of the message content is 0x41 (ASCII A), it is a plaintext ACK fallback frame; otherwise it is a TagoTiP/S envelope. The byte value 0x41 is reserved and MUST NOT be used as a Flags byte in any future protocol version.
21 (header) + tag_length bytes, the server MUST reject any envelope larger than 16,384 + 21 + tag_length bytes (16,413 for CCM suites with 8-byte tag; 16,421 for GCM/ChaCha20 suites with 16-byte tag).Field-level limits (variable name length, metadata pair count, etc.) are defined in TagoTiP.md §4.5.1 and apply equally to headless inner frames.
sensor-01|[temperature:=32;humidity:=65]
0x00.[Flags (1B)] + [Counter (4B)] + [Authorization Hash (8B)] + [Device Hash (8B)] — 21 bytes total.[Flags (1B)] [zero-padding] [Device Hash[:4] (4B)] [Counter (4B)]. Only the first 4 bytes of the Device Hash are used in the nonce.[Header (21B)] + [Ciphertext (NB)] + [Auth Tag].When the receiver processes a TagoTiP/S envelope:
[Flags (1B)] [zero-padding] [Device Hash[:4] (4B)] [Counter (4B)]. Only the first 4 bytes of the Device Hash are used in the nonce.SERIAL|BODY, STATUS|DETAIL) MUST respect the same escape sequences defined in TagoTiP §4.4.
SERIAL|BODY — parse BODY as a push body (structured variables or passthrough)SERIAL|[VARNAME;...] — parse bracket-wrapped variable name listSERIAL — no bodySTATUS[|DETAIL] — parse as a downlink responseThe sequence counter serves a dual purpose in TagoTiP/S: replay protection and nonce derivation for AEAD encryption.
For general counter rules (initial value, increment, wraparound, persistence), see TagoTiP.md §10.
In the TagoTiP/S envelope, the counter is stored as a 4-byte Big-Endian unsigned integer in bytes 1–4 of the envelope header.
0x0000002A0x000003E8The Counter field in the envelope header is the sole source of the sequence number for TagoTiP/S messages. The !N field from standard TagoTiP frames is not present in the headless inner frame.
The server uses the Authorization Hash and Device Hash from the envelope header as routing hints to locate candidate devices and their Encryption Keys. After successful decryption, the server tracks the last-seen counter value per resolved device identity (not per the pre-decryption hash pair).
When the sequence counter is not used, the device MUST still provide a 4-byte value in the Counter field of the envelope. A random 4-byte nonce MAY be used as a last resort on devices without persistent storage, but this sacrifices replay protection and ordering guarantees. Devices with persistent storage MUST NOT use random nonces — they MUST use a monotonic counter. With a 32-bit random nonce, the birthday paradox yields approximately 50% collision probability after ~77,000 messages; deployments using random nonces SHOULD implement application-level safeguards to limit total message count per key.
Downlink frames use method ACK (0x3) in the Flags byte. If the uplink message was received as a TagoTiP/S envelope, the server MUST respond using TagoTiP/S on the same connection or session.
If the server cannot identify the profile or decrypt the envelope, it SHOULD respond with a plaintext ACK|ERR|auth_failed frame, since it cannot construct a valid TagoTiP/S response without the correct Encryption Key. If the envelope has an unsupported version, the server SHOULD respond with a plaintext ACK|ERR|unsupported_version frame. If the cipher suite is unsupported, the server SHOULD respond with a plaintext ACK|ERR|unsupported_cipher frame. If the envelope exceeds the size limits defined in §5.5, the server SHOULD respond with a plaintext ACK|ERR|envelope_too_large frame. On stream transports (TCP), these plaintext fallback frames MUST be length-prefixed per §5.4.
Note that replay counter rejection (when counter enforcement is enabled) occurs after successful decryption and device resolution (see §8.3), so the server responds with an encrypted ACK|ERR|invalid_seq envelope, not a plaintext fallback.
The headless inner frame for downlink is STATUS|DETAIL or just STATUS — the ACK| prefix from plaintext TagoTiP is not present (the method is in the Flags byte). See TagoTiP.md §9 for status codes and semantics.
On a given TagoTiP/S connection or session, the client MUST NOT send a new uplink request until it has received the ACK response for the previous request (i.e., at most one outstanding request at a time). The server MUST preserve request-response ordering.
Note: TagoTiP/S provides cryptographic integrity and confidentiality for downlink messages but does not provide replay protection at the envelope level. Applications that deliver state-changing commands (e.g.,
CMD|unlock_door) SHOULD implement application-level idempotency or command nonces.
Note: The
!Nresponse correlation mechanism from plaintext TagoTiP (§9.5) does not apply to TagoTiP/S. The envelope's Counter field serves as a nonce seed for the AEAD cipher — the server MUST manage its own counter or nonce space for downlink envelopes independently of uplink counter values. Correlation is unnecessary on the constrained transports TagoTiP/S targets.
When using TagoTiP/S with passthrough payloads (>x or >b), the server first unwraps the envelope (AEAD decryption automatically verifies integrity), then processes the resulting headless inner frame. If the body contains a passthrough payload, it is delivered to the payload parser the same way as in plaintext TagoTiP.
See TagoTiP.md §6.5 for passthrough syntax details.
The following is a fully computed test vector. Implementations SHOULD verify their output matches these values byte-for-byte.
Inputs:
Token: ate2bd319014b24e0a8aca9f00aea4c0d0
Serial: sensor-01
Encryption Key: fe 09 da 81 bc 44 00 ee 12 ab 56 cd 78 ef 90 12
Counter: 42
Method: PUSH
Cipher Suite: 0 (AES-128-CCM)
Derived values:
Auth Hash: 4d ee dd 7b ab 88 17 ec (SHA-256(token without "at")[:8])
Device Hash: ab 77 88 d2 2e b7 37 2f (SHA-256("sensor-01")[:8])
Flags: 0x00 (cipher 0, version 0, PUSH)
Headless inner frame (20 bytes):
ASCII: sensor-01|[temp:=32]
Hex: 73 65 6e 73 6f 72 2d 30 31 7c 5b 74 65 6d 70 3a 3d 33 32 5d
AEAD parameters:
Nonce (13B): 00 00 00 00 00 ab 77 88 d2 00 00 00 2a
(uses first 4 bytes of Device Hash)
AAD (21B): 00 00 00 00 2a 4d ee dd 7b ab 88 17 ec ab 77 88 d2 2e b7 37 2f
Output:
Ciphertext (20B): c8 c5 aa 56 d7 55 58 2b ac ea 13 bb 57 24 93 bb 8c b1 08 03
Auth Tag (8B): cf 82 6f db 83 3b 79 c6
Complete envelope (49 bytes):
00 00 00 00 2a 4d ee dd 7b ab 88 17 ec ab 77 88
d2 2e b7 37 2f c8 c5 aa 56 d7 55 58 2b ac ea 13
bb 57 24 93 bb 8c b1 08 03 cf 82 6f db 83 3b 79
c6
Breakdown: 1 (flags) + 4 (counter) + 8 (auth hash) + 8 (device hash) + 20 (ciphertext) + 8 (auth tag) = 49 bytes
Note: This test vector uses a pre-provisioned Encryption Key. The derived key for these credentials (§2.4) would be
e5 05 f0 3c ...— a different value, confirming that key derivation is optional.
Server has queued command for sensor-01.
Server wraps response in envelope using sensor-01's Device Hash, Flags = 0x03 (ACK),
and a unique nonce seed.
Headless inner frame: CMD|ota=https://example.com/v2.1.bin
Device decrypts using its Encryption Key and the reconstructed nonce.
AEAD decryption verifies authenticity; device reads method=ACK from Flags,
parses STATUS=CMD, DETAIL=ota=https://example.com/v2.1.bin.
Headless inner frame: sensor-01|[temp:=32] (20 bytes)
Counter: 1
Flags: 0x80 (cipher 4 = ChaCha20-Poly1305, version 0, PUSH)
Auth Hash: SHA-256("e2bd319014b24e0a8aca9f00aea4c0d0")[:8]
→ 4deedd7bab8817ec (8 bytes)
Device Hash: SHA-256("sensor-01")[:8] → 8 bytes (e.g., 0xab7788d22eb7372f)
Encryption Key: 32 bytes (pre-provisioned, ChaCha20 requires 256-bit key)
Step 1: Build Flags byte = 0x80 (cipher 4, version 0, PUSH)
Step 2: Build header (AAD) = [0x80] + [0x00000001] + [Auth Hash (8B)] + [Device Hash (8B)] → 21 bytes
Step 3: Nonce (12 bytes for ChaCha20) = 0x80 000000 ab7788d2 00000001
↑flags ↑ 3 zeros ↑ dev hash[:4] ↑ counter
Step 4: ChaCha20-Poly1305 encrypt (key, nonce, plaintext=20B inner frame, AAD=21B header)
→ 20 bytes ciphertext + 16 bytes auth tag
Step 5: Envelope = [Header (21B)] + [Ciphertext (20B)] + [Auth Tag (16B)]
Total: 1 + 4 + 8 + 8 + 20 + 16 = 57 bytes
[Flags] [zero-padding] [Device Hash[:4]] [Counter]. Only the first 4 bytes of the Device Hash are used in the nonce.ACK|ERR|auth_failed.Example breakdown for a 103-byte TagoTiP frame:
Full TagoTiP frame (103 bytes):
PUSH|4deedd7bab8817ec|sensor-01|@1694567890000^batch_42[temperature:=32#F@=39.74,-104.99{source=dht22}]
Headless inner frame (81 bytes):
sensor-01|@1694567890000^batch_42[temperature:=32#F@=39.74,-104.99{source=dht22}]
(removed "PUSH|4deedd7bab8817ec|" = 22 bytes)
Envelope (AES-128-CCM): 1 (flags) + 4 (counter) + 8 (auth hash) + 8 (device hash) + 81 (ciphertext) + 8 (auth tag) = 110 bytes
Envelope (GCM/ChaCha20): 1 + 4 + 8 + 8 + 81 + 16 (auth tag) = 118 bytes
Note: 29 bytes overhead with AES-128-CCM (8B tag), 37 bytes with GCM or ChaCha20-Poly1305 (16B tag).
TagoTiP/S is designed to protect constrained IoT links against passive eavesdropping, active message tampering, and replay attacks. The following are explicitly out of scope: key exchange and PKI (keys are provisioned out-of-band), denial-of-service at the transport level, and side-channel attacks on endpoint implementations. The protocol assumes that the Encryption Key has been securely provisioned via an out-of-band mechanism (see §2.3) and that endpoint devices can perform AEAD operations correctly.
at prefix). Because SHA-256 is preimage-resistant, the hash does not leak any bits of the original token. The Device Hash (first 8 bytes of SHA-256 of serial) is similarly protected. Neither hash compromises the Encryption Key[Flags (1B)] [zero-padding] [Device Hash[:4] (4B)] [Counter (4B)] (see §3.3 for length), using the first 4 bytes of the 8-byte Device Hash from the header. Including the Flags byte ensures that different cipher/method combinations produce distinct nonces even when the counter values coincide, preventing nonce reuse across directions and cipher suites. The Device Hash component ensures uniqueness per device even when multiple devices share the same encryption key and profile. When using a monotonic counter, the 4-byte counter field provides 2^32 unique nonces per device per direction; devices MUST re-provision before counter exhaustion. When using a random nonce (§8.4), nonce uniqueness is not guaranteed — the birthday paradox yields approximately 50% collision probability after ~77,000 messages. Devices with persistent storage MUST use a monotonic counter (see §8.4). Monotonic counters are RECOMMENDED for TagoTiP/S.Key rotation is supported without protocol changes. When a device's Encryption Key is rotated, the server simply trial-decrypts with both old and new keys — the same mechanism used for Device Hash collisions (see §2.2 and §12 step 9).
A dedicated Key Epoch header field was considered and intentionally omitted:
Implementations SHOULD support a configurable key rotation window (e.g., accept both old and new keys for a grace period after rotation). After the grace period, the server SHOULD remove the old key to reduce trial-decryption candidates.
When using optional key derivation (§2.4), the Authorization Token becomes the sole secret for all devices in a profile. Compromising the token allows deriving Encryption Keys for every device that uses derived keys under that profile. Pre-provisioned keys provide per-device isolation — compromising one device's key does not affect others.
Deployments requiring per-device key isolation SHOULD use pre-provisioned keys.
This specification is open source, published under the Apache License 2.0.
Anyone is free to implement TagoTiP/S — clients, servers, libraries, gateways, or any other component — for any purpose, including commercial use, without requiring permission from TagoIO Inc. The Apache 2.0 license includes an express patent grant to all implementers.
The names "TagoTiP", "TagoTiP/S", and "TagoIO" are trademarks of TagoIO Inc. See NOTICE for trademark details.
Copyright 2026 TagoIO Inc.