@@ -79,7 +79,7 @@ unsafe impl Send for SqlCipherConnection {}
7979unsafe impl Sync for SqlCipherConnection { }
8080
8181#[ derive( Default ) ]
82- enum MigrationTarget {
82+ pub ( crate ) enum MigrationTarget {
8383 #[ default]
8484 Latest ,
8585 Version ( u16 ) ,
@@ -114,6 +114,29 @@ impl SqlCipherConnection {
114114 Ok ( conn)
115115 }
116116
117+ #[ cfg( test) ]
118+ pub ( crate ) fn init_with_key_at_schema_version (
119+ path : & str ,
120+ key : & DatabaseKey ,
121+ version : MigrationTarget ,
122+ ) -> CryptoKeystoreResult < Self > {
123+ let mut conn = rusqlite:: Connection :: open ( path) ?;
124+
125+ Self :: set_key ( & mut conn, key) ?;
126+
127+ // Disable FOREIGN KEYs - The 2 step blob writing process invalidates foreign key checks unfortunately
128+ conn. pragma_update ( None , "foreign_keys" , "OFF" ) ?;
129+
130+ Self :: run_migrations ( & mut conn, version) ?;
131+
132+ let conn = Self {
133+ path : path. into ( ) ,
134+ conn : Mutex :: new ( conn) ,
135+ } ;
136+
137+ Ok ( conn)
138+ }
139+
117140 #[ cfg( feature = "log-queries" ) ]
118141 fn log_query ( event : TraceEvent ) {
119142 if let TraceEvent :: Stmt ( _, sql) = event {
@@ -295,9 +318,14 @@ impl<'a> DatabaseConnection<'a> for SqlCipherConnection {
295318mod migration_test {
296319 use std:: io:: Write ;
297320
321+ use openmls:: prelude:: Ciphersuite ;
298322 use tempfile:: NamedTempFile ;
299323
300- use crate :: { ConnectionType , Database , DatabaseKey } ;
324+ use crate :: {
325+ ConnectionType , Database , DatabaseKey ,
326+ connection:: { FetchFromDatabase , MigrationTarget } ,
327+ entities:: { EntityFindParams , StoredCredential } ,
328+ } ;
301329
302330 const DB : & [ u8 ] = include_bytes ! ( "../../../../../crypto-ffi/bindings/jvm/src/test/resources/db-v10002003.sqlite" ) ;
303331 const OLD_KEY : & str = "secret" ;
@@ -317,4 +345,105 @@ mod migration_test {
317345
318346 let _db = smol:: block_on ( Database :: open ( ConnectionType :: Persistent ( path) , & new_key) ) . unwrap ( ) ;
319347 }
348+
349+ #[ test]
350+ fn deduplicating_credentials ( ) {
351+ let mut db_file = NamedTempFile :: new ( ) . unwrap ( ) ;
352+ db_file. write_all ( DB ) . unwrap ( ) ;
353+ let path = db_file
354+ . path ( )
355+ . to_str ( )
356+ . expect ( "tmpfile path is representable in unicode" ) ;
357+
358+ let new_key = DatabaseKey :: generate ( ) ;
359+ smol:: block_on ( Database :: migrate_db_key_type_to_bytes ( path, OLD_KEY , & new_key) ) . unwrap ( ) ;
360+
361+ smol:: block_on ( async {
362+ let db = Database :: open_at_schema_version ( path, & new_key, MigrationTarget :: Version ( 18 ) )
363+ . await
364+ . unwrap ( ) ;
365+
366+ let conn_guard = db. conn ( ) . await . unwrap ( ) ;
367+ let conn = conn_guard. conn . lock ( ) . await ;
368+ let mut stmt = conn
369+ . prepare ( & format ! (
370+ "SELECT
371+ id,
372+ credential,
373+ unixepoch(created_at) AS created_at,
374+ ciphersuite,
375+ public_key,
376+ secret_key
377+ FROM {credential_table}" ,
378+ credential_table = "mls_credentials_new" ,
379+ ) )
380+ . expect ( "preparing statement" ) ;
381+
382+ let credential = stmt
383+ . query_one ( [ ] , |row| {
384+ Ok ( StoredCredential {
385+ id : row. get ( "id" ) ?,
386+ credential : row. get ( "credential" ) ?,
387+ created_at : row. get ( "created_at" ) ?,
388+ ciphersuite : row. get ( "ciphersuite" ) ?,
389+ public_key : row. get ( "public_key" ) ?,
390+ secret_key : row. get ( "secret_key" ) ?,
391+ } )
392+ } )
393+ . expect ( "credential from row" ) ;
394+
395+ // Ciphersuites need to be ambiguous w.r.t their signature scheme to be a relevant duplicate
396+ conn. execute (
397+ "UPDATE mls_credentials_new SET ciphersuite = ?1" ,
398+ [ Ciphersuite :: MLS_128_DHKEMX25519_AES128GCM_SHA256_Ed25519 as u16 ] ,
399+ )
400+ . expect ( "updating ciphersuite" ) ;
401+
402+ // Create a duplicate from this credential
403+ conn. execute (
404+ "INSERT INTO mls_credentials_new (
405+ id,
406+ credential,
407+ created_at,
408+ ciphersuite,
409+ public_key,
410+ secret_key
411+ )
412+ VALUES (?1, ?2, datetime(?3, 'unixepoch'), ?4, ?5, ?6)" ,
413+ (
414+ credential. id . clone ( ) ,
415+ credential. credential . clone ( ) ,
416+ credential. created_at ,
417+ Ciphersuite :: MLS_128_DHKEMX25519_CHACHA20POLY1305_SHA256_Ed25519 as u16 ,
418+ credential. public_key . clone ( ) ,
419+ credential. secret_key . clone ( ) ,
420+ ) ,
421+ )
422+ . expect ( "inserting duplicate" ) ;
423+
424+ let count = conn
425+ . query_row ( "SELECT COUNT(*) FROM mls_credentials_new" , [ ] , |row| {
426+ row. get :: < _ , i32 > ( 0 )
427+ } )
428+ . unwrap ( ) ;
429+
430+ assert_eq ! ( count, 2 ) ;
431+
432+ drop ( stmt) ;
433+ drop ( conn) ;
434+ drop ( conn_guard) ;
435+ drop ( db) ;
436+
437+ let db = Database :: open ( ConnectionType :: Persistent ( path) , & new_key)
438+ . await
439+ . unwrap ( ) ;
440+ let deduplicated_count = db
441+ . find_all :: < StoredCredential > ( EntityFindParams :: default ( ) )
442+ . await
443+ . expect ( "deduplicated credentials" )
444+ . len ( ) ;
445+
446+ assert_eq ! ( deduplicated_count, 1 ) ;
447+ } ) ;
448+ }
320449}
0 commit comments