use conundrum::{
purpose,
symm_encr::{EncrKey, Encrypted, DecryptionError},
};
use conventions::{admin_permissions, read_permissions, write_permissions};
use credo::{ClaimKind, Credential, Credo, GroupID, MakeClaimError};
use minitime::MsSinceEpoch;
use jmbl::Input;
use litl::Litl;
use rand07::{rngs::OsRng, RngCore};
use serde_derive::{Deserialize, Serialize};
use core::panic;
use std::{cell::RefCell, collections::HashMap, fmt::Display, rc::Rc, str::FromStr};
use jazz_telepathy::{TelepathyNode, RemoteManager};
use thiserror::Error;
#[cfg(feature = "js")]
use wasm_bindgen::prelude::*;
pub use managed_jmbl::ManagedJMBL;
mod conventions;
mod managed_jmbl;
#[derive(Copy, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
struct DocID([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 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())
}
}
pub struct JazzNode {
content_telepathy: Rc<RefCell<TelepathyNode>>,
credo: Rc<RefCell<Credo>>,
docs: HashMap<ScopedDocID, Rc<RefCell<managed_jmbl::ManagedJMBL>>>,
}
impl JazzNode {
pub fn new(
content_telepathy: Rc<RefCell<TelepathyNode>>,
credo: Rc<RefCell<Credo>>,
) -> Self {
Self {
content_telepathy,
credo,
docs: HashMap::new(),
}
}
pub fn create_document<I: Into<Input>>(
&mut self,
team: GroupID,
branch: String,
input: I,
) -> (ScopedDocID, Rc<RefCell<managed_jmbl::ManagedJMBL>>) {
let doc_id = DocID::new();
let scoped_doc_id = ScopedDocID {
team,
branch,
doc: doc_id,
};
let managed_jmbl = Rc::new(RefCell::new(managed_jmbl::ManagedJMBL::create(
scoped_doc_id.clone(),
input,
Rc::clone(&self.content_telepathy),
Rc::clone(&self.credo),
)));
self.docs
.insert(scoped_doc_id.clone(), Rc::clone(&managed_jmbl));
(scoped_doc_id, managed_jmbl)
}
pub fn load_document(
&mut self,
scoped_doc_id: ScopedDocID,
) -> Rc<RefCell<managed_jmbl::ManagedJMBL>> {
Rc::clone(self.docs.entry(scoped_doc_id.clone()).or_insert_with(|| {
Rc::new(RefCell::new(managed_jmbl::ManagedJMBL::load(
scoped_doc_id,
Rc::clone(&self.content_telepathy),
Rc::clone(&self.credo),
)))
}))
}
pub fn create_team(&mut self) -> GroupID {
let initial_recipient = Credential::new_random();
let team = self
.credo
.borrow_mut()
.create_group(initial_recipient, admin_permissions());
self.credo.borrow_mut().rotate_group_secret(&team).unwrap();
team
}
pub fn create_invitation(
&mut self,
team: GroupID,
kind: InvitationKind,
) -> Result<InvitationToken, MakeClaimError> {
let invitation = Invitation::new(team, kind);
let mut credo = self.credo.borrow_mut();
let invitation_permissions = match kind {
InvitationKind::Reader => read_permissions(),
InvitationKind::Writer => write_permissions(),
InvitationKind::Admin => admin_permissions(),
};
for permission in invitation_permissions {
credo
.make_claim_after_frontier(
&team,
ClaimKind::Permission {
to: invitation.credential.make_claim.public(),
as_of: MsSinceEpoch::now(),
kind: permission,
},
)
.unwrap();
}
credo.make_claim_after_frontier(
&team,
ClaimKind::AddGroupSecretRecipient {
recipient: invitation.credential.accept_secrets.public(),
},
)?;
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),
},
)?;
credo.re_reveal_group_secret(&team)?;
Ok(invitation_token)
}
pub fn join_team(&mut self, invitation_token: InvitationToken) -> Result<(), JoinTeamError> {
let (team_listener, mut team_receiver) = futures::channel::mpsc::unbounded();
self.credo
.borrow_mut()
.subscribe(invitation_token.team, Box::new(team_listener));
let team_state = match team_receiver.try_next() {
Err(_) => {
return Err(JoinTeamError::ExpectedInitialTeamStateReceived(
invitation_token,
))
}
Ok(None) => {
return Err(JoinTeamError::ExpectedInitialTeamStateExisting(
invitation_token,
))
}
Ok(Some(team_state)) => team_state,
};
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, InvitationEncryption>>(value.clone())
.ok()
} else {
None
}
} else {
None
}
}) {
Some(encrypted_invitation) => encrypted_invitation,
None => {
return Err(JoinTeamError::ExpectedEncryptedInvitationInTeamState(
invitation_token,
))
}
};
let invitation = match invitation_token.key.decrypt(&encrypted_invitation) {
Ok(invitation) => invitation,
Err(decryption_error) => return Err(JoinTeamError::ExpectedTokenToDecryptEncryptedInvitation(decryption_error)),
};
self.credo
.borrow_mut()
.add_credential(&invitation_token.team, invitation.credential);
Ok(())
}
pub fn content_and_credo_remotes(&self) -> (RemoteManager, RemoteManager) {
(self.content_telepathy.borrow().remotes(),
self.credo.borrow().untrusted_remotes())
}
pub fn sync_with_remotes(&mut self) {
self.credo.borrow_mut().sync_with_remotes();
self.content_telepathy.borrow_mut().sync_with_remotes();
}
}
#[derive(Copy, Clone, Serialize, Deserialize, Debug)]
pub enum InvitationKind {
Reader,
Writer,
Admin,
}
#[derive(Serialize, Deserialize)]
struct Invitation {
team: GroupID,
kind: InvitationKind,
credential: Credential,
}
purpose!(InvitationEncryption);
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, InvitationEncryption>) {
let key = EncrKey::new_random();
let encrypted = key.encrypt(self);
(
InvitationToken {
key,
team: self.team,
kind: self.kind,
},
encrypted,
)
}
}
#[derive(Debug, Serialize, Deserialize)]
pub struct InvitationToken {
team: GroupID,
kind: InvitationKind,
key: EncrKey<InvitationEncryption>,
}
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),
}