1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
//! Service request and response bodies sealed inside the 1:1 MLS session.
//!
//! `__SVC_REQ__` and `__SVC_RESP__` were signed plaintext before this existed,
//! readable by every device that carried them. They are now sealed toward a
//! peer that routes the sealed form (it advertised `svc_versions` entry 1 in a
//! signed key package, or sent us a sealed service frame) once the session with
//! that peer is confirmed. Toward everyone else the signed plaintext form is
//! still sent, because it is the floor every older peer depends on.
//!
//! A failure to seal on a session that is confirmed is returned as an error,
//! never sent in plaintext instead: the fallback exists for a peer we cannot
//! seal to yet, not for a fault on one we can.
//!
//! Discovery (`__SVC_DISC_Q__` / `__SVC_DISC_R__`) never comes through here
//! sealed: it is a gossip every hop must read to match and forward, and it has
//! no single recipient whose session could seal it.
use super::{OfflineProtocol, OutboxReseal};
use crate::{Error, Result};
use offline_protocol_core::{ContentType, MessageId, MessagePriority};
use offline_protocol_services::{SVC_REQUEST, SVC_RESPONSE};
use tracing::{debug, warn};
impl OfflineProtocol {
/// Sends a service frame to `recipient`, sealed when it can be and signed
/// plaintext otherwise. Every service send goes through here, including
/// the replies the service layer builds on its own (the automatic
/// `not_found`), so a sealed request is never answered in the clear.
///
/// The fallback toward a capable peer without a confirmed session is a
/// stated exception to the capability rule that a downgrade loses the
/// feature rather than the confidentiality: dropping the request instead
/// would break the service for a window the application cannot see. The
/// encrypt attempt is also what starts that session, exactly as it does
/// for a direct message, so the next request seals.
///
/// The exception covers only that window. When the session is confirmed
/// and sealing still fails (an MLS or storage fault), the error is
/// returned, as `send_message` returns it for a direct message: a body the
/// caller expected sealed must not leave readable by every hop, and the
/// caller can retry a refused send but cannot see a downgraded one. A
/// session that vanished from storage is not this case: the confirmed
/// cache evicts it and reports `SessionNotReady`, which is the window.
pub(crate) fn send_service_frame(
&mut self,
recipient: &str,
content: String,
priority: MessagePriority,
) -> Result<MessageId> {
if !self.should_seal_service_frame(recipient, &content) {
return self.send_internal_message(recipient, content, priority);
}
match self.encrypt_content_for_recipient(recipient, &content, priority) {
Ok(sealed) => {
let reseal = OutboxReseal {
content,
priority,
reply_to_msg: None,
forwarded_from: None,
content_type: ContentType::Text,
media_metadata: None,
rich: None,
seal_mark: None,
};
self.send_sealed_internal_message(recipient, sealed, priority, Some(reseal))
}
Err(Error::SessionNotReady(_)) => {
debug!(
recipient = %recipient,
"Session with a sealed-service peer not confirmed yet; service frame sent signed"
);
self.send_internal_message(recipient, content, priority)
}
Err(err) if self.confirmed_sessions.contains(recipient) => {
warn!(
recipient = %recipient,
error = %err,
"Sealing a service frame failed on a confirmed session; not sent"
);
Err(err)
}
Err(err) => {
// No confirmed session: the failure came from starting one
// (importing the key package, creating the group, sending the
// Welcome). That is the documented window, so the frame takes
// the floor exactly as it does on `SessionNotReady`.
debug!(
recipient = %recipient,
error = %err,
"Starting a session with a sealed-service peer failed; service frame sent signed"
);
self.send_internal_message(recipient, content, priority)
}
}
}
/// Whether `content` addressed to `recipient` should be attempted sealed:
/// a request or response (never discovery), our switch on, MLS available,
/// and a peer known to route the sealed form, either because its key
/// package advertises it or because it has sent us a sealed service frame
/// (the ratchet a replayed key package cannot clear).
fn should_seal_service_frame(&self, recipient: &str, content: &str) -> bool {
(content.starts_with(SVC_REQUEST) || content.starts_with(SVC_RESPONSE))
&& self.config.encryption.encrypt_service_messages
&& self.should_auto_encrypt()
&& (self.peer_svc_sealed.contains(recipient)
|| self.svc_sealed_proved_peers.contains(recipient))
}
/// Records that `peer` routes sealed service frames.
///
/// Bounded as a strict subset of the capped encryption-capable set,
/// refused rather than evicted, unlike the feature sets beside it that
/// clear themselves when full. Those can, because forgetting a peer there
/// costs a feature and never confidentiality. Here forgetting a peer sends
/// its service bodies in signed plaintext, so a flood of fresh identities
/// (one keygen each) that cleared the set would downgrade every
/// established peer with no symptom. Refusing at the cap costs a flood's
/// later peers the capability and never costs an earlier peer the sealing
/// it had. The security gate marks a signed sender encryption-capable
/// before any key package reaches dispatch, so a genuine peer passes.
pub(super) fn record_svc_sealed_peer(&mut self, peer: &str) {
if !self.is_encryption_capable(peer) {
return;
}
self.peer_svc_sealed.insert(peer.to_string());
}
}