From 5109b7b7b264d9955d4959ea65cb9114529bec5a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gabriel=20F=C3=A9ron?= Date: Sun, 14 Aug 2022 13:10:46 +0200 Subject: [PATCH] Implement device name encryption/decryption/update --- libsignal-service/Cargo.toml | 2 +- libsignal-service/protobuf/DeviceName.proto | 10 + libsignal-service/src/account_manager.rs | 176 +++++++++++++++++- libsignal-service/src/cipher.rs | 1 + libsignal-service/src/groups_v2/operations.rs | 6 +- libsignal-service/src/lib.rs | 3 +- libsignal-service/src/provisioning/manager.rs | 18 +- libsignal-service/src/provisioning/mod.rs | 5 +- libsignal-service/src/push_service.rs | 8 +- 9 files changed, 213 insertions(+), 16 deletions(-) create mode 100644 libsignal-service/protobuf/DeviceName.proto diff --git a/libsignal-service/Cargo.toml b/libsignal-service/Cargo.toml index 2d603dae1..2c4b5acc2 100644 --- a/libsignal-service/Cargo.toml +++ b/libsignal-service/Cargo.toml @@ -45,7 +45,7 @@ prost-build = "0.10" [dev-dependencies] anyhow = "1.0" -tokio = { version = "1.0", features = ["rt", "macros"] } +tokio = { version = "1.0", features = ["macros", "rt"] } rustls = "0.20" diff --git a/libsignal-service/protobuf/DeviceName.proto b/libsignal-service/protobuf/DeviceName.proto new file mode 100644 index 000000000..512d76505 --- /dev/null +++ b/libsignal-service/protobuf/DeviceName.proto @@ -0,0 +1,10 @@ +// Copyright 2018-2021 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only + +package signalservice; + +message DeviceName { + optional bytes ephemeralPublic = 1; + optional bytes syntheticIv = 2; + optional bytes ciphertext = 3; +} diff --git a/libsignal-service/src/account_manager.rs b/libsignal-service/src/account_manager.rs index 5fed1195e..9d5497603 100644 --- a/libsignal-service/src/account_manager.rs +++ b/libsignal-service/src/account_manager.rs @@ -2,10 +2,16 @@ use std::collections::HashMap; use std::convert::{TryFrom, TryInto}; use std::time::SystemTime; +use aes::cipher::generic_array::GenericArray; +use aes::cipher::{NewCipher, StreamCipher}; +use aes::Aes256Ctr; +use hmac::{Hmac, Mac}; use libsignal_protocol::{ - IdentityKeyStore, KeyPair, PreKeyRecord, PreKeyStore, PublicKey, - SignalProtocolError, SignedPreKeyRecord, SignedPreKeyStore, + IdentityKeyStore, KeyPair, PreKeyRecord, PreKeyStore, PrivateKey, + PublicKey, SignalProtocolError, SignedPreKeyRecord, SignedPreKeyStore, }; +use serde::{Deserialize, Serialize}; +use sha2::Sha256; use zkgroup::profiles::ProfileKey; use crate::{ @@ -18,6 +24,7 @@ use crate::{ push_service::{ AccountAttributes, HttpAuthOverride, PushService, ServiceError, }, + utils::{serde_base64, serde_public_key}, }; pub struct AccountManager { @@ -401,4 +408,169 @@ impl AccountManager { ) -> Result<(), ServiceError> { self.service.set_account_attributes(attributes).await } + + /// Update (encrypted) device name + pub async fn update_device_name( + &mut self, + device_name: &str, + public_key: &PublicKey, + ) -> Result<(), ServiceError> { + let encrypted_device_name: DeviceName = 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 { + #[serde(with = "serde_base64")] + device_name: Vec, + } + + self.service + .put_json( + Endpoint::Service, + "/v1/accounts/name", + HttpAuthOverride::NoOverride, + Data { + device_name: prost::Message::encode_to_vec( + &encrypted_device_name_proto, + ), + }, + ) + .await?; + + Ok(()) + } +} + +#[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], +) -> Result, ServiceError> { + let mut mac = Hmac::::new_from_slice(mac_key) + .map_err(|_| ServiceError::MacError)?; + mac.update(ciphertext); + Ok(mac.finalize().into_bytes().to_vec()) +} + +pub fn encrypt_device_name( + csprng: &mut R, + device_name: &str, + identity_public: &PublicKey, +) -> Result { + let plaintext = device_name.as_bytes().to_vec(); + let ephemeral_key_pair = KeyPair::generate(csprng); + + let master_secret = ephemeral_key_pair + .private_key + .calculate_agreement(identity_public)?; + + let key1 = calculate_hmac256(&master_secret, b"auth")?; + let mut synthetic_iv = calculate_hmac256(&key1, &plaintext)?; + synthetic_iv.truncate(16); + + let key2 = calculate_hmac256(&master_secret, b"cipher")?; + let cipher_key = calculate_hmac256(&key2, &synthetic_iv)?; + + let mut ciphertext = plaintext; + let mut cipher = Aes256Ctr::new( + GenericArray::from_slice(&cipher_key), + GenericArray::from_slice(&[0u8; 16]), + ); + cipher.apply_keystream(&mut ciphertext); + + Ok(DeviceName { + ephemeral_public: ephemeral_key_pair.public_key, + synthetic_iv, + ciphertext, + }) +} + +pub fn decrypt_device_name( + private_key: &PrivateKey, + device_name: &DeviceName, +) -> Result { + let DeviceName { + ephemeral_public, + synthetic_iv, + ciphertext, + } = device_name; + + 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)?; + + let mut plaintext = ciphertext.to_vec(); + let mut cipher = Aes256Ctr::new( + GenericArray::from_slice(&cipher_key), + GenericArray::from_slice(&[0u8; 16]), + ); + cipher.apply_keystream(&mut plaintext); + + let key1 = calculate_hmac256(&master_secret, b"auth")?; + let mut our_synthetic_iv = calculate_hmac256(&key1, &plaintext)?; + our_synthetic_iv.truncate(16); + + if synthetic_iv != &our_synthetic_iv { + Err(ServiceError::MacError) + } else { + Ok(String::from_utf8_lossy(&plaintext).to_string()) + } +} + +#[cfg(test)] +mod tests { + use bytes::Bytes; + use libsignal_protocol::{KeyPair, PrivateKey, PublicKey}; + + use super::{decrypt_device_name, encrypt_device_name, DeviceName}; + + #[test] + fn encrypt_decrypt_device_name() -> anyhow::Result<()> { + let input_device_name = "Nokia 3310 Millenial Edition"; + let mut csprng = rand::thread_rng(); + let identity = KeyPair::generate(&mut csprng); + + let device_name = encrypt_device_name( + &mut csprng, + &input_device_name, + &identity.public_key, + )?; + + let decrypted_device_name = + decrypt_device_name(&identity.private_key, &device_name)?; + + assert_eq!(input_device_name, decrypted_device_name); + + Ok(()) + } } diff --git a/libsignal-service/src/cipher.rs b/libsignal-service/src/cipher.rs index 4da4cf0a3..66b5eb82f 100644 --- a/libsignal-service/src/cipher.rs +++ b/libsignal-service/src/cipher.rs @@ -43,6 +43,7 @@ where P: PreKeyStore + Clone, R: Rng + CryptoRng + Clone, { + #[allow(clippy::too_many_arguments)] pub fn new( session_store: S, identity_key_store: I, diff --git a/libsignal-service/src/groups_v2/operations.rs b/libsignal-service/src/groups_v2/operations.rs index 4188cb508..4191adf03 100644 --- a/libsignal-service/src/groups_v2/operations.rs +++ b/libsignal-service/src/groups_v2/operations.rs @@ -44,7 +44,7 @@ impl fmt::Debug for Member { } } -#[derive(Clone, Debug, PartialEq)] +#[derive(Clone, Debug, PartialEq, Eq)] pub struct PendingMember { pub uuid: Uuid, pub role: Role, @@ -185,12 +185,12 @@ impl fmt::Debug for GroupChange { } } -#[derive(Clone, Debug, PartialEq)] +#[derive(Clone, Debug, PartialEq, Eq)] pub struct Timer { pub duration: u32, } -#[derive(Clone, PartialEq)] +#[derive(Clone, PartialEq, Eq)] pub struct GroupJoinInfo { pub title: String, pub avatar: String, diff --git a/libsignal-service/src/lib.rs b/libsignal-service/src/lib.rs index 7535ed7fc..c447eda48 100644 --- a/libsignal-service/src/lib.rs +++ b/libsignal-service/src/lib.rs @@ -15,6 +15,7 @@ pub mod messagepipe; pub mod models; pub mod pre_keys; pub mod profile_name; +#[allow(clippy::derive_partial_eq_without_eq)] pub mod proto; pub mod provisioning; pub mod push_service; @@ -25,7 +26,7 @@ mod session_store; pub mod utils; pub use crate::account_manager::{ - AccountManager, Profile, ProfileManagerError, + decrypt_device_name, AccountManager, Profile, ProfileManagerError, }; pub use crate::service_address::*; diff --git a/libsignal-service/src/provisioning/manager.rs b/libsignal-service/src/provisioning/manager.rs index d639eff00..d0c223b66 100644 --- a/libsignal-service/src/provisioning/manager.rs +++ b/libsignal-service/src/provisioning/manager.rs @@ -22,14 +22,21 @@ use crate::{ /// Message received when linking a new secondary device. #[derive(Debug, Serialize)] #[serde(rename_all = "camelCase")] -pub struct ConfirmDeviceMessage { +pub(crate) struct ConfirmDeviceMessage { #[serde(with = "serde_base64")] pub signaling_key: Vec, pub supports_sms: bool, pub fetches_messages: bool, pub registration_id: u32, - // FIXME: the name goes back here when we send this via the websocket - //pub name: String, + #[serde(with = "serde_base64", skip_serializing_if = "Vec::is_empty")] + pub name: Vec, +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ConfirmCodeResponse { + pub uuid: Uuid, + pub storage_capable: bool, } #[derive(Debug, Eq, PartialEq)] @@ -151,7 +158,7 @@ impl<'a, P: PushService + 'a> ProvisioningManager<'a, P> { .await } - pub async fn confirm_device( + pub(crate) async fn confirm_device( &mut self, confirm_code: u32, confirm_code_message: ConfirmDeviceMessage, @@ -233,7 +240,7 @@ impl LinkingManager

{ .await?; // see libsignal-protocol-c / signal_protocol_key_helper_generate_registration_id - let registration_id = csprng.gen_range(1, 16380); + let registration_id = csprng.gen_range(1, 256); let provisioning_pipe = ProvisioningPipe::from_socket(ws, stream)?; let provision_stream = provisioning_pipe.stream(); @@ -315,6 +322,7 @@ impl LinkingManager

{ supports_sms: false, fetches_messages: true, registration_id, + name: vec![], }, ) .await?; diff --git a/libsignal-service/src/provisioning/mod.rs b/libsignal-service/src/provisioning/mod.rs index a238c2d51..4b76a7265 100644 --- a/libsignal-service/src/provisioning/mod.rs +++ b/libsignal-service/src/provisioning/mod.rs @@ -4,8 +4,9 @@ mod pipe; pub use cipher::ProvisioningCipher; pub use manager::{ - LinkingManager, ProvisioningManager, SecondaryDeviceProvisioning, - VerificationCodeResponse, VerifyAccountResponse, + ConfirmCodeResponse, LinkingManager, ProvisioningManager, + SecondaryDeviceProvisioning, VerificationCodeResponse, + VerifyAccountResponse, }; use crate::prelude::ServiceError; diff --git a/libsignal-service/src/push_service.rs b/libsignal-service/src/push_service.rs index a5c85bee8..74e35abf5 100644 --- a/libsignal-service/src/push_service.rs +++ b/libsignal-service/src/push_service.rs @@ -97,17 +97,21 @@ pub struct AccountAttributes { pub unrestricted_unidentified_access: bool, pub discoverable_by_phone_number: bool, pub capabilities: DeviceCapabilities, + pub name: String, } #[derive(Debug, Serialize, Deserialize, Default)] #[serde(rename_all = "camelCase")] pub struct DeviceCapabilities { - pub uuid: bool, + // pub uuid: bool, + // pub storage: bool, + pub announcement_group: bool, #[serde(rename = "gv2-3")] pub gv2: bool, - pub storage: bool, #[serde(rename = "gv1-migration")] pub gv1_migration: bool, + pub sender_key: bool, + pub change_number: bool, } #[derive(Clone)]