Skip to content

Commit

Permalink
feat!: canonicalize ClientId keeping only the regular version where t…
Browse files Browse the repository at this point in the history
…he UserId portion is the hyphenated string representation of the UUID. Also apply this to 'getUserIdentities()'
  • Loading branch information
beltram committed Nov 30, 2023
1 parent e16624f commit 2954f01
Show file tree
Hide file tree
Showing 20 changed files with 337 additions and 166 deletions.
12 changes: 8 additions & 4 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -49,25 +49,29 @@ branch = "2.x"
package = "openmls"
git = "https://github.com/wireapp/openmls"
#tag = "v1.0.0-pre.core-crypto-1.0.0"
branch = "feat/rfc9420"
#branch = "feat/rfc9420"
branch = "feat/canonicalize-clientid"

[patch.crates-io.openmls_traits]
package = "openmls_traits"
git = "https://github.com/wireapp/openmls"
#tag = "v1.0.0-pre.core-crypto-1.0.0"
branch = "feat/rfc9420"
#branch = "feat/rfc9420"
branch = "feat/canonicalize-clientid"

[patch.crates-io.openmls_basic_credential]
package = "openmls_basic_credential"
git = "https://github.com/wireapp/openmls"
#tag = "v1.0.0-pre.core-crypto-1.0.0"
branch = "feat/rfc9420"
#branch = "feat/rfc9420"
branch = "feat/canonicalize-clientid"

[patch.crates-io.openmls_x509_credential]
package = "openmls_x509_credential"
git = "https://github.com/wireapp/openmls"
#tag = "v1.0.0-pre.core-crypto-1.0.0"
branch = "feat/rfc9420"
#branch = "feat/rfc9420"
branch = "feat/canonicalize-clientid"

[patch.crates-io.hpke]
git = "https://github.com/wireapp/rust-hpke.git"
Expand Down
2 changes: 1 addition & 1 deletion crypto-ffi/bindings/js/CoreCrypto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1821,7 +1821,7 @@ export class CoreCrypto {
* If no member has a x509 certificate, it will return an empty Vec.
*
* @param conversationId - identifier of the conversation
* @param userIds - user identifiers e.g. t6wRpI8BRSeviBwwiFp5MQ which is a base64UrlUnpadded UUIDv4
* @param userIds - user identifiers hyphenated UUIDv4 e.g. 'bd4c7053-1c5a-4020-9559-cd7bf7961954'
* @returns a Map with all the identities for a given users. Consumers are then recommended to reduce those identities to determine the actual status of a user.
*/
async getUserIdentities(conversationId: ConversationId, userIds: string[]): Promise<Map<string, WireIdentity[]>> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -466,7 +466,7 @@ class MLSClient(private val cc: com.wire.crypto.CoreCrypto) {
* If no member has a x509 certificate, it will return an empty Vec.
*
* @param id conversation identifier
* @param userIds user identifiers e.g. t6wRpI8BRSeviBwwiFp5MQ which is a base64UrlUnpadded UUIDv4
* @param userIds user identifiers hyphenated UUIDv4 e.g. 'bd4c7053-1c5a-4020-9559-cd7bf7961954'
* @returns a Map with all the identities for a given users. Consumers are then recommended to reduce those identities to determine the actual status of a user.
*/
suspend fun getUserIdentities(id: MLSGroupId, userIds: List<String>): Map<String, List<WireIdentity>> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1258,7 +1258,7 @@ public class CoreCryptoWrapper {
/// If no member has a x509 certificate, it will return an empty Vec.
///
/// - parameter conversationId: conversation identifier
/// - parameter userIds: user identifiers e.g. t6wRpI8BRSeviBwwiFp5MQ which is a base64UrlUnpadded UUIDv4
/// - parameter userIds: user identifiers hyphenated UUIDv4 e.g. 'bd4c7053-1c5a-4020-9559-cd7bf7961954'
/// - returns: a Map with all the identities for a given users. Consumers are then recommended to reduce those identities to determine the actual status of a user.
public func getUserIdentities(conversationId: ConversationId, userIds: [String]) async throws -> [String: [WireIdentity]] {
return try await self.coreCrypto.getUserIdentities(conversationId: conversationId, userIds: userIds)
Expand Down
2 changes: 2 additions & 0 deletions crypto/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,8 @@ oid-registry = "0.6"
async-recursion = "1"
uniffi = { workspace = true, optional = true }
itertools = "0.12"
base64-simd = "0.8"
uuid = { version = "1.6", features = ["v4"] }

[dependencies.proteus-wasm]
version = "2.1"
Expand Down
4 changes: 2 additions & 2 deletions crypto/src/e2e_identity/conversation_state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -199,7 +199,7 @@ pub mod tests {
not_after: expiration,
..Default::default()
};
let cert = CertificateBundle::new_from_builder(case.signature_scheme(), builder);
let cert = CertificateBundle::new_from_builder(builder, case.signature_scheme());
let cb = Client::new_x509_credential_bundle(cert).unwrap();
let commit = alice_central.e2ei_rotate(&id, &cb).await.unwrap().commit;
alice_central.commit_accepted(&id).await.unwrap();
Expand Down Expand Up @@ -247,7 +247,7 @@ pub mod tests {
not_after: expiration,
..Default::default()
};
let cert = CertificateBundle::new_from_builder(case.signature_scheme(), builder);
let cert = CertificateBundle::new_from_builder(builder, case.signature_scheme());
let cb = Client::new_x509_credential_bundle(cert).unwrap();
alice_central.e2ei_rotate(&id, &cb).await.unwrap();
alice_central.commit_accepted(&id).await.unwrap();
Expand Down
143 changes: 143 additions & 0 deletions crypto/src/e2e_identity/id.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
use crate::{prelude::ClientId, CryptoError, CryptoResult};

#[cfg(test)]
const DOMAIN: &str = "wire.com";
const COLON: u8 = 58;

/// This format: 'bd4c7053-1c5a-4020-9559-cd7bf7961954:[email protected]'
#[derive(Debug, Clone, PartialEq, Eq, Hash, derive_more::From, derive_more::Into, derive_more::Deref)]
pub struct WireQualifiedClientId(ClientId);

#[cfg(test)]
impl WireQualifiedClientId {
pub fn get_user_id(&self) -> String {
let mut split = self.0.split(|b| b == &COLON);
let user_id = split.next().unwrap();
uuid::Uuid::try_parse_ascii(user_id).unwrap().to_string()
}
}

#[cfg(test)]
impl WireQualifiedClientId {
pub fn generate() -> Self {
let user_id = uuid::Uuid::new_v4().to_string();
let device_id = rand::random::<u64>();
let client_id = format!("{user_id}:{device_id:x}@{DOMAIN}");
Self(client_id.into_bytes().into())
}

pub fn to_static_str(&self) -> &'static str {
Box::leak(Box::new(self.to_string()))
}
}

#[cfg(test)]
impl std::fmt::Display for WireQualifiedClientId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", std::str::from_utf8(self.as_slice()).unwrap())
}
}

/// e.g. from 'vUxwUxxaQCCVWc1795YZVA:[email protected]'
impl<'a> TryFrom<&'a [u8]> for WireQualifiedClientId {
type Error = CryptoError;

fn try_from(bytes: &'a [u8]) -> CryptoResult<Self> {
const COLON: u8 = 58;
let mut split = bytes.split(|b| b == &COLON);
let user_id = split.next().ok_or(CryptoError::InvalidClientId)?;

let user_id = base64_simd::URL_SAFE_NO_PAD
.decode_to_vec(user_id)
.map_err(|_| CryptoError::InvalidClientId)?;

let user_id = uuid::Uuid::from_slice(&user_id).map_err(|_| CryptoError::InvalidClientId)?;
let mut buf = [0; uuid::fmt::Hyphenated::LENGTH];
let user_id = user_id.hyphenated().encode_lower(&mut buf);

let rest = split.next().ok_or(CryptoError::InvalidClientId)?;
if split.next().is_some() {
return Err(CryptoError::InvalidClientId);
}

let client_id = [user_id.as_bytes(), &[COLON], rest].concat();
Ok(Self(client_id.into()))
}
}

impl std::str::FromStr for WireQualifiedClientId {
type Err = CryptoError;

fn from_str(s: &str) -> CryptoResult<Self> {
s.as_bytes().try_into()
}
}

/// This format: 'vUxwUxxaQCCVWc1795YZVA:[email protected]'
#[derive(Debug, Clone, PartialEq, Eq, Hash, derive_more::From, derive_more::Into, derive_more::Deref)]
pub struct QualifiedE2eiClientId(ClientId);

#[cfg(test)]
impl QualifiedE2eiClientId {
pub fn generate() -> Self {
Self::generate_from_user_id(&uuid::Uuid::new_v4())
}

pub fn generate_from_user_id(user_id: &uuid::Uuid) -> Self {
use base64::Engine as _;

let user_id = base64::prelude::BASE64_URL_SAFE_NO_PAD.encode(user_id.as_bytes());

let device_id = rand::random::<u64>();
let client_id = format!("{user_id}:{device_id:x}@{DOMAIN}");
Self(client_id.into_bytes().into())
}

pub fn to_static_str(&self) -> &'static str {
Box::leak(Box::new(self.to_string()))
}

pub fn from_str_unchecked(s: &str) -> Self {
Self(s.as_bytes().into())
}
}

/// e.g. from 'bd4c7053-1c5a-4020-9559-cd7bf7961954:[email protected]'
impl<'a> TryFrom<&'a [u8]> for QualifiedE2eiClientId {
type Error = CryptoError;

fn try_from(bytes: &'a [u8]) -> CryptoResult<Self> {
let mut split = bytes.split(|b| b == &COLON);
let user_id = split.next().ok_or(CryptoError::InvalidClientId)?;

let user_id = std::str::from_utf8(user_id)?
.parse::<uuid::Uuid>()
.map_err(|_| CryptoError::InvalidClientId)?;

let user_id = base64_simd::URL_SAFE_NO_PAD.encode_to_string(user_id.as_bytes());

let rest = split.next().ok_or(CryptoError::InvalidClientId)?;
if split.next().is_some() {
return Err(CryptoError::InvalidClientId);
}

let client_id = [user_id.as_bytes(), &[COLON], rest].concat();
Ok(Self(client_id.into()))
}
}

#[cfg(test)]
impl std::str::FromStr for QualifiedE2eiClientId {
type Err = CryptoError;

fn from_str(s: &str) -> CryptoResult<Self> {
s.as_bytes().try_into()
}
}

#[cfg(test)]
impl std::fmt::Display for QualifiedE2eiClientId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", std::str::from_utf8(self.as_slice()).unwrap())
}
}
61 changes: 32 additions & 29 deletions crypto/src/e2e_identity/identity.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
use itertools::Itertools;
use std::collections::HashMap;
use std::str::FromStr;

use itertools::Itertools;
use x509_cert::der::pem::LineEnding;

use crate::e2e_identity::id::WireQualifiedClientId;
use crate::mls::credential::ext::CredentialExt;
use crate::{
e2e_identity::device_status::DeviceStatus,
Expand Down Expand Up @@ -36,8 +38,11 @@ impl<'a> TryFrom<(wire_e2e_identity::prelude::WireIdentity, &'a [u8])> for WireI
use x509_cert::der::Decode as _;
let document = x509_cert::der::Document::from_der(cert)?;
let certificate = document.to_pem("CERTIFICATE", LineEnding::LF)?;

let client_id = WireQualifiedClientId::from_str(&i.client_id)?.to_string();

Ok(Self {
client_id: i.client_id,
client_id,
handle: i.handle.to_string(),
display_name: i.display_name,
domain: i.domain,
Expand Down Expand Up @@ -121,25 +126,17 @@ impl MlsConversation {
pub mod tests {
use wasm_bindgen_test::*;

use crate::test_utils::*;
use crate::CryptoError;
use crate::{e2e_identity::id::QualifiedE2eiClientId, test_utils::*, CryptoError};

wasm_bindgen_test_configure!(run_in_browser);

#[allow(clippy::redundant_static_lifetimes)]
const ALICE_ANDROID: &'static str = "t6wRpI8BRSeviBwwiFp5MQ:[email protected]";
#[allow(clippy::redundant_static_lifetimes)]
const ALICE_IOS: &'static str = "t6wRpI8BRSeviBwwiFp5MQ:[email protected]";
#[allow(clippy::redundant_static_lifetimes)]
const BOB_ANDROID: &'static str = "wjoxZL5tTzi2-8iND-HimA:[email protected]";

#[async_std::test]
#[wasm_bindgen_test]
pub async fn should_read_device_identities() {
let case = TestCase::default_x509();
run_test_with_client_ids(
case.clone(),
[ALICE_ANDROID, ALICE_IOS],
["alice_android", "alice_ios"],
move |[mut alice_android_central, mut alice_ios_central]| {
Box::pin(async move {
let id = conversation_id();
Expand Down Expand Up @@ -204,7 +201,7 @@ pub mod tests {
let case = TestCase::default();
run_test_with_client_ids(
case.clone(),
[ALICE_ANDROID, ALICE_IOS],
["alice_android", "alice_ios"],
move |[mut alice_android_central, mut alice_ios_central]| {
Box::pin(async move {
let id = conversation_id();
Expand Down Expand Up @@ -241,12 +238,20 @@ pub mod tests {
#[wasm_bindgen_test]
pub async fn should_read_users() {
let case = TestCase::default_x509();

let alice_user_id = uuid::Uuid::new_v4();
let alice_android = QualifiedE2eiClientId::generate_from_user_id(&alice_user_id).to_static_str();
let alice_ios = QualifiedE2eiClientId::generate_from_user_id(&alice_user_id).to_static_str();

let bob_user_id = uuid::Uuid::new_v4();
let bob_android = QualifiedE2eiClientId::generate_from_user_id(&bob_user_id).to_static_str();

run_test_with_deterministic_client_ids(
case.clone(),
[
[ALICE_ANDROID, "alice_wire", "Alice Smith"],
[ALICE_IOS, "alice_wire", "Alice Smith"],
[BOB_ANDROID, "bob_wire", "Bob Doe"],
[alice_android, "alice_wire", "Alice Smith"],
[alice_ios, "alice_wire", "Alice Smith"],
[bob_android, "bob_wire", "Bob Doe"],
],
move |[mut alice_android_central, mut alice_ios_central, mut bob_android_central]| {
Box::pin(async move {
Expand All @@ -267,39 +272,37 @@ pub mod tests {
.len();
assert_eq!(nb_members, 3);

assert_eq!(alice_android_central.get_user_id(), alice_ios_central.get_user_id());

// Finds both Alice's devices
let alice_user_id = alice_android_central.get_user_id();
let alice_identities = alice_android_central
.get_user_identities(&id, &["t6wRpI8BRSeviBwwiFp5MQ".to_string()])
.get_user_identities(&id, &[alice_user_id.clone()])
.await
.unwrap();
assert_eq!(alice_identities.len(), 1);
let identities = alice_identities.get(&"t6wRpI8BRSeviBwwiFp5MQ".to_string()).unwrap();
let identities = alice_identities.get(&alice_user_id).unwrap();
assert_eq!(identities.len(), 2);

// Finds Bob only device
let bob_user_id = bob_android_central.get_user_id();
let bob_identities = alice_android_central
.get_user_identities(&id, &["wjoxZL5tTzi2-8iND-HimA".to_string()])
.get_user_identities(&id, &[bob_user_id.clone()])
.await
.unwrap();
assert_eq!(bob_identities.len(), 1);
let identities = bob_identities.get(&"wjoxZL5tTzi2-8iND-HimA".to_string()).unwrap();
let identities = bob_identities.get(&bob_user_id).unwrap();
assert_eq!(identities.len(), 1);

// Finds all devices
let all_identities = alice_android_central
.get_user_identities(
&id,
&[
"t6wRpI8BRSeviBwwiFp5MQ".to_string(),
"wjoxZL5tTzi2-8iND-HimA".to_string(),
],
)
.get_user_identities(&id, &[alice_user_id.clone(), bob_user_id.clone()])
.await
.unwrap();
assert_eq!(all_identities.len(), 2);
let alice_identities = alice_identities.get(&"t6wRpI8BRSeviBwwiFp5MQ".to_string()).unwrap();
let alice_identities = alice_identities.get(&alice_user_id).unwrap();
assert_eq!(alice_identities.len(), 2);
let bob_identities = bob_identities.get(&"wjoxZL5tTzi2-8iND-HimA".to_string()).unwrap();
let bob_identities = bob_identities.get(&bob_user_id).unwrap();
assert_eq!(bob_identities.len(), 1);

// Not found
Expand Down
Loading

0 comments on commit 2954f01

Please sign in to comment.