vodozemac/lib.rs
1// Copyright 2021 Damir Jelić
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
15//! A Rust implementation of Olm and Megolm
16//!
17//! vodozemac is a Rust reimplementation of
18//! [libolm](https://gitlab.matrix.org/matrix-org/olm), a cryptographic library
19//! used for end-to-end encryption in [Matrix](https://matrix.org). At its core, it
20//! is an implementation of the Olm and Megolm cryptographic ratchets, along
21//! with a high-level API to easily establish cryptographic communication
22//! channels employing those ratchets with other parties. It also implements
23//! some other miscellaneous cryptographic functionality which is useful for
24//! building Matrix clients, such as [SAS][sas].
25//!
26//! [sas]:
27//! <https://spec.matrix.org/v1.2/client-server-api/#short-authentication-string-sas-verification>
28//!
29//! # Olm
30//!
31//! Olm is an implementation of the [Double Ratchet
32//! algorithm](https://whispersystems.org/docs/specifications/doubleratchet/), very
33//! similar to that employed by the Signal Protocol. It allows the establishment
34//! of a 1-to-1 private communication channel, with perfect forward secrecy and
35//! self-healing properties.
36//!
37//! A detailed technical specification can be found at
38//! <https://gitlab.matrix.org/matrix-org/olm/-/blob/master/docs/olm.md>.
39//!
40//! For more information on using vodozemac for Olm, see the [`olm`] module.
41//!
42//! # Megolm
43//!
44//! Megolm is an AES-based single ratchet for group conversations with a large
45//! number of participants, where using Olm would be cost prohibitive because it
46//! would imply encrypting each message individually for each participant.
47//! Megolm sidesteps this by encrypting messages with a symmetric ratchet,
48//! shared once with each participant and then reused for a sequence of messages
49//! before rotating.
50//!
51//! This is a trade-off in which we lose Olm's self-healing properties, because
52//! someone in possession of a Megolm session at a particular state can derive
53//! all future states. However, if the attacker is only able to obtain the
54//! session in a ratcheted state, they cannot use it to decrypt messages
55//! encrypted with an earlier state. This preserves forward secrecy.
56//!
57//! A detailed technical specification can be found at
58//! <https://gitlab.matrix.org/matrix-org/olm/-/blob/master/docs/megolm.md>.
59//!
60//! For more information on using vodozemac for Megolm, see the [`megolm`]
61//! module.
62//!
63//! # Features
64//!
65//! ## Supported
66//!
67//! - [Olm](https://matrix-org.github.io/vodozemac/vodozemac/olm/index.html)
68//! - [Megolm](https://matrix-org.github.io/vodozemac/vodozemac/megolm/index.html)
69//! - [libolm pickle format](#legacy-pickles) (read-only)
70//! - [Modern pickle format](#modern-pickles)
71//! - [SAS (Short Authentication Strings)](https://matrix-org.github.io/vodozemac/vodozemac/sas/index.html)
72//!
73//! ## Unsupported
74//!
75//! - Creating asymmetric [server-side message key
76//! backups][legacy-message-key-backup], since they are slated to be replaced
77//! with symmetric backups.
78//!
79//! ## Planned
80//!
81//! - Symmetric [server-side message key backups][symmetric-message-key-backup]
82//! - Importing asymmetric [server-side message key
83//! backups][legacy-message-key-backup], for compatibility with existing
84//! backups created by libolm.
85//!
86//! [legacy-message-key-backup]:
87//! <https://spec.matrix.org/v1.2/client-server-api/#server-side-key-backups>
88//!
89//! [symmetric-message-key-backup]:
90//! https://github.com/uhoreg/matrix-doc/blob/symmetric-backups/proposals/3270-symmetric-megolm-backup.md
91//!
92//! # Feature flags
93//!
94//! ## Low-level API
95//!
96//! Feature: `low-level-api` (default: off)
97//!
98//! vodozemac exposes some lower-level structs and functions that are only
99//! useful in very advanced use cases. These should *not* be needed by the vast
100//! majority of users.
101//!
102//! Extreme care must be taken when using such APIs, as incorrect usage can lead
103//! to broken sessions.
104//!
105//! ## Precomputed tables
106//!
107//! Feature: `precomputed-tables` (default: on)
108//!
109//! Enables curve25519-dalek's precomputed basepoint tables, which speed up
110//! fixed-base scalar multiplication — key generation and Ed25519 signing — at
111//! the cost of embedding roughly 40 KB of lookup tables in the final binary.
112//! Variable-base operations, such as the X25519 Diffie-Hellman used by the
113//! Double Ratchet, are unaffected.
114//!
115//! Size-sensitive builds can drop the tables by disabling default features:
116//!
117//! ```toml
118//! vodozemac = { version = "0.10.0", default-features = false, features = ["libolm-compat"] }
119//! ```
120//!
121//! # Pickling
122//!
123//! vodozemac supports serializing its entire internal state into a form
124//! a "pickle". The state can subsequently be restored from such a pickle
125//! ("unpickled") in order to continue operation. This is used to support some
126//! Matrix features like device dehydration.
127//!
128//! ## Legacy pickles
129//!
130//! The legacy pickle format is a simple binary format used by libolm.
131//! Implemented for interoperability with current clients which are using
132//! libolm. Only *unpickling* is supported.
133//!
134//! ## Modern pickles
135//!
136//! The crate also implements a modern pickling mechanism using
137//! [Serde](https://serde.rs/). The exact serialization format is not mandated
138//! nor specified by this crate, but you can serialize to and deserialize from
139//! any format supported by Serde.
140//!
141//! The following structs support pickling:
142//!
143//! - [`olm::Account`]
144//! - [`olm::Session`]
145//! - [`megolm::GroupSession`]
146//! - [`megolm::InboundGroupSession`]
147//!
148//! For example, the following will print out the JSON representing the
149//! serialized `Account` and will leave no new copies of the account's secrets
150//! in memory:
151//!
152//! ```rust
153//! use anyhow::Result;
154//! use vodozemac::olm::{Account, AccountPickle};
155//!
156//! const PICKLE_KEY: [u8; 32] = [0u8; 32];
157//!
158//! fn main() -> Result<()>{
159//! let mut account = Account::new();
160//!
161//! account.generate_one_time_keys(10);
162//! account.generate_fallback_key();
163//!
164//! let pickle = account.pickle().encrypt(&PICKLE_KEY);
165//!
166//! let account2: Account = AccountPickle::from_encrypted(&pickle, &PICKLE_KEY)?.into();
167//!
168//! assert_eq!(account.identity_keys(), account2.identity_keys());
169//!
170//! Ok(())
171//! }
172//! ```
173//!
174//! You can unpickle a pickle-able struct directly from its serialized form:
175//!
176//! ```rust
177//! # use anyhow::Result;
178//! # use vodozemac::olm::{Account, AccountPickle};
179//! # use zeroize::Zeroize;
180//! #
181//! # fn main() -> Result<()> {
182//! # let some_account = Account::new();
183//! let mut json_str = serde_json::to_string(&some_account.pickle())?;
184//! // This will produce an account which is identical to `some_account`.
185//! let account: Account = serde_json::from_str::<AccountPickle>(&json_str)?.into();
186//!
187//! json_str.zeroize();
188//! #
189//! # Ok(())
190//! # }
191//! ```
192//!
193//! However, the pickle-able structs do not implement `serde::Serialize`
194//! themselves. If you want to serialize to a format other than JSON, you should
195//! instead call the `.pickle()` method to obtain a special serializable struct.
196//! This struct *does* implement `Serialize` and can therefore be serialized
197//! into any format supported by `serde`. To get back to the original struct
198//! from such as serializable struct, just call `.unpickle()`.
199//!
200//! ```rust
201//! use anyhow::Result;
202//! use vodozemac::olm::Account;
203//!
204//! fn main() -> Result<()> {
205//! let account = Account::new();
206//! let account: Account = account.pickle().into(); // this is identity
207//!
208//! Ok(())
209//! }
210//! ```
211
212#![cfg_attr(docsrs, feature(doc_cfg))]
213
214mod cipher;
215mod types;
216mod utilities;
217
218pub mod ecies;
219pub mod hazmat;
220pub mod hpke;
221pub mod megolm;
222pub mod olm;
223#[cfg(feature = "insecure-pk-encryption")]
224pub mod pk_encryption;
225pub mod sas;
226
227pub use base64::DecodeError as Base64DecodeError;
228pub use prost::DecodeError as ProtoBufDecodeError;
229pub use types::{
230 Curve25519PublicKey, Curve25519SecretKey, Ed25519Keypair, Ed25519PublicKey, Ed25519SecretKey,
231 Ed25519Signature, KeyError, KeyId, SharedSecret, SignatureError,
232};
233pub use utilities::{base64_decode, base64_encode};
234
235/// Error type describing the various ways vodozemac pickles can fail to be
236/// decoded.
237#[derive(Debug, thiserror::Error)]
238pub enum PickleError {
239 /// The pickle wasn't valid base64.
240 #[error("The pickle wasn't valid base64: {0}")]
241 Base64(#[from] base64::DecodeError),
242 /// The encrypted pickle could not have been decrypted.
243 #[error("The pickle couldn't be decrypted: {0}")]
244 Decryption(#[from] cipher::DecryptionError),
245 /// The serialized vodozemac object couldn't be deserialized.
246 #[error("The pickle couldn't be deserialized: {0}")]
247 Serialization(#[from] serde_json::Error),
248}
249
250/// Error type describing the various ways libolm pickles can fail to be
251/// decoded.
252#[derive(Debug, thiserror::Error)]
253pub enum LibolmPickleError {
254 /// The pickle is missing a valid version.
255 #[error("The pickle doesn't contain a version")]
256 MissingVersion,
257 /// The pickle has a unsupported version.
258 #[error("The pickle uses an unsupported version, expected {0}, got {1}")]
259 Version(u32, u32),
260 /// The pickle wasn't valid base64.
261 #[error("The pickle wasn't valid base64: {0}")]
262 Base64(#[from] Base64DecodeError),
263 /// The pickle could not have been decrypted.
264 #[error("The pickle couldn't be decrypted: {0}")]
265 Decryption(#[from] cipher::DecryptionError),
266 /// The pickle contains an invalid public key.
267 #[error("The pickle contained an invalid ed25519 public key {0}")]
268 PublicKey(#[from] KeyError),
269 /// The pickle does not contain a valid receiving or sending chain. A valid
270 /// Olm session needs to have at least one of them.
271 #[error("The pickle didn't contain a valid Olm session")]
272 InvalidSession,
273 /// The payload of the pickle could not be decoded.
274 #[error(transparent)]
275 Decode(#[from] matrix_pickle::DecodeError),
276 /// The object could not be encoded as a pickle.
277 #[error(transparent)]
278 Encode(#[from] matrix_pickle::EncodeError),
279}
280
281/// Error type describing the various ways dehydrated devices can fail to be
282/// decoded.
283#[derive(Debug, thiserror::Error)]
284pub enum DehydratedDeviceError {
285 /// The pickle is missing a valid version.
286 #[error("The pickle doesn't contain a version")]
287 MissingVersion,
288 /// The pickle has a unsupported version.
289 #[error("The pickle uses an unsupported version, expected {0}, got {1}")]
290 Version(u32, u32),
291 /// Invalid nonce.
292 #[error("The nonce was invalid")]
293 InvalidNonce,
294 /// The pickle wasn't valid base64.
295 #[error("The pickle wasn't valid base64: {0}")]
296 Base64(#[from] Base64DecodeError),
297 /// The pickle could not have been decrypted.
298 #[error("The pickle couldn't be decrypted: {0}")]
299 Decryption(#[from] chacha20poly1305::aead::Error),
300 /// There was an error with the libolm pickle format
301 #[error(transparent)]
302 LibolmPickle(#[from] LibolmPickleError),
303 /// The current account was created by libolm and can't be converted into a
304 /// dehydrated device.
305 #[error("The Account can't be turned into a dehydrated device since it was created by libolm")]
306 InvalidAccount,
307}
308
309/// Error type describing the different ways message decoding can fail.
310#[derive(Debug, thiserror::Error)]
311pub enum DecodeError {
312 /// The Olm message has an invalid type.
313 #[error("The message has an invalid type, expected 0 or 1, got {0}")]
314 MessageType(usize),
315 /// The message is missing a valid version.
316 #[error("The message didn't contain a version")]
317 MissingVersion,
318 /// The message doesn't have enough data to be correctly decoded.
319 #[error("The message was too short, it didn't contain a valid payload")]
320 MessageTooShort(usize),
321 /// The message has a unsupported version.
322 #[error("The message didn't have a valid version, expected {0}, got {1}")]
323 InvalidVersion(u8, u8),
324 /// An embedded public key couldn't be decoded.
325 #[error("The message contained an invalid public key: {0}")]
326 InvalidKey(#[from] KeyError),
327 /// The embedded message authentication code couldn't be decoded.
328 #[error("The message contained a MAC with an invalid size, expected {0}, got {1}")]
329 InvalidMacLength(usize, usize),
330 /// An embedded signature couldn't be decoded.
331 #[error("The message contained an invalid Signature: {0}")]
332 Signature(#[from] SignatureError),
333 /// The message couldn't be decoded as a valid protocol buffer message.
334 #[error(transparent)]
335 ProtoBufError(#[from] ProtoBufDecodeError),
336 /// The message wasn't valid base64.
337 #[error("The message wasn't valid base64: {0}")]
338 Base64(#[from] Base64DecodeError),
339}
340
341/// The version of vodozemac that is being used.
342pub static VERSION: &str = env!("CARGO_PKG_VERSION");
343
344#[cfg(test)]
345fn corpus_data_path(fuzz_target: &str) -> std::path::PathBuf {
346 let manifest_dir =
347 std::env::var("CARGO_MANIFEST_DIR").expect("Cargo always sets the manifest dir");
348
349 let mut afl_dir = std::path::PathBuf::from(manifest_dir);
350 afl_dir.push("afl");
351 afl_dir.push(fuzz_target);
352 afl_dir.push("in");
353
354 afl_dir
355}
356
357#[cfg(test)]
358fn run_corpus<F>(fuzz_target: &str, method: F)
359where
360 F: FnOnce(&[u8]) + Copy,
361{
362 let dir = corpus_data_path(fuzz_target);
363 let corpus = std::fs::read_dir(dir).expect("Couldn't read the corpus directory");
364
365 for input in corpus {
366 let input = input.expect("Couldn't read the input file");
367 let data = std::fs::read(input.path()).expect("Couldn't read the input file");
368 method(&data)
369 }
370}