Merge pull request #660 from gnunicorn/ben-encrypt-indexeddb-cryptostore-keys
fix(indexeddb): encrypt crypto-store key
This commit is contained in:
@@ -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 }
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user