credo 0.8.0

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

use litl::{impl_debug_as_litl};
use mofo::Mofo;
use ridl::{
    asymm_encr::RecipientSecret,
    signing::{Signed, SignerSecret},
    symm_encr::{ KeySecret},
};

// TODO(refactor): #35 re-export Remote, RemoteMode
use caro::{Remote, SetItem, SetItemID};

use serde_derive::{Deserialize, Serialize};

mod claims_and_permissions_v1;
mod credential_source;
mod scope;

use claims_and_permissions_v1::CredoV1Claim;
pub use claims_and_permissions_v1::{ClaimBody, PermissionKind, WRITE_TO_SCOPE};
use scope::{
    ScopeSetHeader,
};
pub use scope::{
    scope_state::{ScopeSecretRecipientState, ScopeState, ValidClaims},
    Scope, ScopeID,
    MakeClaimError
};

pub use credential_source::{CredentialSource, SimpleCredentialSource, CredentialSourceHash};

// TODO(design): #38 build this as a peer-to-peer prototcol, with explicit rights for seeing scopes and time witnessing and then just make the credo backend a default trusted one (configurable)

// TODO(design): #39 listeners are only interested in valid claims changing, which happens on: new claims & timestamps reached

struct CredoInner {
    name: String,
    telepathy: caro::Node,
    scopes: HashMap<(ScopeID, CredentialSourceHash), Scope>,
    background: Mofo,
}

#[derive(Clone)]
pub struct Credo(Rc<RefCell<CredoInner>>);

impl Credo {
    pub fn new(name: String, background: Mofo) -> Credo {
        let telepathy = caro::Node::new(background.clone());

        Self::new_with_telepathy(name, telepathy, background)
    }

    pub fn new_with_telepathy(name: String, telepathy: caro::Node, background: Mofo) -> Credo {
        Credo(Rc::new(RefCell::new(CredoInner {
            name,
            telepathy,
            scopes: HashMap::new(),
            background,
        })))
    }

    fn name(&self) -> String {
        self.0.borrow().name.clone()
    }

    pub async fn create_scope(
        &self,
        initial_recipient: Credential,
        initial_permissions: Vec<PermissionKind>,
        parent_scope: Option<ScopeID>,
        credential_source: Box<dyn CredentialSource>,
    ) -> Scope {
        let mut bootstrap_claims = Vec::<SetItem>::new();

        for initial_permission in initial_permissions.into_iter().chain([
            PermissionKind::AddSharedSecretRecipient {
                secret_kind: WRITE_TO_SCOPE.to_string(),
            },
            PermissionKind::RevealSharedSecret {
                secret_kind: WRITE_TO_SCOPE.to_string(),
            },
            PermissionKind::EntrustToSharedSecret {
                secret_kind: WRITE_TO_SCOPE.to_string(),
            },
        ]) {
            bootstrap_claims.push(SetItem::new(
                &Claim::new_v1(
                    &initial_recipient.for_making_claims,
                    ClaimBody::Permission {
                        to: initial_recipient.for_making_claims.pub_id(),
                        permitted: initial_permission,
                        as_of: ti64::now(),
                    },
                ),
                bootstrap_claims.last().map(|item| item.id()),
            ));
        }

        bootstrap_claims.push(SetItem::new(
            &Claim::new_v1(
                &initial_recipient.for_making_claims,
                ClaimBody::AddSharedSecretRecipient {
                    secret_kind: WRITE_TO_SCOPE.to_string(),
                    recipient: initial_recipient.for_accepting_secrets.pub_id(),
                },
            ),
            bootstrap_claims.last().map(|item| item.id()),
        ));

        if let Some(parent_scope) = parent_scope {
            bootstrap_claims.push(SetItem::new(
                &Claim::new_v1(
                    &initial_recipient.for_making_claims,
                    ClaimBody::InheritFrom {
                        parent: parent_scope,
                    },
                ),
                bootstrap_claims.last().map(|item| item.id()),
            ));
        }

        let write_to_scope_shared_secret = KeySecret::new_random();
        let initial_secret_recipient = initial_recipient.for_accepting_secrets.pub_id();

        bootstrap_claims.push(SetItem::new(
            &Claim::new_v1(
                &initial_recipient.for_making_claims,
                ClaimBody::AddSharedSecretRecipient {
                    secret_kind: WRITE_TO_SCOPE.to_string(),
                    recipient: initial_secret_recipient.clone(),
                },
            ),
            bootstrap_claims.last().map(|item| item.id()),
        ));

        bootstrap_claims.push(SetItem::new(
            &Claim::new_v1(
                &initial_recipient.for_making_claims,
                ClaimBody::RevealSharedSecret {
                    secret_kind: WRITE_TO_SCOPE.to_string(),
                    key_id: write_to_scope_shared_secret.id,
                    encrypted_per_recipient: [(
                        initial_secret_recipient.clone(),
                        initial_secret_recipient.encrypt_from_anon(&write_to_scope_shared_secret),
                    )]
                    .into_iter()
                    .collect(),
                },
            ),
            bootstrap_claims.last().map(|item| item.id()),
        ));

        let telepathy = self
        .0
        .borrow()
        .telepathy.clone();

        let (set_id, set_access) = telepathy
            .create_set(Some(ScopeSetHeader {
                bootstrap_claims: bootstrap_claims
                    .iter()
                    .map(|claim_item| claim_item.id())
                    .collect::<BTreeSet<_>>(),
            }))
            .await;

        let mut last_claim_id = bootstrap_claims
            .last()
            .as_ref()
            .expect("Expected at least one bootstrap claim")
            .id();

        let scope = self.get_scope(&ScopeID(set_id), credential_source.clone_ref());

        for claim_item in bootstrap_claims {
            scope.add_claim_raw(&set_access, claim_item).await.unwrap();
        }

        credential_source
            .add_credential(scope.id(), initial_recipient.clone())
            .await;

        let mut revealed_set_access_to_at_least_one_key = false;
        for (_, maybe_key_secret) in scope
            .current_shared_secrets_for(WRITE_TO_SCOPE)
            .await
            .unwrap()
        {
            if let Ok(key_secret) = maybe_key_secret {
                last_claim_id = scope
                    .add_claim(
                        &set_access,
                        Claim::new_v1(
                            &initial_recipient.for_making_claims,
                            ClaimBody::EntrustToSharedSecret {
                                secret_kind: WRITE_TO_SCOPE.to_string(),
                                entrusted_secret_name: ScopeID(set_id).to_string(),
                                for_key_id: key_secret.id,
                                encrypted: key_secret.encrypt(&set_access).as_encrypted_value(),
                            },
                        ),
                        Some(last_claim_id),
                    )
                    .await
                    .unwrap();
                revealed_set_access_to_at_least_one_key = true;
            }
        }

        if !revealed_set_access_to_at_least_one_key {
            panic!("Expected to reveal set access to at least one key");
        }

        scope
    }

    pub fn get_scope(
        &self,
        scope_id: &ScopeID,
        credential_source: Box<dyn CredentialSource>,
    ) -> Scope {
        let scope_key = (*scope_id, credential_source.id_hash());

        if let Some(scope) = self.0.borrow().scopes.get(&scope_key) {
            return scope.clone();
        }

        let telepathy = (*self.0).borrow().telepathy.clone();
        let background = (*self.0).borrow().background.clone();
        let scope = Scope::new(
            self.clone(),
            *scope_id,
            telepathy,
            credential_source,
            background,
        );
        self.0.borrow_mut().scopes.insert(scope_key, scope.clone());

        scope
    }

    pub async fn add_remote(&self, remote: Remote) {
        let telepathy = (*self.0).borrow_mut().telepathy.clone();
        telepathy.add_remote(remote).await
    }
}

#[derive(Clone, Serialize, Deserialize)]
pub struct Credential {
    #[serde(with = "ser_rc")]
    pub for_accepting_secrets: Rc<RecipientSecret>,
    #[serde(with = "ser_rc")]
    pub for_making_claims: Rc<SignerSecret>,
}

mod ser_rc {
    use serde::{Deserialize, Deserializer, Serialize, Serializer};
    use std::rc::Rc;

    pub fn serialize<S, T>(value: &Rc<T>, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: Serializer,
        T: Serialize,
    {
        value.as_ref().serialize(serializer)
    }

    pub fn deserialize<'de, D, T>(deserializer: D) -> Result<Rc<T>, D::Error>
    where
        D: Deserializer<'de>,
        T: Deserialize<'de>,
    {
        Ok(Rc::new(T::deserialize(deserializer)?))
    }
}

impl Credential {
    pub fn new_random() -> Self {
        Self {
            for_accepting_secrets: Rc::new(RecipientSecret::new_random()),
            for_making_claims: Rc::new(SignerSecret::new_random()),
        }
    }
}

pub type ClaimID = SetItemID;

#[derive(Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(tag = "version")]
pub enum Claim {
    //TODO-V1(optimization): Vec<CredoV1Claim> to save on signatures
    #[serde(rename = "v1")]
    CredoV1(Signed<CredoV1Claim>),
}

impl_debug_as_litl!(Claim);

impl Claim {
    pub fn expect_v1(&self) -> &Signed<CredoV1Claim> {
        match self {
            Claim::CredoV1(claim) => claim,
        }
    }

    pub fn new_v1(signer_secret: &SignerSecret, kind: ClaimBody) -> Self {
        Claim::CredoV1(signer_secret.sign(CredoV1Claim {
            by: signer_secret.pub_id(),
            made_at: ti64::now(),
            body: kind,
        }))
    }
}