KSecureMessage ciphertext wire format¶
This document specifies how a KSecureMessage CiphertextMessage is encoded
into EncryptedEnvelope.payload. It is the KSecureMessage format, built on
Kodium's X3DH and Double Ratchet. It is not wire compatible with Signal or
libsignal.
Implementation: core/protocol/.../protocol/CiphertextMessageCodec.kt.
Layering¶
Since milestone 8 the plaintext is a reliability frame (logical message ID plus application body, or an acknowledgement), specified in message-reliability.md. It is encrypted, so this format does not change.
plaintext
-> ProtocolEngine (X3DH + Double Ratchet)
-> CiphertextMessage (RatchetMessage | PreKeyMessage)
-> CiphertextMessageCodec <- this document
-> EncryptedEnvelope.payload
-> transport
The codec only frames data. It performs no cryptography and carries the ratchet bytes unchanged. A future sealed-sender layer would wrap the encoded bytes in an outer envelope; the inner format stays the same.
EncryptedEnvelope.protocolVersion (currently 1) versions the envelope
metadata (routing fields). The wire version below versions the payload bytes.
Conventions¶
- All integers are unsigned 32-bit, big-endian (network byte order), written
u32.u8is one byte. - Prekey IDs are
u32values with the high bit clear:0 .. 2^31-1(0x00000000 .. 0x7FFFFFFF). Values0x80000000 .. 0xFFFFFFFFare invalid. - Public keys are exactly 64 bytes: a 32-byte X25519 key followed by a 32-byte Ed25519 key. They have no length prefix.
- Ratchet bytes are Kodium's serialized ratchet message (header plus
authenticated ciphertext). They are opaque to this format and prefixed with
a
u32length. - There are no optional trailing fields and no padding. A message is exactly the bytes listed below.
Header¶
| Offset | Size | Field | Value |
|---|---|---|---|
| 0 | 1 | wire version | 0x01 |
| 1 | 1 | message type | 0x01 RatchetMessage, 0x02 PreKeyMessage (initiation v1), 0x03 PreKeyMessage (initiation v2, S1) |
All other version and type values are reserved and rejected.
RatchetMessage (type 0x01)¶
A message on an established session.
| Offset | Size | Field |
|---|---|---|
| 0 | 1 | version 0x01 |
| 1 | 1 | type 0x01 |
| 2 | 4 | ratchet length L (u32, 1 ..= 262144) |
| 6 | L | ratchet bytes |
PreKeyMessage (types 0x02 and 0x03)¶
A ratchet message plus the X3DH data the recipient needs to create its side of the session. The initiator sends these until it has decrypted a reply.
Both types have exactly the same layout. The type selects the session
initiation version (S1, session-lifecycle.md):
0x03 = version 2, whose associated data is the canonical transcript that
binds both device addresses and every header field; 0x02 = version 1,
from before S1, which is decoded but only continues an existing version 1
session. Every new initiation is sent as 0x03. The type byte itself is not
covered by the AEAD, but rewriting it changes the associated data the
receiver uses, so a relabeled message fails to decrypt, and a 0x02
message never creates anything.
| Field | Size | Notes |
|---|---|---|
| version | 1 | 0x01 |
| type | 1 | 0x02 (initiation v1) or 0x03 (initiation v2) |
| signed prekey ID | 4 | u32, high bit clear |
| one-time prekey flag | 1 | 0x00 absent, 0x01 present; other values invalid |
| one-time prekey ID | 4 | u32, high bit clear; only if flag is 0x01 |
| identity key | 64 | initiator's identity public key |
| ephemeral key | 64 | initiator's ephemeral (base) public key |
ratchet length L |
4 | u32, 1 ..= 262144 |
| ratchet bytes | L | the first ratchet message |
The ratchet length starts at offset 135 without and 139 with a one-time prekey ID.
Size limits¶
| Limit | Value |
|---|---|
| max ratchet bytes | 262144 (256 KiB) |
| largest header (PreKeyMessage with one-time prekey) | 143 bytes |
| max encoded message | 262287 bytes |
Empty ratchet bytes (L = 0) are invalid.
Decoding rules¶
Treat input as untrusted. A decoder must reject, and never partially accept:
- input longer than the max encoded size (
MessageTooLarge); - input shorter than 2 bytes (
MalformedMessage); - a version other than
0x01(UnsupportedWireVersion); - a type other than
0x01/0x02/0x03(UnknownMessageType); - a ratchet length above 262144, including any value with the high bit set
(
MessageTooLarge), checked before reading the bytes; - truncated fields, a length larger than the remaining input,
L = 0, a prekey ID with the high bit set, a flag other than0x00/0x01, or any trailing bytes (MalformedMessage).
All errors are ProtocolException subclasses. Their messages never contain
payload or key bytes.
Encoding is deterministic: the same message always produces the same bytes.
Examples¶
RatchetMessage with ratchet bytes DE AD BE EF:
01 01 00 00 00 04 DE AD BE EF
PreKeyMessage, signed prekey ID 7, one-time prekey ID 42, identity key
64 × 11, ephemeral key 64 × 22, ratchet bytes CA FE:
01 02 version, type
00 00 00 07 signed prekey ID
01 one-time prekey present
00 00 00 2A one-time prekey ID
11 × 64 identity key
22 × 64 ephemeral key
00 00 00 02 ratchet length
CA FE ratchet bytes
PreKeyMessage, signed prekey ID 0x01020304, no one-time prekey, same keys,
ratchet bytes AB CD EF:
01 02 01 02 03 04 00 version, type, signed prekey ID, flag absent
11 × 64 identity key
22 × 64 ephemeral key
00 00 00 03 AB CD EF ratchet length, ratchet bytes
These vectors are pinned in CiphertextMessageCodecTest, together with the
same two messages as type 0x03 (only byte 1 differs:
01 03 00 00 00 07 01 … and 01 03 01 02 03 04 00 …).
Compatibility¶
- Version 1 is frozen. The test vectors must keep decoding and encoding to the
same bytes. S1 added message type
0x03without changing any layout; a decoder from before S1 rejects it asUnknownMessageType, so peers must be upgraded together. - Any change to the layout, field set or limits that old decoders would misread needs a new wire version. Decoders reject versions they do not know.
- The X3DH and ratchet info strings (
KSecureMessage-X3DH-v1,KSecureMessage-Ratchet-v1, seeProtocolConstants) are part of the protocol. Changing them breaks every session regardless of wire version.