@@ -4,7 +4,7 @@ use std::collections::BTreeSet;
4
4
5
5
use nostr:: PublicKey ;
6
6
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 } ;
8
8
use nostr_mls_storage:: groups:: GroupStorage ;
9
9
use nostr_mls_storage:: messages:: types:: Message ;
10
10
use rusqlite:: { params, OptionalExtension } ;
@@ -197,6 +197,58 @@ impl GroupStorage for NostrMlsSqliteStorage {
197
197
198
198
Ok ( ( ) )
199
199
}
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
+ }
200
252
}
201
253
202
254
#[ cfg( test) ]
@@ -290,4 +342,83 @@ mod tests {
290
342
"wss://relay.example.com"
291
343
) ;
292
344
}
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
+ }
293
424
}
0 commit comments