Skip to main content

Payload

Struct Payload 

Source
pub struct Payload {
    pub sync_id: SyncId,
    pub sender: DeviceId,
    pub records: RecordSet<Record>,
    /* private fields */
}
Expand description

The content of a payload: every record to be sent this time.

Records are held in a RecordSet, keyed by EntryId: union merging treats the same entry appearing twice in one payload as meaningless, so the set type carries the “at most one record per entry” invariant and a duplicate is reported as a structural error while parsing.

§Record order is deterministic

RecordSet is a BTreeMap internally and iterates in ascending EntryId order, so the same content serializes to a byte-identical plaintext. That is not the same as “byte-identical payload”: every record’s nonce is random, so the ciphertexts differ. Determinism is useful for exact byte comparisons in tests and gives a cheap criterion for “do two payloads carry the same data”.

Fields§

§sync_id: SyncId

Identifier of this sync. The receiver uses it to recognize duplicate payloads.

§sender: DeviceId

The device that started this sync.

Takes part in the outer AAD, so it is authenticated: an attacker cannot change a payload’s sender to somebody else. Merging needs to know who is initiating, because conflict resolution is decided by the initiator.

§It is a device identifier, but does not take part in device authentication

The “sender” here is only an authenticated claim: it proves that the party holding the master key claims to be this device. Actually proving the device identity (private key in hardware) is the job of crate::auth, which is a different path. Neither replaces the other: this field stops a payload from being attributed to somebody else, which device authentication does not.

§records: RecordSet<Record>

The records in this payload, still encrypted.

Implementations§

Source§

impl Payload

Source

pub fn new_sync_id() -> Result<SyncId>

Generate a fresh payload identifier.

§Errors

Error::RandomnessUnavailable: the OS random source is unavailable. There must be no fallback to a userspace PRNG: a predictable payload identifier defeats replay protection, because an attacker could precompute an identifier that will be accepted later.

Source

pub const fn kind(&self) -> PayloadKind

What this payload asks the peer to do.

Source

pub fn request(sender: DeviceId) -> Result<Self>

Build a request: send me your full state.

A request is a payload like any other: the same transport, the same authentication, and the same replay protection.

A request carries no records (see PayloadKind::Request), so it is the fixed-length header plus a nonce and the authentication tag — HEADER_LEN + NONCE_LEN + TAG_LEN = 114 bytes plus the compressed plaintext — far smaller than any real full state.

“Small” is relative; it does not mean “always fits the smallest QR code”. The request carries the 32-byte device identifier and the 32-byte payload identifier plus the authentication tag, so it takes up a substantial part of the capacity of any QR code; the smallest versions cannot hold it. Whether it fits is answered by the channel: crate::transport::TransportCapabilities::check_fits can tell before sending.

§Errors

Error::RandomnessUnavailable: the random source is unavailable.

Source

pub fn from_vault( master: &MasterKey, sender: DeviceId, vault: &Vault, ) -> Result<Self>

Build a payload from the local vault.

It seals all live entries and all tombstones. Incremental sync will be a filter parameter on this function rather than a different payload format.

§When an entry and its tombstone coexist, only the tombstone is sent

A vault can hold a live entry together with its tombstone — the state in which adjudication has not finished (crate::vault::Vault::record_tombstone never deletes the live entry). In that state the live entry must not be sent too: the peer would read it as “this one is not deleted yet”, so every sync would trigger the same conflict on the peer and the user would face the same adjudication screen again on every sync.

A tombstone represents a deletion that has already happened, while that live entry is local state awaiting adjudication. Broadcasting state awaiting adjudication would make every device take a decision the user has not taken yet.

§The record key is derived once

A payload can hold tens of thousands of records, and deriving HKDF again per record would multiply two HMAC-SHA256 computations by the number of entries. Deriving once and passing that key to every record reduces the cost from O(n) derivations to one.

§Errors
Source

pub fn peek(data: &[u8]) -> Result<PayloadHeader>

Read a payload header without decrypting anything.

Replay protection takes effect before the vault is touched, and the accounting needs the sync_id, which lives in the plaintext header. peek answers “have I seen this identifier” in a single call and allocates nothing large for any input, so a garbage payload (tampered ciphertext, or plain random bytes) is rejected without a decompression attempt and without its claimed identifier having been recorded.

§It authenticates nothing

Every returned field comes from the plaintext header, so it is only a claim. The one legitimate use is looking the sync_id up in the replay log: a hit is rejected outright (rejecting an unauthenticated payload needs no justification), and a miss still has to go through the full Payload::open.

§Errors

The same header checks as Payload::open: magic, version, length.

Source

pub fn len(&self) -> usize

How many records there are (entries plus tombstones).

Source

pub fn is_empty(&self) -> bool

Whether there is no record at all.

An empty payload is legal: that is exactly what a freshly registered device with no entries yet sends when it starts a sync. It must be accepted normally (the peer learns from it that there is nothing on this side), not treated as an error — that would add a branch needing a special case on the “new device joins” path.

Source

pub fn seal(&self, master: &MasterKey) -> Result<Vec<u8>>

Pack the payload into bytes that can be carried around.

The order is serialize → compress → whole-payload AEAD.

§Errors
Source

pub fn open(master: &MasterKey, data: &[u8]) -> Result<Self>

Take apart bytes carried in from outside, yielding records that are still encrypted.

§It only handles the outer layer

The returned Record values are not decrypted yet; that takes a call to record::open per record. Splitting the two steps is deliberate: an outer authentication failure and one unopenable record are different faults (the whole payload is untrustworthy versus the payload is trustworthy but its content is broken), and callers handle them differently — the former can only be retried, the latter deserves a specific diagnosis.

§Errors
§This function must never panic

The input comes entirely from a channel an attacker controls (any device on the LAN, or any byte string). All out-of-range access is stopped by the read cursor: the end offset is computed with checked_add and then read through get(range), so both overflow and out-of-range access return None instead of panicking.

Source

pub fn open_records(&self, master: &MasterKey) -> Result<Vec<OpenedRecord>>

Open every record in the payload.

§One failure fails the whole payload

It does not “skip what cannot be opened and take the rest”: the outer layer is authenticated, so a record that does not open can only mean the producer used the wrong key (for instance two devices whose vaults come from different master keys), and partial success would make such a fault look like “the sync succeeded but a few entries are missing” — the hardest kind to diagnose.

§Errors

Trait Implementations§

Source§

impl Debug for Payload

Source§

fn fmt(&self, f: &mut Formatter<'_>) -> Result

Formats the value using the given formatter. Read more

Auto Trait Implementations§

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T> Same for T

Source§

type Output = T

Should always be Self
Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = !

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, !>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.