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}