Skip to main content

offline_protocol_leaf/
error.rs

1//! Errors a leaf node can produce.
2
3use alloc::string::String;
4use thiserror::Error;
5
6/// Result type for this crate.
7pub type Result<T> = core::result::Result<T, LeafError>;
8
9/// What can go wrong on a leaf node.
10///
11/// Deliberately not `#[non_exhaustive]`, for the reason
12/// [ADR 0022](https://github.com/Offline-Protocol/offline-protocol-sdk/blob/main/docs/adr/0022-one-sealed-layer-shared-with-the-leaf.md)
13/// gives for `SealedError`: firmware that maps these onto its own error space
14/// should get a compile error when a variant is added, rather than a wildcard
15/// arm that silently renders a new failure as an old one.
16#[derive(Debug, Clone, PartialEq, Eq, Error)]
17pub enum LeafError {
18    /// The backing store failed.
19    #[error("Storage failed: {0}")]
20    Storage(String),
21
22    /// The device has no identity yet, and an operation needed one.
23    #[error("Device is not provisioned")]
24    NotProvisioned,
25
26    /// The device already has an identity, and provisioning would replace it.
27    ///
28    /// Overwriting an identity is not a recoverable state: the device's
29    /// address changes, every peer's paired record names a device that no
30    /// longer exists, and nothing on the wire says why.
31    #[error("Device is already provisioned")]
32    AlreadyProvisioned,
33
34    /// A cryptographic operation failed.
35    #[error("Crypto failed: {0}")]
36    Crypto(String),
37
38    /// MLS refused an operation.
39    #[error("MLS failed: {0}")]
40    Mls(String),
41
42    /// A frame did not parse.
43    #[error("Malformed frame: {0}")]
44    MalformedFrame(String),
45
46    /// A control frame arrived unsigned, or its signature did not verify.
47    ///
48    /// Unsigned is a refusal rather than a downgrade: every control frame in
49    /// this protocol carries a signature, and one that does not is either an
50    /// implementation that skipped the step or an injection.
51    #[error("Control frame refused: {0}")]
52    ControlFrameRefused(String),
53
54    /// A control frame's signature verified, and the frame is too old, too far
55    /// ahead of this device's clock, or built on the payload that states
56    /// neither.
57    ///
58    /// Its own variant rather than a [`Self::ControlFrameRefused`] with
59    /// different text, because firmware has to be able to tell these apart and
60    /// the two mean opposite things about what to do. A signature that does
61    /// not verify accuses the sender. This one, in bulk and across peers,
62    /// accuses **this device's clock**: the signed timestamp is judged against
63    /// it, so a device whose time source is wrong or has not been set refuses
64    /// every honest peer and reports it as though it were under attack.
65    ///
66    /// A leaf gets a time source at pairing and is expected to keep one; see
67    /// the provisioning chapter of the specification for what that obligation
68    /// is and why it is not optional.
69    #[error("Control frame refused as stale: {0}")]
70    StaleControlFrame(String),
71
72    /// A presented key did not derive to the address that claimed it, or the
73    /// claimed identifier is not an address at all.
74    ///
75    /// Both are the same refusal on purpose. An identifier that does not parse
76    /// as an address has no derivation to check, and answering "acceptable"
77    /// for it is the bypass rather than a lenience.
78    #[error("Identity binding failed: {0}")]
79    IdentityBinding(String),
80
81    /// No session exists with this peer.
82    #[error("No session with {0}")]
83    NoSession(String),
84
85    /// A Welcome asked this device to join on a key package it did not mint
86    /// for the peer that sent it.
87    ///
88    /// A key package is a **bearer token**. It rides in a frame that is signed
89    /// but not encrypted, so anyone in radio range copies one off the air, and
90    /// every other gate on a Welcome then passes for them honestly: they do
91    /// hold the key their own address derives from, and they did build the
92    /// group this pair's id names. Only this refusal separates the peer the
93    /// package was minted for from whoever else heard it.
94    ///
95    /// Its own variant rather than an identity binding, because the two send
96    /// firmware to different places. An identity binding failure says a peer
97    /// is not who it claims; this says the peer is exactly who it claims and
98    /// is spending something that was never given to it.
99    #[error("Unsolicited welcome: {0}")]
100    UnsolicitedWelcome(String),
101
102    /// A Welcome spends the key package this device minted for its sender, and
103    /// that package is no longer held.
104    ///
105    /// Neither an attack nor an identity failure: the peer is exactly who it
106    /// claims and is spending exactly what it was given. The package is simply
107    /// gone, because an earlier join consumed it (an init key is single use,
108    /// so a Welcome is not replayable) or because later mints pushed it out of
109    /// the bounded ring.
110    ///
111    /// Its own variant because the repair is a fresh package rather than a
112    /// retry, and because the alternative is the same condition arriving from
113    /// inside MLS as a Welcome that will not decode, which reads as a broken
114    /// peer and sends a bench to the wire.
115    #[error("Stale key package: {0}")]
116    StaleKeyPackage(String),
117
118    /// The device already holds as many peers as it keeps room for, and none
119    /// of them is an incomplete pairing that could be recycled.
120    ///
121    /// Refusing rather than evicting an established peer is deliberate. A
122    /// device with a full table is one a stranger cannot displace the owner
123    /// from; the owner clears a slot with
124    /// [`LeafDevice::unpair`](crate::LeafDevice::unpair).
125    #[error("Peer table is full")]
126    TooManyPeers,
127
128    /// The sealed layer refused a value.
129    #[error("{0}")]
130    Sealed(String),
131}
132
133impl From<offline_protocol_sealed::SealedError> for LeafError {
134    fn from(e: offline_protocol_sealed::SealedError) -> Self {
135        // The inner text passes through rather than the rendered `Display` of
136        // a wrapper, so a failure reads the same here as it does on the phone.
137        use offline_protocol_sealed::SealedError as S;
138        match e {
139            S::Serialization(m) => LeafError::Sealed(alloc::format!("Serialization failed: {m}")),
140            S::Deserialization(m) => {
141                LeafError::Sealed(alloc::format!("Deserialization failed: {m}"))
142            }
143            S::InvalidGroupId(m) => LeafError::Sealed(alloc::format!("Invalid group id: {m}")),
144            S::InvalidPublicKey(m) => LeafError::Sealed(alloc::format!("Invalid public key: {m}")),
145            S::FieldTooLarge(n) => LeafError::Sealed(alloc::format!(
146                "Field too large for canonical payload length prefix: {n} bytes"
147            )),
148        }
149    }
150}