crypto: Save the olm message hash.

master
Damir Jelić 2020-12-01 14:38:03 +01:00
parent ae2391791d
commit 270350cd34
6 changed files with 141 additions and 11 deletions

View File

@ -834,6 +834,7 @@ impl OlmMachine {
}; };
changes.sessions.push(decrypted.session); changes.sessions.push(decrypted.session);
changes.message_hashes.push(decrypted.message_hash);
if let Some(group_session) = decrypted.inbound_group_session { if let Some(group_session) = decrypted.inbound_group_session {
changes.inbound_group_sessions.push(group_session); changes.inbound_group_sessions.push(group_session);

View File

@ -72,15 +72,27 @@ pub struct Account {
pub(crate) store: Store, pub(crate) store: Store,
} }
#[derive(Debug, Clone)]
pub struct OlmDecryptionInfo { pub struct OlmDecryptionInfo {
pub session: Session, pub session: Session,
pub message_hash: String, pub message_hash: OlmMessageHash,
pub event: Raw<AnyToDeviceEvent>, pub event: Raw<AnyToDeviceEvent>,
pub signing_key: String, pub signing_key: String,
pub sender_key: String, pub sender_key: String,
pub inbound_group_session: Option<InboundGroupSession>, pub inbound_group_session: Option<InboundGroupSession>,
} }
/// A hash of a succesfully decrypted Olm message.
///
/// Can be used to check if a message has been replayed to us.
#[derive(Debug, Clone)]
pub struct OlmMessageHash {
/// The curve25519 key of the sender that sent us the Olm message.
pub sender_key: String,
/// The hash of the message.
pub hash: String,
}
impl Deref for Account { impl Deref for Account {
type Target = ReadOnlyAccount; type Target = ReadOnlyAccount;
@ -119,7 +131,7 @@ impl Account {
.chain(&[message_type]) .chain(&[message_type])
.chain(&ciphertext.body); .chain(&ciphertext.body);
let message_hash = encode(sha.finalize().as_slice()); let hash = encode(sha.finalize().as_slice());
// Create a OlmMessage from the ciphertext and the type. // Create a OlmMessage from the ciphertext and the type.
let message = let message =
@ -135,7 +147,10 @@ impl Account {
Ok(OlmDecryptionInfo { Ok(OlmDecryptionInfo {
session, session,
message_hash, message_hash: OlmMessageHash {
hash,
sender_key: content.sender_key.clone(),
},
event, event,
signing_key, signing_key,
sender_key: content.sender_key.clone(), sender_key: content.sender_key.clone(),

View File

@ -24,7 +24,7 @@ mod signing;
mod utility; mod utility;
pub(crate) use account::{Account, OlmDecryptionInfo}; pub(crate) use account::{Account, OlmDecryptionInfo};
pub use account::{AccountPickle, PickledAccount, ReadOnlyAccount}; pub use account::{AccountPickle, OlmMessageHash, PickledAccount, ReadOnlyAccount};
pub use group_sessions::{ pub use group_sessions::{
EncryptionSettings, ExportedRoomKey, InboundGroupSession, InboundGroupSessionPickle, EncryptionSettings, ExportedRoomKey, InboundGroupSession, InboundGroupSessionPickle,
PickledInboundGroupSession, PickledInboundGroupSession,

View File

@ -40,6 +40,7 @@ pub struct MemoryStore {
inbound_group_sessions: GroupSessionStore, inbound_group_sessions: GroupSessionStore,
tracked_users: Arc<DashSet<UserId>>, tracked_users: Arc<DashSet<UserId>>,
users_for_key_query: Arc<DashSet<UserId>>, users_for_key_query: Arc<DashSet<UserId>>,
olm_hashes: Arc<DashMap<String, DashSet<String>>>,
devices: DeviceStore, devices: DeviceStore,
identities: Arc<DashMap<UserId, UserIdentities>>, identities: Arc<DashMap<UserId, UserIdentities>>,
values: Arc<DashMap<String, String>>, values: Arc<DashMap<String, String>>,
@ -52,6 +53,7 @@ impl Default for MemoryStore {
inbound_group_sessions: GroupSessionStore::new(), inbound_group_sessions: GroupSessionStore::new(),
tracked_users: Arc::new(DashSet::new()), tracked_users: Arc::new(DashSet::new()),
users_for_key_query: Arc::new(DashSet::new()), users_for_key_query: Arc::new(DashSet::new()),
olm_hashes: Arc::new(DashMap::new()),
devices: DeviceStore::new(), devices: DeviceStore::new(),
identities: Arc::new(DashMap::new()), identities: Arc::new(DashMap::new()),
values: Arc::new(DashMap::new()), values: Arc::new(DashMap::new()),
@ -120,6 +122,13 @@ impl CryptoStore for MemoryStore {
.insert(identity.user_id().to_owned(), identity.clone()); .insert(identity.user_id().to_owned(), identity.clone());
} }
for hash in changes.message_hashes {
self.olm_hashes
.entry(hash.sender_key.to_owned())
.or_insert_with(DashSet::new)
.insert(hash.hash.clone());
}
Ok(()) Ok(())
} }
@ -218,14 +227,22 @@ impl CryptoStore for MemoryStore {
async fn load_identity(&self) -> Result<Option<PrivateCrossSigningIdentity>> { async fn load_identity(&self) -> Result<Option<PrivateCrossSigningIdentity>> {
Ok(None) Ok(None)
} }
async fn is_message_known(&self, message_hash: &crate::olm::OlmMessageHash) -> Result<bool> {
Ok(self
.olm_hashes
.entry(message_hash.sender_key.to_owned())
.or_insert_with(DashSet::new)
.contains(&message_hash.hash))
}
} }
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use crate::{ use crate::{
identities::device::test::get_device, identities::device::test::get_device,
olm::{test::get_account_and_session, InboundGroupSession}, olm::{test::get_account_and_session, InboundGroupSession, OlmMessageHash},
store::{memorystore::MemoryStore, CryptoStore}, store::{memorystore::MemoryStore, Changes, CryptoStore},
}; };
use matrix_sdk_common::identifiers::room_id; use matrix_sdk_common::identifiers::room_id;
@ -329,4 +346,21 @@ mod test {
assert!(store.is_user_tracked(device.user_id())); assert!(store.is_user_tracked(device.user_id()));
} }
#[tokio::test]
async fn test_message_hash() {
let store = MemoryStore::new();
let hash = OlmMessageHash {
sender_key: "test_sender".to_owned(),
hash: "test_hash".to_owned(),
};
let mut changes = Changes::default();
changes.message_hashes.push(hash.clone());
assert!(!store.is_message_known(&hash).await.unwrap());
store.save_changes(changes).await.unwrap();
assert!(store.is_message_known(&hash).await.unwrap());
}
} }

View File

@ -82,7 +82,9 @@ use matrix_sdk_common_macros::send_sync;
use crate::{ use crate::{
error::SessionUnpicklingError, error::SessionUnpicklingError,
identities::{Device, ReadOnlyDevice, UserDevices, UserIdentities}, identities::{Device, ReadOnlyDevice, UserDevices, UserIdentities},
olm::{InboundGroupSession, PrivateCrossSigningIdentity, ReadOnlyAccount, Session}, olm::{
InboundGroupSession, OlmMessageHash, PrivateCrossSigningIdentity, ReadOnlyAccount, Session,
},
verification::VerificationMachine, verification::VerificationMachine,
}; };
@ -108,6 +110,7 @@ pub(crate) struct Store {
pub struct Changes { pub struct Changes {
pub account: Option<ReadOnlyAccount>, pub account: Option<ReadOnlyAccount>,
pub sessions: Vec<Session>, pub sessions: Vec<Session>,
pub message_hashes: Vec<OlmMessageHash>,
pub inbound_group_sessions: Vec<InboundGroupSession>, pub inbound_group_sessions: Vec<InboundGroupSession>,
pub identities: IdentityChanges, pub identities: IdentityChanges,
pub devices: DeviceChanges, pub devices: DeviceChanges,
@ -444,4 +447,7 @@ pub trait CryptoStore: Debug {
/// Load a serializeable object from the store. /// Load a serializeable object from the store.
async fn get_value(&self, key: &str) -> Result<Option<String>>; async fn get_value(&self, key: &str) -> Result<Option<String>>;
/// Check if a hash for an Olm message stored in the database.
async fn is_message_known(&self, message_hash: &OlmMessageHash) -> Result<bool>;
} }

View File

@ -42,8 +42,9 @@ use crate::{
identities::{LocalTrust, OwnUserIdentity, ReadOnlyDevice, UserIdentities, UserIdentity}, identities::{LocalTrust, OwnUserIdentity, ReadOnlyDevice, UserIdentities, UserIdentity},
olm::{ olm::{
AccountPickle, IdentityKeys, InboundGroupSession, InboundGroupSessionPickle, AccountPickle, IdentityKeys, InboundGroupSession, InboundGroupSessionPickle,
PickledAccount, PickledCrossSigningIdentity, PickledInboundGroupSession, PickledSession, OlmMessageHash, PickledAccount, PickledCrossSigningIdentity, PickledInboundGroupSession,
PicklingMode, PrivateCrossSigningIdentity, ReadOnlyAccount, Session, SessionPickle, PickledSession, PicklingMode, PrivateCrossSigningIdentity, ReadOnlyAccount, Session,
SessionPickle,
}, },
}; };
@ -491,6 +492,24 @@ impl SqliteStore {
) )
.await?; .await?;
connection
.execute(
r#"
CREATE TABLE IF NOT EXISTS olm_hashes (
"id" INTEGER NOT NULL PRIMARY KEY,
"account_id" INTEGER NOT NULL,
"sender_key" TEXT NOT NULL,
"hash" TEXT NOT NULL,
FOREIGN KEY ("account_id") REFERENCES "accounts" ("id")
ON DELETE CASCADE
UNIQUE(account_id,sender_key,hash)
);
CREATE INDEX IF NOT EXISTS "olm_hashes_index" ON "olm_hashes" ("account_id");
"#,
)
.await?;
Ok(()) Ok(())
} }
@ -1466,6 +1485,25 @@ impl SqliteStore {
Ok(()) Ok(())
} }
async fn save_olm_hashses(
&self,
connection: &mut SqliteConnection,
hashes: &[OlmMessageHash],
) -> Result<()> {
let account_id = self.account_id().ok_or(CryptoStoreError::AccountUnset)?;
for hash in hashes {
query("REPLACE INTO olm_hashes (account_id, sender_key, hash) VALUES (?1, ?2, ?3)")
.bind(account_id)
.bind(&hash.sender_key)
.bind(&hash.hash)
.execute(&mut *connection)
.await?;
}
Ok(())
}
async fn save_user_helper( async fn save_user_helper(
&self, &self,
mut connection: &mut SqliteConnection, mut connection: &mut SqliteConnection,
@ -1681,6 +1719,9 @@ impl CryptoStore for SqliteStore {
self.save_user_identities(&mut transaction, &changes.identities.changed) self.save_user_identities(&mut transaction, &changes.identities.changed)
.await?; .await?;
self.save_olm_hashses(&mut transaction, &changes.message_hashes)
.await?;
transaction.commit().await?; transaction.commit().await?;
Ok(()) Ok(())
@ -1796,6 +1837,22 @@ impl CryptoStore for SqliteStore {
Ok(row.map(|r| r.0)) Ok(row.map(|r| r.0))
} }
async fn is_message_known(&self, message_hash: &OlmMessageHash) -> Result<bool> {
let account_id = self.account_id().ok_or(CryptoStoreError::AccountUnset)?;
let mut connection = self.connection.lock().await;
let row: Option<(String,)> = query_as(
"SELECT hash FROM olm_hashes WHERE account_id = ? and sender_key = ? and hash = ?",
)
.bind(account_id)
.bind(&message_hash.sender_key)
.bind(&message_hash.hash)
.fetch_optional(&mut *connection)
.await?;
Ok(row.is_some())
}
} }
#[cfg(not(tarpaulin_include))] #[cfg(not(tarpaulin_include))]
@ -1817,8 +1874,8 @@ mod test {
user::test::{get_other_identity, get_own_identity}, user::test::{get_other_identity, get_own_identity},
}, },
olm::{ olm::{
GroupSessionKey, InboundGroupSession, PrivateCrossSigningIdentity, ReadOnlyAccount, GroupSessionKey, InboundGroupSession, OlmMessageHash, PrivateCrossSigningIdentity,
Session, ReadOnlyAccount, Session,
}, },
store::{Changes, DeviceChanges, IdentityChanges}, store::{Changes, DeviceChanges, IdentityChanges},
}; };
@ -2371,4 +2428,21 @@ mod test {
store.remove_value(&key).await.unwrap(); store.remove_value(&key).await.unwrap();
assert!(store.get_value(&key).await.unwrap().is_none()); assert!(store.get_value(&key).await.unwrap().is_none());
} }
#[tokio::test(threaded_scheduler)]
async fn olm_hash_saving() {
let (_, store, _dir) = get_loaded_store().await;
let hash = OlmMessageHash {
sender_key: "test_sender".to_owned(),
hash: "test_hash".to_owned(),
};
let mut changes = Changes::default();
changes.message_hashes.push(hash.clone());
assert!(!store.is_message_known(&hash).await.unwrap());
store.save_changes(changes).await.unwrap();
assert!(store.is_message_known(&hash).await.unwrap());
}
} }