This repository provides a Rust implementation of the Plabble Transport Protocol.
You can read the old spec here
The Plabble Protocol works with messages called packets. There are two groups of packets: request packets and response packets. Request packets are the packets that are sent from the client to the server and response packets are packets that are sent from the server to the client. We cannot make it easier for you!
Plabble packets can be used in two forms: TOML and binary. The TOML variant is only used for unencrypted payloads or to communicate with a library or service that translates the packets to encrypted, binary payloads. The TOML variant is not meant to be sent over the Plabble network, but can be used with Plabble-over-HTTP(S). In this documentation we use the TOML variant a lot because it is very human-readable and easy to explain.
Every Plabble packet contains of 3 parts, the base, the header and the body. The header and body are different depending on if it is a request or response and the packet type.
-
The Plabble Transport Protocol is build upon several packet types.
-
The packet types, their flags and header fields can be found in type_and_flags.rs.
-
1 SESSION
-
2 GET
-
3 STREAM
-
4 POST
-
5 PATCH
-
6 PUT
-
7 DELETE
-
10 REGISTER
-
11 IDENTIFY
-
12 PROXY
-
13 CUSTOM
-
14 OPCODE
-
15 ERROR
- Every packet extends the Plabble packet base.
- Implementation: base/mod.rs
The base of a packet will not be encrypted for it is required for the processing and decryption of the packet. It also does not contain any sensitive information. However, all encryption methods SHOULD make sure the base cannot be tampered by using a MAC or authenticated data.
A Plabble packet base has the following base structure:
# [u4] the version number of Plabble. 0 = debug. Serialized as first 4 bits of Plabble packet
version = 1
# [bit] 1st bit flag. If set to true, the packet is sent outside of a session and no or only a single response is expected
fire_and_forget = false
# [bit] 2nd bit flag. If set to true, the packet uses a pre-shared key for the encryption of this packet. The PSK ID MUST be included if this flag is set
pre_shared_key = false
# [bit] 3rd bit flag. If set to true, this packet uses the Plabble encryption. If set to false, a MAC MUST be included
use_encryption = false
# [bit] 4th bit flag. If set to true, next byte will contain 7 flags for cryptography settings. If not set, the defaults will be used.
specifiy_crypto_settings = true
# [12B] required if pre_shared_key flag is set.
psk_id = "base64url (no padding) pre-shared key ID"
# [16B] required if pre_shared_key flag is set. Random bytes to salt the key generated from PSK
psk_salt = "base64url (no padding) random generated salt"
# [1B] required if specify_crypto_settings is true
[crypto_settings]
encrypt_with_chacha = true # default true, use ChaCha20(Poly1305)
encrypt_with_aes = false # use AES 256 (CTR/GCM) for encryption
# 1 reserved flag for future use
use_blake3 = false # use Blake3 instead of Blake2
sign_ed25519 = true # default true, use Ed25519 for signing
key_exchange_x25519 = true # default true, use X25519 for exchange
# 1 reserved flag for future use
use_post_quantum = false # if set, include another byte with PQ eencryption settings
# [1B] required if crypto_settings.use_post_quantum is set
[crypto_settings.post_quantum_settings]
sign_pqc_dsa_44 = false # use DSA44 for signing
sign_pqc_dsa_65 = false # use DSA65 for siging
sign_pqc_falcon = false # use Falcom-1024 for signing
sign_pqc_slh_dsa = false # use SLH-DSA-SHA128s for signing
key_exchange_pqc_kem_512 = false # use ML-KEM-512 for key exchange
key_exchange_pqc_kem_768 = false # use ML-KEM-768 for key exchange
# 2 reserved flagsMost of these properties are optional and will only be sent in an initial request.
- A Plabble Request packet contains of the packet base, the request header and the request body.
- Header implementation: packets/header/request_header.rs
- Body implementation: packets/body/mod.rs
- For each request type, a different request body will be used.
The Plabble request packet looks like this:
version = 1
# ... and other base properties/flags
[header]
id = "..." # bucket ID if applicable
packet_type = "Session" # the packet type in PascalCase
# ... type-specific header fields/flags. See type_and_flags.rs
[body]
# ... type-specific properties- A Plabble Response packet contains of the packet base, the response header and the response body.
- Header implementation: packets/header/response_header.rs
- Body implementation: packets/body/mod.rs
- For each response type, a different response body will be used.
The Plabble response packet looks like this:
version = 1
# ... and other base properties/flags
[header]
packet_type = "Session" # the packet type in PascalCase
request_counter = 1 # counter of the request to reply to (the server counts the client requests in a session). Optional. Only required if fire_and_forget is NOT set
# ... type-specific header fields/flags. See type_and_flags.rs
[body]
# ... type-specific properties- Goal: Get the server certificate or another certificate by ID, prove server identity ...
- Implementation: certificate.rs
- The client sends the ID of the certificate it wants to request (or no ID if it wants the certificate of the server), a challenge (or no challenge if the client is not interested in proving the servers' identity)
- The server retrieves the certificates the client requested, and signs the challenge + certificate(s) bytes
- The client verifies if the signature is correct
- The client stores the certificates in its certificate store so it doesn't need to request it another time
Request header flags:
- full_chain: if set, the server does not only return 1 certificate but all certificates in the certificate chain
- full_certs: if set, the server will return the full certificates of the chain. If not set, the chain certificates (all certificates above the certificate that is requested) are not fully sent, but only in a compact form (without body)
- challenge: if set, the client sends a 16-byte random challenge to the server to sign
- query_mode: if set, the client specifies the certificate to retrieve by its certificate ID
Request body:
- id: the ID of the certificate the client wants to retrieve, 16 bytes, REQUIRED when query_mode is set
- challenge: the challenge for the server to sign, 16 bytes, REQUIRED when challenge flag is set.
Example:
version = 1
use_encryption = true
[header]
packet_type = "Certificate"
full_chain = true # this is the default
full_certs = true # this is the default
challenge = true # default is false
query_mode = true # default is false
[body]
id = "AAAAAAAAAAAAAAAAAAAAAA"
challenge = "AQEBAQEBAQEBAQEBAQEBAQ"No response flags.
Response body:
- signatures: signatures for each algorithm determined by the crypto settings in the base, in order. Each signature consist of challenge + list of certificates (as bytes)
- certificates: list of Plabble certificates based on the request.
Example:
version = 1
use_encryption = true
[header]
packet_type = "Certificate"
request_counter = 1
[[body.signatures]]
Ed25519 = "AQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQ"
[[body.certificates]]
id = "AgICAgICAgICAgICAgICAg"
uri = "..."
valid_from = "2025-05-15T12:00:00+00:00"
valid_until = "2026-01-01T00:00:00+00:00"
issuer_uri = "..."
data = "CA=plabble"
[[body.certificates.keys]]
Ed25519 = "AwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwM"
[[body.certificates.signatures]]
Ed25519 = "BAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBA"The client should only trust a certificate if the chain is verified and the signatures are correct!
- Goal: exchange keys with a server, create a Session Key.
- Implementation: session.rs
WARNING: You can only start a secure session if you know and trust the server's certificate!
- The client generates one or more keypairs using one or more algorithms it wants to use for this session.
- The client sends a session request packet. The client COULD specify the cryptographic algorithms it wants to use using the
crypto_settings. The public keys or encapsulation keys should be included. - The server also generates the keypairs for the algorithms the client requested, if applicable. Or it encapsulates a shared secret.
- The server signs the entire plain text request (in binary form) the client sent using each signature algorithm the client specified in the
crypto_settingsAND the response (psk_id, optionally salt and keys) it will return to the client to ensure integrity. - The server generates a shared secret and derives a session key from it. Optionally it stores the key if the client requested to store it as a PSK.
- The server returns a session response.
- The client checks the signatures the server returned using the public keys in the server certificates it already SHOULD know. This step is optional, but strongly recommended for integrity.
- The client also generates a shared secret and derives the session key from it.
Request header flags:
- persist_key: If set to true, persist the generated shared secret key and return a server-generated PSK ID.
- enable_encryption: If set to true, switch to Plabble encrypted communication between the client and the server to enjoy full packet encryption
- with_salt: If set to true, include 16-byte random generated salt by client
- request_salt: If set to true, force server to (also) generate and use a salt
Request body:
- psk_expiration: 4-byte Plabble timestamp to request the server to delete the pre-shared key afterwards. REQUIRED if persist_key header flag is set.
- salt: 16-byte salt for the KDF algorithm that is used to create the session key. REQUIRED if with_salt is set.
- keys: One or more key exchange algorithm from the following options:
X25519(32B),Kem512(800B) orKem768(1184B). They are encoded in the same order as they are in the crypto settings. See key_exchange.rs.
Example:
version = 1
specifiy_crypto_settings = true
[crypto_settings]
key_exchange_x25519 = true # this is the default
sign_ed25519 = true # this is the default
use_post_quantum = true
[crypto_settings.post_quantum_settings]
key_exchange_pqc_kem_512 = true
key_exchange_pqc_kem_768 = true
[header]
packet_type = "Session"
persist_key = true
with_salt = true
request_salt = true
[body]
psk_expiration = 2025-05-27T07:32:00Z
salt = "..."
[[body.keys]]
X25519 = "..."
[[body.keys]]
Kem512 = "..."
[[body.keys]]
Kem786 = "..."The server SHOULD respect the cryptography settings of the client. If the server does not support the cryptographic algorithms the client asked for, it MUST abort the connection with an error code.
Response header flags:
- with_psk: If set, the client requested to store the session key as a PSK. This is a 12-byte ID.
- with_salt: If set to true, the response contains a 16-byte random generated salt by the server.
Response header body:
- psk_id: 12-byte ID the server assigned to the stored key derived from the shared secret of this session. REQUIRED if with_psk is set.
- salt: 16-byte salt generated by server. REQUIRED if with_salt is set.
- keys: List of public keys or encapsulated secrets the server generated according to the request. Similar to the request.
- signatures: List of signatures the server created from the client request and the psk_id and keys to ensure integrity. Encoded the same way as the keys in the request. See signatures.rs
Example:
version = 1
# crypto settings etc. inherited from request
[header]
packet_type = "Session"
with_psk = true
with_salt = true
[body]
psk_id = "..." # base64url (no padding) encoded 12-byte key ID
salt = "..."
[[body.keys]]
X25519 = "..."
[[body.keys]]
Kem512 = "..." # The KEM keys aren't public keys but encapsulated secrets
[[body.keys]]
Kem768 = "..."
[[body.signatures]]
Ed25519 = "..."The client SHOULD validate the signatures and validate if the server returned the algorithms it asked for! Else it should not trust the session and disconnect.
- Goal: request data from one or more slots inside a
bucketon the server, optionally subscribe to updates. - Implementation: bucket.rs
- The client builds a
Getrequest targeting a single bucket (identified by a 16-byte ID) and chooses a range to read. - The server looks up the bucket and returns the requested slots (or an empty result if none match).
- If the
subscribeflag is set, the server may continue to send updates for the requested keys/slots until the subscription is cancelled.
Request header flags:
- binary_keys: keys in the request/response are UTF-8 strings instead of numeric slot indexes.
- subscribe: request a subscription for changes on the requested keys/range.
- range_mode_until: treat a single provided range value as an "until" (end-only) bound.
Request header:
- id: 16-byte bucket identifier (base64/URL-safe when using the TOML representation).
Request body:
- range: either
Numeric(start?, end?)orBinary(start_key?, end_key?)(both bounds are optional). Numeric ranges useu16slots; binary ranges use UTF-8 keys.
Example (numeric range):
version = 1
use_encryption = true
[header]
packet_type = "Get"
id = "AAAAAAAAAAAAAAAAAAAAAA" # 16-byte id (base64url (no padding))
[body]
range.Numeric = [5, 25]Example (binary range):
version = 1
use_encryption = true
[header]
packet_type = "Get"
id = "AAAAAAAAAAAAAAAAAAAAAA"
binary_keys = true
[body]
range.Binary = ["key_start", "key_end"]Response header flags:
- binary_keys: indicates keys in the response are UTF-8 strings (matches the request's
binary_keys).
Response body:
Getresponses carry aBucketBody(see implementation). For numeric buckets this is a key-value set with numbers and binary values, for binary it is a key-value set with both binary keys and values.
Notes:
- Values in the TOML examples are typically encoded as base64 when represented in text (see
BucketBodyin the crate). - When
subscribeis set the server semantics for update delivery are implementation-dependent (push over the current connection or via separate subscription messages) but should follow the protocol's subscription guarantees.
- Goal: Read from or write (stream) bytes to a slot inside a bucket.
- Implementation: stream.rs
Note: Streams operate on a single bucket slot and may require session authentication or bucket permissions depending on the server implementation.
- The client opens a
Streamrequest targeting a bucket and a slot (numeric or binary key). - For reads the server returns either the requested byte range or a stream of bytes (implementation-dependent).
- For writes the client includes
dataand the server applies/append the bytes and returns the new slot size. - If a
subscribeflag is set, the server may send updates for the slot until the subscription is cancelled.
Request header flags:
- binary_keys: Keys in the request are UTF-8 strings instead of numeric slot indices.
- subscribe: Subscribe to changes on the requested slot/range.
- range_mode_until: Treat a single provided range value as an "until" bound.
- write_mode: When set, the request contains
datato write/append instead of a read.
Request header:
- id: 16-byte bucket identifier (base64url when using TOML).
Request body:
- data: Optional bytes to write (Base64 URL-safe, no padding). Present only when
write_modeis set. - range: Either
Numeric(start, from?, to?)orBinary(key, from?, to?)to select the byte range.
Examples (read):
version = 1
[header]
packet_type = "Stream"
id = "AAAAAAAAAAAAAAAAAAAAAA"
[body]
# bucket with id AAA.. at slot 7, from byte 08 to byte 0f
range.Numeric = [7, 8, 0x0f]Examples (write):
version = 1
[header]
packet_type = "Stream"
id = "AAAAAAAAAAAAAAAAAAAAAA"
binary_keys = true
write_mode = true
[body]
data = "1KKeSJOs" # base64url (no padding)
range.Binary = ["test"]Response header flags:
- write_mode: Indicates this is a write response (no
datareturned, butnew_size).
Response body:
- data: Present for read responses. Encoded as Base64 URL-safe (no padding).
- new_size: Present for write responses, new slot size as integer.
Example (read response):
version = 1
[header]
packet_type = "Stream"
request_counter = 2
[body]
data = "1KKeSJOs"Example (write response):
version = 1
[header]
packet_type = "Stream"
request_counter = 2
write_mode = true
[body]
new_size = 7When
datais present in TOML examples it is the Base64 URL-safe (unpadded) representation used by the codec.
- Goal: Create a new bucket on the server ...
- Implementation: post.rs
- Requirement: a Session MUST be established
- The client generates a bucket id and determines the bucket permissions.
- The client sends the request to the server, the server creates the bucket (if it doesn't exist)
- The client and server derive a bucket key from the current session
- The server sends an empty response or an error to indicate if it succeeded
Request header flags:
- binary_keys: If you want to subscribe to the bucket, specify if the keys are binary or numeric
- subscribe: Subscribe to the bucket when it is created
- range_mode_until: Use "until" range mode when subscribing to the bucket
- do_not_persist: Create a Memory bucket instead, not persisting the bucket to the server storage but keeping it in RAM (not all servers will allow this)
Request header
Request body:
- settings: The bucket settings and permissions you want to use
- range: Range if you want to subscribe to the bucket, REQUIRED if subscribe is set.
Example:
version = 1
use_encryption = true
[header]
packet_type = "Post"
subscribe = true
[body]
id = "@test"
range.Numeric = [1, 10] # subscribe to key 1-10
settings.access_control_list = [] # If you want to prefill the ACL, add base64-urlencoded user IDs here
[body.settings.permissions]
public_read = true # this is one of the defaults
# ...Empty response without flags.
- Goal: Update bucket permissions or modify ACL entries
- Implementation: patch.rs
- The client sends a
Patchrequest targeting a bucket and specifying which parts to update (permissions and/or ACL changes). - The server applies the requested changes, avoiding duplicates when adding and ignoring non-existent entries when removing.
Request header flags:
- update_permissions: when set, the
permissionsobject in the body will be applied to the bucket. - add_to_acl: when set, entries in
acl_addin the body will be appended to the ACL (duplicates ignored). - remove_from_acl: when set, entries in
acl_delin the body will be removed from the ACL (non-existent entries ignored).
Request header:
- id: 16-byte bucket identifier (base64url when using TOML).
Request body:
- permissions: Optional
BucketPermissionsobject describing permission flags to set (see post.rs). - acl_add: Optional list of base64url (no padding) encoded 16-byte user IDs to add to the ACL.
- acl_del: Optional list of base64url (no padding) encoded 16-byte user IDs to remove from the ACL.
Example (update permissions):
version = 1
use_encryption = true
[header]
packet_type = "Patch"
id = "AAAAAAAAAAAAAAAAAAAAAA"
update_permissions = true
[body]
[body.permissions]
public_read = true
public_write = true
protected_delete = trueExample (add ACL entries):
version = 1
use_encryption = true
[header]
packet_type = "Patch"
id = "AAAAAAAAAAAAAAAAAAAAAA"
add_to_acl = true
[body]
acl_add = ["AAAAAAAAAAAAAAAAAAAAAAAAAAA"]Example (add and remove ACL entries):
version = 1
use_encryption = true
[header]
packet_type = "Patch"
id = "AAAAAAAAAAAAAAAAAAAAAA"
add_to_acl = true
remove_from_acl = true
[body]
acl_add = ["AAAAAAAAAAAAAAAAAAAAAAAAAAA"]
acl_del = ["AAAAAAAAAAAAAAAAAAAAAAAAAAA"]Empty response without flags.
- Goal: Add or append data to one or more slots in a bucket.
- Implementation: bucket.rs
- The client selects the target bucket by
idand prepares the data to write or append. - The client sends a
PUTrequest describing the keys/slots and their new contents. - The server updates or appends the provided slots and returns an empty success response or an error.
Request header flags:
- binary_keys: Use string keys instead of numeric slot indexes.
- subscribe: Also subscribe to updates for the provided keys after the write.
- assert_keys: Fail if any provided keys already exist (useful for insert-only semantics).
- append: Append to existing slot values instead of overwriting.
Request header:
- id: the Bucket ID identifying the target bucket.
Request body:
- body: the
BucketBodyto write; useNumericfor u16 slot indexes orBinaryfor string keys.
Example (numeric keys, overwrite):
version = 1
use_encryption = true
[header]
packet_type = "Put"
id = "AAAAAAAAAAAAAAAAAAAAAA"
[body]
body.Numeric = { 5 = "AAAAAA", 7 = "AAAAAAAA" }Example (binary keys, append):
version = 1
use_encryption = true
[header]
packet_type = "Put"
id = "AAAAAAAAAAAAAAAAAAAAAA"
binary_keys = true
append = true
[body]
body.Binary = { name = "AAAA", alias = "AAAAAA" }Empty response without flags.
- Goal: Remove entries from a bucket or delete the entire bucket.
- Implementation: bucket.rs
- The client specifies the target bucket by
idand the range or keys to remove. - The client sends a
DELETErequest describing the keys/slot range to delete. - The server removes matching slots (or the whole bucket) and returns an empty success response or an error.
Request header flags:
- binary_keys: Use string keys instead of numeric slot indexes.
- range_mode_until: Treat a single provided range value as an end-only bound (until).
Request header:
- id: 16-byte bucket identifier (base64url when using TOML).
Request body:
- range: a
BucketQuerydescribing which slots to delete. UseNumeric(start?, end?)for u16 ranges orBinary(start_key?, end_key?)for string-keyed buckets. If the range is empty, the entire bucket will be deleted.
Example (numeric range):
version = 1
use_encryption = true
[header]
packet_type = "Delete"
id = "AAAAAAAAAAAAAAAAAAAAAA"
[body]
range.Numeric = [0, 20]Example (binary range):
version = 1
use_encryption = true
[header]
packet_type = "Delete"
id = "AAAAAAAAAAAAAAAAAAAAAA"
binary_keys = true
[body]
range.Binary = ["old_key_start", "old_key_end"]Empty response without flags.
- Goal: Subscribe to updates for one or more keys or a key range inside a bucket.
- Implementation: bucket.rs
- The client sends a
Subscriberequest targeting a bucket and a range of keys. - The server acknowledges and, depending on server semantics, starts delivering update messages for matching keys until the subscription is cancelled with an
Unsubscriberequest or the session ends.
Request header flags:
- binary_keys: keys in the request/response are UTF-8 strings instead of numeric slot indexes.
- range_mode_until: treat a single provided range value as an "until" (end-only) bound.
Request header:
- id: 16-byte bucket identifier (base64url when using TOML).
Request body:
- range: a
BucketQuerydescribing which keys to subscribe to. UseNumeric(start?, end?)for u16 ranges orBinary(start_key?, end_key?)for string-keyed buckets.
Example (numeric range):
version = 1
use_encryption = true
[header]
packet_type = "Subscribe"
id = "AAAAAAAAAAAAAAAAAAAAAA"
[body]
range.Numeric = [5, 25]Empty response without flags (server-specific subscription messages follow on updates).
- Goal: Cancel an active subscription for a bucket range or keys.
- Implementation: bucket.rs
- The client sends an
Unsubscriberequest for the previously subscribed bucket/range. - The server stops sending update messages for that subscription and returns an empty response.
Request header flags:
- binary_keys: keys in the request/response are UTF-8 strings instead of numeric slot indexes.
- range_mode_until: treat a single provided range value as an "until" bound.
Request header:
- id: 16-byte bucket identifier (base64url when using TOML).
Request body:
- range: a
BucketQuerydescribing which keys to stop subscribing to.
Example (numeric range):
version = 1
use_encryption = true
[header]
packet_type = "Unsubscribe"
id = "AAAAAAAAAAAAAAAAAAAAAA"
[body]
range.Numeric = [5, 25]Empty response without flags.
- Goal: Create a new identity on the server (register a certificate).
- Implementation: register.rs
Warning: This request SHOULD NOT be accepted without encryption; onboarding policies may vary by server. The server MUST validate the claims to be unique.
- The client generates one or more signing keypairs according to
crypto_settings. - The client sends a
Registerrequest containing the public keys andclaimsdescribing the identity. - The server validates the request, optionally applies screening/onboarding rules, and issues a certificate.
- The server returns the newly created certificate in the response body. This SHOULD be a full certificate.
Request header:
- packet_type:
Register(no extra header flags defined)
Request body:
- keys:
Vec<VerificationKey>โ public signing keys for the algorithms declared incrypto_settings(multi-enum). - claims:
Stringโ key-value claims, UTF-8, separated by;, e.g.USERNAME=henk;AGE=24.
Example:
version = 1
specify_crypto_settings = true
[crypto_settings]
sign_ed25519 = true
[header]
packet_type = "Register"
[body]
claims = "USERNAME=henk;AGE=24"
[[body.keys]]
Ed25519 = "8KIgA6PQbtFvWSCgPBKXx0LCgb2kiV6nyspoLCdr8Jg"Response body:
- certificate:
Certificateโ the issued certificate for the new identity (seecertificate.rs).
Example:
[header]
packet_type = "Register"
response_to = 0
[body.certificate]
full_cert = true
root_cert = false
keys = ["<public key base64>"]
valid_from = "2026-01-01T00:00:00+00:00"
valid_until = "2027-01-01T00:00:00+00:00"
signatures = ["<CA signature base64>"]
data = "CA=plabble;CN=Username"
uri = "https://certs.plabble.org/certs/test.cert"
parent_uri = "plabble://chat.plabble.org/certificate?id=..."- Goal: prove identity to the server by offering a certificate (or chain) and signatures to bind an identity to the current session.
- Implementation: identify.rs
Warning: The server SHOULD NOT persist identities; identities are intended to be ephemeral and are typically stored in RAM only. The server MUST validate the
timestampto prevent replay attacks and decide an acceptance window for identities.
- The client creates an
Identifyrequest containing atimestamp, one or moresignaturesand acertificateschain. - The server verifies the timestamp is within an acceptable time window to avoid replay attacks.
- The server validates the provided signatures using the public keys present in the certificate chain (algorithms from
crypto_settings). - If valid, the server binds the identity to the session temporarily and may accept identity-bound actions (e.g. ACL updates) for a limited time.
Request header:
- packet_type:
Identify(no additional header flags defined)
Request body:
- timestamp:
PlabbleDateTimeโ Plabble timestamp, REQUIRED. Server checks freshness. - signatures:
Vec<CryptoSignature>โ one or more signatures (multi-enum) covering thetimestampand other session-specific data (session_keyandserver_id, which is the server certificate ID). Algorithms followcrypto_settingsin the base packet. - certificates:
Vec<Certificate>โ certificate chain where the first entry is the certificate of the identity owner (may be a compact or full certificate depending onfull_certfield).
Example:
version = 1
specify_crypto_settings = true
[crypto_settings]
sign_ed25519 = true
[header]
packet_type = "Identify"
[body]
timestamp = "2026-01-01T00:00:00+00:00"
[[body.signatures]]
Ed25519 = "46wfjNJEaxr4S9Jk0mfLDR00Vt_0Qv_jQQQDAuPKJzzMs9oQ3ySqjNT1s2yCNgcMnu-eaBUlhaIJ3Zr8OrYX2A"
[[body.certificates]]
full_cert = false
id = "nRJh5IWAQYQA0czZqIntNw"
uri = "plabble:plabble.org/certs/@maus"Empty response body on success.
- Goal: Allow for custom packet types that are not defined in the Plabble specification but still follow the general packet structure and can be encrypted and signed. This packets are used by so-called sub-protocols.
- Implementation: custom.rs
The server should reject unknown sub-protocols with an
UnsupportedSubProtocolerror. Sub-protocols are expected to define their own packet structure and semantics within thebodyfield, but they must still adhere to the overall Plabble packet format and can leverage encryption and signing as usual.
Request header flags:
- flag1: application-defined flag for sub-protocol (boolean)
- flag2: application-defined flag for sub-protocol (boolean)
- flag3: application-defined flag for sub-protocol (boolean)
- flag4: application-defined flag for sub-protocol (boolean)
Request header:
- packet_type:
Custom
Request body:
- protocol:
u16โ numeric protocol identifier that selects the sub-protocol handler on the server. REQUIRED. - data:
Vec<u8>โ raw binary payload for the chosen sub-protocol. Represented as base64url (no padding) in TOML/JSON; interpretation is protocol-specific.
Behaviour:
- The server SHOULD route the request to the registered handler for the given
protocolid. If the protocol is unknown the server MUST return anUnsupportedSubProtocolerror (seeErrors). - Sub-protocol handlers are responsible for validating, authenticating and safely parsing
data. The server MUST NOT execute untrusted code embedded indata.
Example (request):
version = 1
[header]
packet_type = "Custom"
flag1 = true
flag2 = false
flag3 = true
flag4 = false
[body]
protocol = 42
data = "AQIDBAU"Response header flags:
- request_counter: standard response counter when replying in a session
Response body:
- protocol:
u16โ the protocol id echoed back (optional, handlers may include it) - data:
Vec<u8>โ protocol-specific response bytes
Example (response):
version = 1
[header]
packet_type = "Custom"
request_counter = 7
[body]
protocol = 42
data = "AQIDBAU"- Goal: Execute a small, sandboxed server-side script (an OPCODE) and return its result.
- Implementation: opcode.rs
Warning: OPCODE execution can be dangerous. Servers MUST enforce strict limits (CPU, memory, allowed operations) and only enable
allow_evalorallow_bucket_operationswhen explicitly permitted by policy.
- The client sends an
Opcoderequest containing a script to execute and optional flags requesting capabilities. - The server validates the script and requested flags, enforces execution limits and permissions, then executes the script in a sandboxed environment.
- The server returns an
Opcoderesponse containing an optional binaryresultproduced by the script or an Error packet on failure.
Request header flags:
- allow_bucket_operations: boolean โ allow the script to perform bucket operations (read/write/append) if the server permits it.
- allow_eval: boolean โ allow evaluation operations inside the script; this can be dangerous and servers may disallow it.
Request header:
- packet_type:
Opcode
Request body:
- script:
OpcodeScriptโ the script to run (sequence of opcodes). Seesrc/scripting/opcode_script.rsfor structure.
Example (request):
version = 1
[header]
packet_type = "Opcode"
allow_bucket_operations = false
allow_eval = false
[body.script]
instructions = [{ PUSHINT = 5 }, { PUSHINT = 2 }, { PUSHINT = 3 }, "ADD", "EQ", { PUSH2 = "0102" }]Response header flags:
- request_counter: present when replying in a session to correlate the response.
Response body:
- result:
Option<Vec<u8>>โ optional binary result of the script execution. In TOML/JSON this is represented as a hex string.
Example (response):
version = 1
[header]
packet_type = "Opcode"
request_counter = 7
[body]
result = "0102030405"- Goal: communicate a failure or rejection for a previously received request. The
Errorpacket is response-only and is used to signal protocol, authentication or application-level problems. - Implementation: error.rs
Note: see the consolidated error code list under Errors for human-readable descriptions and the numeric codes.
- When a server encounters a problem processing a request (invalid format, unsupported algorithm, missing resource, permission denied, etc.), it replies with an
Errorresponse packet. - The client should interpret the
typefield and any accompanying fields to determine the cause and whether retry, authentication or other remediation is required.
This packet type is response-only; there is no request variant.
Response header flags:
- request_counter: present when replying inside a session to correlate with the originating request.
Response body:
- type: an enum discriminator indicating the error variant (see implementation in
error.rs).
Variant-specific fields (examples):
UnsupportedVersion:min_version(u8),max_version(u8).UnsupportedAlgorithm:name(string) โ name of the unsupported algorithm.UnsupportedSubProtocol: no additional fields (sub-protocol not implemented).BucketNotFound,BucketAlreadyExists,CertificateNotFound,CertificateInvalid: no extra fields beyond the type (see## Errorslist for contextual meaning).OpcodeScriptError(ScriptError):ScriptErroris a error from the opcode script execution engine, see interpreter.rs for details.
Example (UnsupportedVersion response):
version = 1
use_encryption = true
[header]
packet_type = "Error"
request_counter = 1
[body]
type = "UnsupportedVersion"
min_version = 1
max_version = 3- Implementation: error.rs
- UnsupportedVersion: Requested Plabble protocol version not supported by server. Body:
min_version(min supported version by server),max_version(max supported version by server). Occurence: every request Plabble packet. - UnsupportedAlgorithm: Requested algotithm (in cryptography settings) is not supported by the server. Body:
nameThe name of the algorithm(s) that is not supported, UTF-8 dynint length encoded. Occurence: any packet, but especially Session, Certificate and other packets that use cryptography settings. - UnsupportedSubProtocol: Requested subprotocol is not supported. Occurence: only in Custom packets.
- BucketNotFound: Requested bucket was not found
- BucketAlreadyExists: Bucket with that ID already exists. Occurence: Post
- CertificateNotFound: Requested certificate (by id) was not found. Occurence: Certificate
- CertificateInvalid: Requested certificate was not valid. Occurence: Certificate
- OpcodeScriptError: An error occurred during OPCODE script execution. Body:
ScriptError(seeinterpreter.rsfor details). Occurence: OPCODE
The Plabble Protocol is built around the concept of a bucket. A bucket is an isolated key-value database collection. A server can host many buckets, but a Plabble request is always targeting one bucket. Every bucket contains slots which are the entries inside the buckets. You can modify the binary content of a slot using its key. The value inside the slot is always binary (in bytes), no other data types are supported using the Plabble protocol. Every bucket has a bucket id that is unique per server.
There are two types of keys:
- Numeric keys: A uint16 value between 0 and 65535. So numeric buckets have a maximum amount of slots, exactly 65536. The big advantage of numeric slots is that they are very small to send (only 2 bytes) and that they follow a numeric order.
- Binary keys: A utf-8 encoded string. This gives the protocol more flexibility when working with buckets. The disadvantages are bigger requests and the dynamic length, so we need to prefix the keys with a dynint to encode the length in Plabble packets when using binary keys.
- Implementation: bucket_id.rs
The bucket ID is the 16-byte server-wide unique ID for one bucket. There are 3 ways to notate a bucket ID:
- Base64-URL-encoded without padding. For example:
RKiZXdULZlegN6eDkwRTWw. - UTF-8 with the magic prefix
#: This can be any valid UTF-8 string, like#mybucketor#๐. The content following the # will be hashed into a 16-byte ID usingblake2b-128. This allows the Plabble protocol to use things like usernames, aliases, addresses etc. - UTF-8 with the magic prefix
@: This is similar to the #, but usesblake3-128instead. Not all servers and client are required to support Blake3.
When creating a bucket, you can generate the bucket ID yourself. If it is taken, the server will return an error.
The bucket key is a 64-byte secret that is derived from the session key when creating a bucket. It is stored on both the client and the server and is used for authentication.
- Implementation: post.rs
Every bucket has permissions which are set when creating the bucket (altough they can be changed later). Bucket Permissions come in 3 flavours:
- public: everyone on the internet who knows your bucket ID can do this
- protected: only people who are authenticated using Identify and are on the access_control_list can do this
- private: only people who know the bucket key can do this
The following list of settings/permissions with their default values is supported:
- public_read: (default true), allow everyone to read slots from this bucket
- public_append: (default false), allow everyone to append a slot to the bucket
- public_write: (default false), allow everyone to update a slot
- public_delete: (default false), allow everyone to delete a slot
- public_script_execution: (default false), allow everyone to execute opcode scripts interacting with this bucket (read/write/append/delete)
- protected_read: (default true), allow users on the ACL to read slots from this bucket
- protected_append: (default false), allow users on the ACL to append a slot to the bucket
- protected_write: (default false), allow users on the ACL to update a slot
- protected_delete: (default false), allow users on the ACL to remove a slot
- protected_script_execution: (default false), allow users on the ACL to execute opcode scripts interacting with this bucket (read/write/append/delete)
- protected_bucket_delete: (default false), allow users on the ACL to delete this bucket
- private_read: (default true), allow users owning the bucket key to read slots from this bucket
- private_append: (default true), allow users owning the bucket key to append a slot to the bucket
- private_write: (default true), allow users owning the bucket key to update a slot
- private_delete: (default true), allow users owning the bucket key to remove a slot
- private_script_execution: (default false), allow users owning the bucket key to execute opcode scripts interacting with this bucket (read/write/append/delete)
- private_bucket_delete: (default true), allow users owning the bucket key to delete this bucket
- deny_existence: (default: false) If public read is off and a user queries this bucket, let the server tell them this bucket does not exist
When creating a session, the client and server will generate a session key. The session key is used as key material for cryptographic functions during the session.
This is how to create a session key:
- For each algorithm specified in the request, create a shared secret. Create a hasher using the
blake2b-512orblake3algorithm. Use blake3 if this is set in the crypto settings in the request. For each shared secret, update the hash function. Finalize the hasher into a 64-byte hash that will serve as the input key material. - If the client provided a salt in the session request, provide it as the salt. If the client did NOT provide a salt but asked the server for a salt, use the server salt. If also no server salt is available, use the ASCII equivalent of the string value
PLABBLE-PROTOCOL(which is exactly 16 bytes) - If the client asked the server to create a salt in the session request, provide it as the context. If not, use the ASCII equivalent of the string value
PROTOCOL.PLABBLE(which is be exactly 16 bytes) - Pass the
input key material,saltandcontextto the key generation function - Derive a 64-byte session key and keep it in memory for the connection. If the user asked in the session request to store it, generate a random 12-byte PSK ID and return it to the client.
We don't want an attacker to relate a PSK ID to a bucket key, so the PSK ID is a randomly generated 12-byte identifier.
Plabble has two ways of ensuring the integrity of packets.
When the use_encryption flag in the base packet is off, it will use a Message Authentication Code (MAC).
If the encryption flag is on, Plabble uses Authenticated Encryption with Associated Data (AEAD).
For each request
- Implementation: context.rs
Plabble keys are generated using the blake2b-512 or blake3 functions.
The key derivation mechanism accepts 3 input parameters: ikm (64-byte input key material/existing key), salt (16-byte salt)
and context (16-byte unique(!) context)
For blake3, the context is passed to the derive_key mode/KDF mode of blake3 as a base64-url (no padding)-encoded UTF-8 string,and the hash is updated with the ikm first and then the salt before the hasher is finalized.
For blake2b-512, the MAC mode is used accepting directly a key, salt and persona. The context is passed to the persona.
- If full packet encryption is enabled (within a Session), all outgoing bytes will be encrypted with a crypto stream.
- The base packet is serialized, encrypted and sent. If the crypto_settings are set, the crypto settings of the encryption context are overwritten
- If encryption is enabled on the base packet, the current crypto stream is overwritten with a new crypto stream based on the base packet settings. If no encryption is used, the MAC will be calculated at the end of the stream and appended to the packet.
- The packet header is serialized, encrypted and sent.
- The body is serialized, encrypted and sent using the authenticated data.
- Optionally, if no encryption was used, the MAC is calculated and appended to the stream.
- If full packet encryption is enabled (within a Session), all incoming bytes will be decrypted with acrypto stream.
- The base packet is read, decrypted and parsed. If the crypto_settings are set, the crypto settings of the decryption context are overwritten
- If encryption is enabled on the base packet, the current crypto stream is overwritten with a new crypto stream based on the base packet settings. If no encryption is used, a 16-byte offset will be kept at the end of the stream for the MAC.
- The packet header is read, decrypted and parsed.
- The body bytes are read and decrypted. But this is not yet the plain text, because the body is encrypted twice.
- The associated data/authentication data is generated from the context to make AEAD-decryption possible on the body.
- The body is decrypted and parsed using the authenticated data.
- Optionally, if no encryption was used, the MAC is read from the stream and checked.
- Implementation: context.rs
The authenticated data is a blake2b-256 or blake3 (64-byte) hash of the raw plaintext base packet bytes and header bytes and is used to authenticate the entire packet and decrypt the body. Optionally, also the bucket key is appended if it is needed. When reading a request, the server first tries to decrypt the packet (or verify the MAC) without the bucket key, and then with the bucket key. At least one of them must succeed or the packet will be rejected. Responses never contain the bucket key in the authenticated data.
- Implementation: datetime.rs
A Plabble Timestamp is a uint32-big endian encoded seconds since the Plabble epoch, which is 2025-01-01T00:00:00Z (the minimum datetime). The maximum datetime of the Plabble Timestamp is 2161-02-07T06:28:15.000Z in RFC 3339 format.
The precision of a Plabble Timestamp is thus limited to 1 second which is sufficient for most things.
The advantage of the Plabble Timestamp is that it is thus very small, only taking 4 bytes.
A dynamic int is serialized in a way that every last bit of a byte indicates if another byte is needed for encoding a number. This way, serializing integers can be very efficient.
- Implementation: certificate.rs
Plabble uses its own certificate format, the Plabble Certificate. This is because we want the certificates to be as small as possible. Trust is based on our own root certificate authority. Everyone can become a certificate authority by starting an application. This can be done by creating an issue in this repository. As a Plabble Client programmer, you need to include the root certificate in the client application.
Every certificate has a unique 16-byte certificate ID. The ID is created by hashing the fields valid_from, valid_to, issuer_uri, data together using blake2b-128 (in incremental mode). The data field thus MUST be unique.
A Plabble Identity is a certificate containing some information about a person (for instance a username) and is signed by the user's home server. When refered to a user id, actually the 16-byte certificate ID of the identity certificate is meant. This ID is unqiue per server.
It is needed to have identity certificates to be able to verify if someone is authorized to have access to certain functionalities on a bucket when the bucket permissions are set to protected.
A user can request as many identities as they want using the Register request. When a Session is established, the client can use the Identify request to prove ownership of an identity by sending a certificate chain and signatures. The server will verify the signatures and if they are valid, the identity will be bound to the session so that the user can access protected functionalities on buckets where they are on the ACL.