Merge pull request #660 from gnunicorn/ben-encrypt-indexeddb-cryptostore-keys

fix(indexeddb): encrypt crypto-store key
This commit is contained in:
Benjamin Kampmann
2022-05-11 17:26:41 +02:00
committed by GitHub
3 changed files with 119 additions and 37 deletions
+87 -27
View File
@@ -35,6 +35,7 @@ use matrix_sdk_store_encryption::StoreCipher;
use ruma::{DeviceId, OwnedDeviceId, OwnedUserId, RoomId, TransactionId, UserId};
use serde::{Deserialize, Serialize};
use wasm_bindgen::JsValue;
use web_sys::IdbKeyRange;
use crate::safe_encode::SafeEncode;
@@ -57,6 +58,7 @@ mod KEYS {
pub const OUTGOING_SECRET_REQUESTS: &str = "outgoing_secret_requests";
pub const UNSENT_SECRET_REQUESTS: &str = "unsent_secret_requests";
pub const SECRET_REQUESTS_BY_INFO: &str = "secret_requests_by_info";
pub const KEY_REQUEST: &str = "key_request";
// KEYS
pub const STORE_CIPHER: &str = "store_cipher";
@@ -186,6 +188,35 @@ impl IndexeddbStore {
IndexeddbStore::open_with_store_cipher("crypto".to_owned(), None).await
}
fn encode_key<T>(&self, table_name: &str, key: T) -> JsValue
where
T: SafeEncode,
{
match &self.store_cipher {
Some(cipher) => key.encode_secure(table_name, cipher),
None => key.encode(),
}
}
fn encode_to_range<T>(
&self,
table_name: &str,
key: T,
) -> Result<IdbKeyRange, IndexeddbStoreError>
where
T: SafeEncode,
{
match &self.store_cipher {
Some(cipher) => key.encode_to_range_secure(table_name, cipher),
None => key.encode_to_range(),
}
.map_err(|e| IndexeddbStoreError::DomException {
code: 0,
name: "IdbKeyRangeMakeError".to_owned(),
message: e,
})
}
/// Open a new IndexeddbStore with given name and passphrase
pub async fn open_with_passphrase(prefix: String, passphrase: &str) -> Result<Self> {
let name = format!("{:0}::matrix-sdk-crypto-meta", prefix);
@@ -359,7 +390,7 @@ impl IndexeddbStore {
let session_id = session.session_id();
let pickle = session.pickle().await;
let key = (&sender_key, session_id).encode();
let key = self.encode_key(KEYS::SESSION, (&sender_key, session_id));
sessions.put_key_val(&key, &self.serialize_value(&pickle)?)?;
}
@@ -372,7 +403,8 @@ impl IndexeddbStore {
let room_id = session.room_id();
let sender_key = session.sender_key();
let session_id = session.session_id();
let key = (room_id, sender_key, session_id).encode();
let key = self
.encode_key(KEYS::INBOUND_GROUP_SESSIONS, (room_id, sender_key, session_id));
let pickle = session.pickle().await;
sessions.put_key_val(&key, &self.serialize_value(&pickle)?)?;
@@ -385,7 +417,10 @@ impl IndexeddbStore {
for session in changes.outbound_group_sessions {
let room_id = session.room_id();
let pickle = session.pickle().await;
sessions.put_key_val(&room_id.encode(), &self.serialize_value(&pickle)?)?;
sessions.put_key_val(
&self.encode_key(KEYS::OUTBOUND_GROUP_SESSIONS, &room_id),
&self.serialize_value(&pickle)?,
)?;
}
}
@@ -397,7 +432,7 @@ impl IndexeddbStore {
if !device_changes.new.is_empty() || !device_changes.changed.is_empty() {
let device_store = tx.object_store(KEYS::DEVICES)?;
for device in device_changes.new.iter().chain(&device_changes.changed) {
let key = (device.user_id(), device.device_id()).encode();
let key = self.encode_key(KEYS::DEVICES, (device.user_id(), device.device_id()));
let device = self.serialize_value(&device)?;
device_store.put_key_val(&key, &device)?;
@@ -408,7 +443,7 @@ impl IndexeddbStore {
let device_store = tx.object_store(KEYS::DEVICES)?;
for device in &device_changes.deleted {
let key = (device.user_id(), device.device_id()).encode();
let key = self.encode_key(KEYS::DEVICES, (device.user_id(), device.device_id()));
device_store.delete(&key)?;
}
}
@@ -416,15 +451,20 @@ impl IndexeddbStore {
if !identity_changes.changed.is_empty() || !identity_changes.new.is_empty() {
let identities = tx.object_store(KEYS::IDENTITIES)?;
for identity in identity_changes.changed.iter().chain(&identity_changes.new) {
identities
.put_key_val(&identity.user_id().encode(), &self.serialize_value(&identity)?)?;
identities.put_key_val(
&self.encode_key(KEYS::IDENTITIES, &identity.user_id()),
&self.serialize_value(&identity)?,
)?;
}
}
if !olm_hashes.is_empty() {
let hashes = tx.object_store(KEYS::OLM_HASHES)?;
for hash in &olm_hashes {
hashes.put_key_val(&(&hash.sender_key, &hash.hash).encode(), &JsValue::TRUE)?;
hashes.put_key_val(
&self.encode_key(KEYS::OLM_HASHES, (&hash.sender_key, &hash.hash)),
&JsValue::TRUE,
)?;
}
}
@@ -433,9 +473,12 @@ impl IndexeddbStore {
let unsent_secret_requests = tx.object_store(KEYS::UNSENT_SECRET_REQUESTS)?;
let outgoing_secret_requests = tx.object_store(KEYS::OUTGOING_SECRET_REQUESTS)?;
for key_request in &key_requests {
let key_request_id = key_request.request_id.encode();
secret_requests_by_info
.put_key_val(&key_request.info.as_key().encode(), &key_request_id)?;
let key_request_id =
self.encode_key(KEYS::KEY_REQUEST, key_request.request_id.as_str());
secret_requests_by_info.put_key_val(
&self.encode_key(KEYS::KEY_REQUEST, key_request.info.as_key()),
&key_request_id,
)?;
if key_request.sent_out {
unsent_secret_requests.delete(&key_request_id)?;
@@ -494,7 +537,7 @@ impl IndexeddbStore {
IdbTransactionMode::Readonly,
)?
.object_store(KEYS::OUTBOUND_GROUP_SESSIONS)?
.get(&room_id.encode())?
.get(&self.encode_key(KEYS::OUTBOUND_GROUP_SESSIONS, room_id))?
.await?
{
Ok(Some(
@@ -510,7 +553,7 @@ impl IndexeddbStore {
}
}
async fn get_outgoing_key_request_helper(&self, key: &str) -> Result<Option<GossipRequest>> {
// in this internal we expect key to already be escaped.
// in this internal we expect key to already be escaped or encrypted
let jskey = JsValue::from_str(key);
let dbs = [KEYS::OUTGOING_SECRET_REQUESTS, KEYS::UNSENT_SECRET_REQUESTS];
let tx = self.inner.transaction_on_multi_with_mode(&dbs, IdbTransactionMode::Readonly)?;
@@ -621,7 +664,7 @@ impl IndexeddbStore {
sender_key: &str,
session_id: &str,
) -> Result<Option<InboundGroupSession>> {
let key = (room_id, sender_key, session_id).encode();
let key = self.encode_key(KEYS::INBOUND_GROUP_SESSIONS, (room_id, sender_key, session_id));
if let Some(pickle) = self
.inner
.transaction_on_one_with_mode(
@@ -735,7 +778,7 @@ impl IndexeddbStore {
user_id: &UserId,
device_id: &DeviceId,
) -> Result<Option<ReadOnlyDevice>> {
let key = (user_id, device_id).encode();
let key = self.encode_key(KEYS::DEVICES, (user_id, device_id));
Ok(self
.inner
.transaction_on_one_with_mode(KEYS::DEVICES, IdbTransactionMode::Readonly)?
@@ -750,11 +793,7 @@ impl IndexeddbStore {
&self,
user_id: &UserId,
) -> Result<HashMap<OwnedDeviceId, ReadOnlyDevice>> {
let range = user_id.encode_to_range().map_err(|e| IndexeddbStoreError::DomException {
code: 0,
name: "IdbKeyRangeMakeError".to_owned(),
message: e,
})?;
let range = self.encode_to_range(KEYS::DEVICES, user_id)?;
Ok(self
.inner
.transaction_on_one_with_mode(KEYS::DEVICES, IdbTransactionMode::Readonly)?
@@ -774,7 +813,7 @@ impl IndexeddbStore {
.inner
.transaction_on_one_with_mode(KEYS::IDENTITIES, IdbTransactionMode::Readonly)?
.object_store(KEYS::IDENTITIES)?
.get(&user_id.encode())?
.get(&self.encode_key(KEYS::IDENTITIES, user_id))?
.await?
.map(|i| self.deserialize_value(i))
.transpose()?)
@@ -785,7 +824,7 @@ impl IndexeddbStore {
.inner
.transaction_on_one_with_mode(KEYS::OLM_HASHES, IdbTransactionMode::Readonly)?
.object_store(KEYS::OLM_HASHES)?
.get(&(&hash.sender_key, &hash.hash).encode())?
.get(&self.encode_key(KEYS::OLM_HASHES, (&hash.sender_key, &hash.hash)))?
.await?
.is_some())
}
@@ -801,7 +840,7 @@ impl IndexeddbStore {
IdbTransactionMode::Readonly,
)?
.object_store(KEYS::SECRET_REQUESTS_BY_INFO)?
.get(&key_info.as_key().encode())?
.get(&self.encode_key(KEYS::KEY_REQUEST, key_info.as_key()))?
.await?
.and_then(|i| i.as_string());
if let Some(id) = id {
@@ -827,7 +866,7 @@ impl IndexeddbStore {
}
async fn delete_outgoing_secret_requests(&self, request_id: &TransactionId) -> Result<()> {
let jskey = request_id.as_str().encode();
let jskey = self.encode_key(KEYS::KEY_REQUEST, request_id); //.as_str());
let dbs = [
KEYS::OUTGOING_SECRET_REQUESTS,
KEYS::UNSENT_SECRET_REQUESTS,
@@ -854,7 +893,7 @@ impl IndexeddbStore {
if let Some(inner) = request {
tx.object_store(KEYS::SECRET_REQUESTS_BY_INFO)?
.delete(&inner.info.as_key().encode())?;
.delete(&self.encode_key(KEYS::KEY_REQUEST, &inner.info.as_key()))?;
}
tx.object_store(KEYS::UNSENT_SECRET_REQUESTS)?.delete(&jskey)?;
@@ -1035,10 +1074,12 @@ impl CryptoStore for IndexeddbStore {
#[cfg(test)]
mod tests {
use super::IndexeddbStore;
wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser);
use matrix_sdk_crypto::cryptostore_integration_tests;
use super::IndexeddbStore;
wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser);
async fn get_store(name: String, passphrase: Option<&str>) -> IndexeddbStore {
match passphrase {
Some(pass) => IndexeddbStore::open_with_passphrase(name, pass)
@@ -1051,3 +1092,22 @@ mod tests {
}
cryptostore_integration_tests! { integration }
}
#[cfg(test)]
#[rustfmt::skip]
mod encrypted_tests {
use super::IndexeddbStore;
use matrix_sdk_crypto::cryptostore_integration_tests;
wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser);
async fn get_store(name: String, passphrase: Option<&str>) -> IndexeddbStore {
let pass = passphrase.unwrap_or_else(|| name.as_str());
IndexeddbStore::open_with_passphrase(name.clone(), pass)
.await
.expect("Can't create a passphrase protected store")
}
// FIXME: the tests pass, if run one by one, but run all together locally,
// as well as CI fails... see matrix-org/matrix-rust-sdk#661
// cryptostore_integration_tests! { integration }
}
+10 -10
View File
@@ -269,8 +269,8 @@ impl IndexeddbStore {
&self,
event: &impl Serialize,
) -> std::result::Result<JsValue, SerializationError> {
Ok(match self.store_cipher {
Some(ref cipher) => JsValue::from_serde(&cipher.encrypt_value_typed(event)?)?,
Ok(match &self.store_cipher {
Some(cipher) => JsValue::from_serde(&cipher.encrypt_value_typed(event)?)?,
None => JsValue::from_serde(event)?,
})
}
@@ -279,8 +279,8 @@ impl IndexeddbStore {
&self,
event: JsValue,
) -> std::result::Result<T, SerializationError> {
match self.store_cipher {
Some(ref cipher) => Ok(cipher.decrypt_value_typed(event.into_serde()?)?),
match &self.store_cipher {
Some(cipher) => Ok(cipher.decrypt_value_typed(event.into_serde()?)?),
None => Ok(event.into_serde()?),
}
}
@@ -289,8 +289,8 @@ impl IndexeddbStore {
where
T: SafeEncode,
{
match self.store_cipher {
Some(ref cipher) => key.encode_secure(table_name, cipher),
match &self.store_cipher {
Some(cipher) => key.encode_secure(table_name, cipher),
None => key.encode(),
}
}
@@ -303,8 +303,8 @@ impl IndexeddbStore {
where
T: SafeEncode,
{
match self.store_cipher {
Some(ref cipher) => key.encode_to_range_secure(table_name, cipher),
match &self.store_cipher {
Some(cipher) => key.encode_to_range_secure(table_name, cipher),
None => key.encode_to_range(),
}
.map_err(|e| SerializationError::StoreError(StoreError::Backend(anyhow!(e).into())))
@@ -315,8 +315,8 @@ impl IndexeddbStore {
where
T: SafeEncode,
{
match self.store_cipher {
Some(ref cipher) => key.encode_with_counter_secure(table_name, cipher, i),
match &self.store_cipher {
Some(cipher) => key.encode_with_counter_secure(table_name, cipher, i),
None => key.encode_with_counter(i),
}
}
+22
View File
@@ -1039,3 +1039,25 @@ mod tests {
cryptostore_integration_tests! { integration }
}
#[cfg(test)]
mod encrypted_tests {
use matrix_sdk_crypto::cryptostore_integration_tests;
use once_cell::sync::Lazy;
use tempfile::{tempdir, TempDir};
use super::SledStore;
static TMP_DIR: Lazy<TempDir> = Lazy::new(|| tempdir().unwrap());
async fn get_store(name: String, passphrase: Option<&str>) -> SledStore {
let tmpdir_path = TMP_DIR.path().join(name);
let pass = passphrase.unwrap_or("default_test_password");
let store = SledStore::open_with_passphrase(tmpdir_path.to_str().unwrap(), Some(pass))
.expect("Can't create a passphrase protected store");
store
}
cryptostore_integration_tests! { integration }
}