vodozemac/megolm/group_session.rs
1// Copyright 2021 The Matrix.org Foundation C.I.C.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use serde::{Deserialize, Serialize};
16
17use super::{
18 default_config, message::MegolmMessage, ratchet::Ratchet, session_config::Version,
19 session_keys::SessionKey, SessionConfig,
20};
21use crate::{
22 cipher::Cipher,
23 types::Ed25519Keypair,
24 utilities::{pickle, unpickle},
25 PickleError,
26};
27
28/// A Megolm group session represents a single sending participant in an
29/// encrypted group communication context containing multiple receiving parties.
30///
31/// A group session consists of a ratchet, used for encryption, and an Ed25519
32/// signing key pair, used for authenticity.
33///
34/// A group session containing the signing key pair is also known as an
35/// "outbound" group session. We differentiate this from an *inbound* group
36/// session where this key pair has been removed and which can be used solely
37/// for receipt and decryption of messages.
38///
39/// Such an inbound group session is typically sent by the outbound group
40/// session owner to each of the receiving parties via a secure peer-to-peer
41/// channel (e.g. an Olm channel).
42pub struct GroupSession {
43 ratchet: Ratchet,
44 signing_key: Ed25519Keypair,
45 config: SessionConfig,
46}
47
48impl Default for GroupSession {
49 fn default() -> Self {
50 Self::new(Default::default())
51 }
52}
53
54impl GroupSession {
55 /// Construct a new group session, with a random ratchet state and signing
56 /// key pair.
57 pub fn new(config: SessionConfig) -> Self {
58 let signing_key = Ed25519Keypair::new();
59 Self { signing_key, ratchet: Ratchet::new(), config }
60 }
61
62 /// Returns the globally unique session ID, in base64-encoded form.
63 ///
64 /// A session ID is the public part of the Ed25519 key pair associated with
65 /// the group session. Due to the construction, every session ID is
66 /// (probabilistically) globally unique.
67 pub fn session_id(&self) -> String {
68 self.signing_key.public_key().to_base64()
69 }
70
71 /// Return the current message index.
72 ///
73 /// The message index is incremented each time a message is encrypted with
74 /// the group session.
75 pub const fn message_index(&self) -> u32 {
76 self.ratchet.index()
77 }
78
79 /// Get the [`SessionConfig`] that this [`GroupSession`] is configured
80 /// to use.
81 pub const fn session_config(&self) -> SessionConfig {
82 self.config
83 }
84
85 /// Encrypt the given `plaintext` with the group session.
86 ///
87 /// The resulting ciphertext is MAC-ed, then signed with the group session's
88 /// Ed25519 key pair and finally base64-encoded.
89 pub fn encrypt(&mut self, plaintext: impl AsRef<[u8]>) -> MegolmMessage {
90 let cipher = Cipher::new_megolm(self.ratchet.as_bytes());
91
92 let message = match self.config.version {
93 Version::V1 => MegolmMessage::encrypt_truncated_mac(
94 self.message_index(),
95 &cipher,
96 &self.signing_key,
97 plaintext.as_ref(),
98 ),
99 Version::V2 => MegolmMessage::encrypt_full_mac(
100 self.message_index(),
101 &cipher,
102 &self.signing_key,
103 plaintext.as_ref(),
104 ),
105 };
106
107 self.ratchet.advance();
108
109 message
110 }
111
112 /// Export the group session into a session key.
113 ///
114 /// The session key contains the key version constant, the current message
115 /// index, the ratchet state and the *public* part of the signing key pair.
116 /// It is signed by the signing key pair for authenticity.
117 ///
118 /// The session key is in a portable format, suitable for sending over the
119 /// network. It is typically sent to other group participants so that they
120 /// can reconstruct an inbound group session in order to decrypt messages
121 /// sent by this group session.
122 pub fn session_key(&self) -> SessionKey {
123 let mut session_key = SessionKey::new(&self.ratchet, self.signing_key.public_key());
124 let signature = self.signing_key.sign(&session_key.to_signature_bytes());
125 session_key.signature = signature;
126
127 session_key
128 }
129
130 /// Convert the group session into a struct which implements
131 /// [`serde::Serialize`] and [`serde::Deserialize`].
132 pub fn pickle(&self) -> GroupSessionPickle {
133 GroupSessionPickle {
134 ratchet: self.ratchet.clone(),
135 signing_key: self.signing_key.clone(),
136 config: self.config,
137 }
138 }
139
140 /// Restore a [`GroupSession`] from a previously saved
141 /// [`GroupSessionPickle`].
142 pub fn from_pickle(pickle: GroupSessionPickle) -> Self {
143 pickle.into()
144 }
145
146 /// Creates a [`GroupSession`] object by unpickling a session in the legacy
147 /// libolm pickle format.
148 ///
149 /// These pickles are encrypted and must be decrypted using the provided
150 /// `pickle_key`.
151 #[cfg(feature = "libolm-compat")]
152 pub fn from_libolm_pickle(
153 pickle: &str,
154 pickle_key: &[u8],
155 ) -> Result<Self, crate::LibolmPickleError> {
156 use crate::{megolm::group_session::libolm_compat::Pickle, utilities::unpickle_libolm};
157
158 const PICKLE_VERSION: u32 = 1;
159 unpickle_libolm::<Pickle, _>(pickle, pickle_key, PICKLE_VERSION)
160 }
161}
162
163#[cfg(feature = "libolm-compat")]
164mod libolm_compat {
165 use matrix_pickle::Decode;
166 use zeroize::{Zeroize, ZeroizeOnDrop};
167
168 use super::GroupSession;
169 use crate::{
170 megolm::{libolm::LibolmRatchetPickle, SessionConfig},
171 utilities::LibolmEd25519Keypair,
172 Ed25519Keypair,
173 };
174
175 #[derive(Zeroize, ZeroizeOnDrop, Decode)]
176 pub(super) struct Pickle {
177 version: u32,
178 ratchet: LibolmRatchetPickle,
179 ed25519_keypair: LibolmEd25519Keypair,
180 }
181
182 impl TryFrom<Pickle> for GroupSession {
183 type Error = crate::LibolmPickleError;
184
185 fn try_from(pickle: Pickle) -> Result<Self, Self::Error> {
186 // Removing the borrow doesn't work and clippy complains about
187 // this on nightly.
188 #[allow(clippy::needless_borrow)]
189 let ratchet = (&pickle.ratchet).into();
190 let signing_key =
191 Ed25519Keypair::from_expanded_key(&pickle.ed25519_keypair.private_key)?;
192
193 Ok(Self { ratchet, signing_key, config: SessionConfig::version_1() })
194 }
195 }
196}
197
198/// A format suitable for serialization which implements [`serde::Serialize`]
199/// and [`serde::Deserialize`]. Obtainable by calling [`GroupSession::pickle`].
200#[derive(Serialize, Deserialize)]
201pub struct GroupSessionPickle {
202 ratchet: Ratchet,
203 signing_key: Ed25519Keypair,
204 #[serde(default = "default_config")]
205 config: SessionConfig,
206}
207
208impl GroupSessionPickle {
209 /// Serialize and encrypt the pickle using the given key.
210 ///
211 /// This is the inverse of [`GroupSessionPickle::from_encrypted`].
212 pub fn encrypt(self, pickle_key: &[u8; 32]) -> String {
213 pickle(&self, pickle_key)
214 }
215
216 /// Obtain a pickle from a ciphertext by decrypting and deserializing using
217 /// the given key.
218 ///
219 /// This is the inverse of [`GroupSessionPickle::encrypt`].
220 pub fn from_encrypted(ciphertext: &str, pickle_key: &[u8; 32]) -> Result<Self, PickleError> {
221 unpickle(ciphertext, pickle_key)
222 }
223}
224
225impl From<GroupSessionPickle> for GroupSession {
226 fn from(pickle: GroupSessionPickle) -> Self {
227 Self { ratchet: pickle.ratchet, signing_key: pickle.signing_key, config: pickle.config }
228 }
229}
230
231#[cfg(test)]
232mod test {
233 use crate::megolm::{GroupSession, SessionConfig};
234
235 #[test]
236 fn create_with_session_config() {
237 assert_eq!(
238 GroupSession::new(SessionConfig::version_1()).session_config(),
239 SessionConfig::version_1()
240 );
241 assert_eq!(
242 GroupSession::new(SessionConfig::version_2()).session_config(),
243 SessionConfig::version_2()
244 );
245 }
246}