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}