use std::{cell::RefCell, collections::HashMap, fmt::Display, rc::Rc, str::FromStr};
use audi::Listener;
use credo::{ClaimKind, Credential, Credo, GroupID, MakeClaimError};
use futures::{channel::mpsc::channel, StreamExt};
use jmbl::Input;
use litl::{impl_debug_as_litl, Litl};
use log::debug;
use mofo::Mofo;
use rand07::{rngs::OsRng, RngCore};
use ridl::symm_encr::{DecryptionError, Encrypted, KeySecret};
use serde_derive::{Deserialize, Serialize};
use thiserror::Error;
use tlpt::Remote;
#[cfg(feature = "js")]
use wasm_bindgen::prelude::*;
mod conventions;
mod managed_jmbl;
use conventions::{admin_permissions, read_permissions, write_permissions};
pub use managed_jmbl::ManagedJMBL;
#[derive(Copy, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
struct DocID(
#[serde(with = "serde_bytes_array")]
[u8; 16]
);
impl DocID {
pub fn new() -> Self {
let mut id = [0u8; 16];
OsRng {}.fill_bytes(&mut id);
DocID(id)
}
}
impl Display for DocID {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
Litl::from_se(&self).fmt(f)
}
}
#[derive(Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[cfg_attr(feature = "js", wasm_bindgen)]
pub struct ScopedDocID {
team: GroupID,
branch: String,
doc: DocID,
}
impl_debug_as_litl!(ScopedDocID);
impl Display for ScopedDocID {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"doc_{}",
bs58::encode(Litl::write_from(&self)).into_string()
)
}
}
impl FromStr for ScopedDocID {
type Err = ();
fn from_str(s: &str) -> Result<Self, Self::Err> {
let (_, id_str) = s.split_once('_').unwrap();
Ok(Litl::read_as(&bs58::decode(id_str).into_vec().unwrap()).unwrap())
}
}
struct JazzInner {
content: tlpt::Node,
credo: Credo,
docs: HashMap<ScopedDocID, ManagedJMBL>,
background: Mofo,
}
#[derive(Clone)]
pub struct Jazz(Rc<RefCell<JazzInner>>);
impl Jazz {
pub fn new(name: String, background: Mofo) -> Self {
let node = tlpt::Node::new(background.clone());
let credo = Credo::new_with_telepathy(name, node.clone(), background.clone());
Jazz(Rc::new(RefCell::new(JazzInner {
content: node,
credo,
docs: HashMap::new(),
background,
})))
}
pub async fn create_document<I: Into<Input>>(
&self,
team: GroupID,
input: I,
) -> (ScopedDocID, ManagedJMBL) {
self.create_document_branch(team, "main".to_owned(), input)
.await
}
pub async fn create_document_branch<I: Into<Input>>(
&self,
team: GroupID,
branch: String,
input: I,
) -> (ScopedDocID, ManagedJMBL) {
let doc_id = DocID::new();
let scoped_doc_id = ScopedDocID {
team,
branch,
doc: doc_id,
};
let managed_jmbl = ManagedJMBL::create(
scoped_doc_id.clone(),
input,
self.content(),
self.credo(),
self.0.borrow().background.clone(),
)
.await;
self.0
.borrow_mut()
.docs
.insert(scoped_doc_id.clone(), managed_jmbl.clone());
(scoped_doc_id, managed_jmbl)
}
pub async fn load_document(&self, scoped_doc_id: ScopedDocID) -> ManagedJMBL {
let existing = self.0.borrow().docs.get(&scoped_doc_id).cloned();
match existing {
Some(managed_jmbl) => managed_jmbl,
None => {
let (managed_jmbl, loaded) = ManagedJMBL::load(
scoped_doc_id.clone(),
self.content(),
self.credo(),
self.0.borrow().background.clone(),
);
self.0
.borrow_mut()
.docs
.insert(scoped_doc_id, managed_jmbl.clone());
loaded.await.unwrap();
managed_jmbl
}
}
}
fn credo(&self) -> Credo {
self.0.borrow().credo.clone()
}
fn content(&self) -> tlpt::Node {
self.0.borrow().content.clone()
}
pub async fn create_team(&self) -> GroupID {
let initial_recipient = Credential::new_random();
let credo = self.credo();
let team = credo
.create_group(initial_recipient, admin_permissions())
.await;
credo.rotate_group_secret(&team).await.unwrap();
team
}
pub async fn create_invitation(
&self,
team: GroupID,
kind: InvitationKind,
) -> Result<InvitationToken, MakeClaimError> {
let invitation = Invitation::new(team, kind);
let invitation_permissions = match kind {
InvitationKind::Reader => read_permissions(),
InvitationKind::Writer => write_permissions(),
InvitationKind::Admin => admin_permissions(),
};
let credo = self.0.borrow().credo.clone();
for permission in invitation_permissions {
credo
.make_claim_after_frontier(
&team,
ClaimKind::Permission {
to: invitation.credential.for_making_claims.pub_id(),
as_of: ti64::now(),
permitted: permission,
},
)
.await
.unwrap();
}
credo
.make_claim_after_frontier(
&team,
ClaimKind::AddGroupSecretRecipient {
recipient: invitation.credential.for_accepting_secrets.pub_id(),
},
)
.await?;
match kind {
InvitationKind::Writer | InvitationKind::Admin => {
let set_access = credo.write_access_for(&team).unwrap();
credo
.make_claim_after_frontier(
&team,
ClaimKind::RevealSetAccess {
set_access: invitation
.credential
.for_accepting_secrets
.pub_id()
.encrypt_from_anon(&set_access),
},
)
.await?;
}
_ => {}
}
let (invitation_token, encrypted_invitation) = invitation.encrypt_for_sending();
credo
.make_claim_after_frontier(
&team,
ClaimKind::Statement {
path: format!("invitations/{:?}", invitation_token.key.id),
value: Litl::from_se(&encrypted_invitation),
},
)
.await?;
credo.re_reveal_group_secret(&team).await?;
Ok(invitation_token)
}
pub async fn join_team(
&self,
invitation_token: InvitationToken,
) -> Result<GroupID, JoinTeamError> {
let (team_tx, mut team_rx) = channel(100);
let credo = self.credo();
credo
.subscribe(
invitation_token.team,
Listener::new(&format!("joining-{:?}", invitation_token.team), team_tx),
)
.await;
let joining = async {
loop {
let team_state = match team_rx.next().await {
Some(team_state) => team_state,
None => {
debug!("Joining team, no team state yet");
continue;
}
};
let encrypted_invitation =
match team_state.valid_claims.iter().find_map(|(_, claim)| {
if let ClaimKind::Statement { path, value } = &claim.expect_v1().kind {
if path == &format!("invitations/{:?}", invitation_token.key.id) {
Litl::try_into_de::<Encrypted<Invitation>>(value.clone()).ok()
} else {
None
}
} else {
None
}
}) {
Some(encrypted_invitation) => encrypted_invitation,
None => {
debug!("Joining team, no invitation found in team yet");
continue;
}
};
match invitation_token.key.decrypt(&encrypted_invitation) {
Ok(invitation) => break Ok(invitation),
Err(decryption_error) => {
break Err(JoinTeamError::ExpectedTokenToDecryptEncryptedInvitation(
decryption_error,
))
}
};
}
};
let invitation = joining.await?;
credo.add_credential(&invitation_token.team, invitation.credential);
Ok(invitation_token.team)
}
pub async fn add_remote(&self, remote: Remote) {
self.credo().add_remote(remote).await;
}
}
#[derive(Copy, Clone, Serialize, Deserialize, Debug)]
pub enum InvitationKind {
Reader,
Writer,
Admin,
}
#[derive(Serialize, Deserialize)]
struct Invitation {
team: GroupID,
kind: InvitationKind,
credential: Credential,
}
impl Invitation {
pub fn new(team: GroupID, kind: InvitationKind) -> Invitation {
Invitation {
team,
kind,
credential: Credential::new_random(),
}
}
pub fn encrypt_for_sending(&self) -> (InvitationToken, Encrypted<Invitation>) {
let key = KeySecret::new_random();
let encrypted = key.encrypt(self);
(
InvitationToken {
key,
team: self.team,
kind: self.kind,
},
encrypted,
)
}
}
#[derive(Serialize, Deserialize)]
pub struct InvitationToken {
team: GroupID,
kind: InvitationKind,
key: KeySecret,
}
impl_debug_as_litl!(InvitationToken);
impl Display for InvitationToken {
fn fmt(&self, f: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> {
let team_str = bs58::encode(Litl::write_from(&self.team)).into_string();
let kind_str = match self.kind {
InvitationKind::Reader => "read_key",
InvitationKind::Writer => "write_key",
InvitationKind::Admin => "admin_key",
};
let key_str = bs58::encode(Litl::write_from(&self.key)).into_string();
write!(f, "team_{}_{}_{}", team_str, kind_str, key_str)
}
}
impl FromStr for InvitationToken {
type Err = ();
fn from_str(s: &str) -> Result<Self, Self::Err> {
let (start, key_str) = s.rsplit_once('_').unwrap();
let (_team_str, middle) = start.split_once('_').unwrap();
let (team_str, kind_str) = middle.split_once('_').unwrap();
Ok(InvitationToken {
team: Litl::read_as(&bs58::decode(team_str).into_vec().unwrap()).unwrap(),
kind: match kind_str {
"read_key" => InvitationKind::Reader,
"write_key" => InvitationKind::Writer,
"admin_key" => InvitationKind::Admin,
_ => panic!("Expected key kind to be 'read_key', 'write_key', or 'admin_key'"),
},
key: Litl::read_as(&bs58::decode(key_str).into_vec().unwrap()).unwrap(),
})
}
}
#[derive(Error, Debug)]
pub enum JoinTeamError {
#[error("Expected to have received initial team state")]
ExpectedInitialTeamStateReceived(InvitationToken),
#[error("Expected team state to exist on receipt")]
ExpectedInitialTeamStateExisting(InvitationToken),
#[error("Expected encrypted invitation to exist in team state")]
ExpectedEncryptedInvitationInTeamState(InvitationToken),
#[error(transparent)]
ExpectedTokenToDecryptEncryptedInvitation(DecryptionError),
}
mod serde_bytes_array {
use core::convert::TryInto;
use serde::de::Error;
use serde::{Deserializer, Serializer};
pub(crate) fn serialize<S>(bytes: &[u8], serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serde_bytes::serialize(bytes, serializer)
}
pub(crate) fn deserialize<'de, D, const N: usize>(deserializer: D) -> Result<[u8; N], D::Error>
where
D: Deserializer<'de>,
{
let bytes: Vec<u8> = serde_bytes::deserialize(deserializer)?;
let array: [u8; N] = bytes.as_slice().try_into().map_err(|_| {
let expected = format!("[u8; {}]", N);
D::Error::invalid_length(bytes.len(), &expected.as_str())
})?;
Ok(array)
}
}