diff --git a/crates/matrix-sdk/tests/integration/encryption.rs b/crates/matrix-sdk/tests/integration/encryption.rs index f22d9ff40..d2d383ceb 100644 --- a/crates/matrix-sdk/tests/integration/encryption.rs +++ b/crates/matrix-sdk/tests/integration/encryption.rs @@ -1 +1,2 @@ +mod secret_storage; mod verification; diff --git a/crates/matrix-sdk/tests/integration/encryption/secret_storage.rs b/crates/matrix-sdk/tests/integration/encryption/secret_storage.rs new file mode 100644 index 000000000..14109777b --- /dev/null +++ b/crates/matrix-sdk/tests/integration/encryption/secret_storage.rs @@ -0,0 +1,650 @@ +use std::sync::{Arc, Mutex}; + +use assert_matches::assert_matches; +use matrix_sdk::{ + encryption::secret_storage::SecretStorageError, + matrix_auth::{MatrixSession, MatrixSessionTokens}, +}; +use matrix_sdk_base::SessionMeta; +use matrix_sdk_test::async_test; +use ruma::{ + device_id, + events::{ + secret::request::SecretName, + secret_storage::{ + default_key::SecretStorageDefaultKeyEventContent, secret::SecretEventContent, + }, + }, + user_id, UserId, +}; +use serde_json::json; +use wiremock::{ + matchers::{header, method, path, path_regex}, + Mock, MockServer, ResponseTemplate, +}; + +use crate::{logged_in_client, no_retry_test_client}; + +const SECRET_STORE_KEY: &str = "EsTj 3yST y93F SLpB jJsz eAXc 2XzA ygD3 w69H fGaN TKBj jXEd"; + +async fn mock_secret_store_key( + server: &MockServer, + user_id: &UserId, + key_id: &str, + iv: &str, + mac: &str, +) { + Mock::given(method("GET")) + .and(path(format!( + "_matrix/client/r0/user/{user_id}/account_data/m.secret_storage.default_key" + ))) + .and(header("authorization", "Bearer 1234")) + .respond_with(ResponseTemplate::new(200).set_body_json(json!({ + "key": key_id, + }))) + .expect(1..) + .named("default_key account data GET") + .mount(server) + .await; + + Mock::given(method("GET")) + .and(path(format!( + "_matrix/client/r0/user/{user_id}/account_data/m.secret_storage.key.{key_id}" + ))) + .and(header("authorization", "Bearer 1234")) + .respond_with(ResponseTemplate::new(200).set_body_json(json!({ + "algorithm": "m.secret_storage.v1.aes-hmac-sha2", + "iv": iv, + "mac": mac, + }))) + .expect(1..) + .named("m.direct account data GET") + .mount(server) + .await; +} + +#[async_test] +async fn secret_store_create_default_key() { + let (client, server) = logged_in_client().await; + + let user_id = client.user_id().expect("We should know our user ID by now"); + + let key_id: Arc>> = Mutex::new(None).into(); + + let put_new_key_matcher = { + let key_id = key_id.to_owned(); + + move |request: &wiremock::Request| { + let path_segments = + request.url.path_segments().expect("The URL should be able to be a base"); + + let key_id_segment = path_segments + .last() + .expect("The path should have a key ID as the last segment") + .to_owned(); + + *key_id.lock().unwrap() = Some(key_id_segment); + + true + } + }; + + let put_new_key_id_matcher = move |request: &wiremock::Request| { + let key_id = key_id.lock().unwrap().take().expect("We should know our new key ID by now"); + + let content: SecretStorageDefaultKeyEventContent = + request.body_json().expect("The content should be a default key event content"); + + assert_eq!( + key_id, + format!("m.secret_storage.key.{}", content.key_id), + "The key ID of the key we created should be the same as the key ID we're marking as the default one" + ); + + true + }; + + Mock::given(method("PUT")) + .and(path_regex(format!( + r"_matrix/client/r0/user/{user_id}/account_data/m.secret_storage.key.[A-Za-z0-9]" + ))) + .and(header("authorization", "Bearer 1234")) + .and(put_new_key_matcher) + .respond_with(ResponseTemplate::new(200).set_body_json(json!({}))) + .expect(1) + .named("m.direct account data GET") + .mount(&server) + .await; + + Mock::given(method("PUT")) + .and(path(format!( + "_matrix/client/r0/user/{user_id}/account_data/m.secret_storage.default_key" + ))) + .and(header("authorization", "Bearer 1234")) + .and(put_new_key_id_matcher) + .respond_with(ResponseTemplate::new(200).set_body_json(json!({}))) + .expect(1) + .named("default_key account data GET") + .mount(&server) + .await; + + let _ = client + .encryption() + .secret_storage() + .create_secret_store() + .await + .expect("We should be able to create a new secret store"); + + server.verify().await; +} + +#[async_test] +async fn secret_store_missing_key_info() { + let (client, server) = logged_in_client().await; + + let user_id = client.user_id().expect("We should know our user ID by now"); + let key_id = "bmur2d9ypPUH1msSwCxQOJkuKRmJI55e"; + + Mock::given(method("GET")) + .and(path(format!( + "_matrix/client/r0/user/{user_id}/account_data/m.secret_storage.default_key" + ))) + .and(header("authorization", "Bearer 1234")) + .respond_with(ResponseTemplate::new(200).set_body_json(json!({ + "key": key_id + }))) + .expect(1) + .named("default_key account data GET") + .mount(&server) + .await; + + Mock::given(method("GET")) + .and(path(format!( + "_matrix/client/r0/user/{user_id}/account_data/m.secret_storage.key.{key_id}" + ))) + .and(header("authorization", "Bearer 1234")) + .respond_with(ResponseTemplate::new(404).set_body_json(json!({ + "errcode": "M_NOT_FOUND", + "error": "Account data not found" + }))) + .expect(1) + .named("m.direct account data GET") + .mount(&server) + .await; + + let ret = client.encryption().secret_storage().open_secret_store(SECRET_STORE_KEY).await; + + let found_key_id = assert_matches!( + ret, + Err(SecretStorageError::MissingKeyInfo { key_id: Some(key_id) }) => key_id, + "We should report that the key info for the default key is missing" + ); + + assert_eq!( + key_id, found_key_id, + "The key ID in the error should match to the key ID of the reported default key" + ); + + server.verify().await; +} + +#[async_test] +async fn secret_store_not_setup() { + let (client, server) = logged_in_client().await; + + let user_id = client.user_id().expect("We should know our user ID by now"); + + Mock::given(method("GET")) + .and(path(format!( + "_matrix/client/r0/user/{user_id}/account_data/m.secret_storage.default_key" + ))) + .and(header("authorization", "Bearer 1234")) + .respond_with(ResponseTemplate::new(404).set_body_json(json!({ + "errcode": "M_NOT_FOUND", + "error": "Account data not found" + }))) + .expect(1) + .named("default_key account data GET") + .mount(&server) + .await; + + let ret = client.encryption().secret_storage().open_secret_store(SECRET_STORE_KEY).await; + + assert_matches!( + ret, + Err(SecretStorageError::MissingKeyInfo { key_id: None }), + "We should report that the key info for the default key is missing" + ); + + server.verify().await; +} + +#[async_test] +async fn secret_store_opening() { + let (client, server) = logged_in_client().await; + + mock_secret_store_key( + &server, + client.user_id().unwrap(), + "bmur2d9ypPUH1msSwCxQOJkuKRmJI55e", + "xv5b6/p3ExEw++wTyfSHEg==", + "ujBBbXahnTAMkmPUX2/0+VTfUh63pGyVRuBcDMgmJC8=", + ) + .await; + + Mock::given(method("GET")) + .and(path("_matrix/client/r0/user/@example:localhost/account_data/m.cross_signing.master")) + .and(header("authorization", "Bearer 1234")) + .respond_with(ResponseTemplate::new(200).set_body_json(json!({ + "encrypted": { + "bmur2d9ypPUH1msSwCxQOJkuKRmJI55e": { + "ciphertext": "lCRSSA1lChONEXj/8RyogsgAa8ouQwYDnLr4XBCheRikrZykLRzPCx3doCE=", + "iv": "bdfCwu+ECYgZ/jWTkGrQ/A==", + "mac": "NXeV1dZaOe2JLvQ6Hh6tFto7AgFFdaQnY0l9pruwdtE=" + } + } + }))) + .expect(1..) + .named("m.direct account data GET") + .mount(&server) + .await; + + let secret_store = client + .encryption() + .secret_storage() + .open_secret_store(SECRET_STORE_KEY) + .await + .expect("We should be able to open our secret store"); + + let secret = secret_store + .get_secret(SecretName::CrossSigningMasterKey) + .await + .expect("We should be able to retrieve a secret from the secret store") + .unwrap(); + + assert_eq!(secret, "VcY1+LNyV6aSMne8mvi4lc/q+JiCHe+m6+hKLtgLP30="); + + server.verify().await; +} + +#[async_test] +async fn set_in_secret_store() { + let (client, server) = logged_in_client().await; + + mock_secret_store_key( + &server, + client.user_id().unwrap(), + "bmur2d9ypPUH1msSwCxQOJkuKRmJI55e", + "xv5b6/p3ExEw++wTyfSHEg==", + "ujBBbXahnTAMkmPUX2/0+VTfUh63pGyVRuBcDMgmJC8=", + ) + .await; + + let secret_store = client + .encryption() + .secret_storage() + .open_secret_store(SECRET_STORE_KEY) + .await + .expect("We should be able to open our secret store"); + + let uploaded_content: Arc>> = Mutex::new(None).into(); + + { + // This mock is scoped because, at first we don't have a `foo` event in our + // account data. Only when we call `secret_store.set_secret()` will we + // have one, and a different mock will be required for the next GET request. + let _guard = Mock::given(method("GET")) + .and(path("_matrix/client/r0/user/@example:localhost/account_data/foo")) + .and(header("authorization", "Bearer 1234")) + .respond_with(ResponseTemplate::new(404).set_body_json(json!({ + "errcode": "M_NOT_FOUND", + "error": "Account data not found" + }))) + .expect(1) + .named("foo account data GET") + .mount_as_scoped(&server) + .await; + + // Create a custom matcher to extract the body so we can put it into the + // response for the next GET /account_data request. + let put_matcher = { + let uploaded_content = uploaded_content.to_owned(); + + move |request: &wiremock::Request| { + let content: SecretEventContent = + request.body_json().expect("The request body should be a SecretEventContent"); + + *uploaded_content.lock().unwrap() = Some(content); + + true + } + }; + + Mock::given(method("PUT")) + .and(path("_matrix/client/r0/user/@example:localhost/account_data/foo")) + .and(header("authorization", "Bearer 1234")) + .and(put_matcher) + .respond_with(ResponseTemplate::new(200).set_body_json(json!({}))) + .expect(1) + .named("foo account data PUT") + .mount(&server) + .await; + + secret_store + .put_secret("foo", "It's a secret to everybody") + .await + .expect("We should be able to store a secret to the secret store"); + } + + let uploaded_content = uploaded_content + .lock() + .unwrap() + .take() + .expect("The secret content should have been uploaded"); + + let uploaded_content = serde_json::to_value(uploaded_content).unwrap(); + + Mock::given(method("GET")) + .and(path("_matrix/client/r0/user/@example:localhost/account_data/foo")) + .and(header("authorization", "Bearer 1234")) + .respond_with(ResponseTemplate::new(200).set_body_json(uploaded_content)) + .expect(1) + .named("foo account data GET") + .mount(&server) + .await; + + let secret = secret_store + .get_secret("foo") + .await + .expect("We should be able to retrieve a secret from the secret store") + .unwrap(); + + assert_eq!(secret, "It's a secret to everybody"); + + server.verify().await; +} + +#[async_test] +async fn restore_cross_signing_from_secret_store() { + let user_id = user_id!("@example:morpheus.localhost"); + + let session = MatrixSession { + meta: SessionMeta { + user_id: user_id!("@example:morpheus.localhost").to_owned(), + device_id: device_id!("DEVICEID").to_owned(), + }, + tokens: MatrixSessionTokens { access_token: "1234".to_owned(), refresh_token: None }, + }; + let (client, server) = no_retry_test_client().await; + client.restore_session(session).await.unwrap(); + + mock_secret_store_key( + &server, + user_id, + "bmur2d9ypPUH1msSwCxQOJkuKRmJI55e", + "xv5b6/p3ExEw++wTyfSHEg==", + "ujBBbXahnTAMkmPUX2/0+VTfUh63pGyVRuBcDMgmJC8=", + ) + .await; + + Mock::given(method("POST")) + .and(path("_matrix/client/r0/keys/query")) + .and(header("authorization", "Bearer 1234")) + .respond_with(ResponseTemplate::new(200).set_body_json(json!({ + "master_keys": { + "@example:morpheus.localhost": { + "keys": { + "ed25519:fKJdf5b3ga1hshUT5obkBteMNTtLkfy8qh4h5/XLNew": "fKJdf5b3ga1hshUT5obkBteMNTtLkfy8qh4h5/XLNew" + }, + "signatures": { + "@example:morpheus.localhost": { + "ed25519:QLEYYETKXR": "2UCY7IS+5NFdzGGPgyovI2+uHM13pbrsAJIUp0daUCzJdl6hvp5rK/6L5AgyxiVAhbK/XT4lDDJeSymeJAs7Cg" + } + }, + "usage": [ + "master" + ], + "user_id": "@example:morpheus.localhost" + } + }, + "self_signing_keys": { + "@example:morpheus.localhost": { + "keys": { + "ed25519:vsEoa0HHxc8hmlWgoohTQBGWcCRh1BFoTzI8HNIxg5Q": "vsEoa0HHxc8hmlWgoohTQBGWcCRh1BFoTzI8HNIxg5Q" + }, + "signatures": { + "@example:morpheus.localhost": { + "ed25519:fKJdf5b3ga1hshUT5obkBteMNTtLkfy8qh4h5/XLNew": "YFvSUX40E81EO8jibgn+kklMFkqfGTDPcENsq6UEepMY9UUOp9iredNCA/NR2INlfq4gkQhMJk2QRGZ31pQZCg" + } + }, + "usage": [ + "self_signing" + ], + "user_id": "@example:morpheus.localhost" + } + }, + "user_signing_keys": { + "@example:morpheus.localhost": { + "keys": { + "ed25519:IGfFj6KhzqBWxiMm6/e+Pu/x/5NV6a7iEe8AKhuRiq0": "IGfFj6KhzqBWxiMm6/e+Pu/x/5NV6a7iEe8AKhuRiq0" + }, + "signatures": { + "@example:morpheus.localhost": { + "ed25519:fKJdf5b3ga1hshUT5obkBteMNTtLkfy8qh4h5/XLNew": "+b9IdTCC7WezR/XJX+K/wI7DukcTOfBukyy9iyZJP3SFZCNWVRmMG1Kuc8iQg8txSNY5NP0M3PI/Srv9nHZjDQ" + } + }, + "usage": [ + "user_signing" + ], + "user_id": "@example:morpheus.localhost" + } + } + }))) + .expect(2) + .named("/keys/query POST") + .mount(&server) + .await; + + Mock::given(method("GET")) + .and(path("_matrix/client/r0/user/@example:morpheus.localhost/account_data/m.cross_signing.master")) + .and(header("authorization", "Bearer 1234")) + .respond_with(ResponseTemplate::new(200).set_body_json(json!({ + "encrypted": { + "bmur2d9ypPUH1msSwCxQOJkuKRmJI55e": { + "ciphertext": "lCRSSA1lChONEXj/8RyogsgAa8ouQwYDnLr4XBCheRikrZykLRzPCx3doCE=", + "iv": "bdfCwu+ECYgZ/jWTkGrQ/A==", + "mac": "NXeV1dZaOe2JLvQ6Hh6tFto7AgFFdaQnY0l9pruwdtE=" + } + } + }))) + .expect(1) + .named("m.direct account data GET") + .mount(&server) + .await; + + Mock::given(method("GET")) + .and(path( + "_matrix/client/r0/user/@example:morpheus.localhost/account_data/m.cross_signing.self_signing", + )) + .and(header("authorization", "Bearer 1234")) + .respond_with(ResponseTemplate::new(200).set_body_json(json!({ + "encrypted": { + "bmur2d9ypPUH1msSwCxQOJkuKRmJI55e": { + "ciphertext": "+B9WD02IvtQ8S4OaquhuYEZAx20xvz0oTN7r2VM9VOBxmlOyi+KkkWOvLAo=", + "iv": "3BCaKGCaSMkg1x9WnTqUmw==", + "mac": "xQEDxQbPH0bYeZUFC3wYJh0lsLkP2amcFGdaZ3VdfQg=" + } + } + }))) + .expect(1) + .named("m.direct account data GET") + .mount(&server) + .await; + + Mock::given(method("GET")) + .and(path( + "_matrix/client/r0/user/@example:morpheus.localhost/account_data/m.cross_signing.user_signing", + )) + .and(header("authorization", "Bearer 1234")) + .respond_with(ResponseTemplate::new(200).set_body_json(json!({ + "encrypted": { + "bmur2d9ypPUH1msSwCxQOJkuKRmJI55e": { + "ciphertext": "atqNy5IDzYRkRC+lkKoflwsyHkd0dr4UeoViwJdUzexiq0M8h1i8JMkADNg=", + "iv": "bjb1V2n9YmA8j31Z9muMqQ==", + "mac": "vusvNuV8Kkq50VxtC78oioofVBurnTTVEhiRyZkfu/4=" + } + } + }))) + .expect(1) + .named("m.direct account data GET") + .mount(&server) + .await; + + Mock::given(method("POST")) + .and(path("_matrix/client/unstable/keys/signatures/upload")) + .and(header("authorization", "Bearer 1234")) + .respond_with(ResponseTemplate::new(200).set_body_json(json!({ + "failures": {} + }))) + .expect(1) + .named("signatures upload POST") + .mount(&server) + .await; + + let secret_store = client + .encryption() + .secret_storage() + .open_secret_store(SECRET_STORE_KEY) + .await + .expect("We should be able to open our secret store"); + + let status = client + .encryption() + .cross_signing_status() + .await + .expect("We should be able to check our cross-signing status"); + + assert!(!status.has_master, "Initially we should not have access to our cross signing key"); + + secret_store + .import_secrets() + .await + .expect("We should be able to import all our known secrets from 4S"); + + let status = client + .encryption() + .cross_signing_status() + .await + .expect("We should be able to check our cross-signing status"); + + assert!( + status.is_complete(), + "We should have access to our cross signing key after the import" + ); + + assert_eq!( + SECRET_STORE_KEY, + secret_store.secret_storage_key(), + "We should be able to retrieve the secret storage key from the store", + ); + + server.verify().await; +} + +#[async_test] +async fn is_secret_storage_enabled() { + let user_id = user_id!("@example:morpheus.localhost"); + + let session = MatrixSession { + meta: SessionMeta { + user_id: user_id!("@example:morpheus.localhost").to_owned(), + device_id: device_id!("DEVICEID").to_owned(), + }, + tokens: MatrixSessionTokens { access_token: "1234".to_owned(), refresh_token: None }, + }; + let (client, server) = no_retry_test_client().await; + client.restore_session(session).await.unwrap(); + + { + let _scope = Mock::given(method("GET")) + .and(path(format!( + "_matrix/client/r0/user/{user_id}/account_data/m.secret_storage.default_key" + ))) + .and(header("authorization", "Bearer 1234")) + .respond_with(ResponseTemplate::new(404).set_body_json(json!({ + "errcode": "M_NOT_FOUND", + "error": "Account data not found" + }))) + .expect(1) + .named("default_key account data GET") + .mount_as_scoped(&server) + .await; + + let enabled = client + .encryption() + .secret_storage() + .is_enabled() + .await + .expect("We should be able to check if secret storage is enabled"); + + assert!( + !enabled, + "If we didn't find the default key account data event, we should assume that \ + secret storage is disabled." + ); + } + + { + let _scope = Mock::given(method("GET")) + .and(path(format!( + "_matrix/client/r0/user/{user_id}/account_data/m.secret_storage.default_key" + ))) + .and(header("authorization", "Bearer 1234")) + .respond_with(ResponseTemplate::new(200).set_body_json(json!({}))) + .expect(1) + .named("default_key account data GET") + .mount_as_scoped(&server) + .await; + + let enabled = client + .encryption() + .secret_storage() + .is_enabled() + .await + .expect("We should be able to check if secret storage is enabled"); + + assert!( + !enabled, + "If deserialization of the default key account data event failed, we should assume \ + that secret storage is disabled" + ); + } + + { + let _scope = Mock::given(method("GET")) + .and(path(format!( + "_matrix/client/r0/user/{user_id}/account_data/m.secret_storage.default_key" + ))) + .and(header("authorization", "Bearer 1234")) + .respond_with(ResponseTemplate::new(200).set_body_json(json!({ + "key": "some_key_id", + }))) + .expect(1) + .named("default_key account data GET") + .mount_as_scoped(&server) + .await; + + let enabled = client + .encryption() + .secret_storage() + .is_enabled() + .await + .expect("We should be able to check if secret storage is enabled"); + + assert!( + enabled, + "If there is a default key event and deserialization did not fail, we're assuming \ + that secret storage is enabled" + ); + } +}