From d3c0623bc257359eb3003e7bf8916116f8e0d022 Mon Sep 17 00:00:00 2001 From: Putta Khunchalee Date: Mon, 22 Apr 2024 23:56:27 +0700 Subject: [PATCH] Initializes KeyMgr --- Cargo.toml | 5 +++ README.md | 3 +- src/cmd/init.rs | 13 +++++-- src/cmd/key.rs | 30 ++++++++++++++++ src/cmd/keystore.rs | 51 +++++++++++++++++++++++++++ src/cmd/mod.rs | 4 +++ src/config/mod.rs | 23 ++++++++---- src/key/mod.rs | 62 ++++++++++++++++++++++++++++++++ src/key/store/default.rs | 76 ++++++++++++++++++++++++++++++++++++++++ src/key/store/mod.rs | 17 +++++++++ src/main.rs | 23 +++++++++--- 11 files changed, 294 insertions(+), 13 deletions(-) create mode 100644 src/cmd/key.rs create mode 100644 src/cmd/keystore.rs create mode 100644 src/key/mod.rs create mode 100644 src/key/store/default.rs create mode 100644 src/key/store/mod.rs diff --git a/Cargo.toml b/Cargo.toml index c90e2e4..ea0c6eb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,11 +4,16 @@ version = "0.1.0" edition = "2021" [dependencies] +aes = "0.8.4" clap = "4.4" dirs = "5.0.1" erdp = "0.1.0" +getrandom = { version = "0.2.14", features = ["std"] } serde = { version = "1.0.197", features = ["derive"] } serde_yaml = "0.9.34" +sha3 = "0.10.8" +tabled = "0.15.0" thiserror = "1.0.58" ureq = "2.9.6" url = { version = "2.5.0", features = ["serde"] } +zeroize = "1.7.0" diff --git a/README.md b/README.md index c16d900..535b416 100644 --- a/README.md +++ b/README.md @@ -6,8 +6,9 @@ computers easily. ## Features - Cross-platform. -- Open-sourced, both client and server. +- Open-source, the client are licensed under GPL and the server are licensed under AGPL. - Self-hosting supports. +- Secure and private. All informations stored on the server are encrypted by your keys. These keys never leave your computer. ## License diff --git a/src/cmd/init.rs b/src/cmd/init.rs index 2792f6f..9406ac1 100644 --- a/src/cmd/init.rs +++ b/src/cmd/init.rs @@ -1,4 +1,6 @@ +use super::Key; use crate::config::AppConfig; +use crate::key::KeyMgr; use clap::{value_parser, Arg, ArgMatches, Command}; use std::path::PathBuf; use std::process::ExitCode; @@ -7,13 +9,14 @@ use std::sync::Arc; /// Command to initialize a new respotiroy. pub struct Init { config: Arc, + keymgr: Arc, } impl Init { pub const NAME: &'static str = "init"; - pub fn new(config: Arc) -> Self { - Self { config } + pub fn new(config: Arc, keymgr: Arc) -> Self { + Self { config, keymgr } } } @@ -49,6 +52,12 @@ impl super::Command for Init { } fn exec(&self, _: &ArgMatches) -> ExitCode { + // Check if we have at least one key to encrypt. + if self.keymgr.keys().len() == 0 { + eprintln!("No file encryption keys available, invoke Warp with '{} --help' to see how to create a new key.", Key::NAME); + return ExitCode::FAILURE; + } + todo!() } } diff --git a/src/cmd/key.rs b/src/cmd/key.rs new file mode 100644 index 0000000..a2c0237 --- /dev/null +++ b/src/cmd/key.rs @@ -0,0 +1,30 @@ +use clap::{ArgMatches, Command}; +use std::process::ExitCode; + +/// Command to manage file encryption keys. +pub struct Key {} + +impl Key { + pub const NAME: &'static str = "key"; + + pub fn new() -> Self { + Self {} + } +} + +impl super::Command for Key { + fn is_matched(&self, name: &str) -> bool { + name == Self::NAME + } + + fn definition(&self) -> Command { + Command::new(Self::NAME) + .about("Manage file encryption keys") + .subcommand_required(true) + .subcommand(Command::new("ls").about("List all available keys")) + } + + fn exec(&self, _: &ArgMatches) -> ExitCode { + todo!() + } +} diff --git a/src/cmd/keystore.rs b/src/cmd/keystore.rs new file mode 100644 index 0000000..c517f45 --- /dev/null +++ b/src/cmd/keystore.rs @@ -0,0 +1,51 @@ +use crate::key::KeyMgr; +use clap::{ArgMatches, Command}; +use std::process::ExitCode; +use std::sync::Arc; + +/// Command to manage file encryption keystores. +pub struct Keystore { + keymgr: Arc, +} + +impl Keystore { + pub const NAME: &'static str = "keystore"; + + pub fn new(keymgr: Arc) -> Self { + Self { keymgr } + } + + fn ls(&self) -> ExitCode { + let mut t = tabled::builder::Builder::new(); + + t.push_record(["ID"]); + + for s in self.keymgr.stores() { + t.push_record([s.id()]); + } + + println!("{}", t.build()); + + ExitCode::SUCCESS + } +} + +impl super::Command for Keystore { + fn is_matched(&self, name: &str) -> bool { + name == Self::NAME + } + + fn definition(&self) -> Command { + Command::new(Self::NAME) + .about("Manage file encryption keystores") + .subcommand_required(true) + .subcommand(Command::new("ls").about("List all enabled keystores")) + } + + fn exec(&self, args: &ArgMatches) -> ExitCode { + match args.subcommand().unwrap() { + ("ls", _) => self.ls(), + _ => unreachable!(), + } + } +} diff --git a/src/cmd/mod.rs b/src/cmd/mod.rs index fd74d43..69b443e 100644 --- a/src/cmd/mod.rs +++ b/src/cmd/mod.rs @@ -1,7 +1,11 @@ pub use self::init::*; +pub use self::key::*; +pub use self::keystore::*; use std::process::ExitCode; mod init; +mod key; +mod keystore; /// A single command passed from a command line argument. pub trait Command { diff --git a/src/config/mod.rs b/src/config/mod.rs index c405f36..8fc3314 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -3,21 +3,32 @@ use url::Url; /// Application configurations. #[derive(Deserialize)] +#[serde(default)] pub struct AppConfig { - #[serde(default = "AppConfig::default_default_server")] pub default_server: Url, + pub key: Key, } -impl AppConfig { - fn default_default_server() -> Url { - Url::parse("https://api.warpgate.sh").unwrap() +impl Default for AppConfig { + fn default() -> Self { + Self { + default_server: Url::parse("https://api.warpgate.sh").unwrap(), + key: Key::default(), + } } } -impl Default for AppConfig { +/// Configurations for encryption key. +#[derive(Deserialize)] +#[serde(default)] +pub struct Key { + pub default_storage: bool, +} + +impl Default for Key { fn default() -> Self { Self { - default_server: Self::default_default_server(), + default_storage: true, } } } diff --git a/src/key/mod.rs b/src/key/mod.rs new file mode 100644 index 0000000..fed7261 --- /dev/null +++ b/src/key/mod.rs @@ -0,0 +1,62 @@ +use self::store::{DefaultStore, Keystore}; +use crate::config::AppConfig; +use crate::home::Home; +use std::collections::HashMap; +use std::iter::FusedIterator; +use std::sync::Arc; +use thiserror::Error; + +mod store; + +/// Manage file encryption keys. +pub struct KeyMgr { + stores: HashMap<&'static str, Arc>, + keys: HashMap, +} + +impl KeyMgr { + pub fn new(home: &Arc, config: &Arc) -> Result { + let mut stores = HashMap::<&'static str, Arc>::new(); + let mut keys = HashMap::new(); + + // Initialize default store. + if config.key.default_storage { + let s = Arc::new(DefaultStore::new(home)); + + for k in s.list() { + assert!(keys.insert(k.id().clone(), k).is_none()); + } + + assert!(stores.insert(s.id(), s).is_none()); + } + + Ok(Self { stores, keys }) + } + + pub fn stores(&self) -> impl Iterator + FusedIterator { + self.stores.values().map(|s| s.as_ref()) + } + + pub fn keys(&self) -> impl Iterator + ExactSizeIterator + FusedIterator { + self.keys.values() + } +} + +/// Unique identifier of a [`Key`]. +#[derive(Clone, PartialEq, Eq, Hash)] +pub struct KeyId([u8; 16]); + +/// Key to encrypt/decrypt files in a repository. +pub struct Key { + id: KeyId, +} + +impl Key { + pub fn id(&self) -> &KeyId { + &self.id + } +} + +/// Represents an error when [`KeyMgr`] fails to initialize. +#[derive(Debug, Error)] +pub enum KeyMgrError {} diff --git a/src/key/store/default.rs b/src/key/store/default.rs new file mode 100644 index 0000000..f074193 --- /dev/null +++ b/src/key/store/default.rs @@ -0,0 +1,76 @@ +use super::Keystore; +use crate::home::Home; +use crate::key::Key; +use aes::cipher::{BlockEncrypt, KeyInit}; +use aes::Aes128; +use getrandom::getrandom; +use sha3::digest::{ExtendableOutput, Update, XofReader}; +use sha3::Shake128; +use std::error::Error; +use std::ops::{Deref, DerefMut}; +use std::sync::Arc; +use thiserror::Error; +use zeroize::Zeroizing; + +/// Implementation of [`Keystore`] using native key store of the OS. +pub struct DefaultStore {} + +impl DefaultStore { + pub fn new(_: &Home) -> Self { + Self {} + } +} + +impl Keystore for DefaultStore { + fn id(&self) -> &'static str { + "default" + } + + fn list(self: &Arc) -> impl Iterator + where + Self: Sized, + { + KeyList {} + } + + fn new(self: Arc) -> Result> { + // Generate a new key. + let mut key = Zeroizing::new([0u8; 16]); + + if let Err(e) = getrandom(key.deref_mut()) { + return Err(Box::new(NewError::GenerateKeyFailed(e))); + } + + // Get a key check value. + let mut kcv = [0u8; 16]; + + Aes128::new(key.deref().into()).encrypt_block((&mut kcv).into()); + + // Get key ID. + let mut hasher = Shake128::default(); + let mut id = [0u8; 16]; + + hasher.update(&kcv); + hasher.finalize_xof().read(&mut id); + + todo!() + } +} + +/// Iterator to list all keys in the [`DefaultStore`]. +struct KeyList {} + +impl Iterator for KeyList { + type Item = Key; + + fn next(&mut self) -> Option { + todo!() + } +} + +/// Represents an error when [`DefaultStore::new()`] fails. +#[derive(Debug, Error)] +enum NewError { + #[error("couldn't generate a new key")] + GenerateKeyFailed(#[source] getrandom::Error), +} diff --git a/src/key/store/mod.rs b/src/key/store/mod.rs new file mode 100644 index 0000000..4eb20a5 --- /dev/null +++ b/src/key/store/mod.rs @@ -0,0 +1,17 @@ +pub use self::default::*; +use super::Key; +use std::error::Error; +use std::sync::Arc; + +mod default; + +/// Storage to keep encryption keys. +pub trait Keystore { + fn id(&self) -> &'static str; + + fn list(self: &Arc) -> impl Iterator + where + Self: Sized; + + fn new(self: Arc) -> Result>; +} diff --git a/src/main.rs b/src/main.rs index 80622a9..797702a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,6 +1,7 @@ -use crate::cmd::{Command, Init}; +use crate::cmd::Command; use crate::config::AppConfig; use crate::home::Home; +use crate::key::KeyMgr; use crate::repo::{Repo, RepoLoadError}; use erdp::ErrorDisplay; use std::fs::File; @@ -11,6 +12,7 @@ use std::sync::Arc; mod cmd; mod config; mod home; +mod key; mod repo; fn main() -> ExitCode { @@ -40,10 +42,23 @@ fn main() -> ExitCode { } }; + // Load file encryption keys. + let config = Arc::new(config); + let keymgr = match KeyMgr::new(&home, &config) { + Ok(v) => Arc::new(v), + Err(e) => { + eprintln!("Failed to load file encryption keys: {}.", e.display()); + return ExitCode::FAILURE; + } + }; + // Setup commands. let mut args = clap::Command::new("warp"); - let config = Arc::new(config); - let commands: Vec> = vec![Box::new(Init::new(config.clone()))]; + let commands: Vec> = vec![ + Box::new(self::cmd::Init::new(config.clone(), keymgr.clone())), + Box::new(self::cmd::Key::new()), + Box::new(self::cmd::Keystore::new(keymgr.clone())), + ]; for cmd in &commands { args = args.subcommand(cmd.definition()); @@ -79,7 +94,7 @@ fn warp() -> ExitCode { match Repo::load(&path) { Ok(_) => {} Err(RepoLoadError::NotWarpRepo) => { - eprintln!("{} is not a Warp repository, invoke Warp with '{} --help' to see how to setup a new repository.", path.display(), Init::NAME); + eprintln!("{} is not a Warp repository, invoke Warp with '{} --help' to see how to setup a new repository.", path.display(), self::cmd::Init::NAME); return ExitCode::FAILURE; } Err(e) => {