im-core 0.1.0

Rust IM SDK for Awiki clients built on Agent Network Protocol (ANP)
Documentation
use anp::authentication::{
    build_anp_message_service, create_did_wba_document_with_creation_options,
    AnpMessageServiceOptions, DidDocumentCreationOptions, DidDocumentOptions, DidProfile,
    VM_KEY_AUTH, VM_KEY_E2EE_AGREEMENT, VM_KEY_E2EE_SIGNING,
};
use rand::RngCore;
use serde_json::Value;

const DEFAULT_ANP_SERVICE_PATH: &str = "/anp-im/rpc";
const AGENT_MESSAGE_SERVICE_PROFILES: &[&str] = &[
    "anp.core.binding.v1",
    "anp.direct.base.v1",
    "anp.attachment.v1",
];
const AGENT_MESSAGE_SERVICE_SECURITY_PROFILES: &[&str] = &["transport-protected"];

#[derive(Debug, Clone, PartialEq)]
pub(crate) struct GeneratedIdentity {
    pub(crate) did: crate::ids::Did,
    pub(crate) unique_id: String,
    pub(crate) did_document: Value,
    pub(crate) key1_private_pem: String,
    pub(crate) key1_public_pem: String,
    pub(crate) e2ee_signing_private_pem: String,
    pub(crate) e2ee_agreement_private_pem: String,
}

#[derive(Debug, Clone, PartialEq)]
pub(crate) struct GeneratedIdentityWithDaemonSubkey {
    pub(crate) identity: GeneratedIdentity,
    pub(crate) daemon_subkey_package: crate::identity::DaemonSubkeyPrivatePackage,
}

pub(crate) fn generate_identity_with_default_daemon_subkey<I, S>(
    hostname: &str,
    path_segments: I,
    service_endpoint: Option<&crate::config::ServiceEndpoint>,
    service_did: Option<&crate::ids::Did>,
) -> crate::ImResult<GeneratedIdentityWithDaemonSubkey>
where
    I: IntoIterator<Item = S>,
    S: AsRef<str>,
{
    let daemon_subkey = crate::internal::identity_daemon_subkey::generate_material();
    let generated = generate_identity_with_path_segments_and_daemon_subkey(
        hostname,
        path_segments,
        service_endpoint,
        service_did,
        &daemon_subkey,
    )?;
    let daemon_subkey_package = crate::internal::identity_daemon_subkey::package_from_material(
        generated.did.clone(),
        daemon_subkey,
    );
    crate::internal::identity_daemon_subkey::validate_package_against_did_document(
        &daemon_subkey_package,
        &generated.did_document,
    )?;
    Ok(GeneratedIdentityWithDaemonSubkey {
        identity: generated,
        daemon_subkey_package,
    })
}

pub(crate) fn generate_identity_with_path_segments<I, S>(
    hostname: &str,
    path_segments: I,
    service_endpoint: Option<&crate::config::ServiceEndpoint>,
    service_did: Option<&crate::ids::Did>,
) -> crate::ImResult<GeneratedIdentity>
where
    I: IntoIterator<Item = S>,
    S: AsRef<str>,
{
    generate_identity_with_path_segments_internal(
        hostname,
        path_segments,
        service_endpoint,
        service_did,
        None,
    )
}

fn generate_identity_with_path_segments_and_daemon_subkey<I, S>(
    hostname: &str,
    path_segments: I,
    service_endpoint: Option<&crate::config::ServiceEndpoint>,
    service_did: Option<&crate::ids::Did>,
    daemon_subkey: &crate::internal::identity_daemon_subkey::GeneratedDaemonSubkeyMaterial,
) -> crate::ImResult<GeneratedIdentity>
where
    I: IntoIterator<Item = S>,
    S: AsRef<str>,
{
    generate_identity_with_path_segments_internal(
        hostname,
        path_segments,
        service_endpoint,
        service_did,
        Some(daemon_subkey),
    )
}

fn generate_identity_with_path_segments_internal<I, S>(
    hostname: &str,
    path_segments: I,
    service_endpoint: Option<&crate::config::ServiceEndpoint>,
    service_did: Option<&crate::ids::Did>,
    daemon_subkey: Option<&crate::internal::identity_daemon_subkey::GeneratedDaemonSubkeyMaterial>,
) -> crate::ImResult<GeneratedIdentity>
where
    I: IntoIterator<Item = S>,
    S: AsRef<str>,
{
    let hostname = hostname.trim();
    if hostname.is_empty() {
        return Err(crate::ImError::invalid_input(
            Some("hostname".to_string()),
            "hostname is required",
        ));
    }
    let endpoint = service_endpoint.map_or_else(
        || default_anp_service_endpoint(hostname),
        |endpoint| endpoint.as_str().to_string(),
    );
    let service_did = service_did.map_or_else(
        || default_anp_service_did(hostname),
        |did| did.as_str().to_string(),
    );
    validate_anp_service_did(&service_did)?;
    let path_segments = path_segments
        .into_iter()
        .map(|segment| segment.as_ref().trim().to_string())
        .filter(|segment| !segment.is_empty())
        .collect::<Vec<_>>();
    if path_segments.is_empty() {
        return Err(crate::ImError::invalid_input(
            Some("path_segments".to_string()),
            "DID path prefix is required",
        ));
    }
    let service = build_agent_anp_message_service(&endpoint, &service_did)?;
    let options = DidDocumentOptions {
        path_segments,
        domain: Some(hostname.to_string()),
        challenge: Some(random_hex(16)),
        services: vec![service],
        did_profile: DidProfile::E1,
        ..DidDocumentOptions::default()
    };
    let creation_options = daemon_subkey.map_or_else(
        || DidDocumentCreationOptions::from(options.clone()),
        |subkey| {
            DidDocumentCreationOptions::new(options.clone())
                .with_additional_verification_method(
                    crate::internal::identity_daemon_subkey::creation_verification_method(subkey),
                )
                .with_additional_authentication(
                    crate::internal::identity_daemon_subkey::creation_authentication_reference(),
                )
        },
    );
    let bundle = create_did_wba_document_with_creation_options(hostname, creation_options)
        .map_err(|err| crate::ImError::Internal {
            message: format!("generate did document: {err}"),
        })?;
    let did = bundle
        .did()
        .filter(|value| !value.is_empty())
        .ok_or_else(|| crate::ImError::Internal {
            message: "generated did document is missing id".to_string(),
        })?;
    let key1_private_pem = required_private_key(&bundle, VM_KEY_AUTH)?;
    let key1_public_pem = required_public_key(&bundle, VM_KEY_AUTH)?;
    let e2ee_signing_private_pem = bundle
        .private_key_pem(VM_KEY_E2EE_SIGNING)
        .unwrap_or_default()
        .to_string();
    let e2ee_agreement_private_pem = bundle
        .private_key_pem(VM_KEY_E2EE_AGREEMENT)
        .unwrap_or_default()
        .to_string();
    Ok(GeneratedIdentity {
        did: crate::ids::Did::parse(did)?,
        unique_id: did_suffix(did),
        did_document: bundle.did_document,
        key1_private_pem,
        key1_public_pem,
        e2ee_signing_private_pem,
        e2ee_agreement_private_pem,
    })
}

fn default_anp_service_endpoint(hostname: &str) -> String {
    format!("https://{}{}", hostname.trim(), DEFAULT_ANP_SERVICE_PATH)
}

fn default_anp_service_did(hostname: &str) -> String {
    format!("did:wba:{}", hostname.trim())
}

fn build_agent_anp_message_service(
    service_endpoint: &str,
    service_did: &str,
) -> crate::ImResult<Value> {
    Ok(build_anp_message_service(
        "#message",
        service_endpoint.trim().to_string(),
        AnpMessageServiceOptions::default()
            .with_service_did(service_did.trim().to_string())
            .with_profiles(AGENT_MESSAGE_SERVICE_PROFILES.iter().copied())
            .with_security_profiles(AGENT_MESSAGE_SERVICE_SECURITY_PROFILES.iter().copied()),
    ))
}

fn validate_anp_service_did(service_did: &str) -> crate::ImResult<()> {
    let trimmed = service_did.trim();
    let Some(remainder) = trimmed.strip_prefix("did:wba:") else {
        return Err(crate::ImError::invalid_input(
            Some("anp_service_did".to_string()),
            "ANP service DID must use did:wba",
        ));
    };
    if trimmed.contains('#') || remainder.is_empty() || remainder.contains([':', '/', '?']) {
        return Err(crate::ImError::invalid_input(
            Some("anp_service_did".to_string()),
            "ANP service DID must be a bare-domain did:wba DID",
        ));
    }
    Ok(())
}

fn did_suffix(did: &str) -> String {
    did.rsplit(':')
        .next()
        .filter(|value| !value.is_empty())
        .unwrap_or(did)
        .to_string()
}

fn required_private_key(
    bundle: &anp::authentication::DidDocumentBundle,
    name: &str,
) -> crate::ImResult<String> {
    bundle
        .private_key_pem(name)
        .map(ToString::to_string)
        .ok_or_else(|| crate::ImError::Internal {
            message: format!("generated did document is missing {name}"),
        })
}

fn required_public_key(
    bundle: &anp::authentication::DidDocumentBundle,
    name: &str,
) -> crate::ImResult<String> {
    bundle
        .public_key_pem(name)
        .map(ToString::to_string)
        .ok_or_else(|| crate::ImError::Internal {
            message: format!("generated did document is missing {name}"),
        })
}

fn random_hex(num_bytes: usize) -> String {
    let mut buffer = vec![0_u8; num_bytes];
    rand::thread_rng().fill_bytes(&mut buffer);
    buffer.iter().map(|byte| format!("{byte:02x}")).collect()
}