diff --git a/libsignal-service/src/account_manager.rs b/libsignal-service/src/account_manager.rs index 59355eaeb..35dc382dc 100644 --- a/libsignal-service/src/account_manager.rs +++ b/libsignal-service/src/account_manager.rs @@ -11,11 +11,13 @@ use libsignal_protocol::{ PreKeyRecord, PrivateKey, ProtocolStore, PublicKey, SignalProtocolError, SignedPreKeyRecord, }; +use prost::Message; use serde::{Deserialize, Serialize}; use sha2::Sha256; use zkgroup::profiles::ProfileKey; use crate::pre_keys::KyberPreKeyEntity; +use crate::proto::DeviceName; use crate::push_service::{AvatarWrite, RecaptchaAttributes, ServiceIdType}; use crate::ServiceAddress; use crate::{ @@ -28,7 +30,7 @@ use crate::{ push_service::{ AccountAttributes, HttpAuthOverride, PushService, ServiceError, }, - utils::{serde_base64, serde_public_key}, + utils::serde_base64, }; pub struct AccountManager { @@ -259,8 +261,6 @@ impl AccountManager { destination: &str, env: ProvisionEnvelope, ) -> Result<(), ServiceError> { - use prost::Message; - #[derive(serde::Serialize)] struct ProvisioningMessage { body: String, @@ -465,15 +465,12 @@ impl AccountManager { device_name: &str, public_key: &PublicKey, ) -> Result<(), ServiceError> { - let encrypted_device_name: DeviceName = encrypt_device_name( + let encrypted_device_name = encrypt_device_name( &mut rand::thread_rng(), device_name, public_key, )?; - let encrypted_device_name_proto: crate::proto::DeviceName = - encrypted_device_name.clone().into_proto()?; - #[derive(Serialize)] #[serde(rename_all = "camelCase")] struct Data { @@ -488,9 +485,7 @@ impl AccountManager { &[], HttpAuthOverride::NoOverride, Data { - device_name: prost::Message::encode_to_vec( - &encrypted_device_name_proto, - ), + device_name: encrypted_device_name.encode_to_vec(), }, ) .await?; @@ -526,30 +521,6 @@ impl AccountManager { } } -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct DeviceName { - #[serde(with = "serde_public_key")] - ephemeral_public: PublicKey, - #[serde(with = "serde_base64")] - synthetic_iv: Vec, - #[serde(with = "serde_base64")] - ciphertext: Vec, -} - -impl DeviceName { - pub(crate) fn into_proto( - self, - ) -> Result { - Ok(crate::proto::DeviceName { - ephemeral_public: Some( - self.ephemeral_public.public_key_bytes()?.to_vec(), - ), - synthetic_iv: Some(self.synthetic_iv.to_vec()), - ciphertext: Some(self.ciphertext.clone()), - }) - } -} - fn calculate_hmac256( mac_key: &[u8], ciphertext: &[u8], @@ -586,11 +557,15 @@ pub fn encrypt_device_name( ); cipher.apply_keystream(&mut ciphertext); - Ok(DeviceName { - ephemeral_public: ephemeral_key_pair.public_key, - synthetic_iv, - ciphertext, - }) + let device_name = DeviceName { + ephemeral_public: Some( + ephemeral_key_pair.public_key.serialize().to_vec(), + ), + synthetic_iv: Some(synthetic_iv.to_vec()), + ciphertext: Some(ciphertext), + }; + + Ok(device_name) } pub fn decrypt_device_name( @@ -598,12 +573,17 @@ pub fn decrypt_device_name( device_name: &DeviceName, ) -> Result { let DeviceName { - ephemeral_public, - synthetic_iv, - ciphertext, - } = device_name; + ephemeral_public: Some(ephemeral_public), + synthetic_iv: Some(synthetic_iv), + ciphertext: Some(ciphertext), + } = device_name + else { + return Err(ServiceError::InvalidDeviceName); + }; + + let ephemeral_public = PublicKey::deserialize(ephemeral_public)?; - let master_secret = private_key.calculate_agreement(ephemeral_public)?; + let master_secret = private_key.calculate_agreement(&ephemeral_public)?; let key2 = calculate_hmac256(&master_secret, b"cipher")?; let cipher_key = calculate_hmac256(&key2, synthetic_iv)?; @@ -661,11 +641,11 @@ mod tests { )?)?; let device_name = DeviceName { - ephemeral_public: ephemeral_public_key, - synthetic_iv: base64::decode("86gekHGmltnnZ9QARhiFcg==")?, - ciphertext: base64::decode( + ephemeral_public: Some(ephemeral_public_key.serialize().to_vec()), + synthetic_iv: Some(base64::decode("86gekHGmltnnZ9QARhiFcg==")?), + ciphertext: Some(base64::decode( "MtJ9/9KBWLBVAxfZJD4pLKzP4q+iodRJeCc+/A==", - )?, + )?), }; let decrypted_device_name = diff --git a/libsignal-service/src/pre_keys.rs b/libsignal-service/src/pre_keys.rs index 4dc86e13b..c1c45ca6d 100644 --- a/libsignal-service/src/pre_keys.rs +++ b/libsignal-service/src/pre_keys.rs @@ -1,13 +1,48 @@ -use std::convert::TryFrom; +use std::{convert::TryFrom, time::SystemTime}; use crate::utils::{serde_base64, serde_public_key}; use libsignal_protocol::{ - error::SignalProtocolError, GenericSignedPreKey, KyberPreKeyRecord, - PreKeyRecord, PublicKey, SignedPreKeyRecord, + error::SignalProtocolError, kem, GenericSignedPreKey, KeyPair, + KyberPreKeyRecord, KyberPreKeyStore, PreKeyRecord, PreKeyStore, PublicKey, + SignedPreKeyRecord, SignedPreKeyStore, }; use serde::{Deserialize, Serialize}; +/// Stores the ID of keys published ahead of time +/// +/// +pub trait PreKeysStore: + PreKeyStore + SignedPreKeyStore + KyberPreKeyStore +{ + /// ID of the next pre key + fn pre_keys_offset_id(&self) -> Result; + + /// ID of the next signed pre key + fn next_signed_pre_key_id(&self) -> Result; + + /// ID of the next PQ pre key + fn next_pq_pre_key_id(&self) -> Result; + + /// set the ID of the next pre key + fn set_pre_keys_offset_id( + &mut self, + id: u32, + ) -> Result<(), SignalProtocolError>; + + /// set the ID of the next signed pre key + fn set_next_signed_pre_key_id( + &mut self, + id: u32, + ) -> Result<(), SignalProtocolError>; + + /// set the ID of the next PQ pre key + fn set_next_pq_pre_key_id( + &mut self, + id: u32, + ) -> Result<(), SignalProtocolError>; +} + #[derive(Debug, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct PreKeyEntity { @@ -92,3 +127,51 @@ pub struct PreKeyState { pub pq_last_resort_key: Option, pub pq_pre_keys: Vec, } + +pub(crate) async fn generate_last_resort_kyber_key( + store: &mut S, + identity_key: &KeyPair, +) -> Result { + let id = store.next_pq_pre_key_id()?; + let id = id.max(1); // TODO: Hack, keys start with 1 + + let record = KyberPreKeyRecord::generate( + kem::KeyType::Kyber1024, + id.into(), + &identity_key.private_key, + )?; + + store.save_kyber_pre_key(id.into(), &record).await?; + store.set_next_pq_pre_key_id(id + 1)?; + + Ok(record) +} + +pub(crate) async fn generate_signed_pre_key< + S: PreKeysStore, + R: rand::Rng + rand::CryptoRng, +>( + store: &mut S, + csprng: &mut R, + identity_key: &KeyPair, +) -> Result { + let id = store.next_signed_pre_key_id()?; + let id = id.max(1); // TODO: Hack, keys start with 1 + + let key_pair = KeyPair::generate(csprng); + let signature = identity_key + .private_key + .calculate_signature(&key_pair.public_key.serialize(), csprng)?; + + let unix_time = SystemTime::now() + .duration_since(SystemTime::UNIX_EPOCH) + .unwrap() + .as_millis() as u64; + let record = + SignedPreKeyRecord::new(id.into(), unix_time, &key_pair, &signature); + + store.save_signed_pre_key(id.into(), &record).await?; + store.set_next_signed_pre_key_id(id + 1)?; + + Ok(record) +} diff --git a/libsignal-service/src/provisioning/manager.rs b/libsignal-service/src/provisioning/manager.rs index d18ce351e..94cf292f6 100644 --- a/libsignal-service/src/provisioning/manager.rs +++ b/libsignal-service/src/provisioning/manager.rs @@ -1,10 +1,13 @@ +use std::convert::TryInto; + use derivative::Derivative; use futures::{channel::mpsc::Sender, pin_mut, SinkExt, StreamExt}; -use libsignal_protocol::{PrivateKey, PublicKey}; -use phonenumber::PhoneNumber; +use libsignal_protocol::{DeviceId, KeyPair, PrivateKey, PublicKey}; +use prost::Message; use serde::{Deserialize, Serialize}; use url::Url; use uuid::Uuid; +use zkgroup::profiles::ProfileKey; use super::{ pipe::{ProvisioningPipe, ProvisioningStep}, @@ -12,10 +15,13 @@ use super::{ }; use crate::{ - configuration::{Endpoint, ServiceCredentials, SignalingKey}, - push_service::{ - DeviceId, HttpAuthOverride, PushService, ServiceError, ServiceIds, + account_manager::encrypt_device_name, + configuration::Endpoint, + pre_keys::{ + generate_last_resort_kyber_key, generate_signed_pre_key, + KyberPreKeyEntity, PreKeysStore, SignedPreKey, }, + push_service::{HttpAuth, HttpAuthOverride, PushService, ServiceIds}, utils::serde_base64, }; @@ -40,82 +46,33 @@ pub struct ConfirmCodeResponse { pub storage_capable: bool, } -pub struct ProvisioningManager<'a, P: PushService + 'a> { - push_service: &'a mut P, - phone_number: PhoneNumber, - password: String, -} - -#[derive(Derivative)] -#[derivative(Debug)] +#[derive(Debug)] pub enum SecondaryDeviceProvisioning { Url(Url), - NewDeviceRegistration { - phone_number: phonenumber::PhoneNumber, - device_id: DeviceId, - registration_id: u32, - pni_registration_id: u32, - service_ids: ServiceIds, - #[derivative(Debug = "ignore")] - aci_private_key: PrivateKey, - aci_public_key: PublicKey, - #[derivative(Debug = "ignore")] - pni_private_key: PrivateKey, - pni_public_key: PublicKey, - #[derivative(Debug = "ignore")] - profile_key: Vec, - }, + NewDeviceRegistration(NewDeviceRegistration), } -impl<'a, P: PushService + 'a> ProvisioningManager<'a, P> { - pub fn new( - push_service: &'a mut P, - phone_number: PhoneNumber, - password: String, - ) -> Self { - Self { - push_service, - phone_number, - password, - } - } - - pub(crate) async fn confirm_device( - &mut self, - confirm_code: &str, - confirm_code_message: ConfirmDeviceMessage, - ) -> Result { - self.push_service - .put_json( - Endpoint::Service, - &format!("/v1/devices/{}", confirm_code), - &[], - self.auth_override(), - confirm_code_message, - ) - .await - } - - fn auth_override(&self) -> HttpAuthOverride { - let credentials = ServiceCredentials { - uuid: None, - phonenumber: self.phone_number.clone(), - password: Some(self.password.clone()), - signaling_key: None, - device_id: None, - }; - if let Some(auth) = credentials.authorization() { - HttpAuthOverride::Identified(auth) - } else { - HttpAuthOverride::NoOverride - } - } +#[derive(Derivative)] +#[derivative(Debug)] +pub struct NewDeviceRegistration { + pub phone_number: phonenumber::PhoneNumber, + pub device_id: DeviceId, + pub registration_id: u32, + pub pni_registration_id: u32, + pub service_ids: ServiceIds, + #[derivative(Debug = "ignore")] + pub aci_private_key: PrivateKey, + pub aci_public_key: PublicKey, + #[derivative(Debug = "ignore")] + pub pni_private_key: PrivateKey, + pub pni_public_key: PublicKey, + #[derivative(Debug = "ignore")] + pub profile_key: ProfileKey, } #[derive(Clone)] pub struct LinkingManager { push_service: P, - // forwarded to the `ProvisioningManager` password: String, } @@ -127,10 +84,15 @@ impl LinkingManager

{ } } - pub async fn provision_secondary_device( + pub async fn provision_secondary_device< + S: PreKeysStore, + R: rand::Rng + rand::CryptoRng, + >( &mut self, + aci_store: &mut S, + pni_store: &mut S, csprng: &mut R, - signaling_key: SignalingKey, + device_name: &str, mut tx: Sender, ) -> Result<(), ProvisioningError> { // open a websocket without authentication, to receive a tsurl:// @@ -158,32 +120,6 @@ impl LinkingManager

{ .expect("failed to send provisioning Url in channel"); }, Ok(ProvisioningStep::Message(message)) => { - let aci_uuid = message - .aci - .ok_or(ProvisioningError::InvalidData { - reason: "missing client UUID".into(), - }) - .and_then(|ref s| { - Uuid::parse_str(s).map_err(|e| { - ProvisioningError::InvalidData { - reason: format!("invalid UUID: {}", e), - } - }) - })?; - - let pni_uuid = message - .pni - .ok_or(ProvisioningError::InvalidData { - reason: "missing client UUID".into(), - }) - .and_then(|ref s| { - Uuid::parse_str(s).map_err(|e| { - ProvisioningError::InvalidData { - reason: format!("invalid UUID: {}", e), - } - }) - })?; - let aci_public_key = PublicKey::deserialize( &message.aci_identity_key_public.ok_or( ProvisioningError::InvalidData { @@ -235,48 +171,145 @@ impl LinkingManager

{ } })?; - let mut provisioning_manager = ProvisioningManager::new( - &mut self.push_service, - phone_number.clone(), - self.password.clone(), - ); - let provisioning_code = message.provisioning_code.ok_or( ProvisioningError::InvalidData { reason: "no provisioning confirmation code".into(), }, )?; - let device_id = provisioning_manager - .confirm_device( - &provisioning_code, - ConfirmDeviceMessage { - signaling_key: signaling_key.to_vec(), - supports_sms: false, - fetches_messages: true, - registration_id, - pni_registration_id, - name: vec![], - }, + let aci_key_pair = + KeyPair::new(aci_public_key, aci_private_key); + let pni_key_pair = + KeyPair::new(pni_public_key, pni_private_key); + + let aci_pq_last_resort_pre_key = + generate_last_resort_kyber_key( + aci_store, + &aci_key_pair, + ) + .await?; + let pni_pq_last_resort_pre_key = + generate_last_resort_kyber_key( + pni_store, + &pni_key_pair, ) .await?; - tx.send( - SecondaryDeviceProvisioning::NewDeviceRegistration { - phone_number, - device_id, + let aci_signed_pre_key = generate_signed_pre_key( + aci_store, + csprng, + &aci_key_pair, + ) + .await?; + let pni_signed_pre_key = generate_signed_pre_key( + pni_store, + csprng, + &pni_key_pair, + ) + .await?; + + #[derive(Debug, Serialize)] + #[serde(rename_all = "camelCase")] + struct LinkRequest { + verification_code: String, + account_attributes: AccountAttributes, + aci_signed_pre_key: SignedPreKey, + pni_signed_pre_key: SignedPreKey, + aci_pq_last_resort_pre_key: KyberPreKeyEntity, + pni_pq_last_resort_pre_key: KyberPreKeyEntity, + } + + #[derive(Debug, Serialize)] + #[serde(rename_all = "camelCase")] + struct AccountAttributes { + fetches_messages: bool, + name: String, + registration_id: u32, + pni_registration_id: u32, + capabilities: Capabilities, + } + + #[derive(Debug, Serialize)] + #[serde(rename_all = "camelCase")] + struct Capabilities { + pni: bool, + } + + let encrypted_device_name = base64::encode( + encrypt_device_name( + csprng, + device_name, + &aci_public_key, + )? + .encode_to_vec(), + ); + + let profile_key = ProfileKey::create( + profile_key + .as_slice() + .try_into() + .map_err(ProvisioningError::InvalidProfileKey)?, + ); + + let request = LinkRequest { + verification_code: provisioning_code, + account_attributes: AccountAttributes { registration_id, pni_registration_id, - service_ids: ServiceIds { - aci: aci_uuid, - pni: pni_uuid, - }, - aci_private_key, - aci_public_key, - pni_private_key, - pni_public_key, - profile_key, + fetches_messages: true, + capabilities: Capabilities { pni: true }, + name: encrypted_device_name, }, + aci_signed_pre_key: aci_signed_pre_key.try_into()?, + pni_signed_pre_key: pni_signed_pre_key.try_into()?, + aci_pq_last_resort_pre_key: aci_pq_last_resort_pre_key + .try_into()?, + pni_pq_last_resort_pre_key: pni_pq_last_resort_pre_key + .try_into()?, + }; + + #[derive(Debug, Deserialize)] + #[serde(rename_all = "camelCase")] + struct LinkResponse { + #[serde(rename = "uuid")] + aci: Uuid, + pni: Uuid, + device_id: u32, + } + + let LinkResponse { + aci, + pni, + device_id, + } = self + .push_service + .put_json( + Endpoint::Service, + "/v1/devices/link", + &[], + HttpAuthOverride::Identified(HttpAuth { + username: phone_number.to_string(), + password: self.password.clone(), + }), + &request, + ) + .await?; + + tx.send( + SecondaryDeviceProvisioning::NewDeviceRegistration( + NewDeviceRegistration { + phone_number, + service_ids: ServiceIds { aci, pni }, + device_id: device_id.into(), + registration_id, + pni_registration_id, + aci_private_key, + aci_public_key, + pni_private_key, + pni_public_key, + profile_key, + }, + ), ) .await .expect( diff --git a/libsignal-service/src/provisioning/mod.rs b/libsignal-service/src/provisioning/mod.rs index 7508c2c85..87e6479dc 100644 --- a/libsignal-service/src/provisioning/mod.rs +++ b/libsignal-service/src/provisioning/mod.rs @@ -2,9 +2,11 @@ pub(crate) mod cipher; pub(crate) mod manager; pub(crate) mod pipe; +use std::array::TryFromSliceError; + pub use cipher::ProvisioningCipher; pub use manager::{ - ConfirmCodeResponse, LinkingManager, ProvisioningManager, + ConfirmCodeResponse, LinkingManager, NewDeviceRegistration, SecondaryDeviceProvisioning, }; @@ -30,6 +32,8 @@ pub enum ProvisioningError { ProtocolError(#[from] libsignal_protocol::error::SignalProtocolError), #[error("ProvisioningCipher in encrypt-only mode")] EncryptOnlyProvisioningCipher, + #[error("invalid profile key bytes")] + InvalidProfileKey(TryFromSliceError), } pub fn generate_registration_id( diff --git a/libsignal-service/src/push_service.rs b/libsignal-service/src/push_service.rs index d5c2cb8d4..1536f1817 100644 --- a/libsignal-service/src/push_service.rs +++ b/libsignal-service/src/push_service.rs @@ -522,6 +522,9 @@ pub enum ServiceError { #[error("Not found.")] NotFoundError, + + #[error("invalid device name")] + InvalidDeviceName, } #[cfg_attr(feature = "unsend-futures", async_trait::async_trait(?Send))]