matrix-sdk-crypto 0.2.0

Matrix encryption library
Documentation
// Copyright 2020 The Matrix.org Foundation C.I.C.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

use std::{collections::BTreeMap, fmt, sync::Arc};

use matrix_sdk_common::{
    events::{
        room::encrypted::{CiphertextInfo, EncryptedEventContent, OlmV1Curve25519AesSha2Content},
        EventType,
    },
    identifiers::{DeviceId, DeviceKeyAlgorithm, UserId},
    instant::{Duration, Instant},
    locks::Mutex,
};
use olm_rs::{errors::OlmSessionError, session::OlmSession, PicklingMode};
use serde::{Deserialize, Serialize};
use serde_json::{json, Value};

use super::IdentityKeys;
use crate::{
    error::{EventError, OlmResult, SessionUnpicklingError},
    ReadOnlyDevice,
};

pub use olm_rs::{
    session::{OlmMessage, PreKeyMessage},
    utility::OlmUtility,
};

/// Cryptographic session that enables secure communication between two
/// `Account`s
#[derive(Clone)]
pub struct Session {
    pub(crate) user_id: Arc<UserId>,
    pub(crate) device_id: Arc<Box<DeviceId>>,
    pub(crate) our_identity_keys: Arc<IdentityKeys>,
    pub(crate) inner: Arc<Mutex<OlmSession>>,
    pub(crate) session_id: Arc<str>,
    pub(crate) sender_key: Arc<str>,
    pub(crate) creation_time: Arc<Instant>,
    pub(crate) last_use_time: Arc<Instant>,
}

#[cfg(not(tarpaulin_include))]
impl fmt::Debug for Session {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.debug_struct("Session")
            .field("session_id", &self.session_id())
            .field("sender_key", &self.sender_key)
            .finish()
    }
}

impl Session {
    /// Decrypt the given Olm message.
    ///
    /// Returns the decrypted plaintext or an `OlmSessionError` if decryption
    /// failed.
    ///
    /// # Arguments
    ///
    /// * `message` - The Olm message that should be decrypted.
    pub async fn decrypt(&mut self, message: OlmMessage) -> Result<String, OlmSessionError> {
        let plaintext = self.inner.lock().await.decrypt(message)?;
        self.last_use_time = Arc::new(Instant::now());
        Ok(plaintext)
    }

    /// Encrypt the given plaintext as a OlmMessage.
    ///
    /// Returns the encrypted Olm message.
    ///
    /// # Arguments
    ///
    /// * `plaintext` - The plaintext that should be encrypted.
    pub(crate) async fn encrypt_helper(&mut self, plaintext: &str) -> OlmMessage {
        let message = self.inner.lock().await.encrypt(plaintext);
        self.last_use_time = Arc::new(Instant::now());
        message
    }

    /// Encrypt the given event content content as an m.room.encrypted event
    /// content.
    ///
    /// # Arguments
    ///
    /// * `recipient_device` - The device for which this message is going to be
    ///     encrypted, this needs to be the device that was used to create this
    ///     session with.
    ///
    /// * `event_type` - The type of the event.
    ///
    /// * `content` - The content of the event.
    pub async fn encrypt(
        &mut self,
        recipient_device: &ReadOnlyDevice,
        event_type: EventType,
        content: Value,
    ) -> OlmResult<EncryptedEventContent> {
        let recipient_signing_key = recipient_device
            .get_key(DeviceKeyAlgorithm::Ed25519)
            .ok_or(EventError::MissingSigningKey)?;

        let payload = json!({
            "sender": self.user_id.as_str(),
            "sender_device": self.device_id.as_ref(),
            "keys": {
                "ed25519": self.our_identity_keys.ed25519(),
            },
            "recipient": recipient_device.user_id(),
            "recipient_keys": {
                "ed25519": recipient_signing_key,
            },
            "type": event_type,
            "content": content,
        });

        let plaintext = serde_json::to_string(&payload)?;
        let ciphertext = self.encrypt_helper(&plaintext).await.to_tuple();

        let message_type = ciphertext.0;
        let ciphertext = CiphertextInfo::new(ciphertext.1, (message_type as u32).into());

        let mut content = BTreeMap::new();
        content.insert((&*self.sender_key).to_owned(), ciphertext);

        Ok(EncryptedEventContent::OlmV1Curve25519AesSha2(
            OlmV1Curve25519AesSha2Content::new(
                content,
                self.our_identity_keys.curve25519().to_string(),
            ),
        ))
    }

    /// Check if a pre-key Olm message was encrypted for this session.
    ///
    /// Returns true if it matches, false if not and a OlmSessionError if there
    /// was an error checking if it matches.
    ///
    /// # Arguments
    ///
    /// * `their_identity_key` - The identity/curve25519 key of the account
    /// that encrypted this Olm message.
    ///
    /// * `message` - The pre-key Olm message that should be checked.
    pub async fn matches(
        &self,
        their_identity_key: &str,
        message: PreKeyMessage,
    ) -> Result<bool, OlmSessionError> {
        self.inner
            .lock()
            .await
            .matches_inbound_session_from(their_identity_key, message)
    }

    /// Returns the unique identifier for this session.
    pub fn session_id(&self) -> &str {
        &self.session_id
    }

    /// Store the session as a base64 encoded string.
    ///
    /// # Arguments
    ///
    /// * `pickle_mode` - The mode that was used to pickle the session, either
    /// an unencrypted mode or an encrypted using passphrase.
    pub async fn pickle(&self, pickle_mode: PicklingMode) -> PickledSession {
        let pickle = self.inner.lock().await.pickle(pickle_mode);

        PickledSession {
            pickle: SessionPickle::from(pickle),
            sender_key: self.sender_key.to_string(),
            // FIXME this should use the duration from the unix epoch.
            creation_time: self.creation_time.elapsed(),
            last_use_time: self.last_use_time.elapsed(),
        }
    }

    /// Restore a Session from a previously pickled string.
    ///
    /// Returns the restored Olm Session or a `SessionUnpicklingError` if there
    /// was an error.
    ///
    /// # Arguments
    ///
    /// * `user_id` - Our own user id that the session belongs to.
    ///
    /// * `device_id` - Our own device id that the session belongs to.
    ///
    /// * `our_idenity_keys` - An clone of the Arc to our own identity keys.
    ///
    /// * `pickle` - The pickled version of the `Session`.
    ///
    /// * `pickle_mode` - The mode that was used to pickle the session, either
    /// an unencrypted mode or an encrypted using passphrase.
    pub fn from_pickle(
        user_id: Arc<UserId>,
        device_id: Arc<Box<DeviceId>>,
        our_identity_keys: Arc<IdentityKeys>,
        pickle: PickledSession,
        pickle_mode: PicklingMode,
    ) -> Result<Self, SessionUnpicklingError> {
        let session = OlmSession::unpickle(pickle.pickle.0, pickle_mode)?;
        let session_id = session.session_id();

        // FIXME this should use the UNIX epoch.
        let now = Instant::now();

        let creation_time = now
            .checked_sub(pickle.creation_time)
            .ok_or(SessionUnpicklingError::SessionTimestampError)?;
        let last_use_time = now
            .checked_sub(pickle.last_use_time)
            .ok_or(SessionUnpicklingError::SessionTimestampError)?;

        Ok(Session {
            user_id,
            device_id,
            our_identity_keys,
            inner: Arc::new(Mutex::new(session)),
            session_id: session_id.into(),
            sender_key: pickle.sender_key.into(),
            creation_time: Arc::new(creation_time),
            last_use_time: Arc::new(last_use_time),
        })
    }
}

impl PartialEq for Session {
    fn eq(&self, other: &Self) -> bool {
        self.session_id() == other.session_id()
    }
}

/// A pickled version of a `Session`.
///
/// Holds all the information that needs to be stored in a database to restore
/// a Session.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PickledSession {
    /// The pickle string holding the Olm Session.
    pub pickle: SessionPickle,
    /// The curve25519 key of the other user that we share this session with.
    pub sender_key: String,
    /// The relative time elapsed since the session was created.
    pub creation_time: Duration,
    /// The relative time elapsed since the session was last used.
    pub last_use_time: Duration,
}

/// The typed representation of a base64 encoded string of the Olm Session pickle.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SessionPickle(String);

impl From<String> for SessionPickle {
    fn from(picle_string: String) -> Self {
        SessionPickle(picle_string)
    }
}

impl SessionPickle {
    /// Get the string representation of the pickle.
    pub fn as_str(&self) -> &str {
        &self.0
    }
}