Skip to main content

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}