Skip to content

Commit 9480701

Browse files
erskingardneryukibtc
authored andcommitted
mls-storage: add exporter secret methods to GroupStorage trait
Signed-off-by: Yuki Kishimoto <[email protected]>
1 parent ae9391f commit 9480701

File tree

8 files changed

+395
-4
lines changed

8 files changed

+395
-4
lines changed

crates/nostr-mls-memory-storage/src/groups.rs

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,15 @@ use nostr_mls_storage::messages::types::Message;
1010

1111
use crate::NostrMlsMemoryStorage;
1212

13+
/// Creates a compound key from an MLS group ID and epoch
14+
///
15+
/// The key is created by concatenating the MLS group ID and the epoch as bytes
16+
fn create_compound_key(mls_group_id: &[u8], epoch: u64) -> Vec<u8> {
17+
let mut key = mls_group_id.to_vec();
18+
key.extend_from_slice(&epoch.to_be_bytes());
19+
key
20+
}
21+
1322
impl GroupStorage for NostrMlsMemoryStorage {
1423
fn save_group(&self, group: Group) -> Result<(), GroupError> {
1524
// Store in the MLS group ID cache
@@ -102,4 +111,36 @@ impl GroupStorage for NostrMlsMemoryStorage {
102111

103112
Ok(())
104113
}
114+
115+
fn get_group_exporter_secret(
116+
&self,
117+
mls_group_id: &[u8],
118+
epoch: u64,
119+
) -> Result<Option<GroupExporterSecret>, GroupError> {
120+
// Check if the group exists first
121+
self.find_group_by_mls_group_id(mls_group_id)?;
122+
123+
let cache = self.group_exporter_secrets_cache.read();
124+
// Create a compound key from mls_group_id and epoch
125+
let key = create_compound_key(mls_group_id, epoch);
126+
Ok(cache.peek(&key).cloned())
127+
}
128+
129+
fn save_group_exporter_secret(
130+
&self,
131+
group_exporter_secret: GroupExporterSecret,
132+
) -> Result<(), GroupError> {
133+
// Check if the group exists first
134+
self.find_group_by_mls_group_id(&group_exporter_secret.mls_group_id)?;
135+
136+
let mut cache = self.group_exporter_secrets_cache.write();
137+
// Create a compound key from mls_group_id and epoch
138+
let key = create_compound_key(
139+
&group_exporter_secret.mls_group_id,
140+
group_exporter_secret.epoch,
141+
);
142+
cache.put(key, group_exporter_secret);
143+
144+
Ok(())
145+
}
105146
}

crates/nostr-mls-memory-storage/src/lib.rs

Lines changed: 73 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ use std::num::NonZeroUsize;
1515

1616
use lru::LruCache;
1717
use nostr::EventId;
18-
use nostr_mls_storage::groups::types::{Group, GroupRelay};
18+
use nostr_mls_storage::groups::types::{Group, GroupExporterSecret, GroupRelay};
1919
use nostr_mls_storage::messages::types::{Message, ProcessedMessage};
2020
use nostr_mls_storage::welcomes::types::{ProcessedWelcome, Welcome};
2121
use nostr_mls_storage::{Backend, NostrMlsStorageProvider};
@@ -70,6 +70,8 @@ pub struct NostrMlsMemoryStorage {
7070
messages_by_group_cache: RwLock<LruCache<Vec<u8>, Vec<Message>>>,
7171
/// LRU Cache for ProcessedMessage objects, keyed by Event ID
7272
processed_messages_cache: RwLock<LruCache<EventId, ProcessedMessage>>,
73+
/// LRU Cache for GroupExporterSecret objects, keyed by a compound key of MLS group ID and epoch
74+
group_exporter_secrets_cache: RwLock<LruCache<Vec<u8>, GroupExporterSecret>>,
7375
}
7476

7577
impl Default for NostrMlsMemoryStorage {
@@ -121,6 +123,7 @@ impl NostrMlsMemoryStorage {
121123
messages_cache: RwLock::new(LruCache::new(cache_size)),
122124
messages_by_group_cache: RwLock::new(LruCache::new(cache_size)),
123125
processed_messages_cache: RwLock::new(LruCache::new(cache_size)),
126+
group_exporter_secrets_cache: RwLock::new(LruCache::new(cache_size)),
124127
}
125128
}
126129
}
@@ -168,7 +171,7 @@ mod tests {
168171
use std::collections::BTreeSet;
169172

170173
use nostr::{EventId, Kind, PublicKey, RelayUrl, Tags, Timestamp, UnsignedEvent};
171-
use nostr_mls_storage::groups::types::{Group, GroupState, GroupType};
174+
use nostr_mls_storage::groups::types::{Group, GroupExporterSecret, GroupState, GroupType};
172175
use nostr_mls_storage::groups::GroupStorage;
173176
use nostr_mls_storage::messages::types::{Message, ProcessedMessageState};
174177
use nostr_mls_storage::messages::MessageStorage;
@@ -333,6 +336,74 @@ mod tests {
333336
assert_eq!(found_relays.len(), 2);
334337
}
335338

339+
#[test]
340+
fn test_group_exporter_secret_cache() {
341+
let storage = MemoryStorage::default();
342+
let nostr_storage = NostrMlsMemoryStorage::new(storage);
343+
344+
// Create a test group
345+
let mls_group_id = vec![1, 2, 3, 4];
346+
let group = Group {
347+
mls_group_id: mls_group_id.clone(),
348+
nostr_group_id: "test_group_123".to_string(),
349+
name: "Test Group".to_string(),
350+
description: "A test group".to_string(),
351+
admin_pubkeys: BTreeSet::new(),
352+
last_message_id: None,
353+
last_message_at: None,
354+
group_type: GroupType::Group,
355+
epoch: 0,
356+
state: GroupState::Active,
357+
};
358+
359+
// Save the group
360+
nostr_storage.save_group(group.clone()).unwrap();
361+
362+
// Create a test group exporter secret for epoch 0
363+
let group_exporter_secret_0 = GroupExporterSecret {
364+
mls_group_id: mls_group_id.clone(),
365+
epoch: 0,
366+
secret: vec![1, 2, 3, 4],
367+
};
368+
369+
// Create a test group exporter secret for epoch 1
370+
let group_exporter_secret_1 = GroupExporterSecret {
371+
mls_group_id: mls_group_id.clone(),
372+
epoch: 1,
373+
secret: vec![5, 6, 7, 8],
374+
};
375+
376+
// Save the group exporter secrets
377+
nostr_storage
378+
.save_group_exporter_secret(group_exporter_secret_0.clone())
379+
.unwrap();
380+
nostr_storage
381+
.save_group_exporter_secret(group_exporter_secret_1.clone())
382+
.unwrap();
383+
384+
// Get the group exporter secret for epoch 0
385+
let found_group_exporter_secret_0 = nostr_storage
386+
.get_group_exporter_secret(&mls_group_id, 0)
387+
.unwrap();
388+
assert!(found_group_exporter_secret_0.is_some());
389+
let found_group_exporter_secret_0 = found_group_exporter_secret_0.unwrap();
390+
assert_eq!(found_group_exporter_secret_0, group_exporter_secret_0);
391+
392+
// Get the group exporter secret for epoch 1
393+
let found_group_exporter_secret_1 = nostr_storage
394+
.get_group_exporter_secret(&mls_group_id, 1)
395+
.unwrap();
396+
assert!(found_group_exporter_secret_1.is_some());
397+
let found_group_exporter_secret_1 = found_group_exporter_secret_1.unwrap();
398+
assert_eq!(found_group_exporter_secret_1, group_exporter_secret_1);
399+
400+
// Verify we can't find an exporter secret for a non-existing epoch
401+
let non_existent_secret = nostr_storage
402+
.get_group_exporter_secret(&mls_group_id, 999)
403+
.unwrap();
404+
assert!(non_existent_secret.is_none());
405+
}
406+
336407
#[test]
337408
fn test_welcome_cache() {
338409
let storage = MemoryStorage::default();

crates/nostr-mls-sqlite-storage/migrations/V100__initial.sql

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,17 @@ CREATE TABLE IF NOT EXISTS group_relays (
2929
-- Create index on mls_group_id for faster lookups
3030
CREATE INDEX IF NOT EXISTS idx_group_relays_mls_group_id ON group_relays(mls_group_id);
3131

32+
-- Group Exporter Secrets table
33+
CREATE TABLE IF NOT EXISTS group_exporter_secrets (
34+
mls_group_id BLOB NOT NULL,
35+
epoch INTEGER NOT NULL,
36+
secret BLOB NOT NULL,
37+
PRIMARY KEY (mls_group_id, epoch)
38+
);
39+
40+
-- Create index on mls_group_id for faster lookups
41+
CREATE INDEX IF NOT EXISTS idx_group_exporter_secrets_mls_group_id ON group_exporter_secrets(mls_group_id);
42+
3243
-- Messages table
3344
CREATE TABLE IF NOT EXISTS messages (
3445
id BLOB PRIMARY KEY, -- Event ID as byte array

crates/nostr-mls-sqlite-storage/src/db.rs

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@ use std::io::{Error as IoError, ErrorKind};
55
use std::str::FromStr;
66

77
use nostr::{EventId, JsonUtil, Kind, PublicKey, RelayUrl, Tags, Timestamp, UnsignedEvent};
8-
use nostr_mls_storage::groups::types::{Group, GroupRelay, GroupState, GroupType};
8+
use nostr_mls_storage::groups::types::{
9+
Group, GroupExporterSecret, GroupRelay, GroupState, GroupType,
10+
};
911
use nostr_mls_storage::messages::types::{Message, ProcessedMessage, ProcessedMessageState};
1012
use nostr_mls_storage::welcomes::types::{
1113
ProcessedWelcome, ProcessedWelcomeState, Welcome, WelcomeState,
@@ -97,6 +99,19 @@ pub fn row_to_group_relay(row: &Row) -> SqliteResult<GroupRelay> {
9799
})
98100
}
99101

102+
/// Convert a row to a GroupExporterSecret struct
103+
pub fn row_to_group_exporter_secret(row: &Row) -> SqliteResult<GroupExporterSecret> {
104+
let mls_group_id: Vec<u8> = row.get("mls_group_id")?;
105+
let epoch: u64 = row.get("epoch")?;
106+
let secret: Vec<u8> = row.get("secret")?;
107+
108+
Ok(GroupExporterSecret {
109+
mls_group_id,
110+
epoch,
111+
secret,
112+
})
113+
}
114+
100115
/// Convert a row to a Message struct
101116
pub fn row_to_message(row: &Row) -> SqliteResult<Message> {
102117
let id_blob: &[u8] = row.get_ref("id")?.as_blob()?;

crates/nostr-mls-sqlite-storage/src/groups.rs

Lines changed: 132 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ use std::collections::BTreeSet;
44

55
use nostr::PublicKey;
66
use nostr_mls_storage::groups::error::GroupError;
7-
use nostr_mls_storage::groups::types::{Group, GroupRelay};
7+
use nostr_mls_storage::groups::types::{Group, GroupExporterSecret, GroupRelay};
88
use nostr_mls_storage::groups::GroupStorage;
99
use nostr_mls_storage::messages::types::Message;
1010
use rusqlite::{params, OptionalExtension};
@@ -197,6 +197,58 @@ impl GroupStorage for NostrMlsSqliteStorage {
197197

198198
Ok(())
199199
}
200+
201+
fn get_group_exporter_secret(
202+
&self,
203+
mls_group_id: &[u8],
204+
epoch: u64,
205+
) -> Result<Option<GroupExporterSecret>, GroupError> {
206+
// First verify the group exists
207+
if self.find_group_by_mls_group_id(mls_group_id)?.is_none() {
208+
return Err(GroupError::InvalidParameters(format!(
209+
"Group with MLS ID {:?} not found",
210+
mls_group_id
211+
)));
212+
}
213+
214+
let conn_guard = self.db_connection.lock().map_err(into_group_err)?;
215+
216+
let mut stmt = conn_guard
217+
.prepare("SELECT * FROM group_exporter_secrets WHERE mls_group_id = ? AND epoch = ?")
218+
.map_err(into_group_err)?;
219+
220+
stmt.query_row(
221+
params![mls_group_id, epoch],
222+
db::row_to_group_exporter_secret,
223+
)
224+
.optional()
225+
.map_err(into_group_err)
226+
}
227+
228+
fn save_group_exporter_secret(
229+
&self,
230+
group_exporter_secret: GroupExporterSecret,
231+
) -> Result<(), GroupError> {
232+
if self
233+
.find_group_by_mls_group_id(&group_exporter_secret.mls_group_id)?
234+
.is_none()
235+
{
236+
return Err(GroupError::InvalidParameters(format!(
237+
"Group with MLS ID {:?} not found",
238+
group_exporter_secret.mls_group_id
239+
)));
240+
}
241+
242+
let conn_guard = self.db_connection.lock().map_err(into_group_err)?;
243+
244+
conn_guard.execute(
245+
"INSERT OR REPLACE INTO group_exporter_secrets (mls_group_id, epoch, secret) VALUES (?, ?, ?)",
246+
params![&group_exporter_secret.mls_group_id, &group_exporter_secret.epoch, &group_exporter_secret.secret],
247+
)
248+
.map_err(into_group_err)?;
249+
250+
Ok(())
251+
}
200252
}
201253

202254
#[cfg(test)]
@@ -290,4 +342,83 @@ mod tests {
290342
"wss://relay.example.com"
291343
);
292344
}
345+
346+
#[test]
347+
fn test_group_exporter_secret() {
348+
let storage = NostrMlsSqliteStorage::new_in_memory().unwrap();
349+
350+
// Create a test group
351+
let mls_group_id = vec![1, 2, 3, 4];
352+
let group = Group {
353+
mls_group_id: mls_group_id.clone(),
354+
nostr_group_id: "test_group_123".to_string(),
355+
name: "Test Group".to_string(),
356+
description: "A test group".to_string(),
357+
admin_pubkeys: BTreeSet::new(),
358+
last_message_id: None,
359+
last_message_at: None,
360+
group_type: GroupType::Group,
361+
epoch: 0,
362+
state: GroupState::Active,
363+
};
364+
365+
// Save the group
366+
storage.save_group(group.clone()).unwrap();
367+
368+
// Create a group exporter secret
369+
let secret1 = GroupExporterSecret {
370+
mls_group_id: mls_group_id.clone(),
371+
epoch: 1,
372+
secret: vec![5, 6, 7, 8],
373+
};
374+
375+
// Save the secret
376+
storage.save_group_exporter_secret(secret1.clone()).unwrap();
377+
378+
// Get the secret and verify it was saved correctly
379+
let retrieved_secret = storage
380+
.get_group_exporter_secret(&mls_group_id, 1)
381+
.unwrap()
382+
.unwrap();
383+
assert_eq!(retrieved_secret.secret, vec![5, 6, 7, 8]);
384+
385+
// Create a second secret with same group_id and epoch but different secret value
386+
let secret2 = GroupExporterSecret {
387+
mls_group_id: mls_group_id.clone(),
388+
epoch: 1,
389+
secret: vec![9, 10, 11, 12],
390+
};
391+
392+
// Save the second secret - this should replace the first one due to the "OR REPLACE" in the SQL
393+
storage.save_group_exporter_secret(secret2.clone()).unwrap();
394+
395+
// Get the secret again and verify it was updated
396+
let retrieved_secret = storage
397+
.get_group_exporter_secret(&mls_group_id, 1)
398+
.unwrap()
399+
.unwrap();
400+
assert_eq!(retrieved_secret.secret, vec![9, 10, 11, 12]);
401+
402+
// Verify we can still save a different epoch
403+
let secret3 = GroupExporterSecret {
404+
mls_group_id: mls_group_id.clone(),
405+
epoch: 2,
406+
secret: vec![13, 14, 15, 16],
407+
};
408+
409+
storage.save_group_exporter_secret(secret3.clone()).unwrap();
410+
411+
// Verify both epochs exist
412+
let retrieved_secret1 = storage
413+
.get_group_exporter_secret(&mls_group_id, 1)
414+
.unwrap()
415+
.unwrap();
416+
let retrieved_secret2 = storage
417+
.get_group_exporter_secret(&mls_group_id, 2)
418+
.unwrap()
419+
.unwrap();
420+
421+
assert_eq!(retrieved_secret1.secret, vec![9, 10, 11, 12]);
422+
assert_eq!(retrieved_secret2.secret, vec![13, 14, 15, 16]);
423+
}
293424
}

0 commit comments

Comments
 (0)