tau: use digital signatures to verify tasks authenticity (per-workspace)

and fix configured workspaces keys format and manage write access based on those keys
This commit is contained in:
dasman
2024-08-12 04:57:26 +03:00
parent 35901bc6a1
commit 87d9556c3b
6 changed files with 204 additions and 129 deletions

1
Cargo.lock generated
View File

@@ -7103,6 +7103,7 @@ dependencies = [
"libc",
"log",
"rand 0.8.5",
"ring 0.17.8",
"serde",
"signal-hook",
"signal-hook-async-std",

View File

@@ -32,6 +32,7 @@ sled = "0.34.7"
blake3 = "1.5.1"
crypto_box = { version = "0.9.1", features = ["std", "chacha20"] }
rand = "0.8.5"
ring = "0.17.8"
# Encoding and parsing
bs58 = "0.5.1"
@@ -62,4 +63,3 @@ structopt-toml = "0.5.1"
[lints]
workspace = true

View File

@@ -24,8 +24,7 @@ use std::{
};
use async_trait::async_trait;
use crypto_box::ChaChaBox;
use log::{debug, warn};
use log::{debug, info, warn};
use smol::lock::{Mutex, MutexGuard};
use tinyjson::JsonValue;
@@ -46,9 +45,11 @@ use taud::{
error::{to_json_result, TaudError, TaudResult},
month_tasks::MonthTasks,
task_info::{Comment, TaskInfo},
util::{check_write_access, set_event},
util::set_event,
};
use crate::Workspace;
const DEFAULT_WORKSPACE: &str = "darkfi-dev";
pub struct JsonRpcInterface {
@@ -56,9 +57,7 @@ pub struct JsonRpcInterface {
notify_queue_sender: smol::channel::Sender<TaskInfo>,
nickname: String,
workspace: Mutex<String>,
workspaces: Arc<HashMap<String, ChaChaBox>>,
write: Option<String>,
password: Option<String>,
workspaces: Arc<HashMap<String, Workspace>>,
p2p: net::P2pPtr,
event_graph: EventGraphPtr,
dnet_sub: JsonSubscriber,
@@ -117,9 +116,7 @@ impl JsonRpcInterface {
dataset_path: PathBuf,
notify_queue_sender: smol::channel::Sender<TaskInfo>,
nickname: String,
workspaces: Arc<HashMap<String, ChaChaBox>>,
write: Option<String>,
password: Option<String>,
workspaces: Arc<HashMap<String, Workspace>>,
p2p: net::P2pPtr,
event_graph: EventGraphPtr,
dnet_sub: JsonSubscriber,
@@ -132,8 +129,6 @@ impl JsonRpcInterface {
workspace,
workspaces,
notify_queue_sender,
write,
password,
p2p,
event_graph,
rpc_connections: Mutex::new(HashSet::new()),
@@ -324,12 +319,14 @@ impl JsonRpcInterface {
_ => return Err(TaudError::InvalidData("Invalid parameter \"created_at\"".to_string())),
};
if !check_write_access(self.write.clone(), self.password.clone())? {
let ws = self.workspace.lock().await.clone();
if self.workspaces.get(&ws).unwrap().write_key.is_none() {
info!("You don't have write access!");
return Ok(JsonValue::Boolean(false))
}
let mut new_task: TaskInfo = TaskInfo::new(
self.workspace.lock().await.clone(),
ws,
params["title"].get::<String>().unwrap(),
params["desc"].get::<String>().unwrap(),
&self.nickname,
@@ -401,12 +398,12 @@ impl JsonRpcInterface {
return Err(TaudError::InvalidData("len of params should be 2".into()))
}
if !check_write_access(self.write.clone(), self.password.clone())? {
let ws = self.workspace.lock().await.clone();
if self.workspaces.get(&ws).unwrap().write_key.is_none() {
info!("You don't have write access!");
return Ok(JsonValue::Boolean(false))
}
let ws = self.workspace.lock().await.clone();
let task = self.check_params_for_modify(
params[0].get::<String>().unwrap(),
params[1].get::<HashMap<String, JsonValue>>().unwrap(),
@@ -433,12 +430,12 @@ impl JsonRpcInterface {
return Err(TaudError::InvalidData("len of params should be 2".into()))
}
if !check_write_access(self.write.clone(), self.password.clone())? {
return Ok(JsonValue::Boolean(false))
}
let state = params[1].get::<String>().unwrap();
let ws = self.workspace.lock().await.clone();
if self.workspaces.get(&ws).unwrap().write_key.is_none() {
info!("You don't have write access!");
return Ok(JsonValue::Boolean(false))
}
let mut task: TaskInfo =
self.load_task_by_ref_id(params[0].get::<String>().unwrap(), ws)?;
@@ -465,14 +462,15 @@ impl JsonRpcInterface {
return Err(TaudError::InvalidData("len of params should be 2".into()))
}
if !check_write_access(self.write.clone(), self.password.clone())? {
return Ok(JsonValue::Boolean(false))
}
let ref_id = params[0].get::<String>().unwrap();
let comment_content = params[1].get::<String>().unwrap();
let ws = self.workspace.lock().await.clone();
if self.workspaces.get(&ws).unwrap().write_key.is_none() {
info!("You don't have write access!");
return Ok(JsonValue::Boolean(false))
}
let mut task: TaskInfo = self.load_task_by_ref_id(ref_id, ws)?;
task.set_comment(Comment::new(comment_content, &self.nickname));
@@ -654,13 +652,13 @@ impl JsonRpcInterface {
return Err(TaudError::InvalidData("Invalid path".into()))
}
if !check_write_access(self.write.clone(), self.password.clone())? {
return Ok(JsonValue::Boolean(false))
}
let path = params[0].get::<String>().unwrap();
let path = expand_path(path)?.join("exported_tasks");
let ws = self.workspace.lock().await.clone();
if self.workspaces.get(&ws).unwrap().write_key.is_none() {
info!("You don't have write access!");
return Ok(JsonValue::Boolean(false))
}
let imported_tasks = MonthTasks::load_current_tasks(&path, ws.clone(), true)?;

View File

@@ -38,6 +38,7 @@ use futures::{select, FutureExt};
use libc::mkfifo;
use log::{debug, error, info};
use rand::rngs::OsRng;
use ring::signature::{Ed25519KeyPair, KeyPair, Signature, UnparsedPublicKey, ED25519};
use smol::{fs, lock::RwLock, stream::StreamExt};
use structopt_toml::StructOptToml;
use tinyjson::JsonValue;
@@ -54,7 +55,7 @@ use darkfi::{
server::{listen_and_serve, RequestHandler},
},
system::{sleep, StoppableTask},
util::path::expand_path,
util::path::{expand_path, get_config_path},
Error, Result,
};
@@ -72,25 +73,21 @@ use crate::{
settings::{Args, CONFIG_FILE, CONFIG_FILE_CONTENTS},
};
fn get_workspaces(settings: &Args) -> Result<HashMap<String, ChaChaBox>> {
let mut workspaces = HashMap::new();
struct Workspace {
read_key: ChaChaBox,
write_key: Option<Ed25519KeyPair>,
write_pubkey: UnparsedPublicKey<Vec<u8>>,
}
for workspace in settings.workspaces.iter() {
let workspace: Vec<&str> = workspace.split(':').collect();
let (workspace, secret) = (workspace[0], workspace[1]);
let bytes: [u8; 32] = bs58::decode(secret)
.into_vec()?
.try_into()
.map_err(|_| Error::ParseFailed("Parse secret key failed"))?;
let secret = crypto_box::SecretKey::from(bytes);
let public = secret.public_key();
let chacha_box = crypto_box::ChaChaBox::new(&public, &secret);
workspaces.insert(workspace.to_string(), chacha_box);
impl Workspace {
fn new() -> Self {
let secret_key = SecretKey::generate(&mut OsRng);
Self {
read_key: ChaChaBox::new(&secret_key.public_key(), &secret_key),
write_key: None,
write_pubkey: UnparsedPublicKey::new(&ED25519, vec![0]),
}
}
Ok(workspaces)
}
#[derive(Debug, Clone, SerialEncodable, SerialDecodable)]
@@ -98,16 +95,30 @@ pub struct EncryptedTask {
payload: String,
}
fn encrypt_task(
task: &TaskInfo,
chacha_box: &ChaChaBox,
rng: &mut OsRng,
) -> TaudResult<EncryptedTask> {
debug!(target: "taud", "start encrypting task");
#[derive(SerialEncodable, SerialDecodable)]
struct SignedTask {
task: Vec<u8>,
signature: Vec<u8>,
}
let nonce = ChaChaBox::generate_nonce(rng);
let payload = &serialize(task)[..];
let mut payload = chacha_box.encrypt(&nonce, payload)?;
impl SignedTask {
fn new(task: &TaskInfo, signature: Signature) -> Self {
Self { task: serialize(task), signature: signature.as_ref().to_vec() }
}
}
/// Sign then encrypt a task
fn encrypt_sign_task(task: &TaskInfo, workspace: &Workspace) -> TaudResult<EncryptedTask> {
debug!(target: "taud", "start encrypting task");
if workspace.write_key.is_none() {
error!("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());
@@ -118,7 +129,10 @@ fn encrypt_task(
Ok(EncryptedTask { payload })
}
fn try_decrypt_task(encrypt_task: &EncryptedTask, chacha_box: &ChaChaBox) -> TaudResult<TaskInfo> {
fn try_decrypt_task(
encrypt_task: &EncryptedTask,
chacha_box: &ChaChaBox,
) -> TaudResult<SignedTask> {
debug!(target: "taud", "start decrypting task");
let bytes = match bs58::decode(&encrypt_task.payload).into_vec() {
@@ -139,16 +153,114 @@ fn try_decrypt_task(encrypt_task: &EncryptedTask, chacha_box: &ChaChaBox) -> Tau
// let nonce = encrypt_task.nonce.as_slice();
let decrypted_task = chacha_box.decrypt(nonce, message)?;
let task = deserialize(&decrypted_task)?;
let signed_task = deserialize(&decrypted_task)?;
Ok(task)
Ok(signed_task)
}
fn parse_configured_workspaces(data: &toml::Value) -> Result<HashMap<String, Workspace>> {
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!("Found configured write_public_key for {} workspace", name);
let write_pubkey = write_pubkey.to_string();
let decoded_write_pubkey = bs58::decode(write_pubkey).into_vec().unwrap();
ws.write_pubkey = UnparsedPublicKey::new(&ED25519, decoded_write_pubkey);
}
} 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!("Found configured write_key for {} workspace", name);
let write_key = write_key.to_string();
let pkcs8_bytes = bs58::decode(write_key).into_vec().unwrap();
let ed25519 = match Ed25519KeyPair::from_pkcs8(pkcs8_bytes.as_ref()) {
Ok(key) => key,
Err(e) => {
error!("Failed parsing write_key: {}", e);
return Err(Error::ParseFailed("Failed parsing write_key"))
}
};
ws.write_key = Some(ed25519);
}
} else {
return Err(Error::ParseFailed("Workspace write_key not a string"))
}
}
if let Some(wrt_key) = ws.write_key.as_ref() {
if wrt_key.public_key().as_ref() != ws.write_pubkey.as_ref() {
error!("Wrong keypair for {} workspace, the workspace is not added!", name);
continue
}
}
info!("Configured NaCl box for workspace {}", name);
ret.insert(name.to_string(), ws);
}
Ok(ret)
}
async fn get_workspaces(settings: &Args) -> Result<HashMap<String, Workspace>> {
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!("Failed parsing TOML config: {}", e);
return Err(Error::ParseFailed("Failed parsing TOML config"))
}
};
let workspaces = parse_configured_workspaces(&contents)?;
Ok(workspaces)
}
#[allow(clippy::too_many_arguments)]
async fn start_sync_loop(
event_graph: EventGraphPtr,
broadcast_rcv: smol::channel::Receiver<TaskInfo>,
workspaces: Arc<HashMap<String, ChaChaBox>>,
workspaces: Arc<HashMap<String, Workspace>>,
datastore_path: std::path::PathBuf,
piped: bool,
p2p: P2pPtr,
@@ -164,8 +276,8 @@ async fn start_sync_loop(
task_event = broadcast_rcv.recv().fuse() => {
let tk = task_event.map_err(Error::from)?;
if workspaces.contains_key(&tk.workspace) {
let chacha_box = workspaces.get(&tk.workspace).unwrap();
let encrypted_task = encrypt_task(&tk, chacha_box, &mut OsRng)?;
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(
@@ -183,9 +295,7 @@ async fn start_sync_loop(
error!(target: "taud", "Failed inserting new event to DAG: {}", e);
} else {
// We sent this, so it should be considered seen.
// TODO: should we save task on send or on receive?
// on receive better because it's garanteed your event is out there
// debug!("Marking event {} as seen", event_id);
// debug!(target: "taud", "Marking event {} as seen", event_id);
// seen.get().unwrap().insert(event_id.as_bytes(), &[]).unwrap();
// Otherwise, broadcast it
@@ -219,22 +329,38 @@ async fn start_sync_loop(
}
}
/// Handel a received task, decrypt it, verify it, optionally write it
/// to a named pipe and save it on disk.
async fn on_receive_task(
task: &EncryptedTask,
enc_task: &EncryptedTask,
datastore_path: &Path,
workspaces: &HashMap<String, ChaChaBox>,
workspaces: &HashMap<String, Workspace>,
piped: bool,
) -> TaudResult<()> {
for (workspace, chacha_box) in workspaces.iter() {
let task = try_decrypt_task(task, chacha_box);
if let Err(e) = task {
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
}
let mut task = task.unwrap();
if workspace
.write_pubkey
.verify(&signed_task.as_ref().unwrap().task, &signed_task.as_ref().unwrap().signature)
.is_err()
{
// *verified.lock().await = false;
error!("Task is not verified: wrong write_public_key");
error!("Task is not saved");
continue
}
// else {
// *verified.lock().await = true;
// }
let mut task: TaskInfo = deserialize(&signed_task.unwrap().task)?;
info!(target: "taud", "Save the task: ref: {}", task.ref_id);
task.workspace.clone_from(workspace);
task.workspace.clone_from(ws_name);
if piped {
// if we can't load the task then it's a new task.
// otherwise it's a modification.
@@ -338,7 +464,8 @@ async fn realmain(settings: Args, executor: Arc<smol::Executor<'static>>) -> Res
return Ok(())
}
let workspaces = Arc::new(get_workspaces(&settings)?);
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.");
@@ -525,8 +652,6 @@ async fn realmain(settings: Args, executor: Arc<smol::Executor<'static>>) -> Res
broadcast_snd,
nickname.unwrap(),
workspaces.clone(),
settings.write,
settings.password,
p2p.clone(),
event_graph.clone(),
json_sub,

View File

@@ -22,16 +22,12 @@ use std::{
path::Path,
};
use crypto_box::aead::Aead;
use log::{debug, error};
use log::debug;
use darkfi::{Error, Result};
use rand::{distributions::Alphanumeric, rngs::OsRng, Rng};
use crate::{
error::{TaudError, TaudResult},
task_info::{TaskEvent, TaskInfo},
};
use crate::task_info::{TaskEvent, TaskInfo};
pub fn set_event(task_info: &mut TaskInfo, action: &str, author: &str, content: &str) {
debug!(target: "tau", "TaskInfo::set_event()");
@@ -47,49 +43,3 @@ pub fn pipe_write<P: AsRef<Path>>(path: P) -> Result<File> {
pub fn gen_id(len: usize) -> String {
OsRng.sample_iter(&Alphanumeric).take(len).map(char::from).collect()
}
pub fn check_write_access(write: Option<String>, password: Option<String>) -> TaudResult<bool> {
let secret = if write.is_some() {
let scrt = write.clone().unwrap();
let bytes: [u8; 32] = bs58::decode(scrt)
.into_vec()
.map_err(|_| {
Error::ParseFailed("Parse secret key failed, couldn't decode into vector of bytes")
})?
.try_into()
.map_err(|_| Error::ParseFailed("Parse secret key failed"))?;
crypto_box::SecretKey::from(bytes)
} else {
crypto_box::SecretKey::generate(&mut OsRng)
};
let public = secret.public_key();
let chacha_box = crypto_box::ChaChaBox::new(&public, &secret);
if password.is_some() {
let bytes = match bs58::decode(password.clone().unwrap()).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 pswd = &bytes[24..];
if chacha_box.decrypt(nonce, pswd).is_err() {
error!(target: "taud", "You don't have write access");
return Ok(false);
};
} else {
error!(target: "taud", "You don't have write access");
return Ok(false);
};
Ok(true)
}

View File

@@ -17,8 +17,9 @@
## Current display name
#nickname = "NICKNAME"
## Workspaces
workspaces = ["darkfi-dev:2bCqQTd8BJgeUzH7JQELZxjQuWS8aCmXZ9C6w7ktNS1v"]
[workspace."darkfi-dev"]
read_key = "2bCqQTd8BJgeUzH7JQELZxjQuWS8aCmXZ9C6w7ktNS1v"
write_public_key = "Fgsc8tep4KX3Rb2drq8RxMyrHFWQ7wZaZPpF9F3GQYFG"
# P2P network settings
[net]