/* This file is part of DarkFi (https://dark.fi)
*
* Copyright (C) 2020-2025 Dyne.org foundation
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see .
*/
use std::{
collections::HashMap,
env,
ffi::CString,
fs::{create_dir_all, remove_dir_all},
io::{stdin, Write},
str::FromStr,
sync::{Arc, OnceLock},
};
use crypto_box::{
aead::{Aead, AeadCore},
ChaChaBox, SecretKey,
};
use darkfi_serial::{
async_trait, deserialize, deserialize_async_partial, serialize, serialize_async,
SerialDecodable, SerialEncodable,
};
use futures::{select, FutureExt};
use libc::mkfifo;
use log::{debug, error, info};
use rand::rngs::OsRng;
use sled_overlay::sled;
use smol::{fs, stream::StreamExt};
use structopt_toml::StructOptToml;
use tinyjson::JsonValue;
use darkfi::{
async_daemonize,
event_graph::{
proto::{EventPut, ProtocolEventGraph},
Event, EventGraph, EventGraphPtr,
},
net::{session::SESSION_DEFAULT, P2p, P2pPtr},
rpc::{
jsonrpc::JsonSubscriber,
server::{listen_and_serve, RequestHandler},
},
system::{sleep, StoppableTask},
util::path::{expand_path, get_config_path},
Error, Result,
};
use darkfi_sdk::crypto::{
schnorr::{SchnorrPublic, SchnorrSecret, Signature},
Keypair, PublicKey,
};
mod jsonrpc;
mod settings;
use taud::{
error::{TaudError, TaudResult},
task_info::{TaskEvent, TaskInfo},
util::pipe_write,
};
use crate::{
jsonrpc::JsonRpcInterface,
settings::{Args, CONFIG_FILE, CONFIG_FILE_CONTENTS},
};
struct Workspace {
read_key: ChaChaBox,
write_key: Option,
write_pubkey: PublicKey,
}
impl Workspace {
fn new() -> Self {
let secret_key = SecretKey::generate(&mut OsRng);
let keypair = Keypair::default();
Self {
read_key: ChaChaBox::new(&secret_key.public_key(), &secret_key),
write_key: None,
write_pubkey: keypair.public,
}
}
}
#[derive(Debug, Clone, SerialEncodable, SerialDecodable)]
pub struct EncryptedTask {
payload: String,
}
#[derive(SerialEncodable, SerialDecodable)]
struct SignedTask {
task: Vec,
signature: Signature,
}
impl SignedTask {
fn new(task: &TaskInfo, signature: Signature) -> Self {
Self { task: serialize(task), signature }
}
}
/// Sign then encrypt a task
fn encrypt_sign_task(task: &TaskInfo, workspace: &Workspace) -> TaudResult {
debug!(target: "taud", "start encrypting task");
if workspace.write_key.is_none() {
error!(target: "taud", "You don't have write access")
}
let signature: Signature = workspace.write_key.as_ref().unwrap().sign(&serialize(task)[..]);
let signed_task = SignedTask::new(task, signature);
let nonce = ChaChaBox::generate_nonce(&mut OsRng);
let payload = &serialize(&signed_task)[..];
let mut payload = workspace.read_key.encrypt(&nonce, payload)?;
let mut concat = vec![];
concat.append(&mut nonce.as_slice().to_vec());
concat.append(&mut payload);
let payload = bs58::encode(concat.clone()).into_string();
Ok(EncryptedTask { payload })
}
fn try_decrypt_task(
encrypt_task: &EncryptedTask,
chacha_box: &ChaChaBox,
) -> TaudResult {
debug!(target: "taud", "start decrypting task");
let bytes = match bs58::decode(&encrypt_task.payload).into_vec() {
Ok(v) => v,
Err(_) => return Err(TaudError::DecryptionError("Error decoding payload".to_string())),
};
if bytes.len() < 25 {
return Err(TaudError::DecryptionError("Invalid bytes length".to_string()))
}
// Try extracting the nonce
let nonce = bytes[0..24].into();
// Take the remaining ciphertext
let message = &bytes[24..];
// let nonce = encrypt_task.nonce.as_slice();
let decrypted_task = chacha_box.decrypt(nonce, message)?;
let signed_task = deserialize(&decrypted_task)?;
Ok(signed_task)
}
fn parse_configured_workspaces(data: &toml::Value) -> Result> {
let mut ret = HashMap::new();
let Some(table) = data.as_table() else { return Err(Error::ParseFailed("TOML not a map")) };
let Some(workspace) = table.get("workspace") else { return Ok(ret) };
let Some(workspace) = workspace.as_table() else {
return Err(Error::ParseFailed("`workspace` not a map"))
};
for (name, items) in workspace {
let mut ws = Workspace::new();
if let Some(read_key) = items.get("read_key") {
if let Some(read_key) = read_key.as_str() {
let Ok(read_key_bytes) = bs58::decode(read_key).into_vec() else {
return Err(Error::ParseFailed("Workspace secret not valid base58"))
};
if read_key_bytes.len() != 32 {
return Err(Error::ParseFailed("Workspace read_key not 32 bytes long"))
}
let read_key_bytes: [u8; 32] = read_key_bytes.try_into().unwrap();
let read_key = crypto_box::SecretKey::from(read_key_bytes);
let public = read_key.public_key();
ws.read_key = ChaChaBox::new(&public, &read_key);
} else {
return Err(Error::ParseFailed("Workspace read_key not a string"))
}
} else {
return Err(Error::ParseFailed("Workspace read_key is not set"))
}
if let Some(write_pubkey) = items.get("write_public_key") {
if let Some(write_pubkey) = write_pubkey.as_str() {
if !write_pubkey.is_empty() {
info!(target: "taud", "Found configured write_public_key for {} workspace", name);
let write_key = PublicKey::from_str(write_pubkey).unwrap();
// let write_pubkey = write_pubkey.to_string();
// let decoded_write_pubkey = bs58::decode(write_pubkey).into_vec().unwrap();
ws.write_pubkey = write_key;
}
} else {
return Err(Error::ParseFailed("Workspace write_public_key not a string"))
}
} else {
return Err(Error::ParseFailed("Workspace write_public_key is not set"))
}
if let Some(write_key) = items.get("write_key") {
if let Some(write_key) = write_key.as_str() {
if !write_key.is_empty() {
info!(target: "taud", "Found configured write_key for {} workspace", name);
let write_key = write_key.to_string();
let write_key_bytes = bs58::decode(write_key).into_vec().unwrap();
let secret = match darkfi_sdk::crypto::SecretKey::from_bytes(
write_key_bytes.try_into().unwrap(),
) {
Ok(key) => key,
Err(e) => {
error!(target: "taud", "Failed parsing write_key: {}", e);
return Err(Error::ParseFailed("Failed parsing write_key"))
}
};
ws.write_key = Some(secret);
}
} else {
return Err(Error::ParseFailed("Workspace write_key not a string"))
}
}
if let Some(wrt_key) = ws.write_key.as_ref() {
let pk = PublicKey::from_secret(*wrt_key);
if pk != ws.write_pubkey {
error!(target: "taud", "Wrong keypair for {} workspace, the workspace is not added!", name);
continue
}
}
info!(target: "taud", "Configured NaCl box for workspace {}", name);
ret.insert(name.to_string(), ws);
}
Ok(ret)
}
async fn get_workspaces(settings: &Args) -> Result> {
let config_path = get_config_path(settings.config.clone(), CONFIG_FILE)?;
let contents = fs::read_to_string(config_path).await?;
let contents = match toml::from_str(&contents) {
Ok(v) => v,
Err(e) => {
error!(target: "taud", "Failed parsing TOML config: {}", e);
return Err(Error::ParseFailed("Failed parsing TOML config"))
}
};
let workspaces = parse_configured_workspaces(&contents)?;
Ok(workspaces)
}
/// Atomically mark a message as seen.
pub async fn mark_seen(
sled_db: sled::Db,
seen: OnceLock,
event_id: &blake3::Hash,
) -> Result<()> {
let db = seen.get_or_init(|| sled_db.open_tree("tau_seen").unwrap());
debug!(target: "taud", "Marking event {} as seen", event_id);
let mut batch = sled::Batch::default();
batch.insert(event_id.as_bytes(), &[]);
Ok(db.apply_batch(batch)?)
}
/// Check if a message was already marked seen.
pub async fn is_seen(
sled_db: sled::Db,
seen: OnceLock,
event_id: &blake3::Hash,
) -> Result {
let db = seen.get_or_init(|| sled_db.open_tree("tau_seen").unwrap());
Ok(db.contains_key(event_id.as_bytes())?)
}
#[allow(clippy::too_many_arguments)]
async fn start_sync_loop(
event_graph: EventGraphPtr,
broadcast_rcv: smol::channel::Receiver,
workspaces: Arc>,
sled_db: sled::Db,
settings: Args,
p2p: P2pPtr,
seen: OnceLock,
) -> TaudResult<()> {
let incoming = event_graph.event_pub.clone().subscribe().await;
loop {
select! {
// Process message from Tau client
task_event = broadcast_rcv.recv().fuse() => {
let tk = task_event.map_err(Error::from)?;
if workspaces.contains_key(&tk.workspace) {
let ws = workspaces.get(&tk.workspace).unwrap();
let encrypted_task = encrypt_sign_task(&tk, ws)?;
info!(target: "taud", "Send the task: ref: {}", tk.ref_id);
// Build a DAG event and return it.
let event = Event::new(
serialize_async(&encrypted_task).await,
&event_graph,
)
.await;
// If it fails for some reason, for now, we just note it
// and pass.
if let Err(e) = event_graph.dag_insert(&[event.clone()]).await {
error!(target: "taud", "Failed inserting new event to DAG: {}", e);
} else {
// Otherwise, broadcast it
p2p.broadcast(&EventPut(event)).await;
}
}
}
// Process message from the network. These should only be EncryptedTask.
task_event = incoming.receive().fuse() => {
let event_id = task_event.id();
if is_seen(sled_db.clone(), seen.clone(), &event_id).await? {
continue
}
mark_seen(sled_db.clone(), seen.clone(), &event_id).await?;
// Try to deserialize the `Event`'s content into a `EncryptedTask`
let enc_task: EncryptedTask = match deserialize_async_partial(task_event.content()).await {
Ok((v, _)) => v,
Err(e) => {
error!(target: "taud", "[TAUD] Failed deserializing incoming EncryptedTask event: {}", e);
continue
}
};
on_receive_task(&enc_task, &workspaces, &settings)
.await?;
}
}
}
}
/// Handle a received task, decrypt it, verify it, optionally write it
/// to a named pipe and save it on disk.
async fn on_receive_task(
enc_task: &EncryptedTask,
workspaces: &HashMap,
settings: &Args,
) -> TaudResult<()> {
for (ws_name, workspace) in workspaces.iter() {
let signed_task = try_decrypt_task(enc_task, &workspace.read_key);
if let Err(e) = signed_task {
debug!(target: "taud", "Unable to decrypt the task: {}", e);
continue
}
if !workspace
.write_pubkey
.verify(&signed_task.as_ref().unwrap().task, &signed_task.as_ref().unwrap().signature)
{
error!(target: "taud", "Task is not verified: wrong write_public_key");
error!(target: "taud", "Task is not saved");
continue
}
let mut task: TaskInfo = deserialize(&signed_task.unwrap().task)?;
info!(target: "taud", "Save the task: ref: {}", task.ref_id);
task.workspace.clone_from(ws_name);
let datastore_path = expand_path(&settings.datastore)?;
// Push a notification to a fifo if set
if settings.piped {
// if we can't load the task then it's a new task.
// otherwise it's a modification.
match TaskInfo::load(&task.ref_id, &datastore_path) {
Ok(loaded_task) => {
let loaded_events = loaded_task.events;
let mut events = task.events.clone();
events.retain(|ev| !loaded_events.contains(ev));
let file = settings.pipe_path.clone();
let mut pipe_write = pipe_write(file)?;
let mut task_clone = task.clone();
task_clone.events = events;
let json: JsonValue = (&task_clone).into();
pipe_write.write_all(json.stringify().unwrap().as_bytes())?;
}
Err(_) => {
let file = settings.pipe_path.clone();
let mut pipe_write = pipe_write(file)?;
let mut task_clone = task.clone();
task_clone.events.push(TaskEvent::new(
"add_task".to_string(),
task_clone.owner.clone(),
"".to_string(),
));
let json: JsonValue = (&task_clone).into();
pipe_write.write_all(json.stringify().unwrap().as_bytes())?;
}
}
}
task.save(&datastore_path)?;
}
Ok(())
}
async_daemonize!(realmain);
async fn realmain(settings: Args, executor: Arc>) -> Result<()> {
let datastore_path = expand_path(&settings.datastore)?;
let nickname =
if settings.nickname.is_some() { settings.nickname.clone() } else { env::var("USER").ok() };
if settings.refresh {
println!("Removing local data in: {:?} (yes/no)? ", datastore_path);
let mut confirm = String::new();
stdin().read_line(&mut confirm).expect("Failed to read line");
let confirm = confirm.to_lowercase();
let confirm = confirm.trim();
if confirm == "yes" || confirm == "y" {
remove_dir_all(datastore_path).unwrap_or(());
println!("Local data removed successfully.");
} else {
error!(target: "taud", "Unexpected Value: {}", confirm);
}
return Ok(())
}
if nickname.is_none() {
error!(target: "taud", "Provide a nickname in config file");
return Ok(())
}
if settings.piped {
let file = settings.pipe_path.clone();
let path = CString::new(file).unwrap();
unsafe { mkfifo(path.as_ptr(), 0o644) };
}
// mkdir datastore_path if not exists
create_dir_all(datastore_path.clone())?;
create_dir_all(datastore_path.join("month"))?;
create_dir_all(datastore_path.join("task"))?;
if settings.generate {
println!("Generating a new workspace");
loop {
println!("Name for the new workspace: ");
let mut workspace = String::new();
stdin().read_line(&mut workspace).expect("Failed to read line");
let workspace = workspace.to_lowercase();
let workspace = workspace.trim();
if workspace.is_empty() && workspace.len() < 3 {
error!(target: "taud", "Wrong workspace try again");
continue
}
// Encryption
// Chachabox secret key (read_key) used for encrypting tasks.
let secret_key = SecretKey::generate(&mut OsRng);
let encoded = bs58::encode(secret_key.to_bytes());
// Signature
// Secret key (write_key) used for signing tasks.
let keypair = Keypair::random(&mut OsRng);
let sk = format!("{}", keypair.secret);
// Public key (write_public_key) used for verifying tasks.
let pk = format!("{}", keypair.public);
println!("Please add the following to the config file:");
println!("[workspace.\"{}\"]", workspace);
println!("read_key = \"{}\"", encoded.into_string());
println!("write_key = \"{}\"", sk);
println!("write_public_key = \"{}\"", pk);
break
}
return Ok(())
}
let workspaces = Arc::new(get_workspaces(&settings).await?);
// let verified = Arc::new(Mutex::new(false));
if workspaces.is_empty() {
error!(target: "taud", "Please add at least one workspace to the config file.");
println!("Run `$ taud --generate` to generate new workspace.");
return Ok(())
}
info!(target: "taud", "Initializing taud node");
// Create datastore path if not there already.
let datastore = expand_path(&settings.datastore)?;
fs::create_dir_all(&datastore).await?;
let replay_datastore = expand_path(&settings.replay_datastore)?;
let replay_mode = settings.replay_mode;
info!(target: "taud", "Instantiating event DAG");
let sled_db = sled::open(datastore)?;
let p2p = P2p::new(settings.net.clone().into(), executor.clone()).await?;
let event_graph = EventGraph::new(
p2p.clone(),
sled_db.clone(),
replay_datastore,
replay_mode,
"taud_dag",
0,
executor.clone(),
)
.await?;
info!(target: "taud", "Registering EventGraph P2P protocol");
let event_graph_ = Arc::clone(&event_graph);
let registry = p2p.protocol_registry();
registry
.register(SESSION_DEFAULT, move |channel, _| {
let event_graph_ = event_graph_.clone();
async move { ProtocolEventGraph::init(event_graph_, channel).await.unwrap() }
})
.await;
let (broadcast_snd, broadcast_rcv) = smol::channel::unbounded::();
info!(target: "taud", "Starting P2P network");
p2p.clone().start().await?;
let comms_timeout = p2p.settings().read().await.outbound_connect_timeout;
loop {
if p2p.is_connected() {
info!(target: "taud", "Got peer connection");
// We'll attempt to sync for ever
if !settings.skip_dag_sync {
info!(target: "taud", "Syncing event DAG");
match event_graph.dag_sync().await {
Ok(()) => break,
Err(e) => {
// TODO: Maybe at this point we should prune or something?
// TODO: Or maybe just tell the user to delete the DAG from FS.
error!(target: "taud", "Failed syncing DAG ({}), retrying in {}s...", e, comms_timeout);
sleep(comms_timeout).await;
}
}
} else {
*event_graph.synced.write().await = true;
break
}
} else {
info!(target: "taud", "Waiting for some P2P connections...");
sleep(comms_timeout).await;
}
}
let seen = OnceLock::new();
seen.set(sled_db.open_tree("tau_seen").unwrap()).unwrap();
////////////////////
// get history
////////////////////
let dag_events = event_graph.order_events().await;
for event in dag_events.iter() {
let event_id = event.id();
// If it was seen, skip
if is_seen(sled_db.clone(), seen.clone(), &event_id).await? {
continue
}
mark_seen(sled_db.clone(), seen.clone(), &event_id).await?;
// Try to deserialize it. (Here we skip errors)
let Ok((enc_task, _)) = deserialize_async_partial(event.content()).await else { continue };
// Potentially decrypt the privmsg
on_receive_task(&enc_task, &workspaces, &settings).await.unwrap();
}
////////////////////
// Listner
////////////////////
info!(target: "taud", "Starting sync loop task");
let sync_loop_task = StoppableTask::new();
sync_loop_task.clone().start(
start_sync_loop(
event_graph.clone(),
broadcast_rcv,
workspaces.clone(),
sled_db.clone(),
settings.clone(),
p2p.clone(),
seen.clone(),
),
|res| async {
match res {
Ok(()) | Err(TaudError::Darkfi(Error::DetachedTaskStopped)) => { /* Do nothing */ }
Err(e) => error!(target: "taud", "Failed stopping sync loop task: {}", e),
}
},
TaudError::Darkfi(Error::DetachedTaskStopped),
executor.clone(),
);
// ==============
// p2p dnet setup
// ==============
info!(target: "taud", "Starting dnet subs task");
let json_sub = JsonSubscriber::new("dnet.subscribe_events");
let json_sub_ = json_sub.clone();
let p2p_ = p2p.clone();
let dnet_task = StoppableTask::new();
dnet_task.clone().start(
async move {
let dnet_sub = p2p_.dnet_subscribe().await;
loop {
let event = dnet_sub.receive().await;
debug!(target: "taud", "Got dnet event: {:?}", event);
json_sub_.notify(vec![event.into()].into()).await;
}
},
|res| async {
match res {
Ok(()) | Err(Error::DetachedTaskStopped) => { /* Do nothing */ }
Err(e) => {
error!(target: "taud", "Failed stopping dnet subs task: {}", e)
}
}
},
Error::DetachedTaskStopped,
executor.clone(),
);
info!("Starting deg subs task");
let deg_sub = JsonSubscriber::new("deg.subscribe_events");
let deg_sub_ = deg_sub.clone();
let event_graph_ = event_graph.clone();
let deg_task = StoppableTask::new();
deg_task.clone().start(
async move {
let deg_sub = event_graph_.deg_subscribe().await;
loop {
let event = deg_sub.receive().await;
debug!(target: "taud", "Got deg event: {:?}", event);
deg_sub_.notify(vec![event.into()].into()).await;
}
},
|res| async {
match res {
Ok(()) | Err(Error::DetachedTaskStopped) => { /* Do nothing */ }
Err(e) => panic!("{}", e),
}
},
Error::DetachedTaskStopped,
executor.clone(),
);
//
// RPC interface
//
let rpc_interface = Arc::new(JsonRpcInterface::new(
datastore_path.clone(),
broadcast_snd,
nickname.unwrap(),
workspaces.clone(),
p2p.clone(),
event_graph.clone(),
json_sub,
deg_sub,
));
let rpc_task = StoppableTask::new();
rpc_task.clone().start(
listen_and_serve(settings.rpc_listen, rpc_interface.clone(), None, executor.clone()),
|res| async move {
match res {
Ok(()) | Err(Error::RpcServerStopped) => rpc_interface.stop_connections().await,
Err(e) => error!(target: "taud", "Failed stopping JSON-RPC server: {}", e),
}
},
Error::RpcServerStopped,
executor.clone(),
);
// Signal handling for graceful termination.
let (signals_handler, signals_task) = SignalHandler::new(executor)?;
signals_handler.wait_termination(signals_task).await?;
info!(target: "taud", "Caught termination signal, cleaning up and exiting...");
info!(target: "taud", "Stopping P2P network");
p2p.stop().await;
info!(target: "taud", "Stopping sync loop task...");
sync_loop_task.stop().await;
info!(target: "taud", "Stopping JSON-RPC server...");
rpc_task.stop().await;
dnet_task.stop().await;
deg_task.stop().await;
info!(target: "taud", "Flushing sled database...");
let flushed_bytes = sled_db.flush_async().await?;
info!(target: "taud", "Flushed {} bytes", flushed_bytes);
info!(target: "taud", "Shut down successfully");
Ok(())
}