/* This file is part of DarkFi (https://dark.fi) * * Copyright (C) 2020-2023 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, fs::{create_dir_all, read_dir}, path::{Path, PathBuf}, }; use darkfi_serial::{deserialize, serialize}; use dryoc::{ classic::crypto_secretbox::{crypto_secretbox_easy, crypto_secretbox_open_easy, Key, Nonce}, constants::CRYPTO_SECRETBOX_MACBYTES, dryocbox::NewByteArray, }; use log::{error, info, warn}; use unicode_segmentation::UnicodeSegmentation; use darkfi::{util::path::expand_path, Error, Result}; use crate::{Args, EncryptedPatch, Patch}; /// Split a `&str` into a vector of each of its chars. pub fn str_to_chars(s: &str) -> Vec<&str> { s.graphemes(true).collect::>() } /// Parse a base58 string for a `crypto_secretbox` secret. fn parse_b58_secret(s: &str) -> Result<[u8; 32]> { match bs58::decode(s).into_vec() { Ok(v) => { if v.len() != 32 { return Err(Error::Custom("Secret is not 32 bytes long".to_string())) } Ok(v.try_into().unwrap()) } Err(e) => Err(Error::Custom(format!("Unable to parse secret from base58: {}", e))), } } /// Parse a TOML string for configured workspaces and return an `HashMap` /// of parsed data. Does not error on failures, just warns if something is /// misconfigured. pub fn parse_workspaces(toml_str: &str) -> HashMap { let mut ret = HashMap::new(); let settings: Args = match toml::from_str(toml_str) { Ok(v) => v, Err(e) => { error!("Failed parsing TOML from string: {}", e); return ret } }; for workspace in settings.workspace { let wrk: Vec<&str> = workspace.split(':').collect(); if wrk.len() != 2 { warn!("Invalid workspace: {}", workspace); continue } let secret = match parse_b58_secret(wrk[1]) { Ok(v) => v, Err(e) => { warn!("Failed parsing secret for workspace {}: {}", wrk[0], e); continue } }; let docs_path = match expand_path(&settings.docs) { Ok(v) => v, Err(e) => { warn!("Failed expanding docs path for workspace {}: {}", wrk[0], e); continue } }; if let Err(e) = create_dir_all(docs_path.join(wrk[0])) { warn!("Failed creating directory for workspace {}: {}", wrk[0], e); continue } info!("Added parsed workspace: {}", wrk[0]); ret.insert(wrk[0].to_string(), secret); } ret } /// Encrypt a patch using a NaCl crypto_secretbox given a `Patch` and a `Key`. pub fn encrypt_patch(patch: &Patch, key: &Key) -> Result { let nonce = Nonce::gen(); let payload = serialize(patch); let mut ciphertext = vec![0u8; payload.len() + CRYPTO_SECRETBOX_MACBYTES]; if let Err(e) = crypto_secretbox_easy(&mut ciphertext, &payload, &nonce, key) { error!("encrypt_patch: Failed encrypting patch: {}", e); return Err(Error::Custom(format!("Failed encrypting darkwiki patch: {}", e))) } Ok(EncryptedPatch { nonce, ciphertext }) } /// Decrypt a patch using a NaCl crypto_secretbox given an `EncryptedPatch` and a `Key`. pub fn decrypt_patch(patch: &EncryptedPatch, key: &Key) -> Result { let nonce = &patch.nonce; let ciphertext = &patch.ciphertext; let mut decrypted = vec![0u8; ciphertext.len() - CRYPTO_SECRETBOX_MACBYTES]; if let Err(e) = crypto_secretbox_open_easy(&mut decrypted, ciphertext, nonce, key) { error!("decrypt_patch: Failed decrypting patch: {}", e); return Err(Error::Custom(format!("Failed decrypting darkwiki patch: {}", e))) } Ok(deserialize(&decrypted)?) } /// TODO: DOCUMENT ME /// FIXME: There's checking of file extensions here. Take care that the rest of the code /// is robust against this attack. pub fn get_docs_paths(files: &mut Vec, path: &Path, parent: Option<&Path>) -> Result<()> { let docs = read_dir(path)?; let docs = docs.filter(|d| d.is_ok()).map(|d| d.unwrap().path()).collect::>(); for doc in docs { if let Some(f) = doc.file_name() { let filename = PathBuf::from(f); let filename = if let Some(parent) = parent { parent.join(filename) } else { filename }; if doc.is_file() { if let Some(ext) = doc.extension() { if ext == "md" || ext == "markdown" { files.push(filename); } } } else if doc.is_dir() { if f == ".log" { continue } get_docs_paths(files, &doc, Some(&filename))?; } } } Ok(()) } /// Hash a path and workspace, and encode with base58, providing an ID. pub fn path_to_id(path: &str, workspace: &str) -> String { let mut hasher = blake3::Hasher::new(); hasher.update(path.as_bytes()); hasher.update(workspace.as_bytes()); bs58::encode(hasher.finalize().as_bytes()).into_string() }