@@ -19,6 +19,7 @@ use std::{
1919
2020use async_trait:: async_trait;
2121use gloo_utils:: format:: JsValueSerdeExt ;
22+ use hkdf:: Hkdf ;
2223use indexed_db_futures:: prelude:: * ;
2324use matrix_sdk_crypto:: {
2425 olm:: {
@@ -30,6 +31,7 @@ use matrix_sdk_crypto::{
3031 RoomKeyCounts , RoomSettings ,
3132 } ,
3233 types:: events:: room_key_withheld:: RoomKeyWithheldEvent ,
34+ vodozemac:: base64_encode,
3335 Account , GossipRequest , GossippedSecret , ReadOnlyDevice , ReadOnlyUserIdentities , SecretInfo ,
3436 TrackedUser ,
3537} ;
@@ -38,6 +40,7 @@ use ruma::{
3840 events:: secret:: request:: SecretName , DeviceId , MilliSecondsSinceUnixEpoch , OwnedDeviceId ,
3941 RoomId , TransactionId , UserId ,
4042} ;
43+ use sha2:: Sha256 ;
4144use tokio:: sync:: Mutex ;
4245use tracing:: { debug, warn} ;
4346use wasm_bindgen:: JsValue ;
@@ -272,34 +275,25 @@ impl IndexeddbCryptoStore {
272275 IndexeddbCryptoStore :: open_with_store_cipher ( "crypto" , None ) . await
273276 }
274277
275- /// Open a new `IndexeddbCryptoStore` with given name and passphrase
278+ /// Open an `IndexeddbCryptoStore` with given name and passphrase.
279+ ///
280+ /// If the store previously existed, the encryption cipher is initialised
281+ /// using the given passphrase and the details from the meta store. If the
282+ /// store did not previously exist, a new encryption cipher is derived
283+ /// from the passphrase, and the details are stored to the metastore.
284+ ///
285+ /// The store is then opened, or a new one created, using the encryption
286+ /// cipher.
287+ ///
288+ /// # Arguments
289+ ///
290+ /// * `prefix` - Common prefix for the names of the two IndexedDB stores.
291+ /// * `passphrase` - Passphrase which is used to derive a key to encrypt the
292+ /// key which is used to encrypt the store. Must be the same each time the
293+ /// store is opened.
276294 pub async fn open_with_passphrase ( prefix : & str , passphrase : & str ) -> Result < Self > {
277- let name = format ! ( "{prefix:0}::matrix-sdk-crypto-meta" ) ;
278-
279- debug ! ( "IndexedDbCryptoStore: Opening meta-store {name}" ) ;
280- let mut db_req: OpenDbRequest = IdbDatabase :: open_u32 ( & name, 1 ) ?;
281- db_req. set_on_upgrade_needed ( Some ( |evt : & IdbVersionChangeEvent | -> Result < ( ) , JsValue > {
282- let old_version = evt. old_version ( ) as u32 ;
283- if old_version < 1 {
284- // migrating to version 1
285- let db = evt. db ( ) ;
286-
287- db. create_object_store ( "matrix-sdk-crypto" ) ?;
288- }
289- Ok ( ( ) )
290- } ) ) ;
291-
292- let db: IdbDatabase = db_req. await ?;
293-
294- let tx: IdbTransaction < ' _ > =
295- db. transaction_on_one_with_mode ( "matrix-sdk-crypto" , IdbTransactionMode :: Readonly ) ?;
296- let ob = tx. object_store ( "matrix-sdk-crypto" ) ?;
297-
298- let store_cipher: Option < Vec < u8 > > = ob
299- . get ( & JsValue :: from_str ( keys:: STORE_CIPHER ) ) ?
300- . await ?
301- . map ( |k| k. into_serde ( ) )
302- . transpose ( ) ?;
295+ let db = open_meta_db ( prefix) . await ?;
296+ let store_cipher = load_store_cipher ( & db) . await ?;
303297
304298 let store_cipher = match store_cipher {
305299 Some ( cipher) => {
@@ -315,17 +309,58 @@ impl IndexeddbCryptoStore {
315309 #[ cfg( test) ]
316310 let export = cipher. _insecure_export_fast_for_testing ( passphrase) ;
317311
318- let tx: IdbTransaction < ' _ > = db. transaction_on_one_with_mode (
319- "matrix-sdk-crypto" ,
320- IdbTransactionMode :: Readwrite ,
321- ) ?;
322- let ob = tx. object_store ( "matrix-sdk-crypto" ) ?;
323-
324- ob. put_key_val (
325- & JsValue :: from_str ( keys:: STORE_CIPHER ) ,
326- & JsValue :: from_serde ( & export. map_err ( CryptoStoreError :: backend) ?) ?,
327- ) ?;
328- tx. await . into_result ( ) ?;
312+ let export = export. map_err ( CryptoStoreError :: backend) ?;
313+
314+ save_store_cipher ( & db, & export) . await ?;
315+ cipher
316+ }
317+ } ;
318+
319+ // Must release the database access manually as it's not done when
320+ // dropping it.
321+ db. close ( ) ;
322+
323+ IndexeddbCryptoStore :: open_with_store_cipher ( prefix, Some ( store_cipher. into ( ) ) ) . await
324+ }
325+
326+ /// Open an `IndexeddbCryptoStore` with given name and key.
327+ ///
328+ /// If the store previously existed, the encryption cipher is initialised
329+ /// using the given key and the details from the meta store. If the store
330+ /// did not previously exist, a new encryption cipher is derived from
331+ /// the passphrase, and the details are stored to the metastore.
332+ ///
333+ /// The store is then opened, or a new one created, using the encryption
334+ /// cipher.
335+ ///
336+ /// # Arguments
337+ ///
338+ /// * `prefix` - Common prefix for the names of the two IndexedDB stores.
339+ /// * `key` - Key with which to encrypt the key which is used to encrypt the
340+ /// store. Must be the same each time the store is opened.
341+ pub async fn open_with_key ( prefix : & str , key : & [ u8 ; 32 ] ) -> Result < Self > {
342+ // The application might also use the provided key for something else, so to
343+ // avoid key reuse, we pass the provided key through an HKDF
344+ let mut chacha_key = zeroize:: Zeroizing :: new ( [ 0u8 ; 32 ] ) ;
345+ const HKDF_INFO : & [ u8 ] = b"CRYPTOSTORE_CIPHER" ;
346+ let hkdf = Hkdf :: < Sha256 > :: new ( None , key) ;
347+ hkdf. expand ( HKDF_INFO , & mut * chacha_key)
348+ . expect ( "We should be able to generate a 32-byte key" ) ;
349+
350+ let db = open_meta_db ( prefix) . await ?;
351+ let store_cipher = load_store_cipher ( & db) . await ?;
352+
353+ let store_cipher = match store_cipher {
354+ Some ( cipher) => {
355+ debug ! ( "IndexedDbCryptoStore: decrypting store cipher" ) ;
356+ import_store_cipher_with_key ( & chacha_key, key, & cipher, & db) . await ?
357+ }
358+ None => {
359+ debug ! ( "IndexedDbCryptoStore: encrypting new store cipher" ) ;
360+ let cipher = StoreCipher :: new ( ) . map_err ( CryptoStoreError :: backend) ?;
361+ let export =
362+ cipher. export_with_key ( & chacha_key) . map_err ( CryptoStoreError :: backend) ?;
363+ save_store_cipher ( & db, & export) . await ?;
329364 cipher
330365 }
331366 } ;
@@ -1289,6 +1324,114 @@ impl Drop for IndexeddbCryptoStore {
12891324 }
12901325}
12911326
1327+ /// Open the meta store.
1328+ ///
1329+ /// The meta store contains details about the encryption of the main store.
1330+ async fn open_meta_db ( prefix : & str ) -> Result < IdbDatabase , IndexeddbCryptoStoreError > {
1331+ let name = format ! ( "{prefix:0}::matrix-sdk-crypto-meta" ) ;
1332+
1333+ debug ! ( "IndexedDbCryptoStore: Opening meta-store {name}" ) ;
1334+ let mut db_req: OpenDbRequest = IdbDatabase :: open_u32 ( & name, 1 ) ?;
1335+ db_req. set_on_upgrade_needed ( Some ( |evt : & IdbVersionChangeEvent | -> Result < ( ) , JsValue > {
1336+ let old_version = evt. old_version ( ) as u32 ;
1337+ if old_version < 1 {
1338+ // migrating to version 1
1339+ let db = evt. db ( ) ;
1340+
1341+ db. create_object_store ( "matrix-sdk-crypto" ) ?;
1342+ }
1343+ Ok ( ( ) )
1344+ } ) ) ;
1345+
1346+ Ok ( db_req. await ?)
1347+ }
1348+
1349+ /// Load the serialised store cipher from the meta store.
1350+ ///
1351+ /// # Arguments:
1352+ ///
1353+ /// * `meta_db`: Connection to the meta store, as returned by [`open_meta_db`].
1354+ ///
1355+ /// # Returns:
1356+ ///
1357+ /// The serialised `StoreCipher` object.
1358+ async fn load_store_cipher (
1359+ meta_db : & IdbDatabase ,
1360+ ) -> Result < Option < Vec < u8 > > , IndexeddbCryptoStoreError > {
1361+ let tx: IdbTransaction < ' _ > =
1362+ meta_db. transaction_on_one_with_mode ( "matrix-sdk-crypto" , IdbTransactionMode :: Readonly ) ?;
1363+ let ob = tx. object_store ( "matrix-sdk-crypto" ) ?;
1364+
1365+ let store_cipher: Option < Vec < u8 > > = ob
1366+ . get ( & JsValue :: from_str ( keys:: STORE_CIPHER ) ) ?
1367+ . await ?
1368+ . map ( |k| k. into_serde ( ) )
1369+ . transpose ( ) ?;
1370+ Ok ( store_cipher)
1371+ }
1372+
1373+ /// Save the serialised store cipher to the meta store.
1374+ ///
1375+ /// # Arguments:
1376+ ///
1377+ /// * `meta_db`: Connection to the meta store, as returned by [`open_meta_db`].
1378+ /// * `store_cipher`: The serialised `StoreCipher` object.
1379+ async fn save_store_cipher (
1380+ db : & IdbDatabase ,
1381+ export : & Vec < u8 > ,
1382+ ) -> Result < ( ) , IndexeddbCryptoStoreError > {
1383+ let tx: IdbTransaction < ' _ > =
1384+ db. transaction_on_one_with_mode ( "matrix-sdk-crypto" , IdbTransactionMode :: Readwrite ) ?;
1385+ let ob = tx. object_store ( "matrix-sdk-crypto" ) ?;
1386+
1387+ ob. put_key_val ( & JsValue :: from_str ( keys:: STORE_CIPHER ) , & JsValue :: from_serde ( & export) ?) ?;
1388+ tx. await . into_result ( ) ?;
1389+ Ok ( ( ) )
1390+ }
1391+
1392+ /// Given a serialised store cipher, try importing with the given key.
1393+ ///
1394+ /// This is a helper for [`IndexeddbCryptoStore::open_with_key`].
1395+ ///
1396+ /// # Arguments
1397+ ///
1398+ /// * `chacha_key`: The key to use with [`StoreCipher::import_with_key`].
1399+ /// Derived from `original_key` via an HKDF.
1400+ /// * `original_key`: The key provided by the application. Used to provide a
1401+ /// migration path from an older key derivation system.
1402+ /// * `serialised_cipher`: The serialized `EncryptedStoreCipher`, retrieved from
1403+ /// the database.
1404+ /// * `db`: Connection to the database.
1405+ async fn import_store_cipher_with_key (
1406+ chacha_key : & [ u8 ; 32 ] ,
1407+ original_key : & [ u8 ] ,
1408+ serialised_cipher : & [ u8 ] ,
1409+ db : & IdbDatabase ,
1410+ ) -> Result < StoreCipher , IndexeddbCryptoStoreError > {
1411+ let cipher = match StoreCipher :: import_with_key ( chacha_key, serialised_cipher) {
1412+ Ok ( cipher) => cipher,
1413+ Err ( matrix_sdk_store_encryption:: Error :: KdfMismatch ) => {
1414+ // Old versions of the matrix-js-sdk used to base64-encode their encryption
1415+ // key, and pass it into [`IndexeddbCryptoStore::open_with_passphrase`]. For
1416+ // backwards compatibility, we fall back to that if we discover we have a cipher
1417+ // encrypted with a KDF when we expected it to be encrypted directly with a key.
1418+ let cipher = StoreCipher :: import ( & base64_encode ( original_key) , serialised_cipher)
1419+ . map_err ( |_| CryptoStoreError :: UnpicklingError ) ?;
1420+
1421+ // Loading the cipher with the passphrase was successful. Let's update the
1422+ // stored version of the cipher so that it is encrypted with a key,
1423+ // to save doing this again.
1424+ debug ! ( "IndexedDbCryptoStore: Migrating passphrase-encrypted store cipher to key-encryption" ) ;
1425+
1426+ let export = cipher. export_with_key ( chacha_key) . map_err ( CryptoStoreError :: backend) ?;
1427+ save_store_cipher ( db, & export) . await ?;
1428+ cipher
1429+ }
1430+ Err ( _) => Err ( CryptoStoreError :: UnpicklingError ) ?,
1431+ } ;
1432+ Ok ( cipher)
1433+ }
1434+
12921435/// Fetch items from an object store in batches, transform each item using
12931436/// the supplied function, and stuff the transformed items into a single
12941437/// vector to return.
@@ -1546,7 +1689,14 @@ mod tests {
15461689
15471690#[ cfg( all( test, target_arch = "wasm32" ) ) ]
15481691mod encrypted_tests {
1549- use matrix_sdk_crypto:: cryptostore_integration_tests;
1692+ use matrix_sdk_crypto:: {
1693+ cryptostore_integration_tests,
1694+ olm:: Account ,
1695+ store:: { CryptoStore , PendingChanges } ,
1696+ vodozemac:: base64_encode,
1697+ } ;
1698+ use matrix_sdk_test:: async_test;
1699+ use ruma:: { device_id, user_id} ;
15501700
15511701 use super :: IndexeddbCryptoStore ;
15521702
@@ -1561,4 +1711,36 @@ mod encrypted_tests {
15611711 . expect ( "Can't create a passphrase protected store" )
15621712 }
15631713 cryptostore_integration_tests ! ( ) ;
1714+
1715+ /// Test that we can migrate a store created with a passphrase, to being
1716+ /// encrypted with a key instead.
1717+ #[ async_test]
1718+ async fn migrate_passphrase_to_key ( ) {
1719+ let store_name = "test_migrate_passphrase_to_key" ;
1720+ let passdata: [ u8 ; 32 ] = rand:: random ( ) ;
1721+ let b64_passdata = base64_encode ( passdata) ;
1722+
1723+ // Initialise the store with some account data
1724+ let store = IndexeddbCryptoStore :: open_with_passphrase ( & store_name, & b64_passdata)
1725+ . await
1726+ . expect ( "Can't create a passphrase-protected store" ) ;
1727+
1728+ store
1729+ . save_pending_changes ( PendingChanges {
1730+ account : Some ( Account :: with_device_id (
1731+ user_id ! ( "@alice:example.org" ) ,
1732+ device_id ! ( "ALICEDEVICE" ) ,
1733+ ) ) ,
1734+ } )
1735+ . await
1736+ . expect ( "Can't save account" ) ;
1737+
1738+ // Now reopen the store, passing the key directly rather than as a b64 string.
1739+ let store = IndexeddbCryptoStore :: open_with_key ( & store_name, & passdata)
1740+ . await
1741+ . expect ( "Can't create a key-protected store" ) ;
1742+ let loaded_account =
1743+ store. load_account ( ) . await . expect ( "Can't load account" ) . expect ( "Account was not saved" ) ;
1744+ assert_eq ! ( loaded_account. user_id, user_id!( "@alice:example.org" ) ) ;
1745+ }
15641746}
0 commit comments