cipherstash-client 0.35.0

The official CipherStash SDK
Documentation
#![cfg(feature = "test-utils")]

mod prelude;
use prelude::*;

#[tokio::test]
#[ignore = "e2e"]
async fn encrypt_and_decrypt() {
    let _temp_config_dir = &*CONFIG_DIR;

    let token = build_mock_token();
    let cts_client = build_cts_client(token);
    let workspace_id = create_workspace(&cts_client).await;
    let access_key = create_access_key(&cts_client, workspace_id).await;

    let strategy = build_access_key_strategy(&access_key);
    let zero_kms = build_zerokms(strategy);

    let keyset = create_keyset(&zero_kms).await;
    let client_key = create_client_key(&zero_kms, &keyset).await;

    let strategy = build_access_key_strategy(&access_key);
    let zerokms_client = build_zerokms_with_client_key(strategy, &client_key);

    let record = zerokms_client
        .encrypt_single(
            EncryptPayload::new_with_descriptor(b"plaintext message", "test-descriptor"),
            None,
        )
        .await
        .expect("failed to encrypt record");

    let message = zerokms_client
        .decrypt_single(record, None, None, None)
        .await
        .expect("failed to decrypt record");

    assert_eq!(message, b"plaintext message");
}

// Lock contexts with IdentityClaim require an OIDC provider token (not an access key) so that
// the server can resolve identity claims from the JWT. This test registers a third-party OIDC
// provider, builds an OAuthStrategy with a mock token issued by that provider, and verifies
// that encryption with an identity lock context correctly prevents decryption without context
// while allowing decryption when the matching context is supplied.
#[tokio::test]
#[ignore = "e2e"]
async fn encrypt_and_decrypt_with_lock_context_and_custom_oidc_provider() {
    let _temp_config_dir = &*CONFIG_DIR;

    let token = build_mock_token();
    let cts_client = build_cts_client(token);
    let workspace_id = create_workspace(&cts_client).await;

    // The access key is only used for admin setup (keyset + client key creation).
    // The actual encrypt/decrypt operations use the OAuthStrategy below.
    let access_key = create_access_key(&cts_client, workspace_id).await;

    // Build a service-token-authed client for workspace-scoped operations.
    // The OIDC-authed client can't be used here because it lacks workspace claims.
    let cts_client_with_service_token = build_service_cts_client(&access_key).await;

    // Register a third-party OIDC provider on the workspace
    let oidc_provider = create_oidc_provider(&cts_client_with_service_token).await;

    // Build a token issued by the third-party OIDC provider (no org claim)
    let orgless_token = build_orgless_token(&oidc_provider, workspace_id);

    // Set up keyset + client key using the access key (admin operations)
    let strategy = build_access_key_strategy(&access_key);
    let zero_kms = build_zerokms(strategy);

    let keyset = create_keyset(&zero_kms).await;
    let client_key = create_client_key(&zero_kms, &keyset).await;

    // Federate the OIDC token through CTS to get a CTS-signed service token.
    // ZeroKMS trusts CTS-signed tokens and can resolve identity claims from them.
    let federated = federate_oidc_token(&orgless_token, workspace_id).await;
    let zerokms_client = build_zerokms_with_client_key(federated, &client_key);

    // Encrypt with an identity lock context
    let encrypt_payload =
        EncryptPayload::new_with_descriptor(b"plaintext message", "test-descriptor")
            .set_context(Cow::Owned(vec![Context::IdentityClaim("sub".to_string())]));

    let record = zerokms_client
        .encrypt_single(encrypt_payload, None)
        .await
        .expect("failed to encrypt record");

    // Decryption without context should fail
    let no_context_attempt = zerokms_client
        .decrypt_single(record.clone(), None, None, None)
        .await;

    assert!(no_context_attempt.is_err());
    // The "Context did not satisfy lock requirements" text comes from the
    // server's FailureResponse body, which is now exposed via the source
    // chain rather than the top-level Display — walk the chain to find it.
    let err = no_context_attempt.unwrap_err();
    let mut current: Option<&dyn std::error::Error> = Some(&err);
    let mut found = false;
    while let Some(e) = current {
        if e.to_string()
            .contains("Context did not satisfy lock requirements")
        {
            found = true;
            break;
        }
        current = e.source();
    }
    assert!(
        found,
        "expected 'Context did not satisfy lock requirements' in error chain, got: {err:?}"
    );

    // Decryption with the matching identity claim context should succeed
    let decrypt_payload = record.with_context(Context::IdentityClaim("sub".to_string()));
    let message = zerokms_client
        .decrypt_single(decrypt_payload, None, None, None)
        .await
        .expect("failed to decrypt record");

    assert_eq!(message, b"plaintext message");
}