diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 6544b851f..aa3fb096c 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -21,13 +21,6 @@ jobs: runs-on: ubuntu-latest strategy: fail-fast: false - matrix: - project: ["libsignal-service-actix", "libsignal-service-hyper", "libsignal-service"] - features: ["", "unsend-futures"] - exclude: - # -actix always has unsend futures, so we don't have that feature flag - - project: "libsignal-service-actix" - features: "unsend-futures" steps: - uses: actions/checkout@v3 - name: Install protobuf @@ -37,70 +30,49 @@ jobs: - uses: actions-rs/clippy-check@v1 with: token: ${{ secrets.GITHUB_TOKEN }} - args: --features "${{ matrix.features }}" --manifest-path ${{ matrix.project }}/Cargo.toml build: - name: Build (${{ matrix.project }}, Rust ${{ matrix.toolchain }}) + name: Build / Rust ${{ matrix.toolchain }} runs-on: ubuntu-latest strategy: fail-fast: false matrix: - project: ["libsignal-service-actix", "libsignal-service-hyper", "libsignal-service"] - toolchain: ["stable", "beta", "nightly"] - coverage: [false, true] - features: ["", "unsend-futures"] - exclude: - # Coverage related excludes - - toolchain: stable - coverage: true - - toolchain: beta - coverage: true - - toolchain: nightly - coverage: false - - # Feature flag related excludes - # Actix like above - - project: "libsignal-service-actix" - features: "unsend-futures" - # We don't need to spawn this many jobs to see that unsend-futures works - - features: "unsend-futures" - toolchain: "beta" - - features: "unsend-futures" - toolchain: "nightly" - include: - - project: "libsignal-service-actix" - toolchain: "1.75" - coverage: false + toolchain: ["stable", "beta", "nightly", "1.75"] steps: - - uses: actions/checkout@v3 + - name: Checkout repository + uses: actions/checkout@v3 + - name: Install protobuf run: | sudo apt-get update sudo apt-get install -y libprotobuf-dev libprotobuf-c-dev protobuf-compiler protobuf-c-compiler - - uses: actions-rs/toolchain@v1 + + - uses: dtolnay/rust-toolchain@stable with: toolchain: ${{ matrix.toolchain }} - override: true + + - name: Configure CI cache + uses: Swatinem/rust-cache@v2 - name: Run tests uses: actions-rs/cargo@v1 - if: ${{ !matrix.coverage }} + if: matrix.toolchain != 'nightly' with: command: test - args: --all-targets --no-fail-fast --features "${{ matrix.features }}" --manifest-path ${{ matrix.project }}/Cargo.toml + args: --all-targets --no-fail-fast - name: Build uses: actions-rs/cargo@v1 with: command: build - args: --all-targets --features "${{ matrix.features }}" --manifest-path ${{ matrix.project }}/Cargo.toml + args: --all-targets - - name: Run tests + - name: Run tests with code coverage uses: actions-rs/cargo@v1 - if: ${{ matrix.coverage }} + if: matrix.toolchain == 'nightly' with: command: test - args: --all-targets --no-fail-fast --features "${{ matrix.features }}" --manifest-path ${{ matrix.project }}/Cargo.toml + args: --all-targets --no-fail-fast env: CARGO_INCREMENTAL: '0' RUSTFLAGS: '-Zprofile -Ccodegen-units=1 -Cinline-threshold=0 -Clink-dead-code -Coverflow-checks=off -Cpanic=abort -Zpanic_abort_tests' diff --git a/Cargo.toml b/Cargo.toml index ebd59bfb8..c3213bb4e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,8 +1,62 @@ -[workspace] -members = ["libsignal-service", "libsignal-service-actix", "libsignal-service-hyper"] -default-members = ["libsignal-service", "libsignal-service-hyper"] +[package] +name = "libsignal-service" +version = "0.1.0" +authors = ["Ruben De Smet ", "Gabriel Féron "] +edition = "2021" +license = "AGPL-3.0" +readme = "README.md" -resolver = "2" +[dependencies] +libsignal-protocol = { git = "https://github.com/signalapp/libsignal", tag = "v0.56.1" } +zkgroup = { git = "https://github.com/signalapp/libsignal", tag = "v0.56.1" } + +aes = "0.8" +aes-gcm = "0.10" +cbc = "0.1" +ctr = "0.9" +async-trait = "0.1" +base64 = "0.22" +bincode = "1.3" +bytes = "1" +chrono = { version = "0.4", features = ["serde", "clock"], default-features = false } +derivative = "2.2" +futures = "0.3" +hex = "0.4" +hkdf = "0.12" +hmac = "0.12" +phonenumber = "0.3" +prost = "0.13" +rand = "0.8" +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0.85" +sha2 = "0.10" +thiserror = "1.0" +url = { version = "2.1", features = ["serde"] } +uuid = { version = "1", features = ["serde"] } + +# http +hyper = "1.0" +hyper-util = { version = "0.1", features = ["client", "client-legacy"] } +hyper-rustls = { version = "0.27", default-features = false, features = ["http1", "http2", "ring", "logging"] } +hyper-timeout = "0.5" +headers = "0.4" +http-body-util = "0.1" +mpart-async = "0.7" +async-tungstenite = { version = "0.27", features = ["tokio-rustls-native-certs", "url"] } +tokio = { version = "1.0", features = ["macros"] } +tokio-rustls = { version = "0.26", default-features = false, features = ["logging", "ring"] } + +rustls-pemfile = "2.0" + +tracing = { version = "0.1", features = ["log"] } +tracing-futures = "0.2" + +[build-dependencies] +prost-build = "0.13" + +[dev-dependencies] +anyhow = "1.0" +tokio = { version = "1.0", features = ["macros", "rt"] } [patch.crates-io] curve25519-dalek = { git = 'https://github.com/signalapp/curve25519-dalek', tag = 'signal-curve25519-4.1.3' } diff --git a/README.md b/README.md index c0d44693b..5e0261203 100644 --- a/README.md +++ b/README.md @@ -46,20 +46,14 @@ We're actively trying to make `libsignal-service-rs` fully functional. If you're looking to contribute or want to ask a question, you're more than welcome to join our development channel on Matrix (#whisperfish:rubdos.be) or Libera.chat (#whisperfish) to get in touch with us! -## Feature flags for libsignal-service - -| Feature flag | Description | -| ---------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `unsend-futures` | This feature removes the `Send` requirement on returned futures. Enabling this flag may be necessary for interoperability with other libraries that don't support `Send` such as actix. | - ## License Copyright 2015-2019 Open Whisper Systems Copyright 2020-2023 Signal Messenger, LLC Copyright 2019-2021 Michael F Bryan -Copyright 2019-2023 Ruben De Smet -Copyright 2019-2023 Gabriel Féron -Copyright 2019-2023 Whisperfish contributors +Copyright 2019-2024 Ruben De Smet +Copyright 2019-2024 Gabriel Féron +Copyright 2019-2024 Whisperfish contributors Licensed under the AGPLv3: http://www.gnu.org/licenses/agpl-3.0.html diff --git a/libsignal-service/build.rs b/build.rs similarity index 100% rename from libsignal-service/build.rs rename to build.rs diff --git a/libsignal-service/certs/production-root-ca.pem b/certs/production-root-ca.pem similarity index 100% rename from libsignal-service/certs/production-root-ca.pem rename to certs/production-root-ca.pem diff --git a/libsignal-service/certs/staging-root-ca.pem b/certs/staging-root-ca.pem similarity index 100% rename from libsignal-service/certs/staging-root-ca.pem rename to certs/staging-root-ca.pem diff --git a/libsignal-service/examples/storage.rs b/examples/storage.rs similarity index 100% rename from libsignal-service/examples/storage.rs rename to examples/storage.rs diff --git a/libsignal-service-actix/Cargo.toml b/libsignal-service-actix/Cargo.toml deleted file mode 100644 index 350db0963..000000000 --- a/libsignal-service-actix/Cargo.toml +++ /dev/null @@ -1,42 +0,0 @@ -[package] -name = "libsignal-service-actix" -version = "0.1.0" -authors = ["Ruben De Smet "] -edition = "2021" -license = "AGPL-3.0" -rust-version = "1.70.0" - -[dependencies] -# Contrary to hyper, actix does not have Send compatible futures, which means -# the Send requirement in libsignal-service needs to be lifted by enabling `unsend-futures`. -libsignal-service = { path = "../libsignal-service", features = ["unsend-futures"] } - -awc = { version = "3.2.0", features = ["rustls-0_21"] } -actix = "0.13" -actix-http = "3.2.0" -actix-rt = "2.4" -mpart-async = "0.6" -serde_json = "1.0" -futures = "0.3" -tracing = "0.1" -tracing-futures = "0.2" -bytes = "1" -rustls = "0.21" -rustls-pemfile = "0.3" -url = "2.1" -serde = "1.0" -rand = "0.8" - -thiserror = "1.0" -async-trait = "0.1" - -phonenumber = "0.3" - -[dev-dependencies] -chrono = "0.4" -image = { version = "0.23", default-features = false, features = ["png"] } -opener = "0.5" -qrcode = "0.12" -structopt = "0.3" -tokio = { version = "1", features = ["macros"] } -anyhow = "1.0" diff --git a/libsignal-service-actix/examples/registering.rs b/libsignal-service-actix/examples/registering.rs deleted file mode 100644 index c9ce6076d..000000000 --- a/libsignal-service-actix/examples/registering.rs +++ /dev/null @@ -1,222 +0,0 @@ -//! At install time, clients need to register with the Signal server. -//! -//! ```java -//! private final String URL = "https://my.signal.server.com"; -//! private final TrustStore TRUST_STORE = new MyTrustStoreImpl(); -//! private final String USERNAME = "+14151231234"; -//! private final String PASSWORD = generateRandomPassword(); -//! private final String USER_AGENT = "[FILL_IN]"; -//! -//! SignalServiceAccountManager accountManager = new SignalServiceAccountManager(URL, TRUST_STORE, -//! USERNAME, PASSWORD, USER_AGENT); -//! -//! accountManager.requestSmsVerificationCode(); -//! accountManager.verifyAccountWithCode(receivedSmsVerificationCode, generateRandomSignalingKey(), -//! generateRandomInstallId(), false); -//! accountManager.setGcmId(Optional.of(GoogleCloudMessaging.getInstance(this).register(REGISTRATION_ID))); -//! accountManager.setPreKeys(identityKey.getPublicKey(), lastResortKey, signedPreKeyRecord, oneTimePreKeys); -//! ``` - -use anyhow::Error; -use libsignal_service::configuration::SignalServers; -use libsignal_service::prelude::{ProfileKey, ServiceCredentials}; -use libsignal_service::provisioning::generate_registration_id; -use libsignal_service::push_service::{ - AccountAttributes, DeviceCapabilities, PushService, RegistrationMethod, - VerificationTransport, -}; -use libsignal_service::{AccountManager, USER_AGENT}; -use libsignal_service_actix::prelude::AwcPushService; -use rand::RngCore; -use structopt::StructOpt; - -#[path = "../../libsignal-service/examples/storage.rs"] -mod storage; - -#[actix_rt::main] -async fn main() -> Result<(), Error> { - let client = "libsignal-service-hyper-example"; - let use_voice = false; - - let Args { - servers, - phonenumber, - password, - captcha, - } = Args::from_args(); - - let push_token = None; - // Mobile country code and mobile network code can in theory be extracted from the phone - // number, but it's not necessary for the API to function correctly. - // XXX: We could internalize this if statement to create_verification_session - let (mcc, mnc) = if let Some(carrier) = phonenumber.carrier() { - (Some(&carrier[0..3]), Some(&carrier[3..])) - } else { - (None, None) - }; - - // Only used with MessageSender and MessageReceiver - // let password = args.get_password()?; - - let mut push_service = AwcPushService::new( - servers, - Some(ServiceCredentials { - aci: None, - pni: None, - phonenumber: phonenumber.clone(), - password, - signaling_key: None, - device_id: None, - }), - USER_AGENT.into(), - ); - - let mut session = push_service - .create_verification_session( - &phonenumber.to_string(), - push_token, - mcc, - mnc, - ) - .await - .expect("create a registration verification session"); - println!("Sending registration request..."); - - if session.captcha_required() { - session = push_service - .patch_verification_session( - &session.id, - None, - None, - None, - captcha.as_deref(), - None, - ) - .await - .expect("submit captcha"); - } - - if session.push_challenge_required() { - anyhow::bail!("Push challenge required, but not implemented."); - } - - if !session.allowed_to_request_code { - anyhow::bail!( - "Not allowed to request verification code, reason unknown: {session:?}", - ); - } - - session = push_service - .request_verification_code( - &session.id, - client, - if use_voice { - VerificationTransport::Voice - } else { - VerificationTransport::Sms - }, - ) - .await - .expect("request verification code"); - - let confirmation_code = let_user_enter_confirmation_code(); - - println!("Submitting confirmation code..."); - - session = push_service - .submit_verification_code(&session.id, confirmation_code) - .await - .expect("Sending confirmation code failed."); - - if !session.verified { - anyhow::bail!("Session is not verified"); - } - - let registration_id = generate_registration_id(&mut rand::thread_rng()); - let pni_registration_id = generate_registration_id(&mut rand::thread_rng()); - let signaling_key = generate_signaling_key(); - let mut profile_key = [0u8; 32]; - rand::thread_rng().fill_bytes(&mut profile_key); - let profile_key = ProfileKey::create(profile_key); - let skip_device_transfer = false; - - // Create the prekeys storage - let mut aci_store = storage::ExampleStore::new(); - let mut pni_store = storage::ExampleStore::new(); - - let mut account_manager = AccountManager::new(push_service, None); - let _registration_data = account_manager - .register_account( - &mut rand::thread_rng(), - RegistrationMethod::SessionId(&session.id), - AccountAttributes { - signaling_key: Some(signaling_key.to_vec()), - registration_id, - pni_registration_id, - voice: false, - video: false, - fetches_messages: true, - pin: None, - registration_lock: None, - unidentified_access_key: Some( - profile_key.derive_access_key().to_vec(), - ), - unrestricted_unidentified_access: false, // TODO: make this configurable? - discoverable_by_phone_number: true, - name: Some("libsignal-service-hyper test".into()), - capabilities: DeviceCapabilities::default(), - }, - &mut aci_store, - &mut pni_store, - skip_device_transfer, - ) - .await; - - // You would want to store the registration data - - println!("Registration completed!"); - - Ok(()) -} - -fn let_user_enter_confirmation_code() -> &'static str { - "12345" -} - -fn generate_signaling_key() -> [u8; 52] { - // Signaling key that decrypts the incoming Signal messages - let mut rng = rand::thread_rng(); - let mut signaling_key = [0u8; 52]; - rng.fill_bytes(&mut signaling_key); - signaling_key -} - -#[derive(Debug, Clone, PartialEq, Eq, StructOpt)] -pub struct Args { - #[structopt( - short = "s", - long = "servers", - help = "The servers to connect to", - default_value = "staging" - )] - pub servers: SignalServers, - #[structopt( - short = "u", - long = "username", - help = "Your username or other identifier", - default_value = "+14151231234" - )] - pub phonenumber: phonenumber::PhoneNumber, - #[structopt( - short = "p", - long = "password", - help = "The password to use. Read from stdin if not provided" - )] - pub password: Option, - #[structopt( - short = "c", - long = "captcha", - help = "Captcha for registration" - )] - pub captcha: Option, -} diff --git a/libsignal-service-actix/src/lib.rs b/libsignal-service-actix/src/lib.rs deleted file mode 100644 index 84388618e..000000000 --- a/libsignal-service-actix/src/lib.rs +++ /dev/null @@ -1,9 +0,0 @@ -#![recursion_limit = "256"] -#![allow(clippy::uninlined_format_args)] - -pub mod push_service; -pub mod websocket; - -pub mod prelude { - pub use crate::push_service::*; -} diff --git a/libsignal-service-actix/src/push_service.rs b/libsignal-service-actix/src/push_service.rs deleted file mode 100644 index 47f26dd84..000000000 --- a/libsignal-service-actix/src/push_service.rs +++ /dev/null @@ -1,735 +0,0 @@ -use std::{sync::Arc, time::Duration}; - -use awc::{ - error::{ConnectError, PayloadError, SendRequestError}, - http::StatusCode, - http::{header::HeaderValue, Method}, - Client, ClientRequest, ClientResponse, Connector, -}; -use bytes::Bytes; -use futures::prelude::*; -use libsignal_service::{ - configuration::*, prelude::ProtobufMessage, push_service::*, - websocket::SignalWebSocket, -}; -use serde::{Deserialize, Serialize}; -use tracing_futures::Instrument; - -use crate::websocket::AwcWebSocket; - -#[derive(Clone)] -pub struct AwcPushService { - cfg: ServiceConfiguration, - credentials: Option, - client: awc::Client, -} - -impl AwcPushService { - pub fn new( - cfg: impl Into, - credentials: Option, - user_agent: String, - ) -> Self { - let cfg = cfg.into(); - let client = get_client(&cfg, user_agent); - Self { - cfg, - credentials: credentials.and_then(|c| c.authorization()), - client, - } - } - - fn request( - &self, - method: Method, - endpoint: Endpoint, - path: impl AsRef, - additional_headers: &[(&str, &str)], - credentials_override: HttpAuthOverride, - ) -> Result { - let url = self.cfg.base_url(endpoint).join(path.as_ref())?; - tracing::debug!(%url, %method, "HTTP request"); - let mut builder = self.client.request(method, url.as_str()); - for &header in additional_headers { - builder = builder.insert_header(header); - } - builder = match credentials_override { - HttpAuthOverride::NoOverride => { - if let Some(credentials) = self.credentials.as_ref() { - builder.basic_auth( - &credentials.username, - &credentials.password, - ) - } else { - builder - } - }, - HttpAuthOverride::Identified(HttpAuth { username, password }) => { - builder.basic_auth(username, password) - }, - HttpAuthOverride::Unidentified => builder, - }; - Ok(builder) - } - - fn json(text: &[u8]) -> Result - where - for<'de> T: Deserialize<'de>, - { - let value = if text.is_empty() { - serde_json::from_value(serde_json::Value::Null) - } else { - serde_json::from_slice(text) - }; - value.map_err(|e| ServiceError::JsonDecodeError { - reason: e.to_string(), - }) - } - - #[tracing::instrument(name = "extracting error", skip(response))] - async fn from_response( - response: &mut ClientResponse, - ) -> Result<(), ServiceError> - where - S: Stream> + Unpin, - { - match response.status() { - StatusCode::OK => Ok(()), - StatusCode::NO_CONTENT => Ok(()), - StatusCode::UNAUTHORIZED | StatusCode::FORBIDDEN => { - Err(ServiceError::Unauthorized) - }, - StatusCode::NOT_FOUND => { - // This is 404 and means that e.g. recipient is not registered - Err(ServiceError::NotFoundError) - }, - StatusCode::PAYLOAD_TOO_LARGE => { - // This is 413 and means rate limit exceeded for Signal. - Err(ServiceError::RateLimitExceeded) - }, - StatusCode::CONFLICT => { - let mismatched_devices = - response.json().await.map_err(|e| { - tracing::error!( - ?response, - "Failed to decode HTTP 409 response: {}", - e - ); - ServiceError::UnhandledResponseCode { - http_code: StatusCode::CONFLICT.as_u16(), - } - })?; - Err(ServiceError::MismatchedDevicesException( - mismatched_devices, - )) - }, - StatusCode::GONE => { - let stale_devices = response.json().await.map_err(|e| { - tracing::error!( - ?response, - "Failed to decode HTTP 410 response: {}", - e - ); - ServiceError::UnhandledResponseCode { - http_code: StatusCode::GONE.as_u16(), - } - })?; - Err(ServiceError::StaleDevices(stale_devices)) - }, - StatusCode::LOCKED => { - let locked = response.json().await.map_err(|e| { - tracing::error!( - ?response, - "Failed to decode HTTP 423 response: {}", - e - ); - ServiceError::UnhandledResponseCode { - http_code: StatusCode::LOCKED.as_u16(), - } - })?; - Err(ServiceError::Locked(locked)) - }, - StatusCode::PRECONDITION_REQUIRED => { - let proof_required = response.json().await.map_err(|e| { - tracing::error!( - ?response, - "Failed to decode HTTP 428 response: {}", - e - ); - ServiceError::UnhandledResponseCode { - http_code: StatusCode::PRECONDITION_REQUIRED.as_u16(), - } - })?; - Err(ServiceError::ProofRequiredError(proof_required)) - }, - // XXX: fill in rest from PushServiceSocket - code => { - let contents = response.body().await; - tracing::trace!( - ?response, - "Unhandled response {} with body: {:?}", - code.as_u16(), - contents, - ); - Err(ServiceError::UnhandledResponseCode { - http_code: code.as_u16(), - }) - }, - } - } -} - -#[async_trait::async_trait(?Send)] -impl PushService for AwcPushService { - // This is in principle known at compile time, but long to write out. - type ByteStream = Box; - - async fn get_json( - &mut self, - endpoint: Endpoint, - path: &str, - additional_headers: &[(&str, &str)], - credentials_override: HttpAuthOverride, - ) -> Result - where - for<'de> T: Deserialize<'de>, - { - use awc::error::{ConnectError, SendRequestError}; - let mut response = self - .request( - Method::GET, - endpoint, - path, - additional_headers, - credentials_override, - )? - .send() - .await - .map_err(|e| match e { - SendRequestError::Connect(ConnectError::Timeout) => { - ServiceError::Timeout { - reason: e.to_string(), - } - }, - _ => ServiceError::SendError { - reason: e.to_string(), - }, - })?; - - let _span = tracing::debug_span!("processing response", ?response); - - Self::from_response(&mut response).await?; - - // In order to catch the zero-length output, we have to collect - // the whole response. The actix-web api is meant to used as a - // streaming deserializer, so we have this little awkward match. - // - // This is also the reason we depend directly on serde_json, however - // actix already imports that anyway. - let text = match response.body().await { - Ok(text) => { - tracing::debug!( - "GET response: {:?}", - String::from_utf8_lossy(&text) - ); - text - }, - Err(e) => { - return Err(ServiceError::ResponseError { - reason: e.to_string(), - }) - }, - }; - Self::json(&text) - } - - /// Deletes a resource through the HTTP DELETE verb. - async fn delete_json( - &mut self, - endpoint: Endpoint, - path: &str, - additional_headers: &[(&str, &str)], - ) -> Result - where - for<'de> T: Deserialize<'de>, - { - let mut response = self - .request( - Method::DELETE, - endpoint, - path, - additional_headers, - HttpAuthOverride::NoOverride, - )? - .send() - .await - .map_err(|e| match e { - SendRequestError::Connect(ConnectError::Timeout) => { - ServiceError::Timeout { - reason: e.to_string(), - } - }, - _ => ServiceError::SendError { - reason: e.to_string(), - }, - })?; - - let _span = - tracing::debug_span!("processing response", ?response).entered(); - - Self::from_response(&mut response).await?; - - // In order to catch the zero-length output, we have to collect - // the whole response. The actix-web api is meant to used as a - // streaming deserializer, so we have this little awkward match. - // - // This is also the reason we depend directly on serde_json, however - // actix already imports that anyway. - let text = match response.body().await { - Ok(text) => { - tracing::debug!( - "GET response: {:?}", - String::from_utf8_lossy(&text) - ); - text - }, - Err(e) => { - return Err(ServiceError::ResponseError { - reason: e.to_string(), - }) - }, - }; - - Self::json(&text) - } - - async fn put_json( - &mut self, - endpoint: Endpoint, - path: &str, - additional_headers: &[(&str, &str)], - credentials_override: HttpAuthOverride, - value: S, - ) -> Result - where - for<'de> D: Deserialize<'de>, - S: Serialize, - { - let mut response = self - .request( - Method::PUT, - endpoint, - path, - additional_headers, - credentials_override, - )? - .send_json(&value) - .await - .map_err(|e| ServiceError::SendError { - reason: e.to_string(), - })?; - - let _span = - tracing::debug_span!("processing response", ?response).entered(); - - Self::from_response(&mut response).await?; - - // In order to catch the zero-length output, we have to collect - // the whole response. The actix-web api is meant to used as a - // streaming deserializer, so we have this little awkward match. - // - // This is also the reason we depend directly on serde_json, however - // actix already imports that anyway. - let text = match response.body().await { - Ok(text) => { - tracing::debug!( - "GET response: {:?}", - String::from_utf8_lossy(&text) - ); - text - }, - Err(e) => { - return Err(ServiceError::ResponseError { - reason: e.to_string(), - }) - }, - }; - Self::json(&text) - } - - async fn patch_json( - &mut self, - endpoint: Endpoint, - path: &str, - additional_headers: &[(&str, &str)], - credentials_override: HttpAuthOverride, - value: S, - ) -> Result - where - for<'de> D: Deserialize<'de>, - S: Serialize, - { - let mut response = self - .request( - Method::PATCH, - endpoint, - path, - additional_headers, - credentials_override, - )? - .send_json(&value) - .await - .map_err(|e| ServiceError::SendError { - reason: e.to_string(), - })?; - - let _span = - tracing::debug_span!("processing response", ?response).entered(); - - Self::from_response(&mut response).await?; - - // In order to catch the zero-length output, we have to collect - // the whole response. The actix-web api is meant to used as a - // streaming deserializer, so we have this little awkward match. - // - // This is also the reason we depend directly on serde_json, however - // actix already imports that anyway. - let text = match response.body().await { - Ok(text) => { - tracing::debug!( - "PATCH response: {:?}", - String::from_utf8_lossy(&text) - ); - text - }, - Err(e) => { - return Err(ServiceError::ResponseError { - reason: e.to_string(), - }) - }, - }; - Self::json(&text) - } - - async fn post_json( - &mut self, - endpoint: Endpoint, - path: &str, - additional_headers: &[(&str, &str)], - credentials_override: HttpAuthOverride, - value: S, - ) -> Result - where - for<'de> D: Deserialize<'de>, - S: Serialize, - { - let mut response = self - .request( - Method::POST, - endpoint, - path, - additional_headers, - credentials_override, - )? - .send_json(&value) - .await - .map_err(|e| ServiceError::SendError { - reason: e.to_string(), - })?; - - let _span = - tracing::debug_span!("processing response", ?response).entered(); - - Self::from_response(&mut response).await?; - - // In order to catch the zero-length output, we have to collect - // the whole response. The actix-web api is meant to used as a - // streaming deserializer, so we have this little awkward match. - // - // This is also the reason we depend directly on serde_json, however - // actix already imports that anyway. - let text = match response.body().await { - Ok(text) => { - tracing::debug!( - "GET response: {:?}", - String::from_utf8_lossy(&text) - ); - text - }, - Err(e) => { - return Err(ServiceError::ResponseError { - reason: e.to_string(), - }) - }, - }; - Self::json(&text) - } - - async fn get_protobuf( - &mut self, - endpoint: Endpoint, - path: &str, - additional_headers: &[(&str, &str)], - credentials_override: HttpAuthOverride, - ) -> Result - where - T: Default + ProtobufMessage, - { - let mut response = self - .request( - Method::GET, - endpoint, - path, - additional_headers, - credentials_override, - )? - .send() - .await - .map_err(|e| ServiceError::SendError { - reason: e.to_string(), - })?; - - let text = - response - .body() - .await - .map_err(|e| ServiceError::ResponseError { - reason: e.to_string(), - })?; - Ok(T::decode(text)?) - } - - async fn put_protobuf( - &mut self, - endpoint: Endpoint, - path: &str, - additional_headers: &[(&str, &str)], - value: S, - ) -> Result - where - D: Default + ProtobufMessage, - S: Sized + ProtobufMessage, - { - let buf = value.encode_to_vec(); - - let mut response = self - .request( - Method::PUT, - endpoint, - path, - additional_headers, - HttpAuthOverride::NoOverride, - )? - .content_type(HeaderValue::from_static("application/x-protobuf")) - .send_body(buf) - .await - .map_err(|e| ServiceError::SendError { - reason: e.to_string(), - })?; - - let text = - response - .body() - .await - .map_err(|e| ServiceError::ResponseError { - reason: e.to_string(), - })?; - Ok(D::decode(text)?) - } - - async fn get_from_cdn( - &mut self, - cdn_id: u32, - path: &str, - ) -> Result { - let mut response = self - .request( - Method::GET, - Endpoint::Cdn(cdn_id), - path, - &[], - HttpAuthOverride::Unidentified, - )? - .send() - .await - .map_err(|e| ServiceError::SendError { - reason: e.to_string(), - })?; - - let _span = - tracing::debug_span!("processing response", ?response).entered(); - - Self::from_response(&mut response).await?; - - Ok(Box::new( - response - .map_err(|e| { - use awc::error::PayloadError; - match e { - PayloadError::Io(e) => e, - other => std::io::Error::new( - std::io::ErrorKind::Other, - other, - ), - } - }) - .into_async_read(), - )) - } - - async fn post_to_cdn0<'s, C: std::io::Read + Send + 's>( - &mut self, - path: &str, - value: &[(&str, &str)], - file: Option<(&str, &'s mut C)>, - ) -> Result<(), ServiceError> { - let request = self.request( - Method::POST, - Endpoint::Cdn(0), - path, - &[], - HttpAuthOverride::NoOverride, - )?; - - let mut form = mpart_async::client::MultipartRequest::default(); - - // mpart-async has a peculiar ordering of the form items, - // and Amazon S3 expects them in a very specific order (i.e., the file contents should - // go last. - // - // mpart-async uses a VecDeque internally for ordering the fields in the order given. - // - // https://github.com/cetra3/mpart-async/issues/16 - - for &(k, v) in value { - form.add_field(k, v); - } - - if let Some((filename, file)) = file { - // XXX Actix doesn't cope with none-'static lifetimes - // https://docs.rs/actix-web/3.2.0/actix_web/body/enum.Body.html - let mut buf = Vec::new(); - file.read_to_end(&mut buf) - .expect("infallible Read instance"); - form.add_stream( - "file", - filename, - "application/octet-stream", - futures::future::ok::<_, ()>(Bytes::from(buf)).into_stream(), - ); - } - - let content_type = - format!("multipart/form-data; boundary={}", form.get_boundary()); - - // XXX Amazon S3 needs the Content-Length, but we don't know it without depleting the whole - // stream. Sadly, Content-Length != contents.len(), but should include the whole form. - let mut body_contents = vec![]; - use futures::stream::StreamExt; - while let Some(b) = form.next().await { - // Unwrap, because no error type was used above - body_contents.extend(b.unwrap()); - } - tracing::trace!( - "Sending PUT with Content-Type={} and length {}", - content_type, - body_contents.len() - ); - - let mut response = request - .content_type(&content_type) - .content_length(body_contents.len() as u64) - .send_body(body_contents) - .await - .map_err(|e| ServiceError::SendError { - reason: e.to_string(), - })?; - - let _span = - tracing::debug_span!("processing response", ?response).entered(); - - Self::from_response(&mut response).await?; - - Ok(()) - } - - async fn ws( - &mut self, - path: &str, - keep_alive_path: &str, - additional_headers: &[(&str, &str)], - credentials: Option, - ) -> Result { - let span = tracing::debug_span!("websocket"); - let (ws, stream) = AwcWebSocket::with_client( - &mut self.client, - self.cfg.base_url(Endpoint::Service), - path, - additional_headers, - credentials.as_ref(), - ) - .instrument(span.clone()) - .await?; - let (ws, task) = SignalWebSocket::from_socket( - ws, - stream, - keep_alive_path.to_owned(), - ); - actix_rt::spawn(task.instrument(span)); - Ok(ws) - } -} - -/// Creates a `awc::Client` with usable default settings: -/// Creates a default `awc::Client`. -/// -/// Creates a `awc::Client` with usable default settings: -/// * certificate authority from the `ServiceConfiguration` -/// * 10s timeout on TCP connection -/// * 65s timeout on HTTP request -/// * provided user-agent -fn get_client(cfg: &ServiceConfiguration, user_agent: String) -> Client { - let mut cert_bytes = std::io::Cursor::new(&cfg.certificate_authority); - let roots = - rustls_pemfile::certs(&mut cert_bytes).expect("parseable PEM files"); - let roots = roots.iter().map(|v| rustls::Certificate(v.clone())); - - let mut root_certs = rustls::RootCertStore::empty(); - for root in roots { - root_certs.add(&root).unwrap(); - } - - let mut ssl_config = rustls::ClientConfig::builder() - .with_safe_defaults() - .with_root_certificates(root_certs) - .with_no_client_auth(); - ssl_config.alpn_protocols = vec![b"http/1.1".to_vec()]; - - let connector = Connector::new() - .rustls_021(Arc::new(ssl_config)) - .timeout(Duration::from_secs(10)); // https://github.com/actix/actix-web/issues/1047 - let client = awc::ClientBuilder::new() - .connector(connector) - .add_default_header(("X-Signal-Agent", user_agent.clone())) - .add_default_header(("User-Agent", user_agent)) - .timeout(Duration::from_secs(65)); // as in Signal-Android - - client.finish() -} - -#[cfg(test)] -mod tests { - use libsignal_service::configuration::SignalServers; - - #[test] - fn create_clients() { - let configs = &[SignalServers::Staging, SignalServers::Production]; - - for cfg in configs { - let _ = super::get_client( - &cfg.into(), - "libsignal-service test".to_string(), - ); - } - } -} diff --git a/libsignal-service-actix/src/websocket.rs b/libsignal-service-actix/src/websocket.rs deleted file mode 100644 index 0019afc6d..000000000 --- a/libsignal-service-actix/src/websocket.rs +++ /dev/null @@ -1,212 +0,0 @@ -use awc::{ - error::{WsClientError, WsProtocolError}, - http::StatusCode, - ws, - ws::Frame, -}; -use bytes::Bytes; -use futures::{channel::mpsc::*, prelude::*}; -use url::Url; - -use libsignal_service::{ - configuration::ServiceCredentials, - messagepipe::*, - push_service::{self, ServiceError}, -}; - -pub struct AwcWebSocket { - socket_sink: Box + Unpin>, -} - -#[derive(thiserror::Error, Debug)] -pub enum AwcWebSocketError { - #[error("Could not connect to the Signal Server")] - ConnectionError(#[from] awc::error::WsClientError), - #[error("Error during Websocket connection")] - ProtocolError(#[from] WsProtocolError), -} - -impl From for ServiceError { - fn from(e: AwcWebSocketError) -> ServiceError { - match e { - AwcWebSocketError::ConnectionError(e) => match e { - WsClientError::InvalidResponseStatus(s) => match s { - StatusCode::FORBIDDEN => ServiceError::Unauthorized, - s => ServiceError::WsError { - reason: format!("HTTP status {}", s), - }, - }, - e => ServiceError::WsError { - reason: e.to_string(), - }, - }, - AwcWebSocketError::ProtocolError(e) => match e { - WsProtocolError::Io(e) => match e.kind() { - std::io::ErrorKind::UnexpectedEof => { - ServiceError::WsClosing { - reason: format!( - "WebSocket closing due to unexpected EOF: {}", - e - ), - } - }, - _ => ServiceError::WsError { - reason: format!( - "IO error during WebSocket connection: {}", - e - ), - }, - }, - e => ServiceError::WsError { - reason: e.to_string(), - }, - }, - } - } -} - -/// Process the WebSocket, until it times out. -async fn process( - socket_stream: S, - mut incoming_sink: Sender, -) -> Result<(), AwcWebSocketError> -where - S: Unpin, - S: Stream>, -{ - let mut socket_stream = socket_stream.fuse(); - - let mut ka_interval = actix::clock::interval_at( - actix::clock::Instant::now(), - push_service::KEEPALIVE_TIMEOUT_SECONDS, - ); - - loop { - let tick = ka_interval.tick().fuse(); - futures::pin_mut!(tick); - futures::select! { - _ = tick => { - tracing::trace!("Triggering keep-alive"); - if let Err(e) = incoming_sink.send(WebSocketStreamItem::KeepAliveRequest).await { - tracing::info!("Websocket sink has closed: {:?}.", e); - break; - }; - }, - frame = socket_stream.next() => { - let frame = if let Some(frame) = frame { - frame - } else { - tracing::info!("process: Socket stream ended"); - break; - }; - - let frame = match frame? { - Frame::Binary(s) => s, - - Frame::Continuation(_c) => todo!(), - Frame::Ping(msg) => { - tracing::warn!(?msg, "received Ping"); - - continue; - }, - Frame::Pong(msg) => { - tracing::trace!(?msg, "received Pong"); - - continue; - }, - Frame::Text(frame) => { - tracing::warn!(?frame, "frame::Text",); - - // this is a protocol violation, maybe break; is better? - continue; - }, - - Frame::Close(c) => { - tracing::warn!(?c, "Websocket closing"); - - break; - }, - }; - - // Match SendError - if let Err(e) = incoming_sink.send(WebSocketStreamItem::Message(frame)).await { - tracing::info!("Websocket sink has closed: {:?}.", e); - break; - } - }, - } - } - Ok(()) -} - -impl AwcWebSocket { - pub(crate) async fn with_client( - client: &mut awc::Client, - base_url: impl std::borrow::Borrow, - path: &str, - additional_headers: &[(&str, &str)], - credentials: Option<&ServiceCredentials>, - ) -> Result<(Self, ::Stream), AwcWebSocketError> - { - let mut url = base_url.borrow().join(path).expect("valid url"); - url.set_scheme("wss").expect("valid https base url"); - - if let Some(credentials) = credentials { - url.query_pairs_mut() - .append_pair("login", credentials.login().as_ref()) - .append_pair( - "password", - credentials.password.as_ref().expect("a password"), - ); - } - - tracing::trace!( - url.scheme = url.scheme(), - url.host = ?url.host(), - url.path = url.path(), - url.has_query = ?url.query().is_some(), - "starting websocket", - ); - let mut ws = client.ws(url.as_str()); - for (key, value) in additional_headers { - ws = ws.header(*key, *value); - } - let (response, framed) = ws.connect().await?; - - tracing::debug!(?response, "WebSocket connected"); - - let (incoming_sink, incoming_stream) = channel(5); - - let (socket_sink, socket_stream) = framed.split(); - let processing_task = process(socket_stream, incoming_sink); - - // When the processing_task stops, the consuming stream and sink also - // terminate. - actix_rt::spawn(processing_task.map(|v| match v { - Ok(()) => (), - Err(e) => { - tracing::warn!("Processing task terminated with error: {:?}", e) - }, - })); - - Ok(( - Self { - socket_sink: Box::new(socket_sink), - }, - incoming_stream, - )) - } -} - -#[async_trait::async_trait(?Send)] -impl WebSocketService for AwcWebSocket { - type Stream = Receiver; - - async fn send_message(&mut self, msg: Bytes) -> Result<(), ServiceError> { - self.socket_sink - .send(ws::Message::Binary(msg)) - .await - .map_err(AwcWebSocketError::from)?; - Ok(()) - } -} diff --git a/libsignal-service-hyper/Cargo.toml b/libsignal-service-hyper/Cargo.toml deleted file mode 100644 index df43c566e..000000000 --- a/libsignal-service-hyper/Cargo.toml +++ /dev/null @@ -1,44 +0,0 @@ -[package] -name = "libsignal-service-hyper" -version = "0.1.0" -authors = ["Gabriel Féron "] -edition = "2021" -license = "AGPL-3.0" -rust-version = "1.70.0" - -[dependencies] -libsignal-service = { path = "../libsignal-service" } - -async-trait = "0.1" -bytes = "1.0" -futures = "0.3" -tracing = "0.1" -tracing-futures = "0.2" -mpart-async = "0.7" -serde = "1.0" -serde_json = "1.0" -thiserror = "1.0" -url = "2.1" - -hyper = "1.0" -hyper-util = { version = "0.1", features = ["client", "client-legacy"] } -hyper-rustls = { version = "0.27", default-features = false, features = ["http1", "http2", "ring", "logging"] } -hyper-timeout = "0.5" -headers = "0.4" -http-body-util = "0.1" - -# for websocket support -async-tungstenite = { version = "0.27", features = ["tokio-rustls-native-certs", "url"] } - -tokio = { version = "1.0", features = ["macros"] } -tokio-rustls = { version = "0.26", default-features = false, features = ["logging", "ring"] } - -rustls-pemfile = "2.0" - -[dev-dependencies] -chrono = "0.4" -rand = "0.8" -tokio = { version = "1.0", features = ["rt-multi-thread"] } - -[features] -unsend-futures = ["libsignal-service/unsend-futures"] diff --git a/libsignal-service-hyper/examples/registering.rs b/libsignal-service-hyper/examples/registering.rs deleted file mode 100644 index 7e8355a29..000000000 --- a/libsignal-service-hyper/examples/registering.rs +++ /dev/null @@ -1,196 +0,0 @@ -use std::str::FromStr; - -use libsignal_service::configuration::{ServiceCredentials, SignalServers}; -use libsignal_service::prelude::phonenumber::PhoneNumber; -use libsignal_service::prelude::ProfileKey; -use libsignal_service::provisioning::generate_registration_id; -use libsignal_service::push_service::{ - AccountAttributes, DeviceCapabilities, PushService, RegistrationMethod, - VerificationTransport, -}; -use libsignal_service::{AccountManager, USER_AGENT}; - -use libsignal_service_hyper::prelude::HyperPushService; - -use rand::RngCore; - -#[path = "../../libsignal-service/examples/storage.rs"] -mod storage; - -#[tokio::main] -async fn main() { - let client = "libsignal-service-hyper-example"; - let phonenumber = let_user_enter_phone_number(); - let password = let_user_enter_password(); - let use_voice = does_user_want_voice_confirmation(); - let captcha = let_user_solve_captcha(); - let push_token = None; - // Mobile country code and mobile network code can in theory be extracted from the phone - // number, but it's not necessary for the API to function correctly. - // XXX: We could internalize this if statement to create_verification_session - let (mcc, mnc) = if let Some(carrier) = phonenumber.carrier() { - (Some(&carrier[0..3]), Some(&carrier[3..])) - } else { - (None, None) - }; - - let mut push_service = - create_push_service(phonenumber.clone(), password.clone()); - let mut session = push_service - .create_verification_session( - &phonenumber.to_string(), - push_token, - mcc, - mnc, - ) - .await - .expect("create a registration verification session"); - println!("Sending registration request..."); - - if session.captcha_required() { - session = push_service - .patch_verification_session( - &session.id, - None, - None, - None, - Some(&captcha), - None, - ) - .await - .expect("submit captcha"); - } - - if session.push_challenge_required() { - eprintln!("Push challenge required, but not implemented."); - return; - } - - if !session.allowed_to_request_code { - eprintln!( - "Not allowed to request verification code, reason unknown: {session:?}", - ); - return; - } - - session = push_service - .request_verification_code( - &session.id, - client, - if use_voice { - VerificationTransport::Voice - } else { - VerificationTransport::Sms - }, - ) - .await - .expect("request verification code"); - - let confirmation_code = let_user_enter_confirmation_code(); - - println!("Submitting confirmation code..."); - - session = push_service - .submit_verification_code(&session.id, confirmation_code) - .await - .expect("Sending confirmation code failed."); - - if !session.verified { - eprintln!("Session is not verified"); - return; - } - - let registration_id = generate_registration_id(&mut rand::thread_rng()); - let pni_registration_id = generate_registration_id(&mut rand::thread_rng()); - let signaling_key = generate_signaling_key(); - let mut profile_key = [0u8; 32]; - rand::thread_rng().fill_bytes(&mut profile_key); - let profile_key = ProfileKey::create(profile_key); - let skip_device_transfer = false; - - // Create the prekeys storage - let mut aci_store = storage::ExampleStore::new(); - let mut pni_store = storage::ExampleStore::new(); - - let mut account_manager = AccountManager::new(push_service, None); - let _registration_data = account_manager - .register_account( - &mut rand::thread_rng(), - RegistrationMethod::SessionId(&session.id), - AccountAttributes { - signaling_key: Some(signaling_key.to_vec()), - registration_id, - pni_registration_id, - voice: false, - video: false, - fetches_messages: true, - pin: None, - registration_lock: None, - unidentified_access_key: Some( - profile_key.derive_access_key().to_vec(), - ), - unrestricted_unidentified_access: false, // TODO: make this configurable? - discoverable_by_phone_number: true, - name: Some("libsignal-service-hyper test".into()), - capabilities: DeviceCapabilities::default(), - }, - &mut aci_store, - &mut pni_store, - skip_device_transfer, - ) - .await; - - // You would want to store the registration data - - println!("Registration completed!"); -} - -fn generate_signaling_key() -> [u8; 52] { - // Signaling key that decrypts the incoming Signal messages - let mut rng = rand::thread_rng(); - let mut signaling_key = [0u8; 52]; - rng.fill_bytes(&mut signaling_key); - signaling_key -} - -fn create_push_service( - phonenumber: PhoneNumber, - password: String, -) -> HyperPushService { - HyperPushService::new( - SignalServers::Staging, // You might want to switch to Production servers - Some(ServiceCredentials { - aci: None, - pni: None, - phonenumber, - password: Some(password), - signaling_key: None, - device_id: None, - }), - USER_AGENT.into(), - ) -} - -// ------------------------------------ -// Here come the user interaction mocks - -fn let_user_solve_captcha() -> String { - // Here you want to let the user solve a captcha on https://signalcaptchas.org/registration/generate.html - "EnterCaptchaHere".to_string() -} - -fn let_user_enter_confirmation_code() -> &'static str { - "12345" -} - -fn does_user_want_voice_confirmation() -> bool { - false -} - -fn let_user_enter_phone_number() -> PhoneNumber { - PhoneNumber::from_str("+49301234567").expect("Not a valid phone number") -} - -fn let_user_enter_password() -> String { - "EnterPasswordHere".to_string() -} diff --git a/libsignal-service-hyper/src/lib.rs b/libsignal-service-hyper/src/lib.rs deleted file mode 100644 index 84388618e..000000000 --- a/libsignal-service-hyper/src/lib.rs +++ /dev/null @@ -1,9 +0,0 @@ -#![recursion_limit = "256"] -#![allow(clippy::uninlined_format_args)] - -pub mod push_service; -pub mod websocket; - -pub mod prelude { - pub use crate::push_service::*; -} diff --git a/libsignal-service/.gitignore b/libsignal-service/.gitignore deleted file mode 100644 index 5116d3fb3..000000000 --- a/libsignal-service/.gitignore +++ /dev/null @@ -1 +0,0 @@ -src/proto/*.rs diff --git a/libsignal-service/Cargo.toml b/libsignal-service/Cargo.toml deleted file mode 100644 index 59d84e1f5..000000000 --- a/libsignal-service/Cargo.toml +++ /dev/null @@ -1,48 +0,0 @@ -[package] -name = "libsignal-service" -version = "0.1.0" -authors = ["Ruben De Smet ", "Gabriel Féron ", "Michael Bryan ", "Shady Khalifa "] -edition = "2021" -license = "AGPL-3.0" -readme = "../README.md" - -[dependencies] -libsignal-protocol = { git = "https://github.com/signalapp/libsignal", tag = "v0.56.1" } -zkgroup = { git = "https://github.com/signalapp/libsignal", tag = "v0.56.1" } - -aes = "0.8" -aes-gcm = "0.10" -cbc = "0.1" -ctr = "0.9" -async-trait = "0.1" -base64 = "0.22" -bincode = "1.3" -bytes = "1" -chrono = { version = "0.4", features = ["serde", "clock"], default-features = false } -derivative = "2.2" -futures = "0.3" -hex = "0.4" -hkdf = "0.12" -hmac = "0.12" -phonenumber = "0.3" -prost = "0.13" -rand = "0.8" -serde = { version = "1.0", features = ["derive"] } -serde_json = "1.0.85" -sha2 = "0.10" -thiserror = "1.0" -url = { version = "2.1", features = ["serde"] } -uuid = { version = "1", features = ["serde"] } - -tracing = { version = "0.1", features = ["log"] } -tracing-futures = "0.2" - -[build-dependencies] -prost-build = "0.13" - -[dev-dependencies] -anyhow = "1.0" -tokio = { version = "1.0", features = ["macros", "rt"] } - -[features] -unsend-futures = [] diff --git a/libsignal-service/src/push_service.rs b/libsignal-service/src/push_service.rs deleted file mode 100644 index 4724ce81f..000000000 --- a/libsignal-service/src/push_service.rs +++ /dev/null @@ -1,1410 +0,0 @@ -use std::{collections::HashMap, fmt, time::Duration}; - -use crate::{ - configuration::{Endpoint, ServiceCredentials}, - envelope::*, - groups_v2::GroupDecodingError, - pre_keys::{ - KyberPreKeyEntity, PreKeyEntity, PreKeyState, SignedPreKeyEntity, - }, - profile_cipher::ProfileCipherError, - proto::{attachment_pointer::AttachmentIdentifier, AttachmentPointer}, - sender::{OutgoingPushMessage, OutgoingPushMessages, SendMessageResponse}, - utils::{serde_base64, serde_optional_base64, serde_phone_number}, - websocket::SignalWebSocket, - MaybeSend, ParseServiceAddressError, Profile, ServiceAddress, -}; - -use chrono::prelude::*; -use derivative::Derivative; -use libsignal_protocol::{ - error::SignalProtocolError, - kem::{Key, Public}, - IdentityKey, PreKeyBundle, PublicKey, SenderCertificate, -}; -use phonenumber::PhoneNumber; -use prost::Message as ProtobufMessage; -use serde::{Deserialize, Serialize}; -use uuid::Uuid; -use zkgroup::{ - profiles::{ProfileKeyCommitment, ProfileKeyVersion}, - ZkGroupDeserializationFailure, -}; - -/** -Since we can't use format!() with constants, the URLs here are just for reference purposes -pub const REGISTER_GCM_PATH: &str = "/v1/accounts/gcm/"; -pub const TURN_SERVER_INFO: &str = "/v1/accounts/turn"; -pub const SET_ACCOUNT_ATTRIBUTES: &str = "/v1/accounts/attributes/"; -pub const PIN_PATH: &str = "/v1/accounts/pin/"; -pub const REQUEST_PUSH_CHALLENGE: &str = "/v1/accounts/fcm/preauth/%s/%s"; -pub const WHO_AM_I: &str = "/v1/accounts/whoami"; - -pub const PREKEY_PATH: &str = "/v2/keys/%s"; -pub const PREKEY_DEVICE_PATH: &str = "/v2/keys/%s/%s"; -pub const SIGNED_PREKEY_PATH: &str = "/v2/keys/signed"; - -pub const PROVISIONING_CODE_PATH: &str = "/v1/devices/provisioning/code"; -pub const PROVISIONING_MESSAGE_PATH: &str = "/v1/provisioning/%s"; - -pub const DIRECTORY_TOKENS_PATH: &str = "/v1/directory/tokens"; -pub const DIRECTORY_VERIFY_PATH: &str = "/v1/directory/%s"; -pub const DIRECTORY_AUTH_PATH: &str = "/v1/directory/auth"; -pub const DIRECTORY_FEEDBACK_PATH: &str = "/v1/directory/feedback-v3/%s"; -pub const SENDER_ACK_MESSAGE_PATH: &str = "/v1/messages/%s/%d"; -pub const UUID_ACK_MESSAGE_PATH: &str = "/v1/messages/uuid/%s"; -pub const ATTACHMENT_PATH: &str = "/v2/attachments/form/upload"; - -pub const PROFILE_PATH: &str = "/v1/profile/"; - -pub const SENDER_CERTIFICATE_LEGACY_PATH: &str = "/v1/certificate/delivery"; -pub const SENDER_CERTIFICATE_PATH: &str = - "/v1/certificate/delivery?includeUuid=true"; - -pub const VERIFICATION_SESSION_PATH: &str = "/v1/verification/session"; -pub const VERIFICATION_CODE_PATH: &str = "/v1/verification/session/%s/code"; - -pub const REGISTRATION_PATH: &str = "/v1/registration"; - -pub const ATTACHMENT_DOWNLOAD_PATH: &str = "attachments/%d"; - -pub const STICKER_MANIFEST_PATH: &str = "stickers/%s/manifest.proto"; -pub const STICKER_PATH: &str = "stickers/%s/full/%d"; -**/ - -pub const KEEPALIVE_TIMEOUT_SECONDS: Duration = Duration::from_secs(55); -pub const DEFAULT_DEVICE_ID: u32 = 1; - -#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)] -pub enum ServiceIdType { - /// Account Identity (ACI) - /// - /// An account UUID without an associated phone number, probably in the future to a username - AccountIdentity, - /// Phone number identity (PNI) - /// - /// A UUID associated with a phone number - PhoneNumberIdentity, -} - -impl fmt::Display for ServiceIdType { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - ServiceIdType::AccountIdentity => f.write_str("aci"), - ServiceIdType::PhoneNumberIdentity => f.write_str("pni"), - } - } -} - -#[derive(Debug, Serialize, Deserialize, Clone)] -pub struct ServiceIds { - #[serde(rename = "uuid")] - pub aci: Uuid, - #[serde(default)] // nil when not present (yet) - pub pni: Uuid, -} - -impl ServiceIds { - pub fn aci(&self) -> libsignal_protocol::Aci { - libsignal_protocol::Aci::from_uuid_bytes(self.aci.into_bytes()) - } - - pub fn pni(&self) -> libsignal_protocol::Pni { - libsignal_protocol::Pni::from_uuid_bytes(self.pni.into_bytes()) - } -} - -impl fmt::Display for ServiceIds { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "aci={} pni={}", self.aci, self.pni) - } -} - -#[derive(Debug, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct DeviceId { - pub device_id: u32, -} - -#[derive(Debug, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct DeviceInfo { - pub id: i64, - pub name: Option, - #[serde(with = "chrono::serde::ts_milliseconds")] - pub created: DateTime, - #[serde(with = "chrono::serde::ts_milliseconds")] - pub last_seen: DateTime, -} - -#[derive(Debug, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct AccountAttributes { - #[serde(default, with = "serde_optional_base64")] - pub signaling_key: Option>, - pub registration_id: u32, - pub pni_registration_id: u32, - pub voice: bool, - pub video: bool, - pub fetches_messages: bool, - pub pin: Option, - pub registration_lock: Option, - #[serde(default, with = "serde_optional_base64")] - pub unidentified_access_key: Option>, - pub unrestricted_unidentified_access: bool, - pub discoverable_by_phone_number: bool, - pub capabilities: DeviceCapabilities, - pub name: Option, -} - -#[derive(Debug, Serialize, Deserialize, Default, Eq, PartialEq, Clone)] -#[serde(rename_all = "camelCase")] -pub struct DeviceCapabilities { - #[serde(default)] - pub storage: bool, - #[serde(default)] - pub sender_key: bool, - #[serde(default)] - pub announcement_group: bool, - #[serde(default)] - pub change_number: bool, - #[serde(default)] - pub stories: bool, - #[serde(default)] - pub gift_badges: bool, - #[serde(default)] - pub pni: bool, - #[serde(default)] - pub payment_activation: bool, -} - -#[derive(Debug, Serialize, Deserialize)] -pub struct RecaptchaAttributes { - pub r#type: String, - pub token: String, - pub captcha: String, -} - -#[derive(Debug, Serialize, Deserialize)] -pub struct ProofRequired { - pub token: String, - pub options: Vec, -} - -#[derive(Debug, Deserialize, Default)] -#[serde(rename_all = "camelCase")] -pub struct PreKeyStatus { - pub count: u32, - pub pq_count: u32, -} - -#[derive(Derivative, Clone, Serialize, Deserialize)] -#[derivative(Debug)] -pub struct HttpAuth { - pub username: String, - #[derivative(Debug = "ignore")] - pub password: String, -} - -/// This type is used in registration lock handling. -/// It's identical with HttpAuth, but used to avoid type confusion. -#[derive(Derivative, Clone, Serialize, Deserialize)] -#[derivative(Debug)] -pub struct AuthCredentials { - pub username: String, - #[derivative(Debug = "ignore")] - pub password: String, -} - -#[derive(Debug, Clone)] -pub enum HttpAuthOverride { - NoOverride, - Unidentified, - Identified(HttpAuth), -} - -#[derive(Debug, Clone, Eq, PartialEq)] -pub enum AvatarWrite { - NewAvatar(C), - RetainAvatar, - NoAvatar, -} - -#[derive(Debug, Deserialize)] -#[serde(rename_all = "camelCase")] -struct SenderCertificateJson { - #[serde(with = "serde_base64")] - certificate: Vec, -} - -#[derive(Debug, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct PreKeyResponse { - #[serde(with = "serde_base64")] - pub identity_key: Vec, - pub devices: Vec, -} - -#[derive(Debug, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct WhoAmIResponse { - pub uuid: Uuid, - #[serde(default)] // nil when not present (yet) - pub pni: Uuid, - #[serde(with = "serde_phone_number")] - pub number: PhoneNumber, -} - -#[derive(Debug, Clone, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct RegistrationSessionMetadataResponse { - pub id: String, - #[serde(default)] - pub next_sms: Option, - #[serde(default)] - pub next_call: Option, - #[serde(default)] - pub next_verification_attempt: Option, - pub allowed_to_request_code: bool, - #[serde(default)] - pub requested_information: Vec, - pub verified: bool, -} - -impl RegistrationSessionMetadataResponse { - pub fn push_challenge_required(&self) -> bool { - // .contains() requires &String ... - self.requested_information - .iter() - .any(|x| x.as_str() == "pushChallenge") - } - - pub fn captcha_required(&self) -> bool { - // .contains() requires &String ... - self.requested_information - .iter() - .any(|x| x.as_str() == "captcha") - } -} - -#[derive(Debug, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct RegistrationLockFailure { - pub length: Option, - pub time_remaining: Option, - #[serde(rename = "backup_credentials")] - pub svr1_credentials: Option, - pub svr2_credentials: Option, -} - -#[derive(Debug, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct VerifyAccountResponse { - #[serde(rename = "uuid")] - pub aci: Uuid, - pub pni: Uuid, - pub storage_capable: bool, - #[serde(default)] - pub number: Option, -} - -#[derive(Clone, Copy, Debug, Eq, PartialEq)] -pub enum VerificationTransport { - Sms, - Voice, -} - -impl VerificationTransport { - pub fn as_str(&self) -> &str { - match self { - Self::Sms => "sms", - Self::Voice => "voice", - } - } -} - -#[derive(Clone, Debug)] -pub enum RegistrationMethod<'a> { - SessionId(&'a str), - RecoveryPassword(&'a str), -} - -impl<'a> RegistrationMethod<'a> { - pub fn session_id(&'a self) -> Option<&'a str> { - match self { - Self::SessionId(x) => Some(x), - _ => None, - } - } - - pub fn recovery_password(&'a self) -> Option<&'a str> { - match self { - Self::RecoveryPassword(x) => Some(x), - _ => None, - } - } -} - -#[derive(Debug, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct PreKeyResponseItem { - pub device_id: u32, - pub registration_id: u32, - pub signed_pre_key: SignedPreKeyEntity, - pub pre_key: Option, - pub pq_pre_key: Option, -} - -impl PreKeyResponseItem { - pub(crate) fn into_bundle( - self, - identity: IdentityKey, - ) -> Result { - let b = PreKeyBundle::new( - self.registration_id, - self.device_id.into(), - self.pre_key - .map(|pk| -> Result<_, SignalProtocolError> { - Ok(( - pk.key_id.into(), - PublicKey::deserialize(&pk.public_key)?, - )) - }) - .transpose()?, - // pre_key: Option<(u32, PublicKey)>, - self.signed_pre_key.key_id.into(), - PublicKey::deserialize(&self.signed_pre_key.public_key)?, - self.signed_pre_key.signature, - identity, - )?; - - if let Some(pq_pk) = self.pq_pre_key { - Ok(b.with_kyber_pre_key( - pq_pk.key_id.into(), - Key::::deserialize(&pq_pk.public_key)?, - pq_pk.signature, - )) - } else { - Ok(b) - } - } -} - -#[derive(Debug, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct MismatchedDevices { - pub missing_devices: Vec, - pub extra_devices: Vec, -} - -#[derive(Debug, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct StaleDevices { - pub stale_devices: Vec, -} - -#[derive(Debug, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct LinkRequest { - pub verification_code: String, - pub account_attributes: LinkAccountAttributes, - #[serde(flatten)] - pub device_activation_request: DeviceActivationRequest, -} - -#[derive(Debug, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct DeviceActivationRequest { - pub aci_signed_pre_key: SignedPreKeyEntity, - pub pni_signed_pre_key: SignedPreKeyEntity, - pub aci_pq_last_resort_pre_key: KyberPreKeyEntity, - pub pni_pq_last_resort_pre_key: KyberPreKeyEntity, -} - -#[derive(Debug, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct LinkAccountAttributes { - pub fetches_messages: bool, - pub name: String, - pub registration_id: u32, - pub pni_registration_id: u32, - pub capabilities: LinkCapabilities, -} - -#[derive(Debug, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct LinkCapabilities { - pub delete_sync: bool, - pub versioned_expiration_timer: bool, -} - -// https://github.com/signalapp/Signal-Desktop/blob/1e57db6aa4786dcddc944349e4894333ac2ffc9e/ts/textsecure/WebAPI.ts#L1287 -impl Default for LinkCapabilities { - fn default() -> Self { - Self { - delete_sync: true, - versioned_expiration_timer: true, - } - } -} - -#[derive(Debug, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct LinkResponse { - #[serde(rename = "uuid")] - pub aci: Uuid, - pub pni: Uuid, - pub device_id: u32, -} - -#[derive(Debug, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct SignalServiceProfile { - #[serde(default, with = "serde_optional_base64")] - pub identity_key: Option>, - #[serde(default, with = "serde_optional_base64")] - pub name: Option>, - #[serde(default, with = "serde_optional_base64")] - pub about: Option>, - #[serde(default, with = "serde_optional_base64")] - pub about_emoji: Option>, - - // TODO: not sure whether this is via optional_base64 - // #[serde(default, with = "serde_optional_base64")] - // pub payment_address: Option>, - pub avatar: Option, - pub unidentified_access: Option, - - #[serde(default)] - pub unrestricted_unidentified_access: bool, - - pub capabilities: DeviceCapabilities, -} - -impl SignalServiceProfile { - pub fn decrypt( - &self, - profile_cipher: crate::profile_cipher::ProfileCipher, - ) -> Result { - // Profile decryption - let name = self - .name - .as_ref() - .map(|data| profile_cipher.decrypt_name(data)) - .transpose()? - .flatten(); - let about = self - .about - .as_ref() - .map(|data| profile_cipher.decrypt_about(data)) - .transpose()?; - let about_emoji = self - .about_emoji - .as_ref() - .map(|data| profile_cipher.decrypt_emoji(data)) - .transpose()?; - - Ok(Profile { - name, - about, - about_emoji, - avatar: self.avatar.clone(), - }) - } -} - -#[derive(Debug, serde::Deserialize, Default)] -#[serde(rename_all = "camelCase")] -pub struct AttachmentV2UploadAttributes { - key: String, - credential: String, - acl: String, - algorithm: String, - date: String, - policy: String, - signature: String, - // This is different from Java's implementation, - // and I (Ruben) am unsure why they decide to force-parse at upload-time instead of at registration - // time. - attachment_id: u64, -} - -#[derive(thiserror::Error, Debug)] -pub enum ServiceError { - #[error("Service request timed out: {reason}")] - Timeout { reason: String }, - - #[error("invalid URL: {0}")] - InvalidUrl(#[from] url::ParseError), - - #[error("Error sending request: {reason}")] - SendError { reason: String }, - - #[error("Error decoding response: {reason}")] - ResponseError { reason: String }, - - #[error("Error decoding JSON response: {reason}")] - JsonDecodeError { reason: String }, - #[error("Error decoding protobuf frame: {0}")] - ProtobufDecodeError(#[from] prost::DecodeError), - #[error("error encoding or decoding bincode: {0}")] - BincodeError(#[from] bincode::Error), - #[error("error decoding base64 string: {0}")] - Base64DecodeError(#[from] base64::DecodeError), - - #[error("Rate limit exceeded")] - RateLimitExceeded, - #[error("Authorization failed")] - Unauthorized, - #[error("Registration lock is set: {0:?}")] - Locked(RegistrationLockFailure), - #[error("Unexpected response: HTTP {http_code}")] - UnhandledResponseCode { http_code: u16 }, - - #[error("Websocket error: {reason}")] - WsError { reason: String }, - #[error("Websocket closing: {reason}")] - WsClosing { reason: String }, - - #[error("Invalid frame: {reason}")] - InvalidFrameError { reason: String }, - - #[error("MAC error")] - MacError, - - #[error("Protocol error: {0}")] - SignalProtocolError(#[from] SignalProtocolError), - - #[error("Proof required: {0:?}")] - ProofRequiredError(ProofRequired), - - #[error("{0:?}")] - MismatchedDevicesException(MismatchedDevices), - - #[error("{0:?}")] - StaleDevices(StaleDevices), - - #[error(transparent)] - CredentialsCacheError(#[from] crate::groups_v2::CredentialsCacheError), - - #[error("groups v2 (zero-knowledge) error")] - GroupsV2Error, - - #[error(transparent)] - GroupsV2DecryptionError(#[from] GroupDecodingError), - - #[error(transparent)] - ZkGroupDeserializationFailure(#[from] ZkGroupDeserializationFailure), - - #[error("unsupported content")] - UnsupportedContent, - - #[error(transparent)] - ParseServiceAddress(#[from] ParseServiceAddressError), - - #[error("Not found.")] - NotFoundError, - - #[error("invalid device name")] - InvalidDeviceName, -} - -#[cfg_attr(feature = "unsend-futures", async_trait::async_trait(?Send))] -#[cfg_attr(not(feature = "unsend-futures"), async_trait::async_trait)] -pub trait PushService: MaybeSend { - type ByteStream: futures::io::AsyncRead + MaybeSend + Unpin; - - async fn get_json( - &mut self, - service: Endpoint, - path: &str, - additional_headers: &[(&str, &str)], - credentials_override: HttpAuthOverride, - ) -> Result - where - for<'de> T: Deserialize<'de>; - - async fn delete_json( - &mut self, - service: Endpoint, - path: &str, - additional_headers: &[(&str, &str)], - ) -> Result - where - for<'de> T: Deserialize<'de>; - - async fn put_json( - &mut self, - service: Endpoint, - path: &str, - additional_headers: &[(&str, &str)], - credentials_override: HttpAuthOverride, - value: S, - ) -> Result - where - for<'de> D: Deserialize<'de>, - S: MaybeSend + Serialize; - - async fn patch_json( - &mut self, - service: Endpoint, - path: &str, - additional_headers: &[(&str, &str)], - credentials_override: HttpAuthOverride, - value: S, - ) -> Result - where - for<'de> D: Deserialize<'de>, - S: MaybeSend + Serialize; - - async fn post_json( - &mut self, - service: Endpoint, - path: &str, - additional_headers: &[(&str, &str)], - credentials_override: HttpAuthOverride, - value: S, - ) -> Result - where - for<'de> D: Deserialize<'de>, - S: MaybeSend + Serialize; - - async fn get_protobuf( - &mut self, - service: Endpoint, - path: &str, - additional_headers: &[(&str, &str)], - credentials_override: HttpAuthOverride, - ) -> Result - where - T: Default + ProtobufMessage; - - async fn put_protobuf( - &mut self, - service: Endpoint, - path: &str, - additional_headers: &[(&str, &str)], - value: S, - ) -> Result - where - D: Default + ProtobufMessage, - S: Sized + ProtobufMessage; - - /// Downloads larger files in streaming fashion, e.g. attachments. - async fn get_from_cdn( - &mut self, - cdn_id: u32, - path: &str, - ) -> Result; - - /// Upload larger file to CDN0 in legacy fashion, e.g. for attachments. - /// - /// Implementations are allowed to *panic* when the Read instance throws an IO-Error - async fn post_to_cdn0<'s, C>( - &mut self, - path: &str, - value: &[(&str, &str)], - file: Option<(&str, &'s mut C)>, - ) -> Result<(), ServiceError> - where - C: std::io::Read + Send + 's; - - async fn ws( - &mut self, - path: &str, - keepalive_path: &str, - additional_headers: &[(&str, &str)], - credentials: Option, - ) -> Result; - - /// Fetches a list of all devices tied to the authenticated account. - /// - /// This list include the device that sends the request. - async fn devices(&mut self) -> Result, ServiceError> { - #[derive(serde::Deserialize)] - struct DeviceInfoList { - devices: Vec, - } - - let devices: DeviceInfoList = self - .get_json( - Endpoint::Service, - "/v1/devices/", - &[], - HttpAuthOverride::NoOverride, - ) - .await?; - - Ok(devices.devices) - } - - async fn unlink_device(&mut self, id: i64) -> Result<(), ServiceError> { - self.delete_json(Endpoint::Service, &format!("/v1/devices/{}", id), &[]) - .await - } - - async fn get_pre_key_status( - &mut self, - service_id_type: ServiceIdType, - ) -> Result { - self.get_json( - Endpoint::Service, - &format!("/v2/keys?identity={}", service_id_type), - &[], - HttpAuthOverride::NoOverride, - ) - .await - } - - async fn register_pre_keys( - &mut self, - service_id_type: ServiceIdType, - pre_key_state: PreKeyState, - ) -> Result<(), ServiceError> { - match self - .put_json( - Endpoint::Service, - &format!("/v2/keys?identity={}", service_id_type), - &[], - HttpAuthOverride::NoOverride, - pre_key_state, - ) - .await - { - Err(ServiceError::JsonDecodeError { .. }) => Ok(()), - r => r, - } - } - - async fn get_attachment_by_id( - &mut self, - id: &str, - cdn_id: u32, - ) -> Result { - let path = format!("attachments/{}", id); - self.get_from_cdn(cdn_id, &path).await - } - - async fn get_attachment( - &mut self, - ptr: &AttachmentPointer, - ) -> Result { - match ptr.attachment_identifier.as_ref().unwrap() { - AttachmentIdentifier::CdnId(id) => { - // cdn_number did not exist for this part of the protocol. - // cdn_number(), however, returns 0 when the field does not - // exist. - self.get_attachment_by_id(&format!("{}", id), ptr.cdn_number()) - .await - }, - AttachmentIdentifier::CdnKey(key) => { - self.get_attachment_by_id(key, ptr.cdn_number()).await - }, - } - } - - async fn get_sticker_pack_manifest( - &mut self, - id: &str, - ) -> Result { - let path = format!("/stickers/{}/manifest.proto", id); - self.get_from_cdn(0, &path).await - } - - async fn get_sticker( - &mut self, - pack_id: &str, - sticker_id: u32, - ) -> Result { - let path = format!("/stickers/{}/full/{}", pack_id, sticker_id); - self.get_from_cdn(0, &path).await - } - - async fn send_messages( - &mut self, - messages: OutgoingPushMessages, - ) -> Result { - let path = format!("/v1/messages/{}", messages.destination); - self.put_json( - Endpoint::Service, - &path, - &[], - HttpAuthOverride::NoOverride, - messages, - ) - .await - } - - /// Request AttachmentV2UploadAttributes - /// - /// Equivalent with getAttachmentV2UploadAttributes - async fn get_attachment_v2_upload_attributes( - &mut self, - ) -> Result { - self.get_json( - Endpoint::Service, - "/v2/attachments/form/upload", - &[], - HttpAuthOverride::NoOverride, - ) - .await - } - - /// Upload attachment to CDN - /// - /// Returns attachment ID and the attachment digest - async fn upload_attachment<'s, C>( - &mut self, - attrs: &AttachmentV2UploadAttributes, - content: &'s mut C, - ) -> Result<(u64, Vec), ServiceError> - where - C: std::io::Read + Send + 's, - { - let values = [ - ("acl", &attrs.acl as &str), - ("key", &attrs.key), - ("policy", &attrs.policy), - ("Content-Type", "application/octet-stream"), - ("x-amz-algorithm", &attrs.algorithm), - ("x-amz-credential", &attrs.credential), - ("x-amz-date", &attrs.date), - ("x-amz-signature", &attrs.signature), - ]; - - let mut digester = crate::digeststream::DigestingReader::new(content); - - self.post_to_cdn0( - "attachments/", - &values, - Some(("file", &mut digester)), - ) - .await?; - Ok((attrs.attachment_id, digester.finalize())) - } - - async fn get_messages( - &mut self, - allow_stories: bool, - ) -> Result, ServiceError> { - let entity_list: EnvelopeEntityList = self - .get_json( - Endpoint::Service, - "/v1/messages/", - &[( - "X-Signal-Receive-Stories", - if allow_stories { "true" } else { "false" }, - )], - HttpAuthOverride::NoOverride, - ) - .await?; - Ok(entity_list.messages) - } - - /// Method used to check our own UUID - async fn whoami(&mut self) -> Result { - self.get_json( - Endpoint::Service, - "/v1/accounts/whoami", - &[], - HttpAuthOverride::NoOverride, - ) - .await - } - - async fn retrieve_profile_by_id( - &mut self, - address: ServiceAddress, - profile_key: Option, - ) -> Result { - let endpoint = if let Some(key) = profile_key { - let version = bincode::serialize(&key.get_profile_key_version( - address.aci().expect("profile by ACI ProtocolAddress"), - ))?; - let version = std::str::from_utf8(&version) - .expect("hex encoded profile key version"); - format!("/v1/profile/{}/{}", address.uuid, version) - } else { - format!("/v1/profile/{}", address.uuid) - }; - // TODO: set locale to en_US - self.get_json( - Endpoint::Service, - &endpoint, - &[], - HttpAuthOverride::NoOverride, - ) - .await - } - - async fn retrieve_profile_avatar( - &mut self, - path: &str, - ) -> Result { - self.get_from_cdn(0, path).await - } - - async fn retrieve_groups_v2_profile_avatar( - &mut self, - path: &str, - ) -> Result { - self.get_from_cdn(0, path).await - } - - async fn get_pre_key( - &mut self, - destination: &ServiceAddress, - device_id: u32, - ) -> Result { - let path = - format!("/v2/keys/{}/{}?pq=true", destination.uuid, device_id); - - let mut pre_key_response: PreKeyResponse = self - .get_json( - Endpoint::Service, - &path, - &[], - HttpAuthOverride::NoOverride, - ) - .await?; - assert!(!pre_key_response.devices.is_empty()); - - let identity = IdentityKey::decode(&pre_key_response.identity_key)?; - let device = pre_key_response.devices.remove(0); - Ok(device.into_bundle(identity)?) - } - - async fn get_pre_keys( - &mut self, - destination: &ServiceAddress, - device_id: u32, - ) -> Result, ServiceError> { - let path = if device_id == 1 { - format!("/v2/keys/{}/*?pq=true", destination.uuid) - } else { - format!("/v2/keys/{}/{}?pq=true", destination.uuid, device_id) - }; - let pre_key_response: PreKeyResponse = self - .get_json( - Endpoint::Service, - &path, - &[], - HttpAuthOverride::NoOverride, - ) - .await?; - let mut pre_keys = vec![]; - let identity = IdentityKey::decode(&pre_key_response.identity_key)?; - for device in pre_key_response.devices { - pre_keys.push(device.into_bundle(identity)?); - } - Ok(pre_keys) - } - - async fn get_group( - &mut self, - credentials: HttpAuth, - ) -> Result { - self.get_protobuf( - Endpoint::Storage, - "/v1/groups/", - &[], - HttpAuthOverride::Identified(credentials), - ) - .await - } - - async fn get_sender_certificate( - &mut self, - ) -> Result { - let cert: SenderCertificateJson = self - .get_json( - Endpoint::Service, - "/v1/certificate/delivery", - &[], - HttpAuthOverride::NoOverride, - ) - .await?; - Ok(SenderCertificate::deserialize(&cert.certificate)?) - } - - async fn get_uuid_only_sender_certificate( - &mut self, - ) -> Result { - let cert: SenderCertificateJson = self - .get_json( - Endpoint::Service, - "/v1/certificate/delivery?includeE164=false", - &[], - HttpAuthOverride::NoOverride, - ) - .await?; - Ok(SenderCertificate::deserialize(&cert.certificate)?) - } - - async fn link_device( - &mut self, - link_request: &LinkRequest, - http_auth: HttpAuth, - ) -> Result { - self.put_json( - Endpoint::Service, - "/v1/devices/link", - &[], - HttpAuthOverride::Identified(http_auth), - link_request, - ) - .await - } - - async fn set_account_attributes( - &mut self, - attributes: AccountAttributes, - ) -> Result<(), ServiceError> { - assert!( - attributes.pin.is_none() || attributes.registration_lock.is_none(), - "only one of PIN and registration lock can be set." - ); - - match self - .put_json( - Endpoint::Service, - "/v1/accounts/attributes/", - &[], - HttpAuthOverride::NoOverride, - attributes, - ) - .await - { - Err(ServiceError::JsonDecodeError { .. }) => Ok(()), - r => r, - } - } - - /// Writes a profile and returns the avatar URL, if one was provided. - /// - /// The name, about and emoji fields are encrypted with an [`ProfileCipher`][struct@crate::profile_cipher::ProfileCipher]. - /// See [`AccountManager`][struct@crate::AccountManager] for a convenience method. - /// - /// Java equivalent: `writeProfile` - async fn write_profile<'s, C, S>( - &mut self, - version: &ProfileKeyVersion, - name: &[u8], - about: &[u8], - emoji: &[u8], - commitment: &ProfileKeyCommitment, - avatar: AvatarWrite<&mut C>, - ) -> Result, ServiceError> - where - C: std::io::Read + Send + 's, - S: AsRef, - { - #[derive(Debug, Serialize)] - #[serde(rename_all = "camelCase")] - struct SignalServiceProfileWrite<'s> { - /// Hex-encoded - version: &'s str, - #[serde(with = "serde_base64")] - name: &'s [u8], - #[serde(with = "serde_base64")] - about: &'s [u8], - #[serde(with = "serde_base64")] - about_emoji: &'s [u8], - avatar: bool, - same_avatar: bool, - #[serde(with = "serde_base64")] - commitment: &'s [u8], - } - - // Bincode is transparent and will return a hex-encoded string. - let version = bincode::serialize(version)?; - let version = std::str::from_utf8(&version) - .expect("profile_key_version is hex encoded string"); - let commitment = bincode::serialize(commitment)?; - - let command = SignalServiceProfileWrite { - version, - name, - about, - about_emoji: emoji, - avatar: !matches!(avatar, AvatarWrite::NoAvatar), - same_avatar: matches!(avatar, AvatarWrite::RetainAvatar), - commitment: &commitment, - }; - - // XXX this should be a struct; cfr ProfileAvatarUploadAttributes - let response: Result = self - .put_json( - Endpoint::Service, - "/v1/profile", - &[], - HttpAuthOverride::NoOverride, - command, - ) - .await; - match (response, avatar) { - (Ok(_url), AvatarWrite::NewAvatar(_avatar)) => { - // FIXME - unreachable!("Uploading avatar unimplemented"); - }, - // FIXME cleanup when #54883 is stable and MSRV: - // or-patterns syntax is experimental - // see issue #54883 for more information - ( - Err(ServiceError::JsonDecodeError { .. }), - AvatarWrite::RetainAvatar, - ) - | ( - Err(ServiceError::JsonDecodeError { .. }), - AvatarWrite::NoAvatar, - ) => { - // OWS sends an empty string when there's no attachment - Ok(None) - }, - (Err(e), _) => Err(e), - (Ok(_resp), AvatarWrite::RetainAvatar) - | (Ok(_resp), AvatarWrite::NoAvatar) => { - tracing::warn!( - "No avatar supplied but got avatar upload URL. Ignoring" - ); - Ok(None) - }, - } - } - - // Equivalent of Java's - // RegistrationSessionMetadataResponse createVerificationSession(@Nullable String pushToken, @Nullable String mcc, @Nullable String mnc) - async fn create_verification_session<'a>( - &mut self, - number: &'a str, - push_token: Option<&'a str>, - mcc: Option<&'a str>, - mnc: Option<&'a str>, - ) -> Result { - #[derive(serde::Serialize, Debug)] - #[serde(rename_all = "camelCase")] - struct VerificationSessionMetadataRequestBody<'a> { - number: &'a str, - push_token: Option<&'a str>, - mcc: Option<&'a str>, - mnc: Option<&'a str>, - push_token_type: Option<&'a str>, - } - - let req = VerificationSessionMetadataRequestBody { - number, - push_token_type: push_token.as_ref().map(|_| "fcm"), - push_token, - mcc, - mnc, - }; - - let res: RegistrationSessionMetadataResponse = self - .post_json( - Endpoint::Service, - "/v1/verification/session", - &[], - HttpAuthOverride::Unidentified, - req, - ) - .await?; - Ok(res) - } - - // Equivalent of Java's - // RegistrationSessionMetadataResponse patchVerificationSession(String sessionId, @Nullable String pushToken, @Nullable String mcc, @Nullable String mnc, @Nullable String captchaToken, @Nullable String pushChallengeToken) - async fn patch_verification_session<'a>( - &mut self, - session_id: &'a str, - push_token: Option<&'a str>, - mcc: Option<&'a str>, - mnc: Option<&'a str>, - captcha: Option<&'a str>, - push_challenge: Option<&'a str>, - ) -> Result { - #[derive(serde::Serialize, Debug)] - #[serde(rename_all = "camelCase")] - struct UpdateVerificationSessionRequestBody<'a> { - captcha: Option<&'a str>, - push_token: Option<&'a str>, - push_challenge: Option<&'a str>, - mcc: Option<&'a str>, - mnc: Option<&'a str>, - push_token_type: Option<&'a str>, - } - - let req = UpdateVerificationSessionRequestBody { - captcha, - push_token_type: push_token.as_ref().map(|_| "fcm"), - push_token, - mcc, - mnc, - push_challenge, - }; - - let res: RegistrationSessionMetadataResponse = self - .patch_json( - Endpoint::Service, - &format!("/v1/verification/session/{}", session_id), - &[], - HttpAuthOverride::Unidentified, - req, - ) - .await?; - Ok(res) - } - - // Equivalent of Java's - // RegistrationSessionMetadataResponse requestVerificationCode(String sessionId, Locale locale, boolean androidSmsRetriever, VerificationCodeTransport transport) - /// Request a verification code. - /// - /// Signal requires a client type, and they use these three strings internally: - /// - "android-2021-03" - /// - "android" - /// - "ios" - /// "android-2021-03" allegedly implies FCM support, whereas the other strings don't. In - /// principle, they will consider any string as "unknown", so other strings may work too. - async fn request_verification_code( - &mut self, - session_id: &str, - client: &str, - // XXX: We currently don't support this, because we need to set some headers in the - // post_json() call - // locale: Option, - transport: VerificationTransport, - ) -> Result { - let mut req = std::collections::HashMap::new(); - req.insert("transport", transport.as_str()); - req.insert("client", client); - - let res: RegistrationSessionMetadataResponse = self - .post_json( - Endpoint::Service, - &format!("/v1/verification/session/{}/code", session_id), - &[], - HttpAuthOverride::Unidentified, - req, - ) - .await?; - Ok(res) - } - - async fn submit_verification_code( - &mut self, - session_id: &str, - verification_code: &str, - ) -> Result { - let mut req = std::collections::HashMap::new(); - req.insert("code", verification_code); - - let res: RegistrationSessionMetadataResponse = self - .put_json( - Endpoint::Service, - &format!("/v1/verification/session/{}/code", session_id), - &[], - HttpAuthOverride::Unidentified, - req, - ) - .await?; - Ok(res) - } - - async fn submit_registration_request<'a>( - &mut self, - registration_method: RegistrationMethod<'a>, - account_attributes: AccountAttributes, - skip_device_transfer: bool, - aci_identity_key: &IdentityKey, - pni_identity_key: &IdentityKey, - device_activation_request: DeviceActivationRequest, - ) -> Result { - #[derive(serde::Serialize, Debug)] - #[serde(rename_all = "camelCase")] - struct RegistrationSessionRequestBody<'a> { - // Unhandled response 422 with body: - // {"errors":["deviceActivationRequest.pniSignedPreKey must not be - // null","deviceActivationRequest.pniPqLastResortPreKey must not be - // null","everySignedKeyValid must be true","aciIdentityKey must not be - // null","pniIdentityKey must not be null","deviceActivationRequest.aciSignedPreKey - // must not be null","deviceActivationRequest.aciPqLastResortPreKey must not be null"]} - session_id: Option<&'a str>, - recovery_password: Option<&'a str>, - account_attributes: AccountAttributes, - skip_device_transfer: bool, - every_signed_key_valid: bool, - #[serde(default, with = "serde_base64")] - pni_identity_key: Vec, - #[serde(default, with = "serde_base64")] - aci_identity_key: Vec, - #[serde(flatten)] - device_activation_request: DeviceActivationRequest, - } - - let req = RegistrationSessionRequestBody { - session_id: registration_method.session_id(), - recovery_password: registration_method.recovery_password(), - account_attributes, - skip_device_transfer, - aci_identity_key: aci_identity_key.serialize().into(), - pni_identity_key: pni_identity_key.serialize().into(), - device_activation_request, - every_signed_key_valid: true, - }; - - let res: VerifyAccountResponse = self - .post_json( - Endpoint::Service, - "/v1/registration", - &[], - HttpAuthOverride::NoOverride, - req, - ) - .await?; - Ok(res) - } - - async fn distribute_pni_keys( - &mut self, - pni_identity_key: &IdentityKey, - device_messages: Vec, - device_pni_signed_prekeys: HashMap, - device_pni_last_resort_kyber_prekeys: HashMap< - String, - KyberPreKeyEntity, - >, - pni_registration_ids: HashMap, - signature_valid_on_each_signed_pre_key: bool, - ) -> Result { - #[derive(serde::Serialize, Debug)] - #[serde(rename_all = "camelCase")] - struct PniKeyDistributionRequest { - #[serde(with = "serde_base64")] - pni_identity_key: Vec, - device_messages: Vec, - device_pni_signed_prekeys: HashMap, - #[serde(rename = "devicePniPqLastResortPrekeys")] - device_pni_last_resort_kyber_prekeys: - HashMap, - pni_registration_ids: HashMap, - signature_valid_on_each_signed_pre_key: bool, - } - - let res: VerifyAccountResponse = self - .put_json( - Endpoint::Service, - "/v2/accounts/phone_number_identity_key_distribution", - &[], - HttpAuthOverride::NoOverride, - PniKeyDistributionRequest { - pni_identity_key: pni_identity_key.serialize().into(), - device_messages, - device_pni_signed_prekeys, - device_pni_last_resort_kyber_prekeys, - pni_registration_ids, - signature_valid_on_each_signed_pre_key, - }, - ) - .await?; - Ok(res) - } -} diff --git a/libsignal-service/src/websocket/attachment_service.rs b/libsignal-service/src/websocket/attachment_service.rs deleted file mode 100644 index 4c6e17493..000000000 --- a/libsignal-service/src/websocket/attachment_service.rs +++ /dev/null @@ -1,11 +0,0 @@ -use crate::push_service::AttachmentV2UploadAttributes; - -use super::*; - -impl SignalWebSocket { - pub async fn get_attachment_v2_upload_attributes( - &mut self, - ) -> Result { - self.get_json("/v2/attachments/form/upload").await - } -} diff --git a/libsignal-service/protobuf/DeviceName.proto b/protobuf/DeviceName.proto similarity index 100% rename from libsignal-service/protobuf/DeviceName.proto rename to protobuf/DeviceName.proto diff --git a/libsignal-service/protobuf/Groups.proto b/protobuf/Groups.proto similarity index 100% rename from libsignal-service/protobuf/Groups.proto rename to protobuf/Groups.proto diff --git a/libsignal-service/protobuf/Provisioning.proto b/protobuf/Provisioning.proto similarity index 100% rename from libsignal-service/protobuf/Provisioning.proto rename to protobuf/Provisioning.proto diff --git a/libsignal-service/protobuf/SignalService.proto b/protobuf/SignalService.proto similarity index 100% rename from libsignal-service/protobuf/SignalService.proto rename to protobuf/SignalService.proto diff --git a/libsignal-service/protobuf/StickerResources.proto b/protobuf/StickerResources.proto similarity index 100% rename from libsignal-service/protobuf/StickerResources.proto rename to protobuf/StickerResources.proto diff --git a/libsignal-service/protobuf/UnidentifiedDelivery.proto b/protobuf/UnidentifiedDelivery.proto similarity index 100% rename from libsignal-service/protobuf/UnidentifiedDelivery.proto rename to protobuf/UnidentifiedDelivery.proto diff --git a/libsignal-service/protobuf/WebSocketResources.proto b/protobuf/WebSocketResources.proto similarity index 100% rename from libsignal-service/protobuf/WebSocketResources.proto rename to protobuf/WebSocketResources.proto diff --git a/libsignal-service/protobuf/update-protos.sh b/protobuf/update-protos.sh similarity index 100% rename from libsignal-service/protobuf/update-protos.sh rename to protobuf/update-protos.sh diff --git a/libsignal-service/src/account_manager.rs b/src/account_manager.rs similarity index 99% rename from libsignal-service/src/account_manager.rs rename to src/account_manager.rs index d86af6c67..d8251c92b 100644 --- a/libsignal-service/src/account_manager.rs +++ b/src/account_manager.rs @@ -52,8 +52,8 @@ use crate::{ type Aes256Ctr128BE = ctr::Ctr128BE; -pub struct AccountManager { - service: Service, +pub struct AccountManager { + service: PushService, profile_key: Option, } @@ -73,8 +73,8 @@ pub struct Profile { pub avatar: Option, } -impl AccountManager { - pub fn new(service: Service, profile_key: Option) -> Self { +impl AccountManager { + pub fn new(service: PushService, profile_key: Option) -> Self { Self { service, profile_key, @@ -639,7 +639,7 @@ impl AccountManager { &mut self, aci_protocol_store: &mut Aci, pni_protocol_store: &mut Pni, - mut sender: MessageSender, + mut sender: MessageSender, local_aci: ServiceAddress, e164: PhoneNumber, csprng: &mut R, diff --git a/libsignal-service/src/attachment_cipher.rs b/src/attachment_cipher.rs similarity index 100% rename from libsignal-service/src/attachment_cipher.rs rename to src/attachment_cipher.rs diff --git a/libsignal-service/src/cipher.rs b/src/cipher.rs similarity index 100% rename from libsignal-service/src/cipher.rs rename to src/cipher.rs diff --git a/libsignal-service/src/configuration.rs b/src/configuration.rs similarity index 100% rename from libsignal-service/src/configuration.rs rename to src/configuration.rs diff --git a/libsignal-service/src/content.rs b/src/content.rs similarity index 100% rename from libsignal-service/src/content.rs rename to src/content.rs diff --git a/libsignal-service/src/content/data_message.rs b/src/content/data_message.rs similarity index 100% rename from libsignal-service/src/content/data_message.rs rename to src/content/data_message.rs diff --git a/libsignal-service/src/content/story_message.rs b/src/content/story_message.rs similarity index 100% rename from libsignal-service/src/content/story_message.rs rename to src/content/story_message.rs diff --git a/libsignal-service/src/digeststream.rs b/src/digeststream.rs similarity index 100% rename from libsignal-service/src/digeststream.rs rename to src/digeststream.rs diff --git a/libsignal-service/src/envelope.rs b/src/envelope.rs similarity index 100% rename from libsignal-service/src/envelope.rs rename to src/envelope.rs diff --git a/libsignal-service/src/groups_v2/manager.rs b/src/groups_v2/manager.rs similarity index 98% rename from libsignal-service/src/groups_v2/manager.rs rename to src/groups_v2/manager.rs index ff7d76f97..d5e77dffd 100644 --- a/libsignal-service/src/groups_v2/manager.rs +++ b/src/groups_v2/manager.rs @@ -127,17 +127,17 @@ impl CredentialsCache for &mut T { } } -pub struct GroupsManager { +pub struct GroupsManager { service_ids: ServiceIds, - push_service: S, + push_service: PushService, credentials_cache: C, server_public_params: ServerPublicParams, } -impl GroupsManager { +impl GroupsManager { pub fn new( service_ids: ServiceIds, - push_service: S, + push_service: PushService, credentials_cache: C, server_public_params: ServerPublicParams, ) -> Self { diff --git a/libsignal-service/src/groups_v2/mod.rs b/src/groups_v2/mod.rs similarity index 100% rename from libsignal-service/src/groups_v2/mod.rs rename to src/groups_v2/mod.rs diff --git a/libsignal-service/src/groups_v2/model.rs b/src/groups_v2/model.rs similarity index 100% rename from libsignal-service/src/groups_v2/model.rs rename to src/groups_v2/model.rs diff --git a/libsignal-service/src/groups_v2/operations.rs b/src/groups_v2/operations.rs similarity index 100% rename from libsignal-service/src/groups_v2/operations.rs rename to src/groups_v2/operations.rs diff --git a/libsignal-service/src/groups_v2/utils.rs b/src/groups_v2/utils.rs similarity index 100% rename from libsignal-service/src/groups_v2/utils.rs rename to src/groups_v2/utils.rs diff --git a/libsignal-service/src/kat.bin.rs b/src/kat.bin.rs similarity index 100% rename from libsignal-service/src/kat.bin.rs rename to src/kat.bin.rs diff --git a/libsignal-service/src/lib.rs b/src/lib.rs similarity index 75% rename from libsignal-service/src/lib.rs rename to src/lib.rs index 177325328..2a881ca8d 100644 --- a/libsignal-service/src/lib.rs +++ b/src/lib.rs @@ -48,25 +48,6 @@ pub const GROUP_UPDATE_FLAG: u32 = 1; /// GROUP_LEAVE_FLAG signals that this message is a group leave message. pub const GROUP_LEAVE_FLAG: u32 = 2; -/// This trait allows for the conditional support of Send compatible futures -/// depending on whether or not the `unsend-futures` feature flag is enabled. -/// As this feature is disabled by default, Send is supported by default. -/// -/// This is necessary as actix does not support Send, which means unconditionally -/// imposing this requirement would break libsignal-service-actix. -/// -/// Conversely, hyper does support Send, which is why libsignal-service-hyper -/// does not enable the `unsend-futures` feature flag. -#[cfg(not(feature = "unsend-futures"))] -pub trait MaybeSend: Send {} -#[cfg(not(feature = "unsend-futures"))] -impl MaybeSend for T where T: Send {} - -#[cfg(feature = "unsend-futures")] -pub trait MaybeSend {} -#[cfg(feature = "unsend-futures")] -impl MaybeSend for T {} - pub mod prelude { pub use super::ServiceAddress; pub use crate::{ diff --git a/libsignal-service/src/master_key.rs b/src/master_key.rs similarity index 100% rename from libsignal-service/src/master_key.rs rename to src/master_key.rs diff --git a/libsignal-service/src/messagepipe.rs b/src/messagepipe.rs similarity index 93% rename from libsignal-service/src/messagepipe.rs rename to src/messagepipe.rs index 9da098599..cdb77b7a8 100644 --- a/libsignal-service/src/messagepipe.rs +++ b/src/messagepipe.rs @@ -29,8 +29,7 @@ pub enum Incoming { QueueEmpty, } -#[cfg_attr(feature = "unsend-futures", async_trait::async_trait(?Send))] -#[cfg_attr(not(feature = "unsend-futures"), async_trait::async_trait)] +#[async_trait::async_trait] pub trait WebSocketService { type Stream: FusedStream + Unpin; @@ -139,8 +138,7 @@ impl MessagePipe { pub struct PanicingWebSocketService; #[allow(clippy::diverging_sub_expression)] -#[cfg_attr(feature = "unsend-futures", async_trait::async_trait(?Send))] -#[cfg_attr(not(feature = "unsend-futures"), async_trait::async_trait)] +#[async_trait::async_trait] impl WebSocketService for PanicingWebSocketService { type Stream = futures::channel::mpsc::Receiver; diff --git a/libsignal-service/src/models.rs b/src/models.rs similarity index 100% rename from libsignal-service/src/models.rs rename to src/models.rs diff --git a/libsignal-service/src/pre_keys.rs b/src/pre_keys.rs similarity index 100% rename from libsignal-service/src/pre_keys.rs rename to src/pre_keys.rs diff --git a/libsignal-service/src/profile_cipher.rs b/src/profile_cipher.rs similarity index 100% rename from libsignal-service/src/profile_cipher.rs rename to src/profile_cipher.rs diff --git a/libsignal-service/src/profile_name.rs b/src/profile_name.rs similarity index 100% rename from libsignal-service/src/profile_name.rs rename to src/profile_name.rs diff --git a/libsignal-service/src/profile_service.rs b/src/profile_service.rs similarity index 100% rename from libsignal-service/src/profile_service.rs rename to src/profile_service.rs diff --git a/libsignal-service/src/proto.rs b/src/proto.rs similarity index 100% rename from libsignal-service/src/proto.rs rename to src/proto.rs diff --git a/libsignal-service/src/provisioning/cipher.rs b/src/provisioning/cipher.rs similarity index 100% rename from libsignal-service/src/provisioning/cipher.rs rename to src/provisioning/cipher.rs diff --git a/libsignal-service/src/provisioning/mod.rs b/src/provisioning/mod.rs similarity index 99% rename from libsignal-service/src/provisioning/mod.rs rename to src/provisioning/mod.rs index 39d058cc9..2455409a1 100644 --- a/libsignal-service/src/provisioning/mod.rs +++ b/src/provisioning/mod.rs @@ -136,12 +136,11 @@ pub async fn link_device< R: rand::Rng + rand::CryptoRng, Aci: PreKeysStore, Pni: PreKeysStore, - P: PushService + Clone, >( aci_store: &mut Aci, pni_store: &mut Pni, csprng: &mut R, - mut push_service: P, + mut push_service: PushService, password: &str, device_name: &str, mut tx: Sender, diff --git a/libsignal-service/src/provisioning/pipe.rs b/src/provisioning/pipe.rs similarity index 100% rename from libsignal-service/src/provisioning/pipe.rs rename to src/provisioning/pipe.rs diff --git a/src/push_service/account.rs b/src/push_service/account.rs new file mode 100644 index 000000000..d36eff544 --- /dev/null +++ b/src/push_service/account.rs @@ -0,0 +1,183 @@ +use std::fmt; + +use chrono::{DateTime, Utc}; +use phonenumber::PhoneNumber; +use serde::{Deserialize, Serialize}; +use uuid::Uuid; + +use super::{HttpAuthOverride, PushService, ServiceError}; +use crate::{ + configuration::Endpoint, + utils::{serde_optional_base64, serde_phone_number}, +}; + +#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)] +pub enum ServiceIdType { + /// Account Identity (ACI) + /// + /// An account UUID without an associated phone number, probably in the future to a username + AccountIdentity, + /// Phone number identity (PNI) + /// + /// A UUID associated with a phone number + PhoneNumberIdentity, +} + +impl fmt::Display for ServiceIdType { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + ServiceIdType::AccountIdentity => f.write_str("aci"), + ServiceIdType::PhoneNumberIdentity => f.write_str("pni"), + } + } +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct ServiceIds { + #[serde(rename = "uuid")] + pub aci: Uuid, + #[serde(default)] // nil when not present (yet) + pub pni: Uuid, +} + +impl ServiceIds { + pub fn aci(&self) -> libsignal_protocol::Aci { + libsignal_protocol::Aci::from_uuid_bytes(self.aci.into_bytes()) + } + + pub fn pni(&self) -> libsignal_protocol::Pni { + libsignal_protocol::Pni::from_uuid_bytes(self.pni.into_bytes()) + } +} + +impl fmt::Display for ServiceIds { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "aci={} pni={}", self.aci, self.pni) + } +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct DeviceId { + pub device_id: u32, +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct DeviceInfo { + pub id: i64, + pub name: Option, + #[serde(with = "chrono::serde::ts_milliseconds")] + pub created: DateTime, + #[serde(with = "chrono::serde::ts_milliseconds")] + pub last_seen: DateTime, +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct AccountAttributes { + #[serde(default, with = "serde_optional_base64")] + pub signaling_key: Option>, + pub registration_id: u32, + pub pni_registration_id: u32, + pub voice: bool, + pub video: bool, + pub fetches_messages: bool, + pub pin: Option, + pub registration_lock: Option, + #[serde(default, with = "serde_optional_base64")] + pub unidentified_access_key: Option>, + pub unrestricted_unidentified_access: bool, + pub discoverable_by_phone_number: bool, + pub capabilities: DeviceCapabilities, + pub name: Option, +} + +#[derive(Debug, Serialize, Deserialize, Default, Eq, PartialEq, Clone)] +#[serde(rename_all = "camelCase")] +pub struct DeviceCapabilities { + #[serde(default)] + pub storage: bool, + #[serde(default)] + pub sender_key: bool, + #[serde(default)] + pub announcement_group: bool, + #[serde(default)] + pub change_number: bool, + #[serde(default)] + pub stories: bool, + #[serde(default)] + pub gift_badges: bool, + #[serde(default)] + pub pni: bool, + #[serde(default)] + pub payment_activation: bool, +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct WhoAmIResponse { + pub uuid: Uuid, + #[serde(default)] // nil when not present (yet) + pub pni: Uuid, + #[serde(with = "serde_phone_number")] + pub number: PhoneNumber, +} + +impl PushService { + /// Method used to check our own UUID + pub async fn whoami(&mut self) -> Result { + self.get_json( + Endpoint::Service, + "/v1/accounts/whoami", + &[], + HttpAuthOverride::NoOverride, + ) + .await + } + + /// Fetches a list of all devices tied to the authenticated account. + /// + /// This list include the device that sends the request. + pub async fn devices(&mut self) -> Result, ServiceError> { + #[derive(serde::Deserialize)] + struct DeviceInfoList { + devices: Vec, + } + + let devices: DeviceInfoList = self + .get_json( + Endpoint::Service, + "/v1/devices/", + &[], + HttpAuthOverride::NoOverride, + ) + .await?; + + Ok(devices.devices) + } + + pub async fn set_account_attributes( + &mut self, + attributes: AccountAttributes, + ) -> Result<(), ServiceError> { + assert!( + attributes.pin.is_none() || attributes.registration_lock.is_none(), + "only one of PIN and registration lock can be set." + ); + + match self + .put_json( + Endpoint::Service, + "/v1/accounts/attributes/", + &[], + HttpAuthOverride::NoOverride, + attributes, + ) + .await + { + Err(ServiceError::JsonDecodeError { .. }) => Ok(()), + r => r, + } + } +} diff --git a/src/push_service/cdn.rs b/src/push_service/cdn.rs new file mode 100644 index 000000000..3ba962a6d --- /dev/null +++ b/src/push_service/cdn.rs @@ -0,0 +1,205 @@ +use std::io::{self, Read}; + +use bytes::Bytes; +use futures::{FutureExt, StreamExt, TryStreamExt}; +use http_body_util::BodyExt; +use hyper::Method; +use tracing::debug; + +use crate::{ + configuration::Endpoint, + prelude::AttachmentIdentifier, + proto::AttachmentPointer, + push_service::{HttpAuthOverride, RequestBody}, +}; + +use super::{PushService, ServiceError}; + +#[derive(Debug, serde::Deserialize, Default)] +#[serde(rename_all = "camelCase")] +pub struct AttachmentV2UploadAttributes { + key: String, + credential: String, + acl: String, + algorithm: String, + date: String, + policy: String, + signature: String, + // This is different from Java's implementation, + // and I (Ruben) am unsure why they decide to force-parse at upload-time instead of at registration + // time. + attachment_id: u64, +} + +impl PushService { + #[tracing::instrument(skip(self))] + pub(crate) async fn get_from_cdn( + &mut self, + cdn_id: u32, + path: &str, + ) -> Result { + let response = self + .request( + Method::GET, + Endpoint::Cdn(cdn_id), + path, + &[], + HttpAuthOverride::Unidentified, // CDN requests are always without authentication + None, + ) + .await?; + + Ok(Box::new( + response + .into_body() + .into_data_stream() + .map_err(|e| io::Error::new(io::ErrorKind::Other, e)) + .into_async_read(), + )) + } + + pub async fn get_attachment_by_id( + &mut self, + id: &str, + cdn_id: u32, + ) -> Result { + let path = format!("attachments/{}", id); + self.get_from_cdn(cdn_id, &path).await + } + + pub async fn get_attachment( + &mut self, + ptr: &AttachmentPointer, + ) -> Result { + match ptr.attachment_identifier.as_ref().unwrap() { + AttachmentIdentifier::CdnId(id) => { + // cdn_number did not exist for this part of the protocol. + // cdn_number(), however, returns 0 when the field does not + // exist. + self.get_attachment_by_id(&format!("{}", id), ptr.cdn_number()) + .await + }, + AttachmentIdentifier::CdnKey(key) => { + self.get_attachment_by_id(key, ptr.cdn_number()).await + }, + } + } + + pub async fn get_attachment_v2_upload_attributes( + &mut self, + ) -> Result { + self.get_json( + Endpoint::Service, + "/v2/attachments/form/upload", + &[], + HttpAuthOverride::NoOverride, + ) + .await + } + + /// Upload attachment to CDN + /// + /// Returns attachment ID and the attachment digest + pub async fn upload_attachment<'s, C>( + &mut self, + attrs: &AttachmentV2UploadAttributes, + content: &'s mut C, + ) -> Result<(u64, Vec), ServiceError> + where + C: std::io::Read + Send + 's, + { + let values = [ + ("acl", &attrs.acl as &str), + ("key", &attrs.key), + ("policy", &attrs.policy), + ("Content-Type", "application/octet-stream"), + ("x-amz-algorithm", &attrs.algorithm), + ("x-amz-credential", &attrs.credential), + ("x-amz-date", &attrs.date), + ("x-amz-signature", &attrs.signature), + ]; + + let mut digester = crate::digeststream::DigestingReader::new(content); + + self.post_to_cdn0( + "attachments/", + &values, + Some(("file", &mut digester)), + ) + .await?; + Ok((attrs.attachment_id, digester.finalize())) + } + + #[tracing::instrument(skip(self, value, file), fields(file = file.as_ref().map(|_| "")))] + pub async fn post_to_cdn0<'s, C>( + &mut self, + path: &str, + value: &[(&str, &str)], + file: Option<(&str, &'s mut C)>, + ) -> Result<(), ServiceError> + where + C: Read + Send + 's, + { + let mut form = mpart_async::client::MultipartRequest::default(); + + // mpart-async has a peculiar ordering of the form items, + // and Amazon S3 expects them in a very specific order (i.e., the file contents should + // go last. + // + // mpart-async uses a VecDeque internally for ordering the fields in the order given. + // + // https://github.com/cetra3/mpart-async/issues/16 + + for &(k, v) in value { + form.add_field(k, v); + } + + if let Some((filename, file)) = file { + // XXX Actix doesn't cope with none-'static lifetimes + // https://docs.rs/actix-web/3.2.0/actix_web/body/enum.Body.html + let mut buf = Vec::new(); + file.read_to_end(&mut buf) + .expect("infallible Read instance"); + form.add_stream( + "file", + filename, + "application/octet-stream", + futures::future::ok::<_, ()>(Bytes::from(buf)).into_stream(), + ); + } + + let content_type = + format!("multipart/form-data; boundary={}", form.get_boundary()); + + // XXX Amazon S3 needs the Content-Length, but we don't know it without depleting the whole + // stream. Sadly, Content-Length != contents.len(), but should include the whole form. + let mut body_contents = vec![]; + while let Some(b) = form.next().await { + // Unwrap, because no error type was used above + body_contents.extend(b.unwrap()); + } + tracing::trace!( + "Sending PUT with Content-Type={} and length {}", + content_type, + body_contents.len() + ); + + let response = self + .request( + Method::POST, + Endpoint::Cdn(0), + path, + &[], + HttpAuthOverride::NoOverride, + Some(RequestBody { + contents: body_contents, + content_type, + }), + ) + .await?; + + debug!("HyperPushService::PUT response: {:?}", response); + + Ok(()) + } +} diff --git a/src/push_service/error.rs b/src/push_service/error.rs new file mode 100644 index 000000000..d197667d9 --- /dev/null +++ b/src/push_service/error.rs @@ -0,0 +1,88 @@ +use libsignal_protocol::SignalProtocolError; +use zkgroup::ZkGroupDeserializationFailure; + +use crate::{groups_v2::GroupDecodingError, ParseServiceAddressError}; + +use super::{ + MismatchedDevices, ProofRequired, RegistrationLockFailure, StaleDevices, +}; + +#[derive(thiserror::Error, Debug)] +pub enum ServiceError { + #[error("Service request timed out: {reason}")] + Timeout { reason: String }, + + #[error("invalid URL: {0}")] + InvalidUrl(#[from] url::ParseError), + + #[error("Error sending request: {reason}")] + SendError { reason: String }, + + #[error("Error decoding response: {reason}")] + ResponseError { reason: String }, + + #[error("Error decoding JSON response: {reason}")] + JsonDecodeError { reason: String }, + #[error("Error decoding protobuf frame: {0}")] + ProtobufDecodeError(#[from] prost::DecodeError), + #[error("error encoding or decoding bincode: {0}")] + BincodeError(#[from] bincode::Error), + #[error("error decoding base64 string: {0}")] + Base64DecodeError(#[from] base64::DecodeError), + + #[error("Rate limit exceeded")] + RateLimitExceeded, + #[error("Authorization failed")] + Unauthorized, + #[error("Registration lock is set: {0:?}")] + Locked(RegistrationLockFailure), + #[error("Unexpected response: HTTP {http_code}")] + UnhandledResponseCode { http_code: u16 }, + + #[error("Websocket error: {reason}")] + WsError { reason: String }, + #[error("Websocket closing: {reason}")] + WsClosing { reason: String }, + + #[error("Invalid frame: {reason}")] + InvalidFrameError { reason: String }, + + #[error("MAC error")] + MacError, + + #[error("Protocol error: {0}")] + SignalProtocolError(#[from] SignalProtocolError), + + #[error("Proof required: {0:?}")] + ProofRequiredError(ProofRequired), + + #[error("{0:?}")] + MismatchedDevicesException(MismatchedDevices), + + #[error("{0:?}")] + StaleDevices(StaleDevices), + + #[error(transparent)] + CredentialsCacheError(#[from] crate::groups_v2::CredentialsCacheError), + + #[error("groups v2 (zero-knowledge) error")] + GroupsV2Error, + + #[error(transparent)] + GroupsV2DecryptionError(#[from] GroupDecodingError), + + #[error(transparent)] + ZkGroupDeserializationFailure(#[from] ZkGroupDeserializationFailure), + + #[error("unsupported content")] + UnsupportedContent, + + #[error(transparent)] + ParseServiceAddress(#[from] ParseServiceAddressError), + + #[error("Not found.")] + NotFoundError, + + #[error("invalid device name")] + InvalidDeviceName, +} diff --git a/src/push_service/keys.rs b/src/push_service/keys.rs new file mode 100644 index 000000000..50894d1f2 --- /dev/null +++ b/src/push_service/keys.rs @@ -0,0 +1,182 @@ +use std::collections::HashMap; + +use libsignal_protocol::{IdentityKey, PreKeyBundle, SenderCertificate}; +use serde::Deserialize; + +use crate::{ + configuration::Endpoint, + pre_keys::{KyberPreKeyEntity, PreKeyState, SignedPreKeyEntity}, + push_service::PreKeyResponse, + sender::OutgoingPushMessage, + utils::serde_base64, + ServiceAddress, +}; + +use super::{ + HttpAuthOverride, PushService, SenderCertificateJson, ServiceError, + ServiceIdType, VerifyAccountResponse, +}; + +#[derive(Debug, Deserialize, Default)] +#[serde(rename_all = "camelCase")] +pub struct PreKeyStatus { + pub count: u32, + pub pq_count: u32, +} + +impl PushService { + pub async fn get_pre_key_status( + &mut self, + service_id_type: ServiceIdType, + ) -> Result { + self.get_json( + Endpoint::Service, + &format!("/v2/keys?identity={}", service_id_type), + &[], + HttpAuthOverride::NoOverride, + ) + .await + } + + pub async fn register_pre_keys( + &mut self, + service_id_type: ServiceIdType, + pre_key_state: PreKeyState, + ) -> Result<(), ServiceError> { + match self + .put_json( + Endpoint::Service, + &format!("/v2/keys?identity={}", service_id_type), + &[], + HttpAuthOverride::NoOverride, + pre_key_state, + ) + .await + { + Err(ServiceError::JsonDecodeError { .. }) => Ok(()), + r => r, + } + } + + pub async fn get_pre_key( + &mut self, + destination: &ServiceAddress, + device_id: u32, + ) -> Result { + let path = + format!("/v2/keys/{}/{}?pq=true", destination.uuid, device_id); + + let mut pre_key_response: PreKeyResponse = self + .get_json( + Endpoint::Service, + &path, + &[], + HttpAuthOverride::NoOverride, + ) + .await?; + assert!(!pre_key_response.devices.is_empty()); + + let identity = IdentityKey::decode(&pre_key_response.identity_key)?; + let device = pre_key_response.devices.remove(0); + Ok(device.into_bundle(identity)?) + } + + pub(crate) async fn get_pre_keys( + &mut self, + destination: &ServiceAddress, + device_id: u32, + ) -> Result, ServiceError> { + let path = if device_id == 1 { + format!("/v2/keys/{}/*?pq=true", destination.uuid) + } else { + format!("/v2/keys/{}/{}?pq=true", destination.uuid, device_id) + }; + let pre_key_response: PreKeyResponse = self + .get_json( + Endpoint::Service, + &path, + &[], + HttpAuthOverride::NoOverride, + ) + .await?; + let mut pre_keys = vec![]; + let identity = IdentityKey::decode(&pre_key_response.identity_key)?; + for device in pre_key_response.devices { + pre_keys.push(device.into_bundle(identity)?); + } + Ok(pre_keys) + } + + pub async fn get_sender_certificate( + &mut self, + ) -> Result { + let cert: SenderCertificateJson = self + .get_json( + Endpoint::Service, + "/v1/certificate/delivery", + &[], + HttpAuthOverride::NoOverride, + ) + .await?; + Ok(SenderCertificate::deserialize(&cert.certificate)?) + } + + pub async fn get_uuid_only_sender_certificate( + &mut self, + ) -> Result { + let cert: SenderCertificateJson = self + .get_json( + Endpoint::Service, + "/v1/certificate/delivery?includeE164=false", + &[], + HttpAuthOverride::NoOverride, + ) + .await?; + Ok(SenderCertificate::deserialize(&cert.certificate)?) + } + + pub async fn distribute_pni_keys( + &mut self, + pni_identity_key: &IdentityKey, + device_messages: Vec, + device_pni_signed_prekeys: HashMap, + device_pni_last_resort_kyber_prekeys: HashMap< + String, + KyberPreKeyEntity, + >, + pni_registration_ids: HashMap, + signature_valid_on_each_signed_pre_key: bool, + ) -> Result { + #[derive(serde::Serialize, Debug)] + #[serde(rename_all = "camelCase")] + struct PniKeyDistributionRequest { + #[serde(with = "serde_base64")] + pni_identity_key: Vec, + device_messages: Vec, + device_pni_signed_prekeys: HashMap, + #[serde(rename = "devicePniPqLastResortPrekeys")] + device_pni_last_resort_kyber_prekeys: + HashMap, + pni_registration_ids: HashMap, + signature_valid_on_each_signed_pre_key: bool, + } + + let res: VerifyAccountResponse = self + .put_json( + Endpoint::Service, + "/v2/accounts/phone_number_identity_key_distribution", + &[], + HttpAuthOverride::NoOverride, + PniKeyDistributionRequest { + pni_identity_key: pni_identity_key.serialize().into(), + device_messages, + device_pni_signed_prekeys, + device_pni_last_resort_kyber_prekeys, + pni_registration_ids, + signature_valid_on_each_signed_pre_key, + }, + ) + .await?; + Ok(res) + } +} diff --git a/src/push_service/linking.rs b/src/push_service/linking.rs new file mode 100644 index 000000000..5d9b9ded8 --- /dev/null +++ b/src/push_service/linking.rs @@ -0,0 +1,76 @@ +use serde::{Deserialize, Serialize}; +use uuid::Uuid; + +use crate::configuration::Endpoint; + +use super::{ + DeviceActivationRequest, HttpAuth, HttpAuthOverride, PushService, + ServiceError, +}; + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct LinkAccountAttributes { + pub fetches_messages: bool, + pub name: String, + pub registration_id: u32, + pub pni_registration_id: u32, + pub capabilities: LinkCapabilities, +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct LinkCapabilities { + pub delete_sync: bool, + pub versioned_expiration_timer: bool, +} + +// https://github.com/signalapp/Signal-Desktop/blob/1e57db6aa4786dcddc944349e4894333ac2ffc9e/ts/textsecure/WebAPI.ts#L1287 +impl Default for LinkCapabilities { + fn default() -> Self { + Self { + delete_sync: true, + versioned_expiration_timer: true, + } + } +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct LinkResponse { + #[serde(rename = "uuid")] + pub aci: Uuid, + pub pni: Uuid, + pub device_id: u32, +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct LinkRequest { + pub verification_code: String, + pub account_attributes: LinkAccountAttributes, + #[serde(flatten)] + pub device_activation_request: DeviceActivationRequest, +} + +impl PushService { + pub async fn link_device( + &mut self, + link_request: &LinkRequest, + http_auth: HttpAuth, + ) -> Result { + self.put_json( + Endpoint::Service, + "/v1/devices/link", + &[], + HttpAuthOverride::Identified(http_auth), + link_request, + ) + .await + } + + pub async fn unlink_device(&mut self, id: i64) -> Result<(), ServiceError> { + self.delete_json(Endpoint::Service, &format!("/v1/devices/{}", id), &[]) + .await + } +} diff --git a/libsignal-service-hyper/src/push_service.rs b/src/push_service/mod.rs similarity index 78% rename from libsignal-service-hyper/src/push_service.rs rename to src/push_service/mod.rs index 6a294825e..f839f185b 100644 --- a/libsignal-service-hyper/src/push_service.rs +++ b/src/push_service/mod.rs @@ -1,8 +1,15 @@ -use std::io; -use std::time::Duration; +use std::{io, time::Duration}; + +use crate::{ + configuration::{Endpoint, ServiceCredentials}, + pre_keys::{KyberPreKeyEntity, PreKeyEntity, SignedPreKeyEntity}, + prelude::ServiceConfiguration, + utils::serde_base64, + websocket::{tungstenite::TungsteniteWebSocket, SignalWebSocket}, +}; use bytes::{Buf, Bytes}; -use futures::{FutureExt, StreamExt, TryStreamExt}; +use derivative::Derivative; use headers::{Authorization, HeaderMapExt}; use http_body_util::{BodyExt, Full}; use hyper::{ @@ -16,24 +23,145 @@ use hyper_util::{ client::legacy::{connect::HttpConnector, Client}, rt::TokioExecutor, }; -use libsignal_service::{ - configuration::*, prelude::ProtobufMessage, push_service::*, - websocket::SignalWebSocket, MaybeSend, +use libsignal_protocol::{ + error::SignalProtocolError, + kem::{Key, Public}, + IdentityKey, PreKeyBundle, PublicKey, }; +use prost::Message as ProtobufMessage; use serde::{Deserialize, Serialize}; -use tokio_rustls::rustls::{self, ClientConfig}; -use tracing::{debug, debug_span}; -use tracing_futures::Instrument; +use tokio_rustls::rustls; +use tracing::{debug_span, Instrument}; + +pub const KEEPALIVE_TIMEOUT_SECONDS: Duration = Duration::from_secs(55); +pub const DEFAULT_DEVICE_ID: u32 = 1; + +mod account; +mod cdn; +mod error; +mod keys; +mod linking; +mod profile; +mod registration; +mod stickers; + +pub use account::*; +pub use cdn::*; +pub use error::*; +pub use keys::*; +pub use linking::*; +pub use profile::*; +pub use registration::*; + +#[derive(Debug, Serialize, Deserialize)] +pub struct ProofRequired { + pub token: String, + pub options: Vec, +} -use crate::websocket::TungsteniteWebSocket; +#[derive(Derivative, Clone, Serialize, Deserialize)] +#[derivative(Debug)] +pub struct HttpAuth { + pub username: String, + #[derivative(Debug = "ignore")] + pub password: String, +} -#[derive(Clone)] -pub struct HyperPushService { - cfg: ServiceConfiguration, - user_agent: String, - credentials: Option, - client: - Client>, Full>, +/// This type is used in registration lock handling. +/// It's identical with HttpAuth, but used to avoid type confusion. +#[derive(Derivative, Clone, Serialize, Deserialize)] +#[derivative(Debug)] +pub struct AuthCredentials { + pub username: String, + #[derivative(Debug = "ignore")] + pub password: String, +} + +#[derive(Debug, Clone)] +pub enum HttpAuthOverride { + NoOverride, + Unidentified, + Identified(HttpAuth), +} + +#[derive(Debug, Clone, Eq, PartialEq)] +pub enum AvatarWrite { + NewAvatar(C), + RetainAvatar, + NoAvatar, +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +struct SenderCertificateJson { + #[serde(with = "serde_base64")] + certificate: Vec, +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct PreKeyResponse { + #[serde(with = "serde_base64")] + pub identity_key: Vec, + pub devices: Vec, +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct PreKeyResponseItem { + pub device_id: u32, + pub registration_id: u32, + pub signed_pre_key: SignedPreKeyEntity, + pub pre_key: Option, + pub pq_pre_key: Option, +} + +impl PreKeyResponseItem { + pub(crate) fn into_bundle( + self, + identity: IdentityKey, + ) -> Result { + let b = PreKeyBundle::new( + self.registration_id, + self.device_id.into(), + self.pre_key + .map(|pk| -> Result<_, SignalProtocolError> { + Ok(( + pk.key_id.into(), + PublicKey::deserialize(&pk.public_key)?, + )) + }) + .transpose()?, + // pre_key: Option<(u32, PublicKey)>, + self.signed_pre_key.key_id.into(), + PublicKey::deserialize(&self.signed_pre_key.public_key)?, + self.signed_pre_key.signature, + identity, + )?; + + if let Some(pq_pk) = self.pq_pre_key { + Ok(b.with_kyber_pre_key( + pq_pk.key_id.into(), + Key::::deserialize(&pq_pk.public_key)?, + pq_pk.signature, + )) + } else { + Ok(b) + } + } +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct MismatchedDevices { + pub missing_devices: Vec, + pub extra_devices: Vec, +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct StaleDevices { + pub stale_devices: Vec, } #[derive(Debug)] @@ -42,7 +170,16 @@ struct RequestBody { content_type: String, } -impl HyperPushService { +#[derive(Clone)] +pub struct PushService { + cfg: ServiceConfiguration, + user_agent: String, + credentials: Option, + client: + Client>, Full>, +} + +impl PushService { pub fn new( cfg: impl Into, credentials: Option, @@ -74,7 +211,7 @@ impl HyperPushService { } } - fn tls_config(cfg: &ServiceConfiguration) -> ClientConfig { + fn tls_config(cfg: &ServiceConfiguration) -> rustls::ClientConfig { let mut cert_bytes = io::Cursor::new(&cfg.certificate_authority); let roots = rustls_pemfile::certs(&mut cert_bytes); @@ -286,14 +423,9 @@ impl HyperPushService { } } -#[cfg_attr(feature = "unsend-futures", async_trait::async_trait(?Send))] -#[cfg_attr(not(feature = "unsend-futures"), async_trait::async_trait)] -impl PushService for HyperPushService { - // This is in principle known at compile time, but long to write out. - type ByteStream = Box; - +impl PushService { #[tracing::instrument(skip(self))] - async fn get_json( + pub(crate) async fn get_json( &mut self, service: Endpoint, path: &str, @@ -342,7 +474,7 @@ impl PushService for HyperPushService { } #[tracing::instrument(skip(self, value))] - async fn put_json( + pub async fn put_json( &mut self, service: Endpoint, path: &str, @@ -352,7 +484,7 @@ impl PushService for HyperPushService { ) -> Result where for<'de> D: Deserialize<'de>, - S: MaybeSend + Serialize, + S: Send + Serialize, { let json = serde_json::to_vec(&value).map_err(|e| { ServiceError::JsonDecodeError { @@ -388,7 +520,7 @@ impl PushService for HyperPushService { ) -> Result where for<'de> D: Deserialize<'de>, - S: MaybeSend + Serialize, + S: Send + Serialize, { let json = serde_json::to_vec(&value).map_err(|e| { ServiceError::JsonDecodeError { @@ -424,7 +556,7 @@ impl PushService for HyperPushService { ) -> Result where for<'de> D: Deserialize<'de>, - S: MaybeSend + Serialize, + S: Send + Serialize, { let json = serde_json::to_vec(&value).map_err(|e| { ServiceError::JsonDecodeError { @@ -458,7 +590,7 @@ impl PushService for HyperPushService { credentials_override: HttpAuthOverride, ) -> Result where - T: Default + libsignal_service::prelude::ProtobufMessage, + T: Default + ProtobufMessage, { let mut response = self .request( @@ -483,8 +615,8 @@ impl PushService for HyperPushService { value: S, ) -> Result where - D: Default + libsignal_service::prelude::ProtobufMessage, - S: Sized + libsignal_service::prelude::ProtobufMessage, + D: Default + ProtobufMessage, + S: Sized + ProtobufMessage, { let protobuf = value.encode_to_vec(); @@ -505,106 +637,7 @@ impl PushService for HyperPushService { Self::protobuf(&mut response).await } - #[tracing::instrument(skip(self))] - async fn get_from_cdn( - &mut self, - cdn_id: u32, - path: &str, - ) -> Result { - let response = self - .request( - Method::GET, - Endpoint::Cdn(cdn_id), - path, - &[], - HttpAuthOverride::Unidentified, // CDN requests are always without authentication - None, - ) - .await?; - - Ok(Box::new( - response - .into_body() - .into_data_stream() - .map_err(|e| io::Error::new(io::ErrorKind::Other, e)) - .into_async_read(), - )) - } - - #[tracing::instrument(skip(self, value, file), fields(file = file.as_ref().map(|_| "")))] - async fn post_to_cdn0<'s, C>( - &mut self, - path: &str, - value: &[(&str, &str)], - file: Option<(&str, &'s mut C)>, - ) -> Result<(), ServiceError> - where - C: io::Read + Send + 's, - { - let mut form = mpart_async::client::MultipartRequest::default(); - - // mpart-async has a peculiar ordering of the form items, - // and Amazon S3 expects them in a very specific order (i.e., the file contents should - // go last. - // - // mpart-async uses a VecDeque internally for ordering the fields in the order given. - // - // https://github.com/cetra3/mpart-async/issues/16 - - for &(k, v) in value { - form.add_field(k, v); - } - - if let Some((filename, file)) = file { - // XXX Actix doesn't cope with none-'static lifetimes - // https://docs.rs/actix-web/3.2.0/actix_web/body/enum.Body.html - let mut buf = Vec::new(); - file.read_to_end(&mut buf) - .expect("infallible Read instance"); - form.add_stream( - "file", - filename, - "application/octet-stream", - futures::future::ok::<_, ()>(Bytes::from(buf)).into_stream(), - ); - } - - let content_type = - format!("multipart/form-data; boundary={}", form.get_boundary()); - - // XXX Amazon S3 needs the Content-Length, but we don't know it without depleting the whole - // stream. Sadly, Content-Length != contents.len(), but should include the whole form. - let mut body_contents = vec![]; - while let Some(b) = form.next().await { - // Unwrap, because no error type was used above - body_contents.extend(b.unwrap()); - } - tracing::trace!( - "Sending PUT with Content-Type={} and length {}", - content_type, - body_contents.len() - ); - - let response = self - .request( - Method::POST, - Endpoint::Cdn(0), - path, - &[], - HttpAuthOverride::NoOverride, - Some(RequestBody { - contents: body_contents, - content_type, - }), - ) - .await?; - - debug!("HyperPushService::PUT response: {:?}", response); - - Ok(()) - } - - async fn ws( + pub async fn ws( &mut self, path: &str, keepalive_path: &str, @@ -624,25 +657,35 @@ impl PushService for HyperPushService { let (ws, task) = SignalWebSocket::from_socket(ws, stream, keepalive_path.to_owned()); let task = task.instrument(span); - #[cfg(feature = "unsend-futures")] - tokio::task::spawn_local(task); - #[cfg(not(feature = "unsend-futures"))] tokio::task::spawn(task); Ok(ws) } + + pub(crate) async fn get_group( + &mut self, + credentials: HttpAuth, + ) -> Result { + self.get_protobuf( + Endpoint::Storage, + "/v1/groups/", + &[], + HttpAuthOverride::Identified(credentials), + ) + .await + } } #[cfg(test)] mod tests { + use crate::configuration::SignalServers; use bytes::{Buf, Bytes}; - use libsignal_service::configuration::SignalServers; #[test] fn create_clients() { let configs = &[SignalServers::Staging, SignalServers::Production]; for cfg in configs { - let _ = super::HyperPushService::new( + let _ = super::PushService::new( cfg, None, "libsignal-service test".to_string(), diff --git a/src/push_service/profile.rs b/src/push_service/profile.rs new file mode 100644 index 000000000..0a444fef6 --- /dev/null +++ b/src/push_service/profile.rs @@ -0,0 +1,202 @@ +use serde::{Deserialize, Serialize}; +use zkgroup::profiles::{ProfileKeyCommitment, ProfileKeyVersion}; + +use crate::{ + configuration::Endpoint, + content::ServiceError, + profile_cipher::ProfileCipherError, + push_service::{AvatarWrite, HttpAuthOverride}, + utils::{serde_base64, serde_optional_base64}, + Profile, ServiceAddress, +}; + +use super::{DeviceCapabilities, PushService}; + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SignalServiceProfile { + #[serde(default, with = "serde_optional_base64")] + pub identity_key: Option>, + #[serde(default, with = "serde_optional_base64")] + pub name: Option>, + #[serde(default, with = "serde_optional_base64")] + pub about: Option>, + #[serde(default, with = "serde_optional_base64")] + pub about_emoji: Option>, + + // TODO: not sure whether this is via optional_base64 + // #[serde(default, with = "serde_optional_base64")] + // pub payment_address: Option>, + pub avatar: Option, + pub unidentified_access: Option, + + #[serde(default)] + pub unrestricted_unidentified_access: bool, + + pub capabilities: DeviceCapabilities, +} + +impl SignalServiceProfile { + pub fn decrypt( + &self, + profile_cipher: crate::profile_cipher::ProfileCipher, + ) -> Result { + // Profile decryption + let name = self + .name + .as_ref() + .map(|data| profile_cipher.decrypt_name(data)) + .transpose()? + .flatten(); + let about = self + .about + .as_ref() + .map(|data| profile_cipher.decrypt_about(data)) + .transpose()?; + let about_emoji = self + .about_emoji + .as_ref() + .map(|data| profile_cipher.decrypt_emoji(data)) + .transpose()?; + + Ok(Profile { + name, + about, + about_emoji, + avatar: self.avatar.clone(), + }) + } +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +struct SignalServiceProfileWrite<'s> { + /// Hex-encoded + version: &'s str, + #[serde(with = "serde_base64")] + name: &'s [u8], + #[serde(with = "serde_base64")] + about: &'s [u8], + #[serde(with = "serde_base64")] + about_emoji: &'s [u8], + avatar: bool, + same_avatar: bool, + #[serde(with = "serde_base64")] + commitment: &'s [u8], +} + +impl PushService { + pub async fn retrieve_profile_by_id( + &mut self, + address: ServiceAddress, + profile_key: Option, + ) -> Result { + let endpoint = if let Some(key) = profile_key { + let version = bincode::serialize(&key.get_profile_key_version( + address.aci().expect("profile by ACI ProtocolAddress"), + ))?; + let version = std::str::from_utf8(&version) + .expect("hex encoded profile key version"); + format!("/v1/profile/{}/{}", address.uuid, version) + } else { + format!("/v1/profile/{}", address.uuid) + }; + // TODO: set locale to en_US + self.get_json( + Endpoint::Service, + &endpoint, + &[], + HttpAuthOverride::NoOverride, + ) + .await + } + + pub async fn retrieve_profile_avatar( + &mut self, + path: &str, + ) -> Result { + self.get_from_cdn(0, path).await + } + + pub async fn retrieve_groups_v2_profile_avatar( + &mut self, + path: &str, + ) -> Result { + self.get_from_cdn(0, path).await + } + + /// Writes a profile and returns the avatar URL, if one was provided. + /// + /// The name, about and emoji fields are encrypted with an [`ProfileCipher`][struct@crate::profile_cipher::ProfileCipher]. + /// See [`AccountManager`][struct@crate::AccountManager] for a convenience method. + /// + /// Java equivalent: `writeProfile` + pub async fn write_profile<'s, C, S>( + &mut self, + version: &ProfileKeyVersion, + name: &[u8], + about: &[u8], + emoji: &[u8], + commitment: &ProfileKeyCommitment, + avatar: AvatarWrite<&mut C>, + ) -> Result, ServiceError> + where + C: std::io::Read + Send + 's, + S: AsRef, + { + // Bincode is transparent and will return a hex-encoded string. + let version = bincode::serialize(version)?; + let version = std::str::from_utf8(&version) + .expect("profile_key_version is hex encoded string"); + let commitment = bincode::serialize(commitment)?; + + let command = SignalServiceProfileWrite { + version, + name, + about, + about_emoji: emoji, + avatar: !matches!(avatar, AvatarWrite::NoAvatar), + same_avatar: matches!(avatar, AvatarWrite::RetainAvatar), + commitment: &commitment, + }; + + // XXX this should be a struct; cfr ProfileAvatarUploadAttributes + let response: Result = self + .put_json( + Endpoint::Service, + "/v1/profile", + &[], + HttpAuthOverride::NoOverride, + command, + ) + .await; + match (response, avatar) { + (Ok(_url), AvatarWrite::NewAvatar(_avatar)) => { + // FIXME + unreachable!("Uploading avatar unimplemented"); + }, + // FIXME cleanup when #54883 is stable and MSRV: + // or-patterns syntax is experimental + // see issue #54883 for more information + ( + Err(ServiceError::JsonDecodeError { .. }), + AvatarWrite::RetainAvatar, + ) + | ( + Err(ServiceError::JsonDecodeError { .. }), + AvatarWrite::NoAvatar, + ) => { + // OWS sends an empty string when there's no attachment + Ok(None) + }, + (Err(e), _) => Err(e), + (Ok(_resp), AvatarWrite::RetainAvatar) + | (Ok(_resp), AvatarWrite::NoAvatar) => { + tracing::warn!( + "No avatar supplied but got avatar upload URL. Ignoring" + ); + Ok(None) + }, + } + } +} diff --git a/src/push_service/registration.rs b/src/push_service/registration.rs new file mode 100644 index 000000000..6d353b962 --- /dev/null +++ b/src/push_service/registration.rs @@ -0,0 +1,309 @@ +use libsignal_protocol::IdentityKey; +use serde::{Deserialize, Serialize}; +use uuid::Uuid; + +use super::{AccountAttributes, AuthCredentials, PushService, ServiceError}; +use crate::{ + configuration::Endpoint, + pre_keys::{KyberPreKeyEntity, SignedPreKeyEntity}, + push_service::HttpAuthOverride, + utils::serde_base64, +}; + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct RegistrationLockFailure { + pub length: Option, + pub time_remaining: Option, + #[serde(rename = "backup_credentials")] + pub svr1_credentials: Option, + pub svr2_credentials: Option, +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct VerifyAccountResponse { + #[serde(rename = "uuid")] + pub aci: Uuid, + pub pni: Uuid, + pub storage_capable: bool, + #[serde(default)] + pub number: Option, +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum VerificationTransport { + Sms, + Voice, +} + +impl VerificationTransport { + pub fn as_str(&self) -> &str { + match self { + Self::Sms => "sms", + Self::Voice => "voice", + } + } +} + +#[derive(Clone, Debug)] +pub enum RegistrationMethod<'a> { + SessionId(&'a str), + RecoveryPassword(&'a str), +} + +impl<'a> RegistrationMethod<'a> { + pub fn session_id(&'a self) -> Option<&'a str> { + match self { + Self::SessionId(x) => Some(x), + _ => None, + } + } + + pub fn recovery_password(&'a self) -> Option<&'a str> { + match self { + Self::RecoveryPassword(x) => Some(x), + _ => None, + } + } +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct DeviceActivationRequest { + pub aci_signed_pre_key: SignedPreKeyEntity, + pub pni_signed_pre_key: SignedPreKeyEntity, + pub aci_pq_last_resort_pre_key: KyberPreKeyEntity, + pub pni_pq_last_resort_pre_key: KyberPreKeyEntity, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct RecaptchaAttributes { + pub r#type: String, + pub token: String, + pub captcha: String, +} + +#[derive(Debug, Clone, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct RegistrationSessionMetadataResponse { + pub id: String, + #[serde(default)] + pub next_sms: Option, + #[serde(default)] + pub next_call: Option, + #[serde(default)] + pub next_verification_attempt: Option, + pub allowed_to_request_code: bool, + #[serde(default)] + pub requested_information: Vec, + pub verified: bool, +} + +impl RegistrationSessionMetadataResponse { + pub fn push_challenge_required(&self) -> bool { + // .contains() requires &String ... + self.requested_information + .iter() + .any(|x| x.as_str() == "pushChallenge") + } + + pub fn captcha_required(&self) -> bool { + // .contains() requires &String ... + self.requested_information + .iter() + .any(|x| x.as_str() == "captcha") + } +} + +impl PushService { + pub async fn submit_registration_request<'a>( + &mut self, + registration_method: RegistrationMethod<'a>, + account_attributes: AccountAttributes, + skip_device_transfer: bool, + aci_identity_key: &IdentityKey, + pni_identity_key: &IdentityKey, + device_activation_request: DeviceActivationRequest, + ) -> Result { + #[derive(serde::Serialize, Debug)] + #[serde(rename_all = "camelCase")] + struct RegistrationSessionRequestBody<'a> { + // Unhandled response 422 with body: + // {"errors":["deviceActivationRequest.pniSignedPreKey must not be + // null","deviceActivationRequest.pniPqLastResortPreKey must not be + // null","everySignedKeyValid must be true","aciIdentityKey must not be + // null","pniIdentityKey must not be null","deviceActivationRequest.aciSignedPreKey + // must not be null","deviceActivationRequest.aciPqLastResortPreKey must not be null"]} + session_id: Option<&'a str>, + recovery_password: Option<&'a str>, + account_attributes: AccountAttributes, + skip_device_transfer: bool, + every_signed_key_valid: bool, + #[serde(default, with = "serde_base64")] + pni_identity_key: Vec, + #[serde(default, with = "serde_base64")] + aci_identity_key: Vec, + #[serde(flatten)] + device_activation_request: DeviceActivationRequest, + } + + let req = RegistrationSessionRequestBody { + session_id: registration_method.session_id(), + recovery_password: registration_method.recovery_password(), + account_attributes, + skip_device_transfer, + aci_identity_key: aci_identity_key.serialize().into(), + pni_identity_key: pni_identity_key.serialize().into(), + device_activation_request, + every_signed_key_valid: true, + }; + + let res: VerifyAccountResponse = self + .post_json( + Endpoint::Service, + "/v1/registration", + &[], + HttpAuthOverride::NoOverride, + req, + ) + .await?; + Ok(res) + } + + // Equivalent of Java's + // RegistrationSessionMetadataResponse createVerificationSession(@Nullable String pushToken, @Nullable String mcc, @Nullable String mnc) + pub async fn create_verification_session<'a>( + &mut self, + number: &'a str, + push_token: Option<&'a str>, + mcc: Option<&'a str>, + mnc: Option<&'a str>, + ) -> Result { + #[derive(serde::Serialize, Debug)] + #[serde(rename_all = "camelCase")] + struct VerificationSessionMetadataRequestBody<'a> { + number: &'a str, + push_token: Option<&'a str>, + mcc: Option<&'a str>, + mnc: Option<&'a str>, + push_token_type: Option<&'a str>, + } + + let req = VerificationSessionMetadataRequestBody { + number, + push_token_type: push_token.as_ref().map(|_| "fcm"), + push_token, + mcc, + mnc, + }; + + let res: RegistrationSessionMetadataResponse = self + .post_json( + Endpoint::Service, + "/v1/verification/session", + &[], + HttpAuthOverride::Unidentified, + req, + ) + .await?; + Ok(res) + } + + pub async fn patch_verification_session<'a>( + &mut self, + session_id: &'a str, + push_token: Option<&'a str>, + mcc: Option<&'a str>, + mnc: Option<&'a str>, + captcha: Option<&'a str>, + push_challenge: Option<&'a str>, + ) -> Result { + #[derive(serde::Serialize, Debug)] + #[serde(rename_all = "camelCase")] + struct UpdateVerificationSessionRequestBody<'a> { + captcha: Option<&'a str>, + push_token: Option<&'a str>, + push_challenge: Option<&'a str>, + mcc: Option<&'a str>, + mnc: Option<&'a str>, + push_token_type: Option<&'a str>, + } + + let req = UpdateVerificationSessionRequestBody { + captcha, + push_token_type: push_token.as_ref().map(|_| "fcm"), + push_token, + mcc, + mnc, + push_challenge, + }; + + let res: RegistrationSessionMetadataResponse = self + .patch_json( + Endpoint::Service, + &format!("/v1/verification/session/{}", session_id), + &[], + HttpAuthOverride::Unidentified, + req, + ) + .await?; + Ok(res) + } + + // Equivalent of Java's + // RegistrationSessionMetadataResponse requestVerificationCode(String sessionId, Locale locale, boolean androidSmsRetriever, VerificationCodeTransport transport) + /// Request a verification code. + /// + /// Signal requires a client type, and they use these three strings internally: + /// - "android-2021-03" + /// - "android" + /// - "ios" + /// + /// "android-2021-03" allegedly implies FCM support, whereas the other strings don't. In + /// principle, they will consider any string as "unknown", so other strings may work too. + pub async fn request_verification_code( + &mut self, + session_id: &str, + client: &str, + // XXX: We currently don't support this, because we need to set some headers in the + // post_json() call + // locale: Option, + transport: VerificationTransport, + ) -> Result { + let mut req = std::collections::HashMap::new(); + req.insert("transport", transport.as_str()); + req.insert("client", client); + + let res: RegistrationSessionMetadataResponse = self + .post_json( + Endpoint::Service, + &format!("/v1/verification/session/{}/code", session_id), + &[], + HttpAuthOverride::Unidentified, + req, + ) + .await?; + Ok(res) + } + + pub async fn submit_verification_code( + &mut self, + session_id: &str, + verification_code: &str, + ) -> Result { + let mut req = std::collections::HashMap::new(); + req.insert("code", verification_code); + + let res: RegistrationSessionMetadataResponse = self + .put_json( + Endpoint::Service, + &format!("/v1/verification/session/{}/code", session_id), + &[], + HttpAuthOverride::Unidentified, + req, + ) + .await?; + Ok(res) + } +} diff --git a/src/push_service/stickers.rs b/src/push_service/stickers.rs new file mode 100644 index 000000000..0a75b3a2c --- /dev/null +++ b/src/push_service/stickers.rs @@ -0,0 +1,20 @@ +use super::{PushService, ServiceError}; + +impl PushService { + pub async fn get_sticker_pack_manifest( + &mut self, + id: &str, + ) -> Result { + let path = format!("/stickers/{}/manifest.proto", id); + self.get_from_cdn(0, &path).await + } + + pub async fn get_sticker( + &mut self, + pack_id: &str, + sticker_id: u32, + ) -> Result { + let path = format!("/stickers/{}/full/{}", pack_id, sticker_id); + self.get_from_cdn(0, &path).await + } +} diff --git a/libsignal-service/src/receiver.rs b/src/receiver.rs similarity index 82% rename from libsignal-service/src/receiver.rs rename to src/receiver.rs index 5cb121256..ba69320f0 100644 --- a/libsignal-service/src/receiver.rs +++ b/src/receiver.rs @@ -3,7 +3,6 @@ use bytes::{Buf, Bytes}; use crate::{ attachment_cipher::decrypt_in_place, configuration::ServiceCredentials, - envelope::Envelope, messagepipe::MessagePipe, models::{Contact, ParseContactError}, push_service::*, @@ -11,35 +10,17 @@ use crate::{ /// Equivalent of Java's `SignalServiceMessageReceiver`. #[derive(Clone)] -pub struct MessageReceiver { - service: Service, +pub struct MessageReceiver { + service: PushService, } -impl MessageReceiver { +impl MessageReceiver { // TODO: to avoid providing the wrong service/wrong credentials // change it like LinkingManager or ProvisioningManager - pub fn new(service: Service) -> Self { + pub fn new(service: PushService) -> Self { MessageReceiver { service } } - /// One-off method to receive all pending messages. - /// - /// Equivalent with Java's `SignalServiceMessageReceiver::retrieveMessages`. - /// - /// For streaming messages, use a `MessagePipe` through - /// [`MessageReceiver::create_message_pipe()`]. - pub async fn retrieve_messages( - &mut self, - allow_stories: bool, - ) -> Result, ServiceError> { - let entities = self.service.get_messages(allow_stories).await?; - let entities = entities - .into_iter() - .map(Envelope::try_from) - .collect::>()?; - Ok(entities) - } - pub async fn create_message_pipe( &mut self, credentials: ServiceCredentials, diff --git a/libsignal-service/src/sender.rs b/src/sender.rs similarity index 99% rename from libsignal-service/src/sender.rs rename to src/sender.rs index 99e3d1273..ae8a862e1 100644 --- a/libsignal-service/src/sender.rs +++ b/src/sender.rs @@ -84,10 +84,10 @@ pub struct AttachmentSpec { /// Equivalent of Java's `SignalServiceMessageSender`. #[derive(Clone)] -pub struct MessageSender { +pub struct MessageSender { identified_ws: SignalWebSocket, unidentified_ws: SignalWebSocket, - service: Service, + service: PushService, cipher: ServiceCipher, csprng: R, protocol_store: S, @@ -137,9 +137,8 @@ pub enum ThreadIdentifier { Group(GroupV2Id), } -impl MessageSender +impl MessageSender where - Service: PushService, S: ProtocolStore + SenderKeyStore + SessionStoreExt + Sync + Clone, R: Rng + CryptoRng, { @@ -147,7 +146,7 @@ where pub fn new( identified_ws: SignalWebSocket, unidentified_ws: SignalWebSocket, - service: Service, + service: PushService, cipher: ServiceCipher, csprng: R, protocol_store: S, @@ -219,7 +218,7 @@ where // Request upload attributes let attrs = self - .identified_ws + .service .get_attachment_v2_upload_attributes() .instrument(tracing::trace_span!("requesting upload attributes")) .await?; diff --git a/libsignal-service/src/service_address.rs b/src/service_address.rs similarity index 100% rename from libsignal-service/src/service_address.rs rename to src/service_address.rs diff --git a/libsignal-service/src/session_store.rs b/src/session_store.rs similarity index 100% rename from libsignal-service/src/session_store.rs rename to src/session_store.rs diff --git a/libsignal-service/src/sticker_cipher.rs b/src/sticker_cipher.rs similarity index 100% rename from libsignal-service/src/sticker_cipher.rs rename to src/sticker_cipher.rs diff --git a/libsignal-service/src/timestamp.rs b/src/timestamp.rs similarity index 100% rename from libsignal-service/src/timestamp.rs rename to src/timestamp.rs diff --git a/libsignal-service/src/unidentified_access.rs b/src/unidentified_access.rs similarity index 100% rename from libsignal-service/src/unidentified_access.rs rename to src/unidentified_access.rs diff --git a/libsignal-service/src/utils.rs b/src/utils.rs similarity index 100% rename from libsignal-service/src/utils.rs rename to src/utils.rs diff --git a/libsignal-service/src/websocket.rs b/src/websocket/mod.rs similarity index 97% rename from libsignal-service/src/websocket.rs rename to src/websocket/mod.rs index a998cfbcd..4736edc45 100644 --- a/libsignal-service/src/websocket.rs +++ b/src/websocket/mod.rs @@ -19,8 +19,8 @@ use crate::proto::{ }; use crate::push_service::{MismatchedDevices, ServiceError}; -mod attachment_service; mod sender; +pub(crate) mod tungstenite; type RequestStreamItem = ( WebSocketRequestMessage, @@ -486,21 +486,6 @@ impl SignalWebSocket { } } - pub(crate) async fn get_json( - &mut self, - path: &str, - ) -> Result - where - for<'de> T: Deserialize<'de>, - { - let request = WebSocketRequestMessage { - path: Some(path.into()), - verb: Some("GET".into()), - ..Default::default() - }; - self.request_json(request).await - } - pub(crate) async fn put_json( &mut self, path: &str, diff --git a/libsignal-service/src/websocket/sender.rs b/src/websocket/sender.rs similarity index 100% rename from libsignal-service/src/websocket/sender.rs rename to src/websocket/sender.rs diff --git a/libsignal-service-hyper/src/websocket.rs b/src/websocket/tungstenite.rs similarity index 80% rename from libsignal-service-hyper/src/websocket.rs rename to src/websocket/tungstenite.rs index 89adf3ff9..ae40caf09 100644 --- a/libsignal-service-hyper/src/websocket.rs +++ b/src/websocket/tungstenite.rs @@ -14,23 +14,16 @@ use tokio::time::Instant; use tokio_rustls::rustls; use url::Url; -use libsignal_service::{ +use crate::{ configuration::ServiceCredentials, - messagepipe::*, push_service::{self, ServiceError}, - MaybeSend, }; -// This weird one-time trait is required because MaybeSend, unlike Send, is not -// an auto trait. Only auto traits can be used as additional traits in a trait object. -trait MaybeSendSink: Sink + MaybeSend {} -impl MaybeSendSink for T where - T: Sink + MaybeSend -{ -} +use crate::messagepipe::{WebSocketService, WebSocketStreamItem}; pub struct TungsteniteWebSocket { - socket_sink: Box, + socket_sink: + Box + Send + Unpin>, } #[derive(thiserror::Error, Debug)] @@ -57,33 +50,6 @@ impl From for ServiceError { } } -// impl From for ServiceError { -// fn from(e: AwcWebSocketError) -> ServiceError { -// match e { -// AwcWebSocketError::ConnectionError(e) => match e { -// WsClientError::InvalidResponseStatus(s) => match s { -// StatusCode::FORBIDDEN => ServiceError::Unauthorized, -// s => ServiceError::WsError { -// reason: format!("HTTP status {}", s), -// }, -// }, -// e => ServiceError::WsError { -// reason: e.to_string(), -// }, -// }, -// } -// } -// } - -// impl From for AwcWebSocketError { -// fn from(e: WsProtocolError) -> AwcWebSocketError { -// todo!("error conversion {:?}", e) -// // return Some(Err(ServiceError::WsError { -// // reason: e.to_string(), -// // })); -// } -// } - // Process the WebSocket, until it times out. async fn process( socket_stream: S, @@ -222,8 +188,7 @@ impl TungsteniteWebSocket { } } -#[cfg_attr(feature = "unsend-futures", async_trait::async_trait(?Send))] -#[cfg_attr(not(feature = "unsend-futures"), async_trait::async_trait)] +#[async_trait::async_trait] impl WebSocketService for TungsteniteWebSocket { type Stream = Receiver;