credo 0.8.0

A framework for trust-free distributed cryptographic claims and secret management
Documentation
use std::collections::{BTreeSet, HashMap};

use litl::impl_debug_as_litl;
use ridl::{
    asymm_encr::{AsymmDecryptionError, RecipientID},
    hashing::HashOf,
    symm_encr::{KeyID, KeySecret},
};
use serde_derive::{Deserialize, Serialize};
use thiserror::Error;
use ti64::MsSinceEpoch;

use crate::{claims_and_permissions_v1::CredoV1Claim, ClaimID, ClaimBody, Credential, ScopeID};

use super::combined_claim_set::ClaimInvalidReason;

pub type ValidClaims = HashMap<ClaimID, (CredoV1Claim, ScopeID)>;
pub type InvalidClaims = HashMap<ClaimID, ClaimInvalidReason>;
pub type SecretRecipients = HashMap<String, BTreeSet<RecipientID>>;
pub type ScopeSecretRecipientState = HashOf<SecretRecipients>;

#[derive(Clone, Serialize, Deserialize)]
pub struct ScopeState {
    pub valid_claims: ValidClaims,
    pub invalid_claims: InvalidClaims,
    pub secret_recipients: SecretRecipients,
}

impl_debug_as_litl!(ScopeState);

impl ScopeState {
    pub fn secret_state(&self) -> ScopeSecretRecipientState {
        HashOf::hash(&self.secret_recipients)
    }

    pub fn newest_shared_secret_ids(&self, secret_kind: &str) -> HashMap<ScopeID, KeyID> {
        let mut newest_shared_secret_per_scope = HashMap::<ScopeID, (KeyID, MsSinceEpoch)>::new();

        for (claim, source_scope) in self.valid_claims.values() {
            if let ClaimBody::RevealSharedSecret {
                key_id,
                secret_kind: revealed_secret_kind,
                ..
            } = &claim.body
            {
                if secret_kind == revealed_secret_kind {
                    match newest_shared_secret_per_scope.entry(*source_scope) {
                        std::collections::hash_map::Entry::Occupied(mut existing) => {
                            if existing.get().1 < claim.made_at {
                                existing.insert((*key_id, claim.made_at));
                            }
                        }
                        std::collections::hash_map::Entry::Vacant(vacant) => {
                            vacant.insert((*key_id, claim.made_at));
                        }
                    }
                }
            }
        }

        newest_shared_secret_per_scope
            .into_iter()
            .map(|(scope, (key_id, _))| (scope, key_id))
            .collect()
    }

    pub fn get_current_shared_secrets(
        &self,
        secret_kind: &str,
        credentials_to_try: &[Credential],
    ) -> Result<HashMap<ScopeID, Result<KeySecret, GetSharedSecretError>>, GetSharedSecretError>
    {
        let newest_key_ids = self.newest_shared_secret_ids(secret_kind);

        if newest_key_ids.is_empty() {
            return Err(GetSharedSecretError::NoNewestKeyId);
        }

        Ok(newest_key_ids
            .into_iter()
            .map(|(scope, key_id)| {
                (
                    scope,
                    self.get_shared_secret(secret_kind, key_id, credentials_to_try),
                )
            })
            .collect())
    }

    pub fn get_shared_secret(
        &self,
        shared_secret_kind: &str,
        shared_secret_key_id: KeyID,
        credentials_to_try: &[Credential],
    ) -> Result<KeySecret, GetSharedSecretError> {
        let mut encrypted_per_recipient_entries = self
            .valid_claims
            .iter()
            .filter_map(|(_, (claim, _))| match &claim.body {
                ClaimBody::RevealSharedSecret {
                    key_id,
                    encrypted_per_recipient,
                    secret_kind,
                } if key_id == &shared_secret_key_id && secret_kind == shared_secret_kind => {
                    Some(encrypted_per_recipient)
                }
                _ => None,
            })
            .flatten()
            .peekable();

        if encrypted_per_recipient_entries.peek().is_none() {
            Err(GetSharedSecretError::NoRevelationFound(
                shared_secret_kind.to_string(),
                shared_secret_key_id,
            ))
        } else if let Some((credential, matching_encrypted_key)) = encrypted_per_recipient_entries
            .find_map(|(recipent, encrypted_key)| {
                credentials_to_try.iter().find_map(|credential| {
                    if recipent == &credential.for_accepting_secrets.pub_id() {
                        Some((credential, encrypted_key))
                    } else {
                        None
                    }
                })
            })
        {
            Ok(credential
                .for_accepting_secrets
                .decrypt(matching_encrypted_key)?)
        } else {
            Err(GetSharedSecretError::NoRevelationFoundForCredentials(
                shared_secret_kind.to_string(),
                shared_secret_key_id,
                credentials_to_try
                    .iter()
                    .map(|c| c.for_accepting_secrets.pub_id())
                    .collect(),
            ))
        }
    }
}

#[derive(Error, Debug)]
pub enum GetSharedSecretError {
    #[error("Could not find newest key id")]
    NoNewestKeyId,
    #[error("Could not find revelation of shared secret of kind {0}, id {1:?}")]
    NoRevelationFound(String, KeyID),
    #[error("Could not find revelation of shared secret of kind {0}, id {1:?} for available credentials {2:?}")]
    NoRevelationFoundForCredentials(String, KeyID, Vec<RecipientID>),
    #[error("No credentials for scope")]
    NoCredentialsForScope,
    #[error(transparent)]
    DecryptionError(#[from] AsymmDecryptionError),
}