Quellcodebibliothek Statistik Leitseite products/Sources/formale Sprachen/C/Threema/libthreema/lib/src/https/   (Social Medium ©)  Datei vom 26.9.2026 mit Größe 33 kB image not shown  

Quelle  work_directory.rs   Sprache: Rust

 

//! Work directory endpoint.
use libthreema_dependencies::crypto::digest::{self, Mac as _};
use serde::{Deserialize, Serialize};

use super::endpoint::{HttpsEndpointError, TIMEOUT, https_headers_with_authentication};
use crate::{
    common::{
        ClientInfo, ThreemaId,
        config::{WorkContext, WorkServerBaseUrl, WorkServerLegacyBaseUrl},
        keys::{ClientKey, PublicKey, RemoteSecret, RemoteSecretAuthenticationToken},
    },
    https::{HttpsHeadersBuilder, HttpsMethod, HttpsRequest, HttpsResponse, HttpsResult},
    model::contact::{Contact, ContactUpdateError},
    protobuf::d2d_sync::contact as protobuf_contact,
    utils::{
        serde::{base64, string},
        update::ApplySome as _,
    },
};

#[derive(Serialize)]
struct WorkCredentials<'creds> {
    #[serde(rename = "username")]
    username: &'creds str,

    #[serde(rename = "password")]
    password: &'creds str,
}
impl<'creds> From<&'creds crate::common::config::WorkCredentials> for WorkCredentials<'creds> {
    fn from(credentials: &'creds crate::common::config::WorkCredentials) -> Self {
        Self {
            username: &credentials.username,
            password: &credentials.password,
        }
    }
}

#[inline]
fn https_headers(context: Option<&WorkContext>) -> HttpsHeadersBuilder {
    match context {
        None => HttpsHeadersBuilder::default(),
        Some(context) => https_headers_with_authentication(context),
    }
}

#[derive(Deserialize)]
#[serde(tag = "code")]
enum UnauthorizedDetails {
    #[serde(rename = "invalid-credentials")]
    InvalidCredentials,
    #[serde(rename = "challenge-expired")]
    ChallengeExpired,
    #[serde(rename = "invalid-challenge-response")]
    InvalidChallengeResponse,
    #[serde(other)]
    Unknown,
}

fn handle_status<TStatusFn>(
    result: HttpsResult,
    unexpected_status_map_fn: TStatusFn,
) -> Result<HttpsResponse, HttpsEndpointError>
where
    TStatusFn: FnOnce(u16) -> Option<HttpsEndpointError>,
{
    let response = result?;
    match response.status {
        200 | 204 => Ok(response),
        401 => match serde_json::from_slice::<UnauthorizedDetails>(&response.body) {
            Ok(UnauthorizedDetails::ChallengeExpired) => Err(HttpsEndpointError::ChallengeExpired),
            Ok(UnauthorizedDetails::InvalidChallengeResponse) => {
                Err(HttpsEndpointError::InvalidChallengeResponse)
            },
            Ok(UnauthorizedDetails::InvalidCredentials | UnauthorizedDetails::Unknown) | Err(_) => {
                Err(HttpsEndpointError::InvalidCredentials)
            },
        },
        429 => Err(HttpsEndpointError::RateLimitExceeded),
        status => {
            Err(unexpected_status_map_fn(status).unwrap_or(HttpsEndpointError::UnexpectedStatus(status)))
        },
    }
}

#[derive(Deserialize)]
struct AuthenticationChallenge {
    #[serde(rename = "challengePublicKey", with = "base64::fixed_length")]
    public_key: [u8; PublicKey::LENGTH],

    #[serde(rename = "challenge", with = "base64::variable_length")]
    challenge: Vec<u8>,
}

#[derive(Serialize)]
pub(crate) struct AuthenticationChallengeResponse {
    #[serde(rename = "challenge", with = "base64::variable_length")]
    challenge: Vec<u8>,

    #[serde(rename = "response", with = "base64::fixed_length")]
    response: [u8; digest::MAC_256_LENGTH],
}

/// Process the result and solve the authentication challenge.
pub(crate) fn handle_authentication_challenge(
    client_key: &ClientKey,
    result: HttpsResult,
) -> Result<AuthenticationChallengeResponse, HttpsEndpointError> {
    let response = handle_status(result, |_| None)?;
    let challenge: AuthenticationChallenge = serde_json::from_slice(&response.body)?;
    let response = AuthenticationChallengeResponse {
        challenge: challenge.challenge.clone(),
        response: client_key
            .derive_work_directory_authentication_key(&PublicKey::from(challenge.public_key))
            .ok_or(HttpsEndpointError::InvalidChallenge)?
            .0
            .chain_update(&challenge.challenge)
            .finalize()
            .into_bytes()
            .into(),
    };
    Ok(response)
}

#[derive(Serialize)]
struct WorkContactsRequest<'body> {
    #[serde(flatten)]
    credentials: WorkCredentials<'body>,

    #[serde(rename = "contacts")]
    identities: &'body [ThreemaId],
}

/// Request additional properties associated to a set of contacts of the same Work
/// subscription.
pub(crate) fn request_contacts(
    client_info: &ClientInfo,
    work_server_url: &WorkServerLegacyBaseUrl,
    context: &WorkContext,
    identities: &[ThreemaId],
) -> HttpsRequest {
    HttpsRequest {
        timeout: TIMEOUT,
        url: work_server_url.request_contacts_path(),
        method: HttpsMethod::Post,
        headers: https_headers(Some(context))
            .accept("application/json")
            .build(client_info),
        body: serde_json::to_vec(&WorkContactsRequest {
            credentials: (&context.credentials).into(),
            identities,
        })
        .expect("Failed to create work directory contacts request body"),
    }
}

/// Additional Work properties associated to a specific contact.
#[derive(Debug, Deserialize)]
pub(crate) struct WorkContact {
    #[serde(rename = "id")]
    pub(crate) identity: ThreemaId,

    #[serde(rename = "pk", with = "base64::fixed_length")]
    public_key: [u8; PublicKey::LENGTH],

    #[serde(rename = "first", deserialize_with = "string::empty_to_optional::deserialize")]
    first_name: Option<String>,

    #[serde(rename = "last", deserialize_with = "string::empty_to_optional::deserialize")]
    last_name: Option<String>,
}
impl WorkContact {
    pub(crate) fn update(self, contact: &mut Contact) -> Result<(), ContactUpdateError> {
        let Self {
            identity,
            public_key,
            first_name,
            last_name,
        } = self;

        // Ensure the identity and public key equal before updating
        if identity != contact.identity {
            return Err(ContactUpdateError::IdentityMismatch {
                expected: contact.identity,
                actual: identity,
            });
        }
        if &public_key != contact.public_key.0.as_bytes() {
            return Err(ContactUpdateError::PublicKeyMismatch);
        }

        // Bump verification levels (if needed)
        if contact.verification_level == protobuf_contact::VerificationLevel::Unverified {
            contact.verification_level = protobuf_contact::VerificationLevel::ServerVerified;
        }
        if contact.work_verification_level == protobuf_contact::WorkVerificationLevel::None {
            contact.work_verification_level =
                protobuf_contact::WorkVerificationLevel::WorkSubscriptionVerified;
        }

        // Update first and last name if provided
        contact.first_name.apply_some(first_name);
        contact.last_name.apply_some(last_name);

        // Done
        Ok(())
    }
}

/// Process the result and map it to a subset of the provided contacts that are part of the same
/// Work subscription with the associated additional Work properties.
pub(crate) fn handle_contacts_result(result: HttpsResult) -> Result<Vec<WorkContact>, HttpsEndpointError> {
    let response = handle_status(result, |_| None)?;
    let amendments: Vec<WorkContact> = serde_json::from_slice(&response.body)?;
    Ok(amendments)
}

#[derive(Serialize)]
struct WorkPropertiesRequest<'body> {
    #[serde(rename = "identity")]
    identity: ThreemaId,

    #[serde(flatten)]
    credentials: WorkCredentials<'body>,

    #[serde(rename = "properties", with = "base64::variable_length")]
    work_properties: Vec<u8>,
}

/// Request an authentication challenge to update the work properties.
pub(crate) fn update_work_properties_authentication_request(
    client_info: &ClientInfo,
    work_server_url: &WorkServerBaseUrl,
    context: &WorkContext,
    identity: ThreemaId,
    encoded_work_properties: Vec<u8>,
) -> HttpsRequest {
    HttpsRequest {
        timeout: TIMEOUT,
        url: work_server_url.work_properties_path(),
        method: HttpsMethod::Post,
        headers: https_headers(Some(context))
            .accept("application/json")
            .build(client_info),
        body: serde_json::to_vec(&WorkPropertiesRequest {
            identity,
            credentials: (&context.credentials).into(),
            work_properties: encoded_work_properties,
        })
        .expect("Failed to create work properties update challenge request body"),
    }
}

#[derive(Serialize)]
struct CreateWorkPropertiesAuthenticatedRequest<'body> {
    #[serde(flatten)]
    request: WorkPropertiesRequest<'body>,

    #[serde(flatten)]
    authentication: AuthenticationChallengeResponse,
}

/// Update the work properties.
pub(crate) fn update_work_properties_request(
    client_info: &ClientInfo,
    work_server_url: &WorkServerBaseUrl,
    context: &WorkContext,
    identity: ThreemaId,
    authentication: AuthenticationChallengeResponse,
    encoded_work_properties: Vec<u8>,
) -> HttpsRequest {
    HttpsRequest {
        timeout: TIMEOUT,
        url: work_server_url.work_properties_path(),
        method: HttpsMethod::Post,
        headers: https_headers(Some(context))
            .accept("application/json")
            .build(client_info),
        body: serde_json::to_vec(&CreateWorkPropertiesAuthenticatedRequest {
            request: WorkPropertiesRequest {
                identity,
                credentials: (&context.credentials).into(),
                work_properties: encoded_work_properties,
            },
            authentication,
        })
        .expect("Failed to create update work properties request body"),
    }
}

/// Process the result after attempting to update work properties.
pub(crate) fn handle_update_work_properties_result(result: HttpsResult) -> Result<(), HttpsEndpointError> {
    let _ = handle_status(result, |_| None)?;
    Ok(())
}

#[derive(Serialize)]
struct WorkCreateRemoteSecretRequest<'body> {
    #[serde(rename = "identity")]
    identity: ThreemaId,

    #[serde(flatten)]
    credentials: WorkCredentials<'body>,

    #[serde(rename = "secret", with = "base64::fixed_length")]
    remote_secret: &'body [u8; RemoteSecret::LENGTH],
}

/// Request an authentication challenge to create a remote secret.
pub(crate) fn create_remote_secret_authentication_request(
    client_info: &ClientInfo,
    work_server_url: &WorkServerBaseUrl,
    context: &WorkContext,
    identity: ThreemaId,
    remote_secret: &RemoteSecret,
) -> HttpsRequest {
    HttpsRequest {
        timeout: TIMEOUT,
        url: work_server_url.remote_secret_path(),
        method: HttpsMethod::Put,
        headers: https_headers(Some(context))
            .accept("application/json")
            .build(client_info),
        body: serde_json::to_vec(&WorkCreateRemoteSecretRequest {
            identity,
            credentials: (&context.credentials).into(),
            remote_secret: &remote_secret.0,
        })
        .expect("Failed to create remote secret creation challenge request body"),
    }
}

#[derive(Serialize)]
struct WorkCreateRemoteSecretAuthenticatedRequest<'body> {
    #[serde(flatten)]
    request: WorkCreateRemoteSecretRequest<'body>,

    #[serde(flatten)]
    authentication: AuthenticationChallengeResponse,
}

/// Create a remote secret.
pub(crate) fn create_remote_secret_request(
    client_info: &ClientInfo,
    work_server_url: &WorkServerBaseUrl,
    identity: ThreemaId,
    context: &WorkContext,
    authentication: AuthenticationChallengeResponse,
    remote_secret: &RemoteSecret,
) -> HttpsRequest {
    HttpsRequest {
        timeout: TIMEOUT,
        url: work_server_url.remote_secret_path(),
        method: HttpsMethod::Put,
        headers: https_headers(Some(context))
            .accept("application/json")
            .build(client_info),
        body: serde_json::to_vec(&WorkCreateRemoteSecretAuthenticatedRequest {
            request: WorkCreateRemoteSecretRequest {
                identity,
                credentials: (&context.credentials).into(),
                remote_secret: &remote_secret.0,
            },
            authentication,
        })
        .expect("Failed to create remote secret creation request body"),
    }
}

#[derive(Deserialize)]
struct WorkCreateRemoteSecretResponse {
    #[serde(rename = "secretAuthenticationToken", with = "base64::fixed_length")]
    remote_secret_authentication_token: [u8; RemoteSecretAuthenticationToken::LENGTH],
}

/// Process the result after attempting to create a remote secret.
pub(crate) fn handle_create_remote_secret_result(
    result: HttpsResult,
) -> Result<RemoteSecretAuthenticationToken, HttpsEndpointError> {
    let response = handle_status(result, |_| None)?;
    let response: WorkCreateRemoteSecretResponse = serde_json::from_slice(&response.body)?;
    Ok(RemoteSecretAuthenticationToken(
        response.remote_secret_authentication_token,
    ))
}

#[derive(Serialize)]
struct WorkDeleteRemoteSecretRequest<'body> {
    #[serde(rename = "identity")]
    identity: ThreemaId,

    #[serde(flatten)]
    credentials: WorkCredentials<'body>,

    #[serde(rename = "secretAuthenticationToken", with = "base64::fixed_length")]
    remote_secret_authentication_token: &'body [u8; RemoteSecretAuthenticationToken::LENGTH],
}

/// Request an authentication challenge to remove a remote secret.
pub(crate) fn delete_remote_secret_authentication_request(
    client_info: &ClientInfo,
    work_server_url: &WorkServerBaseUrl,
    context: &WorkContext,
    identity: ThreemaId,
    remote_secret_authentication_token: &RemoteSecretAuthenticationToken,
) -> HttpsRequest {
    HttpsRequest {
        timeout: TIMEOUT,
        url: work_server_url.remote_secret_path(),
        method: HttpsMethod::Delete,
        headers: https_headers(Some(context))
            .accept("application/json")
            .build(client_info),
        body: serde_json::to_vec(&WorkDeleteRemoteSecretRequest {
            identity,
            credentials: (&context.credentials).into(),
            remote_secret_authentication_token: &remote_secret_authentication_token.0,
        })
        .expect("Failed to create remote secret deletion challenge request body"),
    }
}

#[derive(Serialize)]
struct WorkDeleteRemoteSecretAuthenticatedRequest<'body> {
    #[serde(flatten)]
    request: WorkDeleteRemoteSecretRequest<'body>,

    #[serde(flatten)]
    authentication: AuthenticationChallengeResponse,
}

/// Remove a remote secret.
pub(crate) fn delete_remote_secret_request(
    client_info: &ClientInfo,
    work_server_url: &WorkServerBaseUrl,
    context: &WorkContext,
    identity: ThreemaId,
    authentication: AuthenticationChallengeResponse,
    remote_secret_authentication_token: &RemoteSecretAuthenticationToken,
) -> HttpsRequest {
    HttpsRequest {
        timeout: TIMEOUT,
        url: work_server_url.remote_secret_path(),
        method: HttpsMethod::Delete,
        headers: https_headers(Some(context))
            .accept("application/json")
            .build(client_info),
        body: serde_json::to_vec(&WorkDeleteRemoteSecretAuthenticatedRequest {
            request: WorkDeleteRemoteSecretRequest {
                identity,
                credentials: (&context.credentials).into(),
                remote_secret_authentication_token: &remote_secret_authentication_token.0,
            },
            authentication,
        })
        .expect("Failed to create remote secret deletion request body"),
    }
}

/// Process the result after attempting to remove a remote secret.
pub(crate) fn handle_delete_remote_secret_result(result: HttpsResult) -> Result<(), HttpsEndpointError> {
    let _ = handle_status(result, |_| None)?;
    Ok(())
}

#[derive(Serialize)]
struct WorkFetchRemoteSecretRequest<'body> {
    #[serde(rename = "secretAuthenticationToken", with = "base64::fixed_length")]
    remote_secret_authentication_token: &'body [u8; RemoteSecretAuthenticationToken::LENGTH],
}

/// Retrieve a remote secret.
pub(crate) fn request_remote_secret(
    client_info: &ClientInfo,
    work_server_url: &WorkServerBaseUrl,
    remote_secret_authentication_token: &RemoteSecretAuthenticationToken,
) -> HttpsRequest {
    HttpsRequest {
        timeout: TIMEOUT,
        url: work_server_url.remote_secret_path(),
        method: HttpsMethod::Post,
        headers: https_headers(None).accept("application/json").build(client_info),
        body: serde_json::to_vec(&WorkFetchRemoteSecretRequest {
            remote_secret_authentication_token: &remote_secret_authentication_token.0,
        })
        .expect("Failed to create remote secret request body"),
    }
}

#[derive(Deserialize)]
pub(crate) struct WorkFetchRemoteSecretResponse {
    #[serde(rename = "secret", with = "base64::fixed_length")]
    pub(crate) remote_secret: [u8; RemoteSecret::LENGTH],

    #[serde(rename = "checkIntervalS")]
    pub(crate) check_interval_s: u32,

    #[serde(rename = "nMissedChecksMax")]
    pub(crate) n_missed_checks_max: u16,
}

/// Process the result after attempting to retrieve a remote secret.
pub(crate) fn handle_remote_secret_result(
    result: HttpsResult,
) -> Result<WorkFetchRemoteSecretResponse, HttpsEndpointError> {
    let response = handle_status(result, |status| match status {
        403 => Some(HttpsEndpointError::Forbidden),
        404 => Some(HttpsEndpointError::NotFound),
        _ => None,
    })?;
    let remote_secret_response: WorkFetchRemoteSecretResponse = serde_json::from_slice(&response.body)?;
    Ok(remote_secret_response)
}

#[cfg(test)]
mod tests {
    use assert_matches::assert_matches;
    use libthreema_dependencies::data_encoding::HEXLOWER_PERMISSIVE;
    use prost::Message as _;
    use rstest::rstest;
    use serde_json::json;

    use super::*;
    use crate::{
        common::config::{Config, WorkCredentials, WorkFlavor},
        https::HttpsResponse,
        protobuf,
        utils::test::ResultUnwrapErrorQuiet as _,
        work::properties::{WorkAvailabilityStatus, WorkProperties},
    };

    fn work_server_url() -> WorkServerBaseUrl {
        Config::testing().work_server_url
    }

    fn work_context() -> WorkContext {
        WorkContext {
            credentials: WorkCredentials {
                username: "klo".to_owned(),
                password: "bürste".to_owned(),
            },
            flavor: WorkFlavor::Work,
        }
    }

    fn work_properties() -> WorkProperties {
        WorkProperties {
            availability_status: Some(WorkAvailabilityStatus::Busy(String::default())),
        }
    }

    #[test]
    fn common_status_challenge_expired() {
        assert_matches!(
            handle_status(
                Ok(HttpsResponse {
                    status: 401,
                    body: br#"{"code": "challenge-expired"}"#.to_vec(),
                }),
                |_| None
            ),
            Err(HttpsEndpointError::ChallengeExpired)
        );
    }

    #[test]
    fn common_status_invalid_challenge_response() {
        assert_matches!(
            handle_status(
                Ok(HttpsResponse {
                    status: 401,
                    body: br#"{"code": "invalid-challenge-response"}"#.to_vec(),
                }),
                |_| None
            ),
            Err(HttpsEndpointError::InvalidChallengeResponse)
        );
    }

    #[rstest]
    #[case(br#"{"code": "invalid-credentials"}"#)]
    #[case(br#"{"code": "don't-like-you"}"#)]
    #[case(b"roflcopter")]
    fn common_status_invalid_credentials(#[case] body: &'static [u8]) {
        assert_matches!(
            handle_status(
                Ok(HttpsResponse {
                    status: 401,
                    body: body.to_vec(),
                }),
                |_| None
            ),
            Err(HttpsEndpointError::InvalidCredentials)
        );
    }

    #[test]
    fn common_status_rate_limit_exceeded() {
        assert_matches!(
            handle_status(
                Ok(HttpsResponse {
                    status: 429,
                    body: vec![],
                }),
                |_| None
            ),
            Err(HttpsEndpointError::RateLimitExceeded)
        );
    }

    #[test]
    fn common_status_unexpected_custom_map() {
        assert_matches!(
            handle_status(
                Ok(HttpsResponse {
                    status: 429,
                    body: vec![],
                }),
                |_| None
            ),
            Err(HttpsEndpointError::RateLimitExceeded)
        );
    }

    #[rstest]
    fn common_status_unexpected(#[values(0, 403, 404, 1234)] expected_status: u16) {
        assert_matches!(
            handle_status(Ok(HttpsResponse { status: expected_status, body: vec![] }), |_| None),
            Err(HttpsEndpointError::UnexpectedStatus(actual_status)) => {
                assert_eq!(actual_status, expected_status);
            }
        );
    }

    #[rstest]
    #[case(0, || HttpsEndpointError::CustomPossiblyLocalizedError("why not".to_owned()))]
    #[case(403, || HttpsEndpointError::Forbidden)]
    #[case(404, || HttpsEndpointError::NotFound)]
    fn common_status_unexpected_mapped<TErrorFn>(#[case] status: u16, #[case] error_fn: TErrorFn)
    where
        TErrorFn: Fn() -> HttpsEndpointError,
    {
        assert_eq!(
            handle_status(Ok(HttpsResponse { status, body: vec![] }), |unexpected_status| {
                if unexpected_status == status {
                    Some(error_fn())
                } else {
                    None
                }
            })
            .unwrap_err()
            .to_string(),
            error_fn().to_string(),
        );
    }

    #[rstest]
    #[case(200, b"Sure, dude")]
    #[case(200, br#"{"code": "invalid-credentials"}"#)]
    #[case(204, b"")]
    fn common_status_valid(#[case] status: u16, #[case] body: &'static [u8]) -> anyhow::Result<()> {
        let _ = handle_status(
            Ok(HttpsResponse {
                status,
                body: body.to_vec(),
            }),
            |_| None,
        )?;
        Ok(())
    }

    #[test]
    fn authentication_challenge_with_non_contributory_public_key() {
        let error = handle_authentication_challenge(
            &ClientKey::from([0; ClientKey::LENGTH]),
            Ok(HttpsResponse {
                status: 200,
                body: serde_json::to_vec(&json!({
                    "challengePublicKey": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=",
                    "challenge": "bWVvdw==",
                }))
                .unwrap(),
            }),
        )
        .unwrap_err_quiet();
        assert_matches!(error, HttpsEndpointError::InvalidChallenge);
    }

    #[test]
    fn authentication_challenge_valid() -> anyhow::Result<()> {
        let response = handle_authentication_challenge(
            &ClientKey::from([0; ClientKey::LENGTH]),
            Ok(HttpsResponse {
                status: 200,
                body: serde_json::to_vec(&json!({
                    "challengePublicKey": "BAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=",
                    "challenge": "bWVvdw==",
                }))?,
            }),
        )?;
        assert_eq!(response.challenge, b"meow".to_vec());
        assert_eq!(
            response.response.to_vec(),
            HEXLOWER_PERMISSIVE
                .decode(b"a9faac27a48cd83f6eed4f16c0bb6c11ca19b0ffd1fbc37c13746e068500282a")
                .unwrap()
        );
        Ok(())
    }

    #[test]
    fn create_work_properties_challenge_request() -> anyhow::Result<()> {
        let request = update_work_properties_authentication_request(
            &ClientInfo::Libthreema,
            &work_server_url(),
            &work_context(),
            ThreemaId::predefined(*b"TITATEST"),
            protobuf::directory::WorkProperties::from(work_properties()).encode_to_vec(),
        );

        assert_eq!(
            request.url,
            "https://work.example.threema.ch/api-client/v1/user-properties"
        );

        assert_eq!(request.method, HttpsMethod::Post);
        let body: serde_json::Value = serde_json::from_slice(&request.body)?;
        assert_eq!(body["identity"], "TITATEST");
        assert_eq!(body["username"], "klo");
        assert_eq!(body["password"], "bürste");
        assert_eq!(body["properties"], "CgIIAg==");
        Ok(())
    }

    #[test]
    fn create_work_properties_request_valid() -> anyhow::Result<()> {
        let request = update_work_properties_request(
            &ClientInfo::Libthreema,
            &work_server_url(),
            &work_context(),
            ThreemaId::predefined(*b"TITATEST"),
            AuthenticationChallengeResponse {
                challenge: b"kekse".to_vec(),
                response: [0_u8; 32],
            },
            protobuf::directory::WorkProperties::from(work_properties()).encode_to_vec(),
        );

        assert_eq!(
            request.url,
            "https://work.example.threema.ch/api-client/v1/user-properties"
        );
        assert_eq!(request.method, HttpsMethod::Post);
        let body: serde_json::Value = serde_json::from_slice(&request.body)?;
        assert_eq!(body["identity"], "TITATEST");
        assert_eq!(body["username"], "klo");
        assert_eq!(body["password"], "bürste");
        assert_eq!(body["challenge"], "a2Vrc2U=");
        assert_eq!(body["response"], "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=");
        assert_eq!(body["properties"], "CgIIAg==");

        Ok(())
    }

    #[test]
    fn create_remote_secret_challenge_request() -> anyhow::Result<()> {
        let request = create_remote_secret_authentication_request(
            &ClientInfo::Libthreema,
            &work_server_url(),
            &work_context(),
            ThreemaId::predefined(*b"TESTTEST"),
            &RemoteSecret([1_u8; 32]),
        );
        assert_eq!(
            request.url,
            "https://work.example.threema.ch/api-client/v1/remote-secret"
        );
        assert_eq!(request.method, HttpsMethod::Put);
        let body: serde_json::Value = serde_json::from_slice(&request.body)?;
        assert_eq!(body["identity"], "TESTTEST");
        assert_eq!(body["username"], "klo");
        assert_eq!(body["password"], "bürste");
        assert_eq!(body["secret"], "AQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQE=");
        Ok(())
    }

    #[test]
    fn create_remote_secret_request_valid() -> anyhow::Result<()> {
        let request = create_remote_secret_request(
            &ClientInfo::Libthreema,
            &work_server_url(),
            ThreemaId::predefined(*b"TESTTEST"),
            &work_context(),
            AuthenticationChallengeResponse {
                challenge: b"kekse".to_vec(),
                response: [0_u8; 32],
            },
            &RemoteSecret([1_u8; 32]),
        );
        assert_eq!(
            request.url,
            "https://work.example.threema.ch/api-client/v1/remote-secret"
        );
        assert_eq!(request.method, HttpsMethod::Put);
        let body: serde_json::Value = serde_json::from_slice(&request.body)?;
        assert_eq!(body["identity"], "TESTTEST");
        assert_eq!(body["username"], "klo");
        assert_eq!(body["password"], "bürste");
        assert_eq!(body["challenge"], "a2Vrc2U=");
        assert_eq!(body["response"], "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=");
        assert_eq!(body["secret"], "AQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQE=");
        Ok(())
    }

    #[test]
    fn create_remote_secret_response_valid() -> anyhow::Result<()> {
        let response = handle_create_remote_secret_result(Ok(HttpsResponse {
            status: 200,
            body: serde_json::to_vec(&json!({
                "secretAuthenticationToken": "AgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgI=",
            }))?,
        }))?;
        assert_eq!(response.0, [2_u8; 32]);
        Ok(())
    }

    #[test]
    fn delete_remote_secret_authentication_request_valid() -> anyhow::Result<()> {
        let request = delete_remote_secret_authentication_request(
            &ClientInfo::Libthreema,
            &work_server_url(),
            &work_context(),
            ThreemaId::predefined(*b"TESTTEST"),
            &RemoteSecretAuthenticationToken([2_u8; 32]),
        );
        assert_eq!(
            request.url,
            "https://work.example.threema.ch/api-client/v1/remote-secret"
        );
        assert_eq!(request.method, HttpsMethod::Delete);
        let body: serde_json::Value = serde_json::from_slice(&request.body)?;
        assert_eq!(body["identity"], "TESTTEST");
        assert_eq!(body["username"], "klo");
        assert_eq!(body["password"], "bürste");
        assert_eq!(
            body["secretAuthenticationToken"],
            "AgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgI="
        );
        Ok(())
    }

    #[test]
    fn delete_remote_secret_request_valid() -> anyhow::Result<()> {
        let request = delete_remote_secret_request(
            &ClientInfo::Libthreema,
            &work_server_url(),
            &work_context(),
            ThreemaId::predefined(*b"TESTTEST"),
            AuthenticationChallengeResponse {
                challenge: b"kekse".to_vec(),
                response: [0_u8; 32],
            },
            &RemoteSecretAuthenticationToken([2_u8; 32]),
        );
        assert_eq!(
            request.url,
            "https://work.example.threema.ch/api-client/v1/remote-secret"
        );
        assert_eq!(request.method, HttpsMethod::Delete);
        let body: serde_json::Value = serde_json::from_slice(&request.body)?;
        assert_eq!(body["identity"], "TESTTEST");
        assert_eq!(body["username"], "klo");
        assert_eq!(body["password"], "bürste");
        assert_eq!(body["challenge"], "a2Vrc2U=");
        assert_eq!(body["response"], "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=");
        assert_eq!(
            body["secretAuthenticationToken"],
            "AgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgI="
        );
        Ok(())
    }

    #[test]
    fn remote_secret_request() -> anyhow::Result<()> {
        let request = request_remote_secret(
            &ClientInfo::Libthreema,
            &work_server_url(),
            &RemoteSecretAuthenticationToken([2_u8; 32]),
        );
        assert_eq!(
            request.url,
            "https://work.example.threema.ch/api-client/v1/remote-secret"
        );
        assert_eq!(request.method, HttpsMethod::Post);
        let body: serde_json::Value = serde_json::from_slice(&request.body)?;
        assert_eq!(
            body["secretAuthenticationToken"],
            "AgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgI=",
        );
        Ok(())
    }

    #[test]
    fn remote_secret_response_valid() -> anyhow::Result<()> {
        let response = handle_remote_secret_result(Ok(HttpsResponse {
            status: 200,
            body: serde_json::to_vec(&json!({
                "secret": "AQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQE=",
                "checkIntervalS": 1_u32,
                "nMissedChecksMax": 0_u16,
            }))?,
        }))?;
        assert_eq!(response.remote_secret, [1_u8; 32]);
        assert_eq!(response.check_interval_s, 1);
        assert_eq!(response.n_missed_checks_max, 0);
        Ok(())
    }

    // Note: These tests cover some common excess JSON decoding cases as well. Other endpoint tests do not
    // need to be this elaborate.
    #[rstest]
    #[case(b"Not ok")]
    #[case(
        // Invalid `secret` base64
        br#"{
            "secret": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=",
            "checkIntervalS": 1,
            "nMissedChecksMax": 1
        }"#
    )]
    #[case(
        // Invalid `secret` length
        br#"{
            "secret": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
            "checkIntervalS": 1,
            "nMissedChecksMax": 1
        }"#
    )]
    #[case(
        // Missing `checkIntervalS`
        br#"{
            "secret": "AQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQE=",
            "nMissedChecksMax": 1
        }"#
    )]
    #[case(
        // Invalid `checkIntervalS` type
        br#"{
            "secret": "AQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQE=",
            "checkIntervalS": "nein",
            "nMissedChecksMax": 1
        }"#
    )]
    #[case(
        // Invalid `checkIntervalS` type
        br#"{
            "secret": "AQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQE=",
            "checkIntervalS": [0, 1, 2, 3],
            "nMissedChecksMax": 1
        }"#
    )]
    #[case(
        // Invalid `checkIntervalS` type
        br#"{
            "secret": "AQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQE=",
            "checkIntervalS": 0.0,
            "nMissedChecksMax": 1
        }"#
    )]
    #[case(
        // Invalid `checkIntervalS` type
        br#"{
            "secret": "AQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQE=",
            "checkIntervalS": -1,
            "nMissedChecksMax": 1
        }"#
    )]
    #[case(
        // Invalid `checkIntervalS` type
        br#"{
            "secret": "AQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQE=",
            "checkIntervalS": 4294967296,
            "nMissedChecksMax": 1
        }"#
    )]
    #[case(
        // Missing `nMissedChecksMax`
        br#"{
            "secret": "AQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQE=",
            "checkIntervalS": 1
        }"#
    )]
    #[case(
        // Invalid `nMissedChecksMax` type
        br#"{
            "secret": "AQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQE=",
            "checkIntervalS": 1,
            "nMissedChecksMax": 65536
        }"#
    )]
    fn remote_secret_response_invalid_content(#[case] body: &'static [u8]) {
        let error = handle_remote_secret_result(Ok(HttpsResponse {
            status: 200,
            body: body.to_vec(),
        }))
        .unwrap_err_quiet();
        assert_matches!(error, HttpsEndpointError::DecodingFailed(_));
    }
}

Messung
CHZG
819710092

¤ Dauer der Verarbeitung: 0.6 Sekunden  (vorverarbeitet am  2026-11-04) ¤

*© Formatika GbR, Deutschland






Wurzel

Suchen

PVS Prover

Isabelle Prover


Cephes Mathematical Library

Vienna Development Method

Haftungshinweis

Die Informationen auf dieser Webseite wurden nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit, noch Qualität der bereit gestellten Informationen zugesichert.

Bemerkung:

Die farbliche Syntaxdarstellung und die Messung sind noch experimentell.