diff --git a/libsignal-service-actix/examples/link.rs b/libsignal-service-actix/examples/link.rs index 9a7c27961..0dd53f579 100644 --- a/libsignal-service-actix/examples/link.rs +++ b/libsignal-service-actix/examples/link.rs @@ -7,10 +7,13 @@ use qrcode::QrCode; use rand::{distributions::Alphanumeric, Rng, RngCore}; use structopt::StructOpt; -use libsignal_protocol::{crypto::DefaultCrypto, Context}; -use libsignal_service_actix::provisioning::{ - provision_secondary_device, SecondaryDeviceProvisioning, +use libsignal_protocol::Context; + +use libsignal_service::{ + provisioning::LinkingManager, provisioning::SecondaryDeviceProvisioning, + USER_AGENT, }; +use libsignal_service_actix::prelude::AwcPushService; #[derive(Debug, StructOpt)] struct Args { @@ -34,7 +37,7 @@ async fn main() -> Result<(), Error> { // generate a random 16 bytes password let mut rng = rand::rngs::OsRng::default(); let password: Vec = rng.sample_iter(&Alphanumeric).take(24).collect(); - let password = std::str::from_utf8(&password)?; + let password = String::from_utf8(password)?; // generate a 52 bytes signaling key let mut signaling_key = [0u8; 52]; @@ -44,17 +47,17 @@ async fn main() -> Result<(), Error> { base64::encode(&signaling_key.to_vec()) ); - let signal_context = Context::new(DefaultCrypto::default()).unwrap(); - let service_configuration = args.servers.into(); + let signal_context = Context::default(); + + let mut provision_manager: LinkingManager = + LinkingManager::new(args.servers, USER_AGENT.into(), password); let (tx, mut rx) = channel(1); let (fut1, fut2) = future::join( - provision_secondary_device( + provision_manager.provision_secondary_device( &signal_context, - &service_configuration, - &signaling_key, - password, + signaling_key, &args.device_name, tx, ), diff --git a/libsignal-service-actix/examples/registering.rs b/libsignal-service-actix/examples/registering.rs index 57fec9fb5..fb4d03a35 100644 --- a/libsignal-service-actix/examples/registering.rs +++ b/libsignal-service-actix/examples/registering.rs @@ -18,46 +18,32 @@ //! ``` use failure::Error; -use libsignal_protocol::Context; -use libsignal_service::{configuration::*, AccountManager}; -use libsignal_service_actix::push_service::AwcPushService; -use std::io; +use libsignal_service::{ + configuration::*, provisioning::ProvisioningManager, USER_AGENT, +}; +use libsignal_service_actix::prelude::AwcPushService; use structopt::StructOpt; #[actix_rt::main] async fn main() -> Result<(), Error> { + env_logger::init(); + let args = Args::from_args(); // Only used with MessageSender and MessageReceiver - let password = args.get_password()?; - - let config: ServiceConfiguration = SignalServers::Staging.into(); - - let mut signaling_key = [0u8; 52]; - base64::decode_config_slice( - args.signaling_key, - base64::STANDARD, - &mut signaling_key, - ) - .unwrap(); - let credentials = ServiceCredentials { - uuid: None, - phonenumber: args.username.clone(), - password: Some(password), - signaling_key: Some(signaling_key), - device_id: None, - }; + // let password = args.get_password()?; - let signal_context = Context::default(); + let mut provision_manager: ProvisioningManager = + ProvisioningManager::new( + args.servers, + USER_AGENT.into(), + args.username, + args.password.unwrap(), + ); - let push_service = - AwcPushService::new(config, Some(credentials), &args.user_agent); - - let mut account_manager = - AccountManager::new(signal_context, push_service, None); - account_manager + provision_manager // You probably want to generate a reCAPTCHA though! - .request_sms_verification_code(args.username, None, None) + .request_sms_verification_code(None, None) .await?; Ok(()) @@ -67,8 +53,8 @@ async fn main() -> Result<(), Error> { pub struct Args { #[structopt( short = "s", - long = "server", - help = "The server to connect to", + long = "servers", + help = "The servers to connect to", default_value = "staging" )] pub servers: SignalServers, @@ -85,28 +71,4 @@ pub struct Args { help = "The password to use. Read from stdin if not provided" )] pub password: Option, - #[structopt( - long = "user-agent", - help = "The user agent to use when contacting servers", - default_value = "libsignal_service::USER_AGENT" - )] - pub user_agent: String, - #[structopt( - long = "signaling-key", - help = "The key used to encrypt and authenticate messages in transit, base64 encoded." - )] - pub signaling_key: String, -} - -impl Args { - pub fn get_password(&self) -> Result { - if let Some(ref pw) = self.password { - return Ok(pw.clone()); - } - - let mut line = String::new(); - io::stdin().read_line(&mut line)?; - - Ok(line.trim().to_string()) - } } diff --git a/libsignal-service-actix/src/lib.rs b/libsignal-service-actix/src/lib.rs index 8cce01527..d91a8aa65 100644 --- a/libsignal-service-actix/src/lib.rs +++ b/libsignal-service-actix/src/lib.rs @@ -6,5 +6,3 @@ pub mod websocket; pub mod prelude { pub use crate::push_service::*; } - -pub mod provisioning; diff --git a/libsignal-service-actix/src/provisioning.rs b/libsignal-service-actix/src/provisioning.rs deleted file mode 100644 index 681164546..000000000 --- a/libsignal-service-actix/src/provisioning.rs +++ /dev/null @@ -1,161 +0,0 @@ -use futures::{pin_mut, prelude::Sink, SinkExt, StreamExt}; -use std::fmt::Debug; -use url::Url; - -use crate::push_service::AwcPushService; -use libsignal_protocol::{ - generate_registration_id, - keys::{PrivateKey, PublicKey}, - Context, -}; -use libsignal_service::{ - configuration::ServiceConfiguration, - messagepipe::ServiceCredentials, - prelude::PushService, - provisioning::{ProvisioningError, ProvisioningPipe, ProvisioningStep}, - push_service::{ConfirmDeviceMessage, DeviceId}, - USER_AGENT, -}; - -#[derive(Debug)] -pub enum SecondaryDeviceProvisioning { - Url(Url), - NewDeviceRegistration { - phone_number: phonenumber::PhoneNumber, - device_id: DeviceId, - registration_id: u32, - uuid: String, - private_key: PrivateKey, - public_key: PublicKey, - profile_key: Vec, - }, -} - -pub async fn provision_secondary_device( - ctx: &Context, - service_configuration: &ServiceConfiguration, - signaling_key: &[u8; 52], - password: &str, - device_name: &str, - mut tx: S, -) -> Result<(), ProvisioningError> -where - S: Sink + Unpin, - >::Error: Debug, -{ - assert_eq!( - password.len(), - 24, - "the password needs to be a 24 characters ASCII string" - ); - - let mut push_service = - AwcPushService::new(service_configuration.clone(), None, USER_AGENT); - - let (ws, stream) = - push_service.ws("/v1/websocket/provisioning/", None).await?; - - let registration_id = generate_registration_id(&ctx, 0)?; - - let provisioning_pipe = ProvisioningPipe::from_socket(ws, stream, &ctx)?; - let provision_stream = provisioning_pipe.stream(); - pin_mut!(provision_stream); - while let Some(step) = provision_stream.next().await { - match step { - Ok(ProvisioningStep::Url(url)) => { - tx.send(SecondaryDeviceProvisioning::Url(url)) - .await - .expect("failed to send provisioning Url in channel"); - } - Ok(ProvisioningStep::Message(message)) => { - let uuid = - message.uuid.ok_or(ProvisioningError::InvalidData { - reason: "missing client UUID".into(), - })?; - - let public_key = PublicKey::decode_point( - &ctx, - &message.identity_key_public.ok_or( - ProvisioningError::InvalidData { - reason: "missing public key".into(), - }, - )?, - )?; - - let private_key = PrivateKey::decode_point( - &ctx, - &message.identity_key_private.ok_or( - ProvisioningError::InvalidData { - reason: "missing public key".into(), - }, - )?, - )?; - - let profile_key = message.profile_key.ok_or( - ProvisioningError::InvalidData { - reason: "missing profile key".into(), - }, - )?; - - let phone_number = - message.number.ok_or(ProvisioningError::InvalidData { - reason: "missing phone number".into(), - })?; - let phone_number = phonenumber::parse(None, phone_number) - .map_err(|e| ProvisioningError::InvalidData { - reason: format!("invalid phone number ({})", e), - })?; - - // we need to authenticate with the phone number - // to confirm the new device - // TODO: we should now be able to override credentials? - let mut push_service = AwcPushService::new( - service_configuration.clone(), - Some(ServiceCredentials { - phonenumber: phone_number.clone(), - uuid: None, - password: Some(password.to_string()), - signaling_key: Some(*signaling_key), - device_id: None, - }), - USER_AGENT, - ); - - let device_id = push_service - .confirm_device( - message - .provisioning_code - .ok_or(ProvisioningError::InvalidData { - reason: "no provisioning confirmation code" - .into(), - })? - .parse() - .unwrap(), - ConfirmDeviceMessage { - signaling_key: signaling_key.to_vec(), - supports_sms: false, - fetches_messages: true, - registration_id, - name: device_name.to_string(), - }, - ) - .await?; - - tx.send(SecondaryDeviceProvisioning::NewDeviceRegistration { - phone_number, - device_id, - registration_id, - uuid, - private_key, - public_key, - profile_key, - }) - .await - .expect("failed to send provisioning message in rx channel"); - } - Err(e) => return Err(e), - } - } - - Ok(()) -} diff --git a/libsignal-service-actix/src/push_service.rs b/libsignal-service-actix/src/push_service.rs index 4850ea82f..c5a4a09ce 100644 --- a/libsignal-service-actix/src/push_service.rs +++ b/libsignal-service-actix/src/push_service.rs @@ -26,20 +26,6 @@ pub struct AwcPushService { } impl AwcPushService { - /// Creates a new AwcPushService - pub fn new( - cfg: ServiceConfiguration, - credentials: Option, - user_agent: &str, - ) -> Self { - let client = get_client(&cfg, user_agent); - Self { - cfg, - credentials: credentials.and_then(|c| c.authorization()), - client, - } - } - fn request( &self, method: Method, @@ -120,6 +106,20 @@ impl PushService for AwcPushService { type ByteStream = Box; type WebSocket = AwcWebSocket; + 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, + } + } + async fn get_json( &mut self, endpoint: Endpoint, @@ -240,6 +240,7 @@ impl PushService for AwcPushService { &mut self, endpoint: Endpoint, path: &str, + credentials_override: Option, value: S, ) -> Result where @@ -247,7 +248,7 @@ impl PushService for AwcPushService { S: Serialize, { let mut response = self - .request(Method::PUT, endpoint, path, None)? + .request(Method::PUT, endpoint, path, credentials_override)? .send_json(&value) .await .map_err(|e| ServiceError::SendError { @@ -486,7 +487,7 @@ impl PushService for AwcPushService { /// * 10s timeout on TCP connection /// * 65s timeout on HTTP request /// * provided user-agent -fn get_client(cfg: &ServiceConfiguration, user_agent: &str) -> Client { +fn get_client(cfg: &ServiceConfiguration, user_agent: String) -> Client { let mut ssl_config = rustls::ClientConfig::new(); ssl_config.alpn_protocols = vec![b"http/1.1".to_vec()]; ssl_config diff --git a/libsignal-service-hyper/src/lib.rs b/libsignal-service-hyper/src/lib.rs index 778722637..d91a8aa65 100644 --- a/libsignal-service-hyper/src/lib.rs +++ b/libsignal-service-hyper/src/lib.rs @@ -6,5 +6,3 @@ pub mod websocket; pub mod prelude { pub use crate::push_service::*; } - -// pub mod provisioning; diff --git a/libsignal-service-hyper/src/push_service.rs b/libsignal-service-hyper/src/push_service.rs index 4f4e227bd..c5df66012 100644 --- a/libsignal-service-hyper/src/push_service.rs +++ b/libsignal-service-hyper/src/push_service.rs @@ -22,7 +22,7 @@ use crate::websocket::TungsteniteWebSocket; #[derive(Clone)] pub struct HyperPushService { cfg: ServiceConfiguration, - user_agent: &'static str, + user_agent: String, credentials: Option, client: Client>>, } @@ -34,33 +34,6 @@ struct RequestBody { } impl HyperPushService { - pub fn new( - cfg: ServiceConfiguration, - credentials: Option, - user_agent: &'static str, - ) -> Self { - let tls_config = Self::tls_config(&cfg); - - let http = HttpConnector::new(); - let https = HttpsConnector::from((http, tls_config)); - - // as in Signal-Android - let mut timeout_connector = TimeoutConnector::new(https); - timeout_connector.set_connect_timeout(Some(Duration::from_secs(10))); - timeout_connector.set_read_timeout(Some(Duration::from_secs(65))); - timeout_connector.set_write_timeout(Some(Duration::from_secs(65))); - - let client: Client<_, hyper::Body> = - Client::builder().build(timeout_connector); - - Self { - cfg, - credentials: credentials.and_then(|c| c.authorization()), - client, - user_agent, - } - } - fn tls_config(cfg: &ServiceConfiguration) -> rustls::ClientConfig { let mut tls_config = rustls::ClientConfig::new(); tls_config.alpn_protocols = vec![b"http/1.1".to_vec()]; @@ -86,7 +59,7 @@ impl HyperPushService { let mut builder = Request::builder() .method(method) .uri(url.as_str()) - .header(USER_AGENT, self.user_agent); + .header(USER_AGENT, &self.user_agent); if let Some(http_credentials) = credentials_override .as_ref() .or_else(|| self.credentials.as_ref()) @@ -236,6 +209,34 @@ impl PushService for HyperPushService { type ByteStream = Box; type WebSocket = TungsteniteWebSocket; + fn new( + cfg: impl Into, + credentials: Option, + user_agent: String, + ) -> Self { + let cfg = cfg.into(); + let tls_config = Self::tls_config(&cfg); + + let http = HttpConnector::new(); + let https = HttpsConnector::from((http, tls_config)); + + // as in Signal-Android + let mut timeout_connector = TimeoutConnector::new(https); + timeout_connector.set_connect_timeout(Some(Duration::from_secs(10))); + timeout_connector.set_read_timeout(Some(Duration::from_secs(65))); + timeout_connector.set_write_timeout(Some(Duration::from_secs(65))); + + let client: Client<_, hyper::Body> = + Client::builder().build(timeout_connector); + + Self { + cfg, + credentials: credentials.and_then(|c| c.authorization()), + client, + user_agent, + } + } + async fn get_json( &mut self, service: Endpoint, @@ -271,6 +272,7 @@ impl PushService for HyperPushService { &mut self, service: Endpoint, path: &str, + credentials_override: Option, value: S, ) -> Result where @@ -288,7 +290,7 @@ impl PushService for HyperPushService { Method::PUT, service, path, - None, + credentials_override, Some(RequestBody { contents: json, content_type: "application/json".into(), diff --git a/libsignal-service/src/account_manager.rs b/libsignal-service/src/account_manager.rs index c6365d232..127054712 100644 --- a/libsignal-service/src/account_manager.rs +++ b/libsignal-service/src/account_manager.rs @@ -1,14 +1,13 @@ -use crate::pre_keys::{PreKeyEntity, PreKeyState}; -use crate::profile_cipher::{ProfileCipher, ProfileCipherError}; -use crate::profile_name::ProfileName; -use crate::provisioning::*; -use crate::push_service::{ - ConfirmDeviceMessage, DeviceId, PushService, ServiceError, - SmsVerificationCodeResponse, VoiceVerificationCodeResponse, -}; use crate::{ - configuration::ServiceCredentials, - push_service::{AccountAttributes, DeviceCapabilities}, + configuration::{Endpoint, ServiceCredentials}, + pre_keys::{PreKeyEntity, PreKeyState}, + profile_cipher::{ProfileCipher, ProfileCipherError}, + profile_name::ProfileName, + proto::{ProvisionEnvelope, ProvisionMessage, ProvisioningVersion}, + provisioning::{ProvisioningCipher, ProvisioningError}, + push_service::{ + AccountAttributes, DeviceCapabilities, PushService, ServiceError, + }, }; use std::collections::HashMap; @@ -18,8 +17,6 @@ use std::time::SystemTime; use libsignal_protocol::keys::PublicKey; use libsignal_protocol::{Context, StoreContext}; -use phonenumber::PhoneNumber; - use zkgroup::profiles::ProfileKey; pub struct AccountManager { @@ -66,41 +63,6 @@ impl AccountManager { } } - pub async fn request_sms_verification_code( - &mut self, - phone_number: PhoneNumber, - captcha: Option<&str>, - challenge: Option<&str>, - ) -> Result { - Ok(self - .service - .request_sms_verification_code(phone_number, captcha, challenge) - .await?) - } - - pub async fn request_voice_verification_code( - &mut self, - phone_number: PhoneNumber, - captcha: Option<&str>, - challenge: Option<&str>, - ) -> Result { - Ok(self - .service - .request_voice_verification_code(phone_number, captcha, challenge) - .await?) - } - - pub async fn confirm_device( - &mut self, - confirmation_code: u32, - confirm_device_message: ConfirmDeviceMessage, - ) -> Result { - Ok(self - .service - .confirm_device(confirmation_code, confirm_device_message) - .await?) - } - /// Checks the availability of pre-keys, and updates them as necessary. /// /// Parameters are the protocol's `StoreContext`, and the offsets for the next pre-key and @@ -177,6 +139,49 @@ impl AccountManager { )) } + async fn new_device_provisioning_code( + &mut self, + ) -> Result { + #[derive(serde::Deserialize)] + #[serde(rename_all = "camelCase")] + struct DeviceCode { + verification_code: String, + } + + let dc: DeviceCode = self + .service + .get_json(Endpoint::Service, "/v1/devices/provisioning/code", None) + .await?; + Ok(dc.verification_code) + } + + async fn send_provisioning_message( + &mut self, + destination: &str, + env: ProvisionEnvelope, + ) -> Result<(), ServiceError> { + use prost::Message; + + #[derive(serde::Serialize)] + struct ProvisioningMessage { + body: String, + } + + let mut body = Vec::with_capacity(env.encoded_len()); + env.encode(&mut body).expect("infallible encode"); + + self.service + .put_json( + Endpoint::Service, + &format!("/v1/provisioning/{}", destination), + None, + &ProvisioningMessage { + body: base64::encode(body), + }, + ) + .await + } + /// Link a new device, given a tsurl. /// /// Equivalent of Java's `AccountManager::addDevice()` @@ -211,8 +216,7 @@ impl AccountManager { log::warn!("No local UUID set"); } - let provisioning_code = - self.service.new_device_provisioning_code().await?; + let provisioning_code = self.new_device_provisioning_code().await?; let msg = ProvisionMessage { identity_key_public: Some( @@ -237,8 +241,7 @@ impl AccountManager { ProvisioningCipher::from_public(self.context.clone(), pub_key); let encrypted = cipher.encrypt(msg)?; - self.service - .send_provisioning_message(ephemeral_id, encrypted) + self.send_provisioning_message(ephemeral_id, encrypted) .await?; Ok(()) } diff --git a/libsignal-service/src/provisioning.rs b/libsignal-service/src/provisioning.rs deleted file mode 100644 index b5af2d175..000000000 --- a/libsignal-service/src/provisioning.rs +++ /dev/null @@ -1,440 +0,0 @@ -use aes::Aes256; -use block_modes::{block_padding::Pkcs7, BlockMode, Cbc}; -use bytes::{Bytes, BytesMut}; -use futures::{ - channel::mpsc::{self, Sender}, - prelude::*, - stream::FuturesUnordered, -}; -use hmac::{Hmac, Mac, NewMac}; -use pin_project::pin_project; -use prost::Message; -use rand::Rng; -use sha2::Sha256; -use url::Url; - -use libsignal_protocol::{ - keys::{KeyPair, PublicKey}, - Context, -}; - -pub use crate::proto::{ - ProvisionEnvelope, ProvisionMessage, ProvisioningVersion, -}; - -use crate::{ - envelope::{CIPHER_KEY_SIZE, IV_LENGTH, IV_OFFSET}, - messagepipe::{WebSocketService, WebSocketStreamItem}, - proto::{ - web_socket_message, ProvisioningUuid, WebSocketMessage, - WebSocketRequestMessage, WebSocketResponseMessage, - }, - push_service::ServiceError, -}; - -const VERSION: u8 = 1; - -#[derive(Debug)] -enum CipherMode { - Decrypt(KeyPair), - Encrypt(PublicKey), -} - -impl CipherMode { - fn public(&self) -> PublicKey { - match self { - CipherMode::Decrypt(pair) => pair.public(), - CipherMode::Encrypt(pub_key) => pub_key.clone(), - } - } -} - -#[derive(Debug)] -pub struct ProvisioningCipher { - ctx: Context, - key_material: CipherMode, -} - -#[derive(thiserror::Error, Debug)] -pub enum ProvisioningError { - #[error("Invalid provisioning data: {reason}")] - InvalidData { reason: String }, - #[error("Protobuf decoding error: {0}")] - DecodeError(#[from] prost::DecodeError), - #[error("Websocket error: {reason}")] - WsError { reason: String }, - #[error("Websocket closing: {reason}")] - WsClosing { reason: String }, - #[error("Service error: {0}")] - ServiceError(#[from] ServiceError), - #[error("libsignal-protocol error: {0}")] - ProtocolError(#[from] libsignal_protocol::Error), - #[error("ProvisioningCipher in encrypt-only mode")] - EncryptOnlyProvisioningCipher, -} - -impl ProvisioningCipher { - pub fn new(ctx: Context) -> Result { - let key_pair = libsignal_protocol::generate_key_pair(&ctx)?; - Ok(Self { - ctx, - key_material: CipherMode::Decrypt(key_pair), - }) - } - - pub fn from_public(ctx: Context, key: PublicKey) -> Self { - Self { - ctx, - key_material: CipherMode::Encrypt(key), - } - } - - pub fn from_key_pair(ctx: Context, key_pair: KeyPair) -> Self { - Self { - ctx, - key_material: CipherMode::Decrypt(key_pair), - } - } - - pub fn public_key(&self) -> PublicKey { - self.key_material.public() - } - - pub fn encrypt( - &self, - msg: ProvisionMessage, - ) -> Result { - let msg = { - let mut encoded = Vec::with_capacity(msg.encoded_len()); - msg.encode(&mut encoded).expect("infallible encoding"); - encoded - }; - - let mut rng = rand::thread_rng(); - let our_key_pair = libsignal_protocol::generate_key_pair(&self.ctx)?; - let agreement = self - .public_key() - .calculate_agreement(&our_key_pair.private())?; - let hkdf = libsignal_protocol::create_hkdf(&self.ctx, 3)?; - - let shared_secrets = hkdf.derive_secrets( - 64, - &agreement, - &[], - b"TextSecure Provisioning Message", - )?; - - let aes_key = &shared_secrets[0..32]; - let mac_key = &shared_secrets[32..]; - let iv: [u8; IV_LENGTH] = rng.gen(); - - let cipher = Cbc::::new_var(&aes_key, &iv) - .expect("initalization of CBC/AES/PKCS7"); - let ciphertext = cipher.encrypt_vec(&msg); - let mut mac = Hmac::::new_varkey(&mac_key) - .expect("HMAC can take any size key"); - mac.update(&[VERSION]); - mac.update(&iv); - mac.update(&ciphertext); - let mac = mac.finalize().into_bytes(); - - let body: Vec = std::iter::once(VERSION) - .chain(iv.iter().cloned()) - .chain(ciphertext) - .chain(mac) - .collect(); - - Ok(ProvisionEnvelope { - public_key: Some( - our_key_pair.public().to_bytes()?.as_slice().to_vec(), - ), - body: Some(body), - }) - } - - pub fn decrypt( - &self, - provision_envelope: ProvisionEnvelope, - ) -> Result { - let key_pair = match self.key_material { - CipherMode::Decrypt(ref key_pair) => key_pair, - CipherMode::Encrypt(_) => { - return Err(ProvisioningError::EncryptOnlyProvisioningCipher); - } - }; - let master_ephemeral = PublicKey::decode_point( - &self.ctx, - &provision_envelope.public_key.expect("no public key"), - )?; - let body = provision_envelope - .body - .expect("no body in ProvisionMessage"); - if body[0] != VERSION { - return Err(ProvisioningError::InvalidData { - reason: "Bad version number".into(), - }); - } - - let iv = &body[IV_OFFSET..(IV_LENGTH + IV_OFFSET)]; - let mac = &body[(body.len() - 32)..]; - let cipher_text = &body[16 + 1..(body.len() - CIPHER_KEY_SIZE)]; - let iv_and_cipher_text = &body[0..(body.len() - CIPHER_KEY_SIZE)]; - debug_assert_eq!(iv.len(), IV_LENGTH); - debug_assert_eq!(mac.len(), 32); - - let agreement = - master_ephemeral.calculate_agreement(&key_pair.private())?; - let hkdf = libsignal_protocol::create_hkdf(&self.ctx, 3)?; - - let shared_secrets = hkdf.derive_secrets( - 64, - &agreement, - &[], - b"TextSecure Provisioning Message", - )?; - - let parts1 = &shared_secrets[0..32]; - let parts2 = &shared_secrets[32..]; - - let mut verifier = Hmac::::new_varkey(&parts2) - .expect("HMAC can take any size key"); - verifier.update(&iv_and_cipher_text); - let our_mac = verifier.finalize().into_bytes(); - debug_assert_eq!(our_mac.len(), mac.len()); - if &our_mac[..32] != mac { - return Err(ProvisioningError::InvalidData { - reason: "wrong MAC".into(), - }); - } - - // libsignal-service-java uses Pkcs5, - // but that should not matter. - // https://crypto.stackexchange.com/questions/9043/what-is-the-difference-between-pkcs5-padding-and-pkcs7-padding - let cipher = Cbc::::new_var(&parts1, &iv) - .expect("initalization of CBC/AES/PKCS7"); - let input = cipher.decrypt_vec(cipher_text).map_err(|e| { - ProvisioningError::InvalidData { - reason: format!("CBC/Padding error: {:?}", e), - } - })?; - - Ok(prost::Message::decode(Bytes::from(input))?) - } -} - -#[pin_project] -pub struct ProvisioningPipe { - ws: WS, - #[pin] - stream: WS::Stream, - provisioning_cipher: ProvisioningCipher, -} - -#[derive(Debug)] -pub enum ProvisioningStep { - Url(Url), - Message(ProvisionMessage), -} - -impl ProvisioningPipe { - pub fn from_socket( - ws: WS, - stream: WS::Stream, - ctx: &Context, - ) -> Result { - Ok(ProvisioningPipe { - ws, - stream, - provisioning_cipher: ProvisioningCipher::new(ctx.clone())?, - }) - } - - async fn send_ok_response( - &mut self, - id: Option, - ) -> Result<(), ProvisioningError> { - self.send_response(WebSocketResponseMessage { - id, - status: Some(200), - message: Some("OK".into()), - body: None, - headers: vec![], - }) - .await - } - - async fn send_response( - &mut self, - r: WebSocketResponseMessage, - ) -> Result<(), ProvisioningError> { - let msg = WebSocketMessage { - r#type: Some(web_socket_message::Type::Response.into()), - response: Some(r), - ..Default::default() - }; - let mut buffer = BytesMut::with_capacity(msg.encoded_len()); - msg.encode(&mut buffer).unwrap(); - Ok(self.ws.send_message(buffer.into()).await?) - } - - /// Worker task that - async fn run( - mut self, - mut sink: Sender>, - ) -> Result<(), mpsc::SendError> { - use futures::future::LocalBoxFuture; - - // This is a runtime-agnostic, poor man's `::spawn(Future)`. - let mut background_work = FuturesUnordered::>::new(); - // a pending task is added, as to never end the background worker until - // it's dropped. - background_work.push(futures::future::pending().boxed_local()); - - loop { - futures::select! { - // WebsocketConnection::onMessage(ByteString) - frame = self.stream.next() => match frame { - Some(WebSocketStreamItem::Message(frame)) => { - let env = self.process_frame(frame).await.transpose(); - if let Some(env) = env { - sink.send(env).await?; - } - }, - // TODO: implement keep-alive? - Some(WebSocketStreamItem::KeepAliveRequest) => continue, - None => break, - }, - _ = background_work.next() => { - // no op - }, - complete => { - log::info!("select! complete"); - } - } - } - - Ok(()) - } - - async fn process_frame( - &mut self, - frame: Bytes, - ) -> Result, ProvisioningError> { - let msg = WebSocketMessage::decode(frame)?; - use web_socket_message::Type; - match (msg.r#type(), msg.request, msg.response) { - (Type::Request, Some(request), _) => { - match request { - // step 1: we get a ProvisioningUUID that we need to build a - // registration link - WebSocketRequestMessage { - id, - verb, - path, - body, - .. - } if verb == Some("PUT".into()) - && path == Some("/v1/address".into()) => - { - let uuid: ProvisioningUuid = - prost::Message::decode(Bytes::from(body.unwrap()))?; - let mut provisioning_url = Url::parse("tsdevice://") - .map_err(|e| ProvisioningError::WsError { - reason: e.to_string(), - })?; - provisioning_url - .query_pairs_mut() - .append_pair("uuid", &uuid.uuid.unwrap()) - .append_pair( - "pub_key", - &format!( - "{}", - self.provisioning_cipher.public_key() - ), - ); - - // acknowledge - self.send_ok_response(id).await?; - - Ok(Some(ProvisioningStep::Url(provisioning_url))) - } - // step 2: once the QR code is scanned by the (already - // validated) main device - // we get a ProvisionMessage, that contains a bunch of - // useful things - WebSocketRequestMessage { - id, - verb, - path, - body, - .. - } if verb == Some("PUT".into()) - && path == Some("/v1/message".into()) => - { - let provision_envelope: ProvisionEnvelope = - prost::Message::decode(Bytes::from(body.unwrap()))?; - let provision_message = self - .provisioning_cipher - .decrypt(provision_envelope)?; - - // acknowledge - self.send_ok_response(id).await?; - - Ok(Some(ProvisioningStep::Message(provision_message))) - } - _ => Err(ProvisioningError::WsError { - reason: "Incorrect request".into(), - }), - } - } - _ => Err(ProvisioningError::WsError { - reason: "Incorrect request".into(), - }), - } - } - - pub fn stream( - self, - ) -> impl Stream> { - let (sink, stream) = mpsc::channel(1); - - let stream = stream.map(Some); - let runner = self.run(sink).map(|_| { - log::info!("Sink closed, provisioning is done!"); - None - }); - - let combined = futures::stream::select(stream, runner.into_stream()); - combined.filter_map(|x| async { x }) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn encrypt_provisioning_roundtrip() { - let ctx = Context::default(); - let cipher = ProvisioningCipher::new(ctx.clone()).unwrap(); - let encrypt_cipher = - ProvisioningCipher::from_public(ctx.clone(), cipher.public_key()); - - assert_eq!( - cipher.public_key(), - encrypt_cipher.public_key(), - "copy public key" - ); - - let msg = ProvisionMessage::default(); - let encrypted = encrypt_cipher.encrypt(msg.clone()).unwrap(); - - assert!(matches!( - encrypt_cipher.decrypt(encrypted.clone()), - Err(ProvisioningError::EncryptOnlyProvisioningCipher) - )); - - let decrypted = cipher.decrypt(encrypted).expect("decryptability"); - assert_eq!(msg, decrypted); - } -} diff --git a/libsignal-service/src/provisioning/cipher.rs b/libsignal-service/src/provisioning/cipher.rs new file mode 100644 index 000000000..5522e89b2 --- /dev/null +++ b/libsignal-service/src/provisioning/cipher.rs @@ -0,0 +1,223 @@ +use aes::Aes256; +use block_modes::{block_padding::Pkcs7, BlockMode, Cbc}; +use bytes::Bytes; +use hmac::{Hmac, Mac, NewMac}; +use prost::Message; +use rand::Rng; +use sha2::Sha256; + +use libsignal_protocol::{ + keys::{KeyPair, PublicKey}, + Context, +}; + +pub use crate::proto::{ + ProvisionEnvelope, ProvisionMessage, ProvisioningVersion, +}; + +use crate::{ + envelope::{CIPHER_KEY_SIZE, IV_LENGTH, IV_OFFSET}, + provisioning::ProvisioningError, +}; + +#[derive(Debug)] +enum CipherMode { + DecryptAndEncrypt(KeyPair), + EncryptOnly(PublicKey), +} + +impl CipherMode { + fn public(&self) -> PublicKey { + match self { + CipherMode::DecryptAndEncrypt(pair) => pair.public(), + CipherMode::EncryptOnly(pub_key) => pub_key.clone(), + } + } +} + +const VERSION: u8 = 1; + +#[derive(Debug)] +pub struct ProvisioningCipher { + ctx: Context, + key_material: CipherMode, +} + +impl ProvisioningCipher { + pub fn new(ctx: Context) -> Result { + let key_pair = libsignal_protocol::generate_key_pair(&ctx)?; + Ok(Self { + ctx, + key_material: CipherMode::DecryptAndEncrypt(key_pair), + }) + } + + pub fn from_public(ctx: Context, key: PublicKey) -> Self { + Self { + ctx, + key_material: CipherMode::EncryptOnly(key), + } + } + + pub fn from_key_pair(ctx: Context, key_pair: KeyPair) -> Self { + Self { + ctx, + key_material: CipherMode::DecryptAndEncrypt(key_pair), + } + } + + pub fn public_key(&self) -> PublicKey { + self.key_material.public() + } + + pub fn encrypt( + &self, + msg: ProvisionMessage, + ) -> Result { + let msg = { + let mut encoded = Vec::with_capacity(msg.encoded_len()); + msg.encode(&mut encoded).expect("infallible encoding"); + encoded + }; + + let mut rng = rand::thread_rng(); + let our_key_pair = libsignal_protocol::generate_key_pair(&self.ctx)?; + let agreement = self + .public_key() + .calculate_agreement(&our_key_pair.private())?; + let hkdf = libsignal_protocol::create_hkdf(&self.ctx, 3)?; + + let shared_secrets = hkdf.derive_secrets( + 64, + &agreement, + &[], + b"TextSecure Provisioning Message", + )?; + + let aes_key = &shared_secrets[0..32]; + let mac_key = &shared_secrets[32..]; + let iv: [u8; IV_LENGTH] = rng.gen(); + + let cipher = Cbc::::new_var(&aes_key, &iv) + .expect("initalization of CBC/AES/PKCS7"); + let ciphertext = cipher.encrypt_vec(&msg); + let mut mac = Hmac::::new_varkey(&mac_key) + .expect("HMAC can take any size key"); + mac.update(&[VERSION]); + mac.update(&iv); + mac.update(&ciphertext); + let mac = mac.finalize().into_bytes(); + + let body: Vec = std::iter::once(VERSION) + .chain(iv.iter().cloned()) + .chain(ciphertext) + .chain(mac) + .collect(); + + Ok(ProvisionEnvelope { + public_key: Some( + our_key_pair.public().to_bytes()?.as_slice().to_vec(), + ), + body: Some(body), + }) + } + + pub fn decrypt( + &self, + provision_envelope: ProvisionEnvelope, + ) -> Result { + let key_pair = match self.key_material { + CipherMode::DecryptAndEncrypt(ref key_pair) => key_pair, + CipherMode::EncryptOnly(_) => { + return Err(ProvisioningError::EncryptOnlyProvisioningCipher); + } + }; + let master_ephemeral = PublicKey::decode_point( + &self.ctx, + &provision_envelope.public_key.expect("no public key"), + )?; + let body = provision_envelope + .body + .expect("no body in ProvisionMessage"); + if body[0] != VERSION { + return Err(ProvisioningError::InvalidData { + reason: "Bad version number".into(), + }); + } + + let iv = &body[IV_OFFSET..(IV_LENGTH + IV_OFFSET)]; + let mac = &body[(body.len() - 32)..]; + let cipher_text = &body[16 + 1..(body.len() - CIPHER_KEY_SIZE)]; + let iv_and_cipher_text = &body[0..(body.len() - CIPHER_KEY_SIZE)]; + debug_assert_eq!(iv.len(), IV_LENGTH); + debug_assert_eq!(mac.len(), 32); + + let agreement = + master_ephemeral.calculate_agreement(&key_pair.private())?; + let hkdf = libsignal_protocol::create_hkdf(&self.ctx, 3)?; + + let shared_secrets = hkdf.derive_secrets( + 64, + &agreement, + &[], + b"TextSecure Provisioning Message", + )?; + + let parts1 = &shared_secrets[0..32]; + let parts2 = &shared_secrets[32..]; + + let mut verifier = Hmac::::new_varkey(&parts2) + .expect("HMAC can take any size key"); + verifier.update(&iv_and_cipher_text); + let our_mac = verifier.finalize().into_bytes(); + debug_assert_eq!(our_mac.len(), mac.len()); + if &our_mac[..32] != mac { + return Err(ProvisioningError::InvalidData { + reason: "wrong MAC".into(), + }); + } + + // libsignal-service-java uses Pkcs5, + // but that should not matter. + // https://crypto.stackexchange.com/questions/9043/what-is-the-difference-between-pkcs5-padding-and-pkcs7-padding + let cipher = Cbc::::new_var(&parts1, &iv) + .expect("initalization of CBC/AES/PKCS7"); + let input = cipher.decrypt_vec(cipher_text).map_err(|e| { + ProvisioningError::InvalidData { + reason: format!("CBC/Padding error: {:?}", e), + } + })?; + + Ok(prost::Message::decode(Bytes::from(input))?) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn encrypt_provisioning_roundtrip() { + let ctx = Context::default(); + let cipher = ProvisioningCipher::new(ctx.clone()).unwrap(); + let encrypt_cipher = + ProvisioningCipher::from_public(ctx.clone(), cipher.public_key()); + + assert_eq!( + cipher.public_key(), + encrypt_cipher.public_key(), + "copy public key" + ); + + let msg = ProvisionMessage::default(); + let encrypted = encrypt_cipher.encrypt(msg.clone()).unwrap(); + + assert!(matches!( + encrypt_cipher.decrypt(encrypted.clone()), + Err(ProvisioningError::EncryptOnlyProvisioningCipher) + )); + + let decrypted = cipher.decrypt(encrypted).expect("decryptability"); + assert_eq!(msg, decrypted); + } +} diff --git a/libsignal-service/src/provisioning/manager.rs b/libsignal-service/src/provisioning/manager.rs new file mode 100644 index 000000000..59993f51b --- /dev/null +++ b/libsignal-service/src/provisioning/manager.rs @@ -0,0 +1,412 @@ +use futures::{channel::mpsc::Sender, pin_mut, SinkExt, StreamExt}; +use phonenumber::PhoneNumber; +use serde::{Deserialize, Serialize}; +use url::Url; + +use super::{ + pipe::{ProvisioningPipe, ProvisioningStep}, + ProvisioningError, +}; + +pub use crate::proto::{ + ProvisionEnvelope, ProvisionMessage, ProvisioningVersion, +}; + +use libsignal_protocol::{ + generate_registration_id, + keys::{PrivateKey, PublicKey}, + Context, +}; + +use crate::{ + configuration::{Endpoint, ServiceConfiguration, SignalingKey}, + messagepipe::ServiceCredentials, + push_service::{DeviceCapabilities, DeviceId, PushService, ServiceError}, + utils::{serde_base64, serde_optional_base64}, +}; + +/// Message received after confirming the SMS/voice code on registration. +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ConfirmCodeMessage { + #[serde(with = "serde_base64")] + pub signaling_key: Vec, + pub supports_sms: bool, + pub registration_id: u32, + pub voice: bool, + pub video: bool, + pub fetches_messages: bool, + pub pin: Option, + #[serde(with = "serde_optional_base64")] + pub unidentified_access_key: Option>, + pub unrestricted_unidentified_access: bool, + pub discoverable_by_phone_number: bool, + pub capabilities: DeviceCapabilities, +} +/// Message received when linking a new secondary device. +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ConfirmDeviceMessage { + #[serde(with = "serde_base64")] + pub signaling_key: Vec, + pub supports_sms: bool, + pub fetches_messages: bool, + pub registration_id: u32, + pub name: String, +} + +impl ConfirmCodeMessage { + pub fn new( + signaling_key: Vec, + registration_id: u32, + unidentified_access_key: Vec, + ) -> Self { + Self { + signaling_key, + supports_sms: false, + registration_id, + voice: false, + video: false, + fetches_messages: true, + pin: None, + unidentified_access_key: Some(unidentified_access_key), + unrestricted_unidentified_access: false, + discoverable_by_phone_number: true, + capabilities: DeviceCapabilities::default(), + } + } + + pub fn new_without_unidentified_access( + signaling_key: Vec, + registration_id: u32, + ) -> Self { + Self { + signaling_key, + supports_sms: false, + registration_id, + voice: false, + video: false, + fetches_messages: true, + pin: None, + unidentified_access_key: None, + unrestricted_unidentified_access: false, + discoverable_by_phone_number: true, + capabilities: DeviceCapabilities::default(), + } + } +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ConfirmCodeResponse { + pub uuid: String, + pub storage_capable: bool, +} + +#[derive(Debug, Eq, PartialEq)] +pub enum SmsVerificationCodeResponse { + CaptchaRequired, + SmsSent, +} + +#[derive(Debug, Eq, PartialEq)] +pub enum VoiceVerificationCodeResponse { + CaptchaRequired, + CallIssued, +} + +#[derive(Clone)] +pub struct ProvisioningManager { + push_service: P, + phone_number: PhoneNumber, +} +pub enum SecondaryDeviceProvisioning { + Url(Url), + NewDeviceRegistration { + phone_number: phonenumber::PhoneNumber, + device_id: DeviceId, + registration_id: u32, + uuid: String, + private_key: PrivateKey, + public_key: PublicKey, + profile_key: Vec, + }, +} + +impl ProvisioningManager

{ + pub fn new( + cfg: impl Into, + user_agent: String, + phone_number: PhoneNumber, + password: String, + ) -> Self { + Self { + phone_number: phone_number.clone(), + push_service: P::new( + cfg, + Some(ServiceCredentials { + phonenumber: phone_number, + password: Some(password), + uuid: None, + signaling_key: None, + device_id: None, + }), + user_agent, + ), + } + } + + pub async fn request_sms_verification_code( + &mut self, + captcha: Option<&str>, + challenge: Option<&str>, + ) -> Result { + let res = match self + .push_service + .get_json( + Endpoint::Service, + self.build_verification_code_request_url( + "sms", captcha, challenge, + ) + .as_ref(), + None, + ) + .await + { + Err(ServiceError::JsonDecodeError { .. }) => Ok(()), + r => r, + }; + match res { + Ok(_) => Ok(SmsVerificationCodeResponse::SmsSent), + Err(ServiceError::UnhandledResponseCode { http_code: 402 }) => { + Ok(SmsVerificationCodeResponse::CaptchaRequired) + } + Err(e) => Err(e), + } + } + + pub async fn request_voice_verification_code( + &mut self, + captcha: Option<&str>, + challenge: Option<&str>, + ) -> Result { + let res = match self + .push_service + .get_json( + Endpoint::Service, + self.build_verification_code_request_url( + "voice", captcha, challenge, + ) + .as_ref(), + None, + ) + .await + { + Err(ServiceError::JsonDecodeError { .. }) => Ok(()), + r => r, + }; + match res { + Ok(_) => Ok(VoiceVerificationCodeResponse::CallIssued), + Err(ServiceError::UnhandledResponseCode { http_code: 402 }) => { + Ok(VoiceVerificationCodeResponse::CaptchaRequired) + } + Err(e) => Err(e), + } + } + + pub async fn confirm_verification_code( + &mut self, + confirm_code: u32, + confirm_verification_message: ConfirmCodeMessage, + ) -> Result { + self.push_service + .put_json( + Endpoint::Service, + &format!("/v1/accounts/code/{}", confirm_code), + None, + confirm_verification_message, + ) + .await + } + + pub async fn confirm_device( + &mut self, + confirm_code: u32, + confirm_code_message: ConfirmDeviceMessage, + ) -> Result { + self.push_service + .put_json( + Endpoint::Service, + &format!("/v1/devices/{}", confirm_code), + None, + confirm_code_message, + ) + .await + } + + fn build_verification_code_request_url( + &self, + msg_type: &str, + captcha: Option<&str>, + challenge: Option<&str>, + ) -> String { + let phone_number = + self.phone_number.format().mode(phonenumber::Mode::E164); + if let Some(cl) = challenge { + format!( + "/v1/accounts/{}/code/{}?challenge={}", + msg_type, phone_number, cl + ) + } else if let Some(cc) = captcha { + format!( + "/v1/accounts/{}/code/{}?captcha={}", + msg_type, phone_number, cc + ) + } else { + format!("/v1/accounts/{}/code/{}", msg_type, phone_number) + } + } +} + +#[derive(Clone)] +pub struct LinkingManager { + cfg: ServiceConfiguration, + user_agent: String, + password: String, + push_service: P, +} + +impl LinkingManager

{ + pub fn new( + cfg: impl Into + Clone, + user_agent: String, + password: String, + ) -> Self { + Self { + cfg: cfg.clone().into(), + user_agent: user_agent.clone(), + password: password.clone(), + push_service: P::new(cfg, None, user_agent), + } + } + + pub async fn provision_secondary_device( + &mut self, + ctx: &Context, + signaling_key: SignalingKey, + device_name: &str, + mut tx: Sender, + ) -> Result<(), ProvisioningError> { + // open a websocket without authentication, to receive a tsurl:// + let (ws, stream) = self + .push_service + .ws("/v1/websocket/provisioning/", None) + .await?; + + let registration_id = generate_registration_id(&ctx, 0)?; + + let provisioning_pipe = + ProvisioningPipe::from_socket(ws, stream, &ctx)?; + let provision_stream = provisioning_pipe.stream(); + pin_mut!(provision_stream); + while let Some(step) = provision_stream.next().await { + match step { + Ok(ProvisioningStep::Url(url)) => { + tx.send(SecondaryDeviceProvisioning::Url(url)) + .await + .expect("failed to send provisioning Url in channel"); + } + Ok(ProvisioningStep::Message(message)) => { + let uuid = + message.uuid.ok_or(ProvisioningError::InvalidData { + reason: "missing client UUID".into(), + })?; + + let public_key = PublicKey::decode_point( + &ctx, + &message.identity_key_public.ok_or( + ProvisioningError::InvalidData { + reason: "missing public key".into(), + }, + )?, + )?; + + let private_key = PrivateKey::decode_point( + &ctx, + &message.identity_key_private.ok_or( + ProvisioningError::InvalidData { + reason: "missing public key".into(), + }, + )?, + )?; + + let profile_key = message.profile_key.ok_or( + ProvisioningError::InvalidData { + reason: "missing profile key".into(), + }, + )?; + + let phone_number = message.number.ok_or( + ProvisioningError::InvalidData { + reason: "missing phone number".into(), + }, + )?; + + let phone_number = phonenumber::parse(None, phone_number) + .map_err(|e| { + ProvisioningError::InvalidData { + reason: format!("invalid phone number ({})", e), + } + })?; + + let mut provisioning_manager: ProvisioningManager

= + ProvisioningManager::new( + self.cfg.clone(), + self.user_agent.clone(), + phone_number.clone(), + self.password.to_string(), + ); + + let device_id = provisioning_manager + .confirm_device( + message + .provisioning_code + .ok_or(ProvisioningError::InvalidData { + reason: "no provisioning confirmation code" + .into(), + })? + .parse() + .unwrap(), + ConfirmDeviceMessage { + signaling_key: signaling_key.to_vec(), + supports_sms: false, + fetches_messages: true, + registration_id, + name: device_name.to_string(), + }, + ) + .await?; + + tx.send( + SecondaryDeviceProvisioning::NewDeviceRegistration { + phone_number, + device_id, + registration_id, + uuid, + private_key, + public_key, + profile_key, + }, + ) + .await + .expect( + "failed to send provisioning message in rx channel", + ); + } + Err(e) => return Err(e.into()), + } + } + + Ok(()) + } +} diff --git a/libsignal-service/src/provisioning/mod.rs b/libsignal-service/src/provisioning/mod.rs new file mode 100644 index 000000000..4c768adb2 --- /dev/null +++ b/libsignal-service/src/provisioning/mod.rs @@ -0,0 +1,32 @@ +mod cipher; +mod manager; +mod pipe; + +pub use cipher::ProvisioningCipher; +pub use manager::{ + ConfirmCodeMessage, ConfirmDeviceMessage, LinkingManager, + ProvisioningManager, SecondaryDeviceProvisioning, +}; + +use crate::prelude::ServiceError; +pub use crate::proto::{ + ProvisionEnvelope, ProvisionMessage, ProvisioningVersion, +}; + +#[derive(thiserror::Error, Debug)] +pub enum ProvisioningError { + #[error("Invalid provisioning data: {reason}")] + InvalidData { reason: String }, + #[error("Protobuf decoding error: {0}")] + DecodeError(#[from] prost::DecodeError), + #[error("Websocket error: {reason}")] + WsError { reason: String }, + #[error("Websocket closing: {reason}")] + WsClosing { reason: String }, + #[error("Service error: {0}")] + ServiceError(#[from] ServiceError), + #[error("libsignal-protocol error: {0}")] + ProtocolError(#[from] libsignal_protocol::Error), + #[error("ProvisioningCipher in encrypt-only mode")] + EncryptOnlyProvisioningCipher, +} diff --git a/libsignal-service/src/provisioning/pipe.rs b/libsignal-service/src/provisioning/pipe.rs new file mode 100644 index 000000000..b2bcc4af3 --- /dev/null +++ b/libsignal-service/src/provisioning/pipe.rs @@ -0,0 +1,213 @@ +use bytes::{Bytes, BytesMut}; +use futures::{ + channel::mpsc::{self, Sender}, + prelude::*, + stream::FuturesUnordered, +}; +use pin_project::pin_project; +use prost::Message; +use url::Url; + +use libsignal_protocol::Context; + +pub use crate::proto::{ + ProvisionEnvelope, ProvisionMessage, ProvisioningVersion, +}; + +use crate::{ + messagepipe::{WebSocketService, WebSocketStreamItem}, + proto::{ + web_socket_message, ProvisioningUuid, WebSocketMessage, + WebSocketRequestMessage, WebSocketResponseMessage, + }, + provisioning::ProvisioningError, +}; + +use super::cipher::ProvisioningCipher; + +#[pin_project] +pub struct ProvisioningPipe { + ws: WS, + #[pin] + stream: WS::Stream, + provisioning_cipher: ProvisioningCipher, +} + +#[derive(Debug)] +pub enum ProvisioningStep { + Url(Url), + Message(ProvisionMessage), +} + +impl ProvisioningPipe { + pub fn from_socket( + ws: WS, + stream: WS::Stream, + ctx: &Context, + ) -> Result { + Ok(ProvisioningPipe { + ws, + stream, + provisioning_cipher: ProvisioningCipher::new(ctx.clone())?, + }) + } + + async fn send_ok_response( + &mut self, + id: Option, + ) -> Result<(), ProvisioningError> { + self.send_response(WebSocketResponseMessage { + id, + status: Some(200), + message: Some("OK".into()), + body: None, + headers: vec![], + }) + .await + } + + async fn send_response( + &mut self, + r: WebSocketResponseMessage, + ) -> Result<(), ProvisioningError> { + let msg = WebSocketMessage { + r#type: Some(web_socket_message::Type::Response.into()), + response: Some(r), + ..Default::default() + }; + let mut buffer = BytesMut::with_capacity(msg.encoded_len()); + msg.encode(&mut buffer).unwrap(); + Ok(self.ws.send_message(buffer.into()).await?) + } + + /// Worker task that + async fn run( + mut self, + mut sink: Sender>, + ) -> Result<(), mpsc::SendError> { + use futures::future::LocalBoxFuture; + + // This is a runtime-agnostic, poor man's `::spawn(Future)`. + let mut background_work = FuturesUnordered::>::new(); + // a pending task is added, as to never end the background worker until + // it's dropped. + background_work.push(futures::future::pending().boxed_local()); + + loop { + futures::select! { + // WebsocketConnection::onMessage(ByteString) + frame = self.stream.next() => match frame { + Some(WebSocketStreamItem::Message(frame)) => { + let env = self.process_frame(frame).await.transpose(); + if let Some(env) = env { + sink.send(env).await?; + } + }, + // TODO: implement keep-alive? + Some(WebSocketStreamItem::KeepAliveRequest) => continue, + None => break, + }, + _ = background_work.next() => { + // no op + }, + complete => { + log::info!("select! complete"); + } + } + } + + Ok(()) + } + + async fn process_frame( + &mut self, + frame: Bytes, + ) -> Result, ProvisioningError> { + let msg = WebSocketMessage::decode(frame)?; + use web_socket_message::Type; + match (msg.r#type(), msg.request, msg.response) { + (Type::Request, Some(request), _) => { + match request { + // step 1: we get a ProvisioningUUID that we need to build a + // registration link + WebSocketRequestMessage { + id, + verb, + path, + body, + .. + } if verb == Some("PUT".into()) + && path == Some("/v1/address".into()) => + { + let uuid: ProvisioningUuid = + prost::Message::decode(Bytes::from(body.unwrap()))?; + let mut provisioning_url = Url::parse("tsdevice://") + .map_err(|e| ProvisioningError::WsError { + reason: e.to_string(), + })?; + provisioning_url + .query_pairs_mut() + .append_pair("uuid", &uuid.uuid.unwrap()) + .append_pair( + "pub_key", + &format!( + "{}", + self.provisioning_cipher.public_key() + ), + ); + + // acknowledge + self.send_ok_response(id).await?; + + Ok(Some(ProvisioningStep::Url(provisioning_url))) + } + // step 2: once the QR code is scanned by the (already + // validated) main device + // we get a ProvisionMessage, that contains a bunch of + // useful things + WebSocketRequestMessage { + id, + verb, + path, + body, + .. + } if verb == Some("PUT".into()) + && path == Some("/v1/message".into()) => + { + let provision_envelope: ProvisionEnvelope = + prost::Message::decode(Bytes::from(body.unwrap()))?; + let provision_message = self + .provisioning_cipher + .decrypt(provision_envelope)?; + + // acknowledge + self.send_ok_response(id).await?; + + Ok(Some(ProvisioningStep::Message(provision_message))) + } + _ => Err(ProvisioningError::WsError { + reason: "Incorrect request".into(), + }), + } + } + _ => Err(ProvisioningError::WsError { + reason: "Incorrect request".into(), + }), + } + } + + pub fn stream( + self, + ) -> impl Stream> { + let (sink, stream) = mpsc::channel(1); + + let stream = stream.map(Some); + let runner = self.run(sink).map(|_| { + log::info!("Sink closed, provisioning is done!"); + None + }); + + let combined = futures::stream::select(stream, runner.into_stream()); + combined.filter_map(|x| async { x }) + } +} diff --git a/libsignal-service/src/push_service.rs b/libsignal-service/src/push_service.rs index 6ff649d74..d5801e49f 100644 --- a/libsignal-service/src/push_service.rs +++ b/libsignal-service/src/push_service.rs @@ -1,15 +1,12 @@ use std::time::Duration; use crate::{ - configuration::{Endpoint, ServiceCredentials}, + configuration::{Endpoint, ServiceConfiguration, ServiceCredentials}, envelope::*, groups_v2::GroupDecryptionError, messagepipe::WebSocketService, pre_keys::{PreKeyEntity, PreKeyState, SignedPreKeyEntity}, - proto::{ - attachment_pointer::AttachmentIdentifier, AttachmentPointer, - ProvisionEnvelope, - }, + proto::{attachment_pointer::AttachmentIdentifier, AttachmentPointer}, sender::{OutgoingPushMessages, SendMessageResponse}, utils::{serde_base64, serde_optional_base64}, ServiceAddress, @@ -21,7 +18,6 @@ use aes_gcm::{ }; use chrono::prelude::*; -use phonenumber::PhoneNumber; use prost::Message as ProtobufMessage; use libsignal_protocol::{keys::PublicKey, Context, PreKeyBundle}; @@ -67,18 +63,6 @@ pub const STICKER_PATH: &str = "stickers/%s/full/%d"; pub const KEEPALIVE_TIMEOUT_SECONDS: Duration = Duration::from_secs(55); pub const DEFAULT_DEVICE_ID: i32 = 1; -#[derive(Debug, Eq, PartialEq)] -pub enum SmsVerificationCodeResponse { - CaptchaRequired, - SmsSent, -} - -#[derive(Debug, Eq, PartialEq)] -pub enum VoiceVerificationCodeResponse { - CaptchaRequired, - CallIssued, -} - #[derive(Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct DeviceId { @@ -96,35 +80,6 @@ pub struct DeviceInfo { pub last_seen: DateTime, } -#[derive(Debug, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct ConfirmDeviceMessage { - #[serde(with = "serde_base64")] - pub signaling_key: Vec, - pub supports_sms: bool, - pub fetches_messages: bool, - pub registration_id: u32, - pub name: String, -} - -#[derive(Debug, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct ConfirmCodeMessage { - #[serde(with = "serde_base64")] - pub signaling_key: Vec, - pub supports_sms: bool, - pub registration_id: u32, - pub voice: bool, - pub video: bool, - pub fetches_messages: bool, - pub pin: Option, - #[serde(with = "serde_optional_base64")] - pub unidentified_access_key: Option>, - pub unrestricted_unidentified_access: bool, - pub discoverable_by_phone_number: bool, - pub capabilities: DeviceCapabilities, -} - #[derive(Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct AccountAttributes { @@ -178,54 +133,6 @@ impl ProfileKey { } } -impl ConfirmCodeMessage { - pub fn new( - signaling_key: Vec, - registration_id: u32, - unidentified_access_key: Vec, - ) -> Self { - Self { - signaling_key, - supports_sms: false, - registration_id, - voice: false, - video: false, - fetches_messages: true, - pin: None, - unidentified_access_key: Some(unidentified_access_key), - unrestricted_unidentified_access: false, - discoverable_by_phone_number: true, - capabilities: DeviceCapabilities::default(), - } - } - - pub fn new_without_unidentified_access( - signaling_key: Vec, - registration_id: u32, - ) -> Self { - Self { - signaling_key, - supports_sms: false, - registration_id, - voice: false, - video: false, - fetches_messages: true, - pin: None, - unidentified_access_key: None, - unrestricted_unidentified_access: false, - discoverable_by_phone_number: true, - capabilities: DeviceCapabilities::default(), - } - } -} - -#[derive(Debug, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct ConfirmCodeResponse { - pub uuid: String, - pub storage_capable: bool, -} - #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] pub struct PreKeyResponse { @@ -365,6 +272,12 @@ pub trait PushService { type WebSocket: WebSocketService; type ByteStream: futures::io::AsyncRead + Unpin; + fn new( + cfg: impl Into, + credentials: Option, + user_agent: String, + ) -> Self; + async fn get_json( &mut self, service: Endpoint, @@ -386,6 +299,7 @@ pub trait PushService { &mut self, service: Endpoint, path: &str, + credentials_override: Option, value: S, ) -> Result where @@ -440,92 +354,6 @@ pub trait PushService { ServiceError, >; - fn build_verification_code_request_url( - msg_type: &str, - phone_number: PhoneNumber, - captcha: Option<&str>, - challenge: Option<&str>, - ) -> String { - let phone_number = phone_number.format().mode(phonenumber::Mode::E164); - if let Some(cl) = challenge { - format!( - "/v1/accounts/{}/code/{}?challenge={}", - msg_type, phone_number, cl - ) - } else if let Some(cc) = captcha { - format!( - "/v1/accounts/{}/code/{}?captcha={}", - msg_type, phone_number, cc - ) - } else { - format!("/v1/accounts/{}/code/{}", msg_type, phone_number) - } - } - - async fn request_sms_verification_code( - &mut self, - phone_number: PhoneNumber, - captcha: Option<&str>, - challenge: Option<&str>, - ) -> Result { - let res = match self - .get_json( - Endpoint::Service, - Self::build_verification_code_request_url( - "sms", - phone_number, - captcha, - challenge, - ) - .as_ref(), - None, - ) - .await - { - Err(ServiceError::JsonDecodeError { .. }) => Ok(()), - r => r, - }; - match res { - Ok(_) => Ok(SmsVerificationCodeResponse::SmsSent), - Err(ServiceError::UnhandledResponseCode { http_code: 402 }) => { - Ok(SmsVerificationCodeResponse::CaptchaRequired) - } - Err(e) => Err(e), - } - } - - async fn request_voice_verification_code( - &mut self, - phone_number: PhoneNumber, - captcha: Option<&str>, - challenge: Option<&str>, - ) -> Result { - let res = match self - .get_json( - Endpoint::Service, - Self::build_verification_code_request_url( - "voice", - phone_number, - captcha, - challenge, - ) - .as_ref(), - None, - ) - .await - { - Err(ServiceError::JsonDecodeError { .. }) => Ok(()), - r => r, - }; - match res { - Ok(_) => Ok(VoiceVerificationCodeResponse::CallIssued), - Err(ServiceError::UnhandledResponseCode { http_code: 402 }) => { - Ok(VoiceVerificationCodeResponse::CaptchaRequired) - } - Err(e) => Err(e), - } - } - /// Fetches a list of all devices tied to the authenticated account. /// /// This list include the device that sends the request. @@ -547,70 +375,6 @@ pub trait PushService { .await } - async fn send_provisioning_message( - &mut self, - destination: &str, - env: ProvisionEnvelope, - ) -> Result<(), ServiceError> { - #[derive(serde::Serialize)] - struct ProvisioningMessage { - body: String, - } - - let mut body = Vec::with_capacity(env.encoded_len()); - env.encode(&mut body).expect("infallible encode"); - - self.put_json( - Endpoint::Service, - &format!("/v1/provisioning/{}", destination), - &ProvisioningMessage { - body: base64::encode(body), - }, - ) - .await - } - - async fn new_device_provisioning_code( - &mut self, - ) -> Result { - #[derive(serde::Deserialize)] - #[serde(rename_all = "camelCase")] - struct DeviceCode { - verification_code: String, - } - - let dc: DeviceCode = self - .get_json(Endpoint::Service, "/v1/devices/provisioning/code", None) - .await?; - Ok(dc.verification_code) - } - - async fn confirm_verification_code( - &mut self, - confirm_code: u32, - confirm_verification_message: ConfirmCodeMessage, - ) -> Result { - self.put_json( - Endpoint::Service, - &format!("/v1/accounts/code/{}", confirm_code), - confirm_verification_message, - ) - .await - } - - async fn confirm_device( - &mut self, - confirm_code: u32, - confirm_code_message: ConfirmDeviceMessage, - ) -> Result { - self.put_json( - Endpoint::Service, - &format!("/v1/devices/{}", confirm_code), - confirm_code_message, - ) - .await - } - async fn get_pre_key_status( &mut self, ) -> Result { @@ -622,7 +386,7 @@ pub trait PushService { pre_key_state: PreKeyState, ) -> Result<(), ServiceError> { match self - .put_json(Endpoint::Service, "/v2/keys/", pre_key_state) + .put_json(Endpoint::Service, "/v2/keys/", None, pre_key_state) .await { Err(ServiceError::JsonDecodeError { .. }) => Ok(()), @@ -662,7 +426,8 @@ pub trait PushService { messages: OutgoingPushMessages<'a>, ) -> Result { let path = format!("/v1/messages/{}", messages.destination); - self.put_json(Endpoint::Service, &path, messages).await + self.put_json(Endpoint::Service, &path, None, messages) + .await } /// Request AttachmentV2UploadAttributes @@ -838,7 +603,12 @@ pub trait PushService { ); match self - .put_json(Endpoint::Service, "/v1/accounts/attributes/", attributes) + .put_json( + Endpoint::Service, + "/v1/accounts/attributes/", + None, + attributes, + ) .await { Err(ServiceError::JsonDecodeError { .. }) => Ok(()), @@ -895,7 +665,7 @@ pub trait PushService { // XXX this should be a struct; cfr ProfileAvatarUploadAttributes let response: Result = self - .put_json(Endpoint::Service, "/v1/profile", command) + .put_json(Endpoint::Service, "/v1/profile", None, command) .await; match (response, avatar) { (Ok(_url), Some(_avatar)) => {