diff --git a/src/crypto/error.rs b/src/crypto/error.rs index ba46220b..d6167063 100644 --- a/src/crypto/error.rs +++ b/src/crypto/error.rs @@ -37,6 +37,8 @@ pub enum OlmError { MissingCiphertext, #[error("decryption failed because the session to decrypt the message is missing")] MissingSession, + #[error("the Encrypted message is missing the signing key of the sender")] + MissingSigningKey, #[error("can't finish Olm Session operation {0}")] OlmSession(#[from] OlmSessionError), #[error("can't finish Olm Session operation {0}")] diff --git a/src/crypto/machine.rs b/src/crypto/machine.rs index 95a2095d..8296137c 100644 --- a/src/crypto/machine.rs +++ b/src/crypto/machine.rs @@ -524,8 +524,14 @@ impl OlmMachine { match event.content.algorithm { Algorithm::MegolmV1AesSha2 => { // TODO check for all the valid fields. + let signing_key = event + .keys + .get("ed25519") + .ok_or(OlmError::MissingSigningKey)?; + let session = InboundGroupSession::new( sender_key, + signing_key, &event.content.room_id.to_string(), &event.content.session_key, )?; diff --git a/src/crypto/olm.rs b/src/crypto/olm.rs index 7fc4dcb9..4a470db8 100644 --- a/src/crypto/olm.rs +++ b/src/crypto/olm.rs @@ -207,6 +207,7 @@ impl PartialEq for Session { pub struct InboundGroupSession { inner: OlmInboundGroupSession, pub(crate) sender_key: String, + pub(crate) signing_key: String, pub(crate) room_id: String, forwarding_chains: Option>, } @@ -214,12 +215,14 @@ pub struct InboundGroupSession { impl InboundGroupSession { pub fn new( sender_key: &str, + signing_key: &str, room_id: &str, session_key: &str, ) -> Result { Ok(InboundGroupSession { inner: OlmInboundGroupSession::new(session_key)?, sender_key: sender_key.to_owned(), + signing_key: signing_key.to_owned(), room_id: room_id.to_owned(), forwarding_chains: None, }) @@ -229,6 +232,10 @@ impl InboundGroupSession { self.inner.session_id() } + pub fn pickle(&self, pickle_mode: PicklingMode) -> String { + self.inner.pickle(pickle_mode) + } + pub fn first_known_index(&self) -> u32 { self.inner.first_known_index() } diff --git a/src/crypto/store/mod.rs b/src/crypto/store/mod.rs index bb10378e..beb660db 100644 --- a/src/crypto/store/mod.rs +++ b/src/crypto/store/mod.rs @@ -24,7 +24,7 @@ use serde_json::Error as SerdeError; use thiserror::Error; use tokio::sync::Mutex; -use super::olm::{Account, Session}; +use super::olm::{Account, InboundGroupSession, Session}; use olm_rs::errors::{OlmAccountError, OlmSessionError}; use olm_rs::PicklingMode; diff --git a/src/crypto/store/sqlite.rs b/src/crypto/store/sqlite.rs index 348de1b8..865f2c63 100644 --- a/src/crypto/store/sqlite.rs +++ b/src/crypto/store/sqlite.rs @@ -25,8 +25,8 @@ use sqlx::{query, query_as, sqlite::SqliteQueryAs, Connect, Executor, SqliteConn use tokio::sync::Mutex; use zeroize::Zeroizing; -use super::{Account, CryptoStore, CryptoStoreError, Result, Session}; -use crate::crypto::memory_stores::SessionStore; +use super::{Account, CryptoStore, CryptoStoreError, InboundGroupSession, Result, Session}; +use crate::crypto::memory_stores::{GroupSessionStore, SessionStore}; pub struct SqliteStore { user_id: Arc, @@ -34,6 +34,7 @@ pub struct SqliteStore { account_id: Option, path: PathBuf, sessions: SessionStore, + inbound_group_sessions: GroupSessionStore, connection: Arc>, pickle_passphrase: Option>, } @@ -78,6 +79,7 @@ impl SqliteStore { device_id: Arc::new(device_id.to_owned()), account_id: None, sessions: SessionStore::new(), + inbound_group_sessions: GroupSessionStore::new(), path: path.as_ref().to_owned(), connection: Arc::new(Mutex::new(connection)), pickle_passphrase: passphrase, @@ -122,6 +124,25 @@ impl SqliteStore { ) .await?; + connection + .execute( + r#" + CREATE TABLE IF NOT EXISTS inbound_group_sessions ( + "session_id" TEXT NOT NULL PRIMARY KEY, + "account_id" INTEGER NOT NULL, + "sender_key" TEXT NOT NULL, + "signing_key" TEXT NOT NULL, + "room_id" TEXT NOT NULL, + "pickle" BLOB NOT NULL, + FOREIGN KEY ("account_id") REFERENCES "accounts" ("id") + ON DELETE CASCADE + ); + + CREATE INDEX "olm_groups_sessions_account_id" ON "inbound_group_sessions" ("account_id"); + "#, + ) + .await?; + Ok(()) } @@ -180,6 +201,34 @@ impl SqliteStore { .collect::>>>>()?) } + async fn save_inbound_group_session(&mut self, session: InboundGroupSession) -> Result<()> { + let account_id = self.account_id.ok_or(CryptoStoreError::AccountUnset)?; + let pickle = session.pickle(self.get_pickle_mode()); + let mut connection = self.connection.lock().await; + + query( + "INSERT INTO inbound_group_sessions ( + session_id, account_id, sender_key, signing_key, + room_id, pickle + ) VALUES (?1, ?2, ?3, ?4, ?5, ?6) + ON CONFLICT(session_id) DO UPDATE SET + pickle = ?6 + WHERE session_id = ?1 + ", + ) + .bind(&session.session_id()) + .bind(account_id) + .bind(&session.sender_key) + .bind(&session.signing_key) + .bind(&session.room_id) + .bind(&pickle) + .execute(&mut *connection) + .await?; + + self.inbound_group_sessions.add(session); + Ok(()) + } + fn get_pickle_mode(&self) -> PicklingMode { match &self.pickle_passphrase { Some(p) => PicklingMode::Encrypted { @@ -303,11 +352,12 @@ impl std::fmt::Debug for SqliteStore { #[cfg(test)] mod test { + use olm_rs::outbound_group_session::OlmOutboundGroupSession; use std::sync::Arc; use tempfile::tempdir; use tokio::sync::Mutex; - use super::{Account, CryptoStore, Session, SqliteStore}; + use super::{Account, CryptoStore, InboundGroupSession, Session, SqliteStore}; static USER_ID: &str = "@example:localhost"; static DEVICE_ID: &str = "DEVICEID"; @@ -443,4 +493,31 @@ mod test { assert_eq!(*sess, *loaded_session.lock().await); } + + #[tokio::test] + async fn save_inbound_group_session() { + let mut store = get_store().await; + let account = get_account(); + + store + .save_account(account.clone()) + .await + .expect("Can't save account"); + + let acc = account.lock().await; + let identity_keys = acc.identity_keys(); + let outbound_session = OlmOutboundGroupSession::new(); + let session = InboundGroupSession::new( + identity_keys.curve25519(), + identity_keys.ed25519(), + "!test:localhost", + &outbound_session.session_key(), + ) + .expect("Can't create session"); + + store + .save_inbound_group_session(session) + .await + .expect("Can't save group session"); + } }