enigma-identity 0.1.0

Enigma Identity: local identity + X3DH bundle + shared secret derivation
Documentation
use crate::error::{EnigmaIdentityError, Result};
use crate::types::{LocalUser, X3dhBundle, X3dhResponderKeys};
use ed25519_dalek::{Signer, SigningKey, VerifyingKey};
use rand::rngs::OsRng;
use uuid::Uuid;
use x25519_dalek::{PublicKey as XPublicKey, StaticSecret as XSecret};

const MAX_USERNAME_LEN: usize = 64;

pub struct LocalIdentity {
    user: LocalUser,
    identity_signing: SigningKey,
    signed_prekey: XSecret,
    signed_prekey_signature: [u8; 64],
}

impl LocalIdentity {
    pub fn new(username: &str) -> Result<Self> {
        let username = Self::validate_username(username)?;

        let mut rng = OsRng;
        let identity_signing = SigningKey::generate(&mut rng);

        let signed_prekey = XSecret::random_from_rng(&mut rng);
        let signed_prekey_public = XPublicKey::from(&signed_prekey);

        let payload =
            Self::signed_prekey_payload(&identity_signing.verifying_key(), &signed_prekey_public);

        let signature = identity_signing.sign(&payload);
        let signed_prekey_signature = signature.to_bytes();

        Ok(Self {
            user: LocalUser {
                uuid: Uuid::new_v4(),
                username: username.to_string(),
            },
            identity_signing,
            signed_prekey,
            signed_prekey_signature,
        })
    }

    pub fn user(&self) -> &LocalUser {
        &self.user
    }

    pub fn bundle(&self) -> X3dhBundle {
        let id_pk = self.identity_signing.verifying_key().to_bytes();
        let spk_pk = XPublicKey::from(&self.signed_prekey).to_bytes();

        X3dhBundle {
            username: self.user.username.clone(),
            identity_public_key: id_pk,
            signed_prekey_public_key: spk_pk,
            signed_prekey_signature: self.signed_prekey_signature,
        }
    }

    pub fn responder_keys(&self) -> X3dhResponderKeys {
        let id_pk = self.identity_signing.verifying_key().to_bytes();
        let id_sk = self.identity_signing.to_bytes();
        let spk_pk = XPublicKey::from(&self.signed_prekey).to_bytes();
        let spk_sk = self.signed_prekey.to_bytes();

        X3dhResponderKeys {
            identity_public_key: id_pk,
            identity_secret_key: id_sk,
            signed_prekey_public_key: spk_pk,
            signed_prekey_secret_key: spk_sk,
            signed_prekey_signature: self.signed_prekey_signature,
        }
    }

    pub fn verify_bundle(bundle: &X3dhBundle) -> Result<()> {
        Self::validate_username(&bundle.username)?;

        let vk = VerifyingKey::from_bytes(&bundle.identity_public_key)
            .map_err(|_| EnigmaIdentityError::InvalidData)?;
        let spk_pk = XPublicKey::from(bundle.signed_prekey_public_key);

        let payload = Self::signed_prekey_payload(&vk, &spk_pk);

        let sig = ed25519_dalek::Signature::from_bytes(&bundle.signed_prekey_signature);
        vk.verify_strict(&payload, &sig)
            .map_err(|_| EnigmaIdentityError::InvalidBundleSignature)
    }

    fn signed_prekey_payload(vk: &VerifyingKey, signed_prekey_public: &XPublicKey) -> Vec<u8> {
        let mut out = Vec::with_capacity(64);
        out.extend_from_slice(&vk.to_bytes());
        out.extend_from_slice(&signed_prekey_public.to_bytes());
        out
    }

    fn validate_username(input: &str) -> Result<&str> {
        let trimmed = input.trim();
        if trimmed.is_empty() || trimmed.len() > MAX_USERNAME_LEN {
            return Err(EnigmaIdentityError::InvalidUsername);
        }
        Ok(trimmed)
    }
}