Kaynağa Gözat

tau: use digital signatures to verify tasks authenticity (per-workspace)
and fix configured workspaces keys format and manage write access based on those keys

dasman 2 yıl önce
ebeveyn
işleme
87d9556c3b

+ 1 - 0
Cargo.lock

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

+ 1 - 1
bin/tau/taud/Cargo.toml

@@ -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
-

+ 26 - 28
bin/tau/taud/src/jsonrpc.rs

@@ -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)?;
 

+ 170 - 45
bin/tau/taud/src/main.rs

@@ -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();
-
-    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"))?;
+struct Workspace {
+    read_key: ChaChaBox,
+    write_key: Option<Ed25519KeyPair>,
+    write_pubkey: UnparsedPublicKey<Vec<u8>>,
+}
 
-        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> {
+#[derive(SerialEncodable, SerialDecodable)]
+struct SignedTask {
+    task: Vec<u8>,
+    signature: Vec<u8>,
+}
+
+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(rng);
-    let payload = &serialize(task)[..];
-    let mut payload = chacha_box.encrypt(&nonce, payload)?;
+    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(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
+            }
+        }
 
-    Ok(task)
+        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,

+ 2 - 52
bin/tau/taud/src/util.rs

@@ -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)
-}

+ 3 - 2
bin/tau/taud/taud_config.toml

@@ -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]