diff --git a/Cargo.lock b/Cargo.lock index 80b50ed..0b55f08 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -649,18 +649,29 @@ checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" [[package]] name = "herocrypto" version = "0.1.0" +dependencies = [ + "libcrypto", + "libcryptoa", + "redis", + "thiserror", +] [[package]] name = "herodb" version = "0.1.0" dependencies = [ + "age", "anyhow", + "base64 0.22.1", "bytes", "clap", + "ed25519-dalek", "libcryptoa", "libdbstorage", "log", + "rand", "redis", + "secrecy", "serde", "tokio", ] @@ -933,7 +944,6 @@ name = "libcrypto" version = "0.1.0" dependencies = [ "chacha20poly1305", - "libdbstorage", "rand", "sha2", "thiserror", @@ -955,6 +965,7 @@ dependencies = [ name = "libdbstorage" version = "0.1.0" dependencies = [ + "libcrypto", "redb", "serde", "serde_json", @@ -1545,6 +1556,11 @@ version = "0.1.0" [[package]] name = "supervisorrpc" version = "0.1.0" +dependencies = [ + "herocrypto", + "redis", + "tokio", +] [[package]] name = "syn" diff --git a/Cargo.toml b/Cargo.toml index 4484630..8cf76e7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,37 +1,38 @@ [workspace] resolver = "2" members = [ - "crates/herodb", - "crates/libdbstorage", - "crates/libcrypto", - "crates/libcryptoa", - "crates/herocrypto", - "crates/supervisorrpc", - "crates/supervisor", + "crates/herodb", + "crates/libdbstorage", + "crates/libcrypto", + "crates/libcryptoa", + "crates/herocrypto", + "crates/supervisor", + "crates/supervisorrpc", ] [workspace.dependencies] -# Central place for dependencies shared across the workspace +# Common anyhow = "1.0" tokio = { version = "1", features = ["full"] } serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" thiserror = "1.0" log = "0.4" +bytes = "1.3" -# Crypto deps -chacha20poly1305 = "0.10" -rand = "0.8" -sha2 = "0.10" +# Crypto - Asymmetric age = "0.10" secrecy = "0.8" ed25519-dalek = "2" base64 = "0.22" -# DB +# Crypto - Symmetric & Utilities +chacha20poly1305 = "0.10" +rand = "0.8" +sha2 = "0.10" + +# Database redb = "2.1" -[profile.release] -lto = true -codegen-units =1 -strip = true \ No newline at end of file +# CLI +clap = { version = "4.5", features = ["derive"] } \ No newline at end of file diff --git a/crates/herocrypto/Cargo.toml b/crates/herocrypto/Cargo.toml index d34c159..17859d3 100644 --- a/crates/herocrypto/Cargo.toml +++ b/crates/herocrypto/Cargo.toml @@ -1,6 +1,10 @@ [package] name = "herocrypto" version = "0.1.0" -edition = "2024" +edition = "2021" [dependencies] +redis = { version = "0.24", features = ["tokio-comp"] } +thiserror = { workspace = true } +libcrypto = { path = "../libcrypto" } +libcryptoa = { path = "../libcryptoa" } \ No newline at end of file diff --git a/crates/herocrypto/src/lib.rs b/crates/herocrypto/src/lib.rs index b93cf3f..c9f6370 100644 --- a/crates/herocrypto/src/lib.rs +++ b/crates/herocrypto/src/lib.rs @@ -1,14 +1,45 @@ -pub fn add(left: u64, right: u64) -> u64 { - left + right +// In crates/herocrypto/src/lib.rs +use redis::{Commands, RedisResult}; +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum Error { + #[error("Redis connection error: {0}")] + Redis(#[from] redis::RedisError), + #[error("Asymmetric crypto error: {0}")] + Asymmetric(#[from] libcryptoa::AsymmetricCryptoError), + #[error("Key not found in database: {0}")] + KeyNotFound(String), + #[error("Command failed on server: {0}")] + CommandError(String), } -#[cfg(test)] -mod tests { - use super::*; +pub struct HeroCrypto { + // e.g., using a connection manager from redis-rs + client: redis::Client, +} - #[test] - fn it_works() { - let result = add(2, 2); - assert_eq!(result, 4); +impl HeroCrypto { + pub fn new(redis_url: &str) -> Result { + Ok(Self { client: redis::Client::open(redis_url)? }) } -} + + // --- High-level functions to be implemented --- + + /// Generates a new keypair and stores it in HeroDB under the given name. + pub async fn generate_keypair(&self, name: &str) -> Result<(), Error> { + let mut con = self.client.get_async_connection().await?; + let (_pub, _priv): (String, String) = redis::cmd("AGE") + .arg("KEYGEN") + .arg(name) + .query_async(&mut con) + .await?; + Ok(()) + } + + /// Encrypts a message using a key stored in HeroDB. + pub async fn encrypt_by_name(&self, key_name: &str, plaintext: &str) -> Result { + // Implementation will call 'AGE ENCRYPTNAME ...' + unimplemented!() + } +} \ No newline at end of file diff --git a/crates/herodb/Cargo.toml b/crates/herodb/Cargo.toml index be59310..613e7f2 100644 --- a/crates/herodb/Cargo.toml +++ b/crates/herodb/Cargo.toml @@ -1,10 +1,9 @@ [package] name = "herodb" version = "0.1.0" -authors = ["Pin Fang "] edition = "2021" +authors = ["Pin Fang "] -# THIS IS A BINARY, NOT A LIBRARY [[bin]] name = "herodb" path = "src/main.rs" @@ -15,14 +14,18 @@ anyhow = { workspace = true } tokio = { workspace = true } serde = { workspace = true } log = { workspace = true } +clap = { workspace = true } +bytes = { workspace = true } +base64 = { workspace = true } +age = { workspace = true } +secrecy = { workspace = true } +ed25519-dalek = { workspace = true } +rand = { workspace = true } # Local Crate Dependencies libdbstorage = { path = "../libdbstorage" } +# We will create these libraries in the next steps libcryptoa = { path = "../libcryptoa" } -# Other dependencies -clap = { version = "4.5", features = ["derive"] } -bytes = "1.3.0" # Example, keep specific versions if needed - [dev-dependencies] redis = { version = "0.24", features = ["aio", "tokio-comp"] } \ No newline at end of file diff --git a/crates/herodb/age.rs b/crates/herodb/age.rs index 77501da..5152320 100644 --- a/crates/herodb/age.rs +++ b/crates/herodb/age.rs @@ -10,146 +10,164 @@ // age:signpriv:{name} -> Ed25519 signing secret key (private, used to sign) // - Base64 wrapping for ciphertext/signature binary blobs. -use std::str::FromStr; - -use secrecy::ExposeSecret; -use age::{Decryptor, Encryptor}; -use age::x25519; - -use ed25519_dalek::{Signature, Signer, Verifier, SigningKey, VerifyingKey}; - -use base64::{engine::general_purpose::STANDARD as B64, Engine as _}; - use crate::protocol::Protocol; use crate::server::Server; -use crate::error::DBError; +use libdbstorage::DBError; +use libcryptoa::AsymmetricCryptoError; -// ---------- Internal helpers ---------- +// ---------- Storage helpers ---------- -#[derive(Debug)] -pub enum AgeWireError { - ParseKey, - Crypto(String), - Utf8, - SignatureLen, - NotFound(&'static str), // which kind of key was missing - Storage(String), +fn sget(server: &Server, key: &str) -> Result, DBError> { + let st = server.current_storage()?; + st.get(key) +} +fn sset(server: &Server, key: &str, val: &str) -> Result<(), DBError> { + let st = server.current_storage()?; + st.set(key.to_string(), val.to_string()) } -impl AgeWireError { - fn to_protocol(self) -> Protocol { - match self { - AgeWireError::ParseKey => Protocol::err("ERR age: invalid key"), - AgeWireError::Crypto(e) => Protocol::err(&format!("ERR age: {e}")), - AgeWireError::Utf8 => Protocol::err("ERR age: invalid UTF-8 plaintext"), - AgeWireError::SignatureLen => Protocol::err("ERR age: bad signature length"), - AgeWireError::NotFound(w) => Protocol::err(&format!("ERR age: missing {w}")), - AgeWireError::Storage(e) => Protocol::err(&format!("ERR storage: {e}")), - } +fn enc_pub_key_key(name: &str) -> String { format!("age:key:{name}") } +fn enc_priv_key_key(name: &str) -> String { format!("age:privkey:{name}") } +fn sign_pub_key_key(name: &str) -> String { format!("age:signpub:{name}") } +fn sign_priv_key_key(name: &str) -> String { format!("age:signpriv:{name}") } + +// ---------- Command handlers (RESP Protocol) ---------- +// Basic (stateless) ones kept for completeness + +pub async fn cmd_age_genenc() -> Protocol { + let (recip, ident) = libcryptoa::gen_enc_keypair(); + Protocol::Array(vec![Protocol::BulkString(recip), Protocol::BulkString(ident)]) +} + +pub async fn cmd_age_gensign() -> Protocol { + let (verify, secret) = libcryptoa::gen_sign_keypair(); + Protocol::Array(vec![Protocol::BulkString(verify), Protocol::BulkString(secret)]) +} + +pub async fn cmd_age_encrypt(recipient: &str, message: &str) -> Protocol { + match libcryptoa::encrypt_b64(recipient, message) { + Ok(b64) => Protocol::BulkString(b64), + Err(e) => Protocol::err(&format!("ERR age: {e}")), } } -fn parse_recipient(s: &str) -> Result { - x25519::Recipient::from_str(s).map_err(|_| AgeWireError::ParseKey) -} -fn parse_identity(s: &str) -> Result { - x25519::Identity::from_str(s).map_err(|_| AgeWireError::ParseKey) -} -fn parse_ed25519_signing_key(s: &str) -> Result { - // Parse base64-encoded signing key - let bytes = B64.decode(s).map_err(|_| AgeWireError::ParseKey)?; - if bytes.len() != 32 { - return Err(AgeWireError::ParseKey); +pub async fn cmd_age_decrypt(identity: &str, ct_b64: &str) -> Protocol { + match libcryptoa::decrypt_b64(identity, ct_b64) { + Ok(pt) => Protocol::BulkString(pt), + Err(e) => Protocol::err(&format!("ERR age: {e}")), } - let key_bytes: [u8; 32] = bytes.try_into().map_err(|_| AgeWireError::ParseKey)?; - Ok(SigningKey::from_bytes(&key_bytes)) } -fn parse_ed25519_verifying_key(s: &str) -> Result { - // Parse base64-encoded verifying key - let bytes = B64.decode(s).map_err(|_| AgeWireError::ParseKey)?; - if bytes.len() != 32 { - return Err(AgeWireError::ParseKey); + +pub async fn cmd_age_sign(secret: &str, message: &str) -> Protocol { + match libcryptoa::sign_b64(secret, message) { + Ok(b64sig) => Protocol::BulkString(b64sig), + Err(e) => Protocol::err(&format!("ERR age: {e}")), } - let key_bytes: [u8; 32] = bytes.try_into().map_err(|_| AgeWireError::ParseKey)?; - VerifyingKey::from_bytes(&key_bytes).map_err(|_| AgeWireError::ParseKey) } -// ---------- Stateless crypto helpers (string in/out) ---------- - -pub fn gen_enc_keypair() -> (String, String) { - let id = x25519::Identity::generate(); - let pk = id.to_public(); - (pk.to_string(), id.to_string().expose_secret().to_string()) // (recipient, identity) -} - -pub fn gen_sign_keypair() -> (String, String) { - use rand::RngCore; - use rand::rngs::OsRng; - - // Generate random 32 bytes for the signing key - let mut secret_bytes = [0u8; 32]; - OsRng.fill_bytes(&mut secret_bytes); - - let signing_key = SigningKey::from_bytes(&secret_bytes); - let verifying_key = signing_key.verifying_key(); - - // Encode as base64 for storage - let signing_key_b64 = B64.encode(signing_key.to_bytes()); - let verifying_key_b64 = B64.encode(verifying_key.to_bytes()); - - (verifying_key_b64, signing_key_b64) // (verify_pub, signing_secret) -} - -/// Encrypt `msg` for `recipient_str` (X25519). Returns base64(ciphertext). -pub fn encrypt_b64(recipient_str: &str, msg: &str) -> Result { - let recipient = parse_recipient(recipient_str)?; - let enc = Encryptor::with_recipients(vec![Box::new(recipient)]) - .expect("failed to create encryptor"); // Handle Option - let mut out = Vec::new(); - { - use std::io::Write; - let mut w = enc.wrap_output(&mut out).map_err(|e| AgeWireError::Crypto(e.to_string()))?; - w.write_all(msg.as_bytes()).map_err(|e| AgeWireError::Crypto(e.to_string()))?; - w.finish().map_err(|e| AgeWireError::Crypto(e.to_string()))?; +pub async fn cmd_age_verify(verify_pub: &str, message: &str, sig_b64: &str) -> Protocol { + match libcryptoa::verify_b64(verify_pub, message, sig_b64) { + Ok(true) => Protocol::SimpleString("1".to_string()), + Ok(false) => Protocol::SimpleString("0".to_string()), + Err(e) => Protocol::err(&format!("ERR age: {e}")), } - Ok(B64.encode(out)) } -/// Decrypt base64(ciphertext) with `identity_str`. Returns plaintext String. -pub fn decrypt_b64(identity_str: &str, ct_b64: &str) -> Result { - let id = parse_identity(identity_str)?; - let ct = B64.decode(ct_b64.as_bytes()).map_err(|e| AgeWireError::Crypto(e.to_string()))?; - let dec = Decryptor::new(&ct[..]).map_err(|e| AgeWireError::Crypto(e.to_string()))?; - - // The decrypt method returns a Result - let mut r = match dec { - Decryptor::Recipients(d) => d.decrypt(std::iter::once(&id as &dyn age::Identity)) - .map_err(|e| AgeWireError::Crypto(e.to_string()))?, - Decryptor::Passphrase(_) => return Err(AgeWireError::Crypto("Expected recipients, got passphrase".to_string())), +// ---------- NEW: Persistent, named-key commands ---------- + +pub async fn cmd_age_keygen(server: &Server, name: &str) -> Protocol { + let (recip, ident) = libcryptoa::gen_enc_keypair(); + if let Err(e) = sset(server, &enc_pub_key_key(name), &recip) { return Protocol::err(&e.0); } + if let Err(e) = sset(server, &enc_priv_key_key(name), &ident) { return Protocol::err(&e.0); } + Protocol::Array(vec![Protocol::BulkString(recip), Protocol::BulkString(ident)]) +} + +pub async fn cmd_age_signkeygen(server: &Server, name: &str) -> Protocol { + let (verify, secret) = libcryptoa::gen_sign_keypair(); + if let Err(e) = sset(server, &sign_pub_key_key(name), &verify) { return Protocol::err(&e.0); } + if let Err(e) = sset(server, &sign_priv_key_key(name), &secret) { return Protocol::err(&e.0); } + Protocol::Array(vec![Protocol::BulkString(verify), Protocol::BulkString(secret)]) +} + +pub async fn cmd_age_encrypt_name(server: &Server, name: &str, message: &str) -> Protocol { + let recip = match sget(server, &enc_pub_key_key(name)) { + Ok(Some(v)) => v, + Ok(None) => return Protocol::err(&format!("ERR age: missing recipient (age:key:{name})")), + Err(e) => return Protocol::err(&e.0), }; - - let mut pt = Vec::new(); - use std::io::Read; - r.read_to_end(&mut pt).map_err(|e| AgeWireError::Crypto(e.to_string()))?; - String::from_utf8(pt).map_err(|_| AgeWireError::Utf8) -} - -/// Sign bytes of `msg` (detached). Returns base64(signature bytes, 64 bytes). -pub fn sign_b64(signing_secret_str: &str, msg: &str) -> Result { - let signing_key = parse_ed25519_signing_key(signing_secret_str)?; - let sig = signing_key.sign(msg.as_bytes()); - Ok(B64.encode(sig.to_bytes())) -} - -/// Verify detached signature (base64) for `msg` with pubkey. -pub fn verify_b64(verify_pub_str: &str, msg: &str, sig_b64: &str) -> Result { - let verifying_key = parse_ed25519_verifying_key(verify_pub_str)?; - let sig_bytes = B64.decode(sig_b64.as_bytes()).map_err(|e| AgeWireError::Crypto(e.to_string()))?; - if sig_bytes.len() != 64 { - return Err(AgeWireError::SignatureLen); + match libcryptoa::encrypt_b64(&recip, message) { + Ok(ct) => Protocol::BulkString(ct), + Err(e) => Protocol::err(&format!("ERR age: {e}")), } - let sig = Signature::from_bytes(sig_bytes[..].try_into().unwrap()); - Ok(verifying_key.verify(msg.as_bytes(), &sig).is_ok()) +} + +pub async fn cmd_age_decrypt_name(server: &Server, name: &str, ct_b64: &str) -> Protocol { + let ident = match sget(server, &enc_priv_key_key(name)) { + Ok(Some(v)) => v, + Ok(None) => return Protocol::err(&format!("ERR age: missing identity (age:privkey:{name})")), + Err(e) => return Protocol::err(&e.0), + }; + match libcryptoa::decrypt_b64(&ident, ct_b64) { + Ok(pt) => Protocol::BulkString(pt), + Err(e) => Protocol::err(&format!("ERR age: {e}")), + } +} + +pub async fn cmd_age_sign_name(server: &Server, name: &str, message: &str) -> Protocol { + let sec = match sget(server, &sign_priv_key_key(name)) { + Ok(Some(v)) => v, + Ok(None) => return Protocol::err(&format!("ERR age: missing signing secret (age:signpriv:{name})")), + Err(e) => return Protocol::err(&e.0), + }; + match libcryptoa::sign_b64(&sec, message) { + Ok(sig) => Protocol::BulkString(sig), + Err(e) => Protocol::err(&format!("ERR age: {e}")), + } +} + +pub async fn cmd_age_verify_name(server: &Server, name: &str, message: &str, sig_b64: &str) -> Protocol { + let pubk = match sget(server, &sign_pub_key_key(name)) { + Ok(Some(v)) => v, + Ok(None) => return Protocol::err(&format!("ERR age: missing verify pubkey (age:signpub:{name})")), + Err(e) => return Protocol::err(&e.0), + }; + match libcryptoa::verify_b64(&pubk, message, sig_b64) { + Ok(true) => Protocol::SimpleString("1".to_string()), + Ok(false) => Protocol::SimpleString("0".to_string()), + Err(e) => Protocol::err(&format!("ERR age: {e}")), + } +} + +pub async fn cmd_age_list(server: &Server) -> Protocol { + // Returns 4 arrays: ["encpub", ], ["encpriv", ...], ["signpub", ...], ["signpriv", ...] + let st = match server.current_storage() { Ok(s) => s, Err(e) => return Protocol::err(&e.0) }; + + let pull = |pat: &str, prefix: &str| -> Result, DBError> { + let keys = st.keys(pat)?; + let mut names: Vec = keys.into_iter() + .filter_map(|k| k.strip_prefix(prefix).map(|x| x.to_string())) + .collect(); + names.sort(); + Ok(names) + }; + + let encpub = match pull("age:key:*", "age:key:") { Ok(v) => v, Err(e)=> return Protocol::err(&e.0) }; + let encpriv = match pull("age:privkey:*", "age:privkey:") { Ok(v) => v, Err(e)=> return Protocol::err(&e.0) }; + let signpub = match pull("age:signpub:*", "age:signpub:") { Ok(v) => v, Err(e)=> return Protocol::err(&e.0) }; + let signpriv= match pull("age:signpriv:*", "age:signpriv:") { Ok(v) => v, Err(e)=> return Protocol::err(&e.0) }; + + let to_arr = |label: &str, v: Vec| { + let mut out = vec![Protocol::BulkString(label.to_string())]; + out.push(Protocol::Array(v.into_iter().map(Protocol::BulkString).collect())); + Protocol::Array(out) + }; + + Protocol::Array(vec![ + to_arr("encpub", encpub), + to_arr("encpriv", encpriv), + to_arr("signpub", signpub), + to_arr("signpriv", signpriv), + ]) } // ---------- Storage helpers ---------- diff --git a/crates/herodb/cmd.rs b/crates/herodb/cmd.rs index c036b4a..8936514 100644 --- a/crates/herodb/cmd.rs +++ b/crates/herodb/cmd.rs @@ -1,4 +1,7 @@ -use crate::{error::DBError, protocol::Protocol, server::Server}; +use crate::protocol::Protocol; +use crate::server::Server; +use libdbstorage::DBError; +use libcryptoa; use serde::Serialize; #[derive(Debug, Clone)] @@ -538,12 +541,12 @@ impl Cmd { Cmd::LRange(key, start, stop) => lrange_cmd(server, &key, start, stop).await, Cmd::FlushDb => flushdb_cmd(server).await, // AGE (rage): stateless - Cmd::AgeGenEnc => Ok(crate::age::cmd_age_genenc().await), - Cmd::AgeGenSign => Ok(crate::age::cmd_age_gensign().await), - Cmd::AgeEncrypt(recipient, message) => Ok(crate::age::cmd_age_encrypt(&recipient, &message).await), - Cmd::AgeDecrypt(identity, ct_b64) => Ok(crate::age::cmd_age_decrypt(&identity, &ct_b64).await), - Cmd::AgeSign(secret, message) => Ok(crate::age::cmd_age_sign(&secret, &message).await), - Cmd::AgeVerify(vpub, msg, sig_b64) => Ok(crate::age::cmd_age_verify(&vpub, &msg, &sig_b64).await), + Cmd::AgeGenEnc => Ok(libcryptoa::gen_enc_keypair().await), + Cmd::AgeGenSign => Ok(libcryptoa::gen_sign_keypair().await), + Cmd::AgeEncrypt(recipient, message) => Ok(libcryptoa::encrypt_b64(&recipient, &message).await), + Cmd::AgeDecrypt(identity, ct_b64) => Ok(libcryptoa::decrypt_b64(&identity, &ct_b64).await), + Cmd::AgeSign(secret, message) => Ok(libcryptoa::sign_b64(&secret, &message).await), + Cmd::AgeVerify(vpub, msg, sig_b64) => Ok(libcryptoa::verify_b64(&vpub, &msg, &sig_b64).await), // AGE (rage): persistent named keys Cmd::AgeKeygen(name) => Ok(crate::age::cmd_age_keygen(server, &name).await), diff --git a/crates/herodb/crypto.rs b/crates/herodb/crypto.rs deleted file mode 100644 index e43317b..0000000 --- a/crates/herodb/crypto.rs +++ /dev/null @@ -1,73 +0,0 @@ -use chacha20poly1305::{ - aead::{Aead, KeyInit, OsRng}, - XChaCha20Poly1305, XNonce, -}; -use rand::RngCore; -use sha2::{Digest, Sha256}; - -const VERSION: u8 = 1; -const NONCE_LEN: usize = 24; -const TAG_LEN: usize = 16; - -#[derive(Debug)] -pub enum CryptoError { - Format, // wrong length / header - Version(u8), // unknown version - Decrypt, // wrong key or corrupted data -} - -impl From for crate::error::DBError { - fn from(e: CryptoError) -> Self { - crate::error::DBError(format!("Crypto error: {:?}", e)) - } -} - -/// Super-simple factory: new(secret) + encrypt(bytes) + decrypt(bytes) -pub struct CryptoFactory { - key: chacha20poly1305::Key, -} - -impl CryptoFactory { - /// Accepts any secret bytes; turns them into a 32-byte key (SHA-256). - pub fn new>(secret: S) -> Self { - let mut h = Sha256::new(); - h.update(b"xchacha20poly1305-factory:v1"); // domain separation - h.update(secret.as_ref()); - let digest = h.finalize(); // 32 bytes - let key = chacha20poly1305::Key::from_slice(&digest).to_owned(); - Self { key } - } - - /// Output layout: [version:1][nonce:24][ciphertext||tag] - pub fn encrypt(&self, plaintext: &[u8]) -> Vec { - let cipher = XChaCha20Poly1305::new(&self.key); - - let mut nonce_bytes = [0u8; NONCE_LEN]; - OsRng.fill_bytes(&mut nonce_bytes); - let nonce = XNonce::from_slice(&nonce_bytes); - - let mut out = Vec::with_capacity(1 + NONCE_LEN + plaintext.len() + TAG_LEN); - out.push(VERSION); - out.extend_from_slice(&nonce_bytes); - - let ct = cipher.encrypt(nonce, plaintext).expect("encrypt"); - out.extend_from_slice(&ct); - out - } - - pub fn decrypt(&self, blob: &[u8]) -> Result, CryptoError> { - if blob.len() < 1 + NONCE_LEN + TAG_LEN { - return Err(CryptoError::Format); - } - let ver = blob[0]; - if ver != VERSION { - return Err(CryptoError::Version(ver)); - } - - let nonce = XNonce::from_slice(&blob[1..1 + NONCE_LEN]); - let ct = &blob[1 + NONCE_LEN..]; - - let cipher = XChaCha20Poly1305::new(&self.key); - cipher.decrypt(nonce, ct).map_err(|_| CryptoError::Decrypt) - } -} \ No newline at end of file diff --git a/crates/herodb/lib.rs b/crates/herodb/lib.rs index a66b56e..2d0b68f 100644 --- a/crates/herodb/lib.rs +++ b/crates/herodb/lib.rs @@ -1,8 +1,4 @@ pub mod age; // NEW pub mod cmd; -pub mod crypto; -pub mod error; -pub mod options; pub mod protocol; pub mod server; -pub mod storage; diff --git a/crates/herodb/server.rs b/crates/herodb/server.rs index c286e21..cec4157 100644 --- a/crates/herodb/server.rs +++ b/crates/herodb/server.rs @@ -30,7 +30,7 @@ impl Server { } } - pub fn current_storage(&self) -> Result, DBError> { + pub fn current_storage(&self) -> Result, libdbstorage::DBError> { let mut cache = self.db_cache.write().unwrap(); if let Some(storage) = cache.get(&self.selected_db) { diff --git a/crates/herodb/storage/mod.rs b/crates/herodb/storage/mod.rs deleted file mode 100644 index 7c33028..0000000 --- a/crates/herodb/storage/mod.rs +++ /dev/null @@ -1,126 +0,0 @@ -use std::{ - path::Path, - time::{SystemTime, UNIX_EPOCH}, -}; - -use redb::{Database, TableDefinition}; -use serde::{Deserialize, Serialize}; - -use crate::crypto::CryptoFactory; -use crate::error::DBError; - -// Re-export modules -mod storage_basic; -mod storage_hset; -mod storage_lists; -mod storage_extra; - -// Re-export implementations -// Note: These imports are used by the impl blocks in the submodules -// The compiler shows them as unused because they're not directly used in this file -// but they're needed for the Storage struct methods to be available -pub use storage_extra::*; - -// Table definitions for different Redis data types -const TYPES_TABLE: TableDefinition<&str, &str> = TableDefinition::new("types"); -const STRINGS_TABLE: TableDefinition<&str, &[u8]> = TableDefinition::new("strings"); -const HASHES_TABLE: TableDefinition<(&str, &str), &[u8]> = TableDefinition::new("hashes"); -const LISTS_TABLE: TableDefinition<&str, &[u8]> = TableDefinition::new("lists"); -const STREAMS_META_TABLE: TableDefinition<&str, &[u8]> = TableDefinition::new("streams_meta"); -const STREAMS_DATA_TABLE: TableDefinition<(&str, &str), &[u8]> = TableDefinition::new("streams_data"); -const ENCRYPTED_TABLE: TableDefinition<&str, u8> = TableDefinition::new("encrypted"); -const EXPIRATION_TABLE: TableDefinition<&str, u64> = TableDefinition::new("expiration"); - -#[derive(Serialize, Deserialize, Debug, Clone)] -pub struct StreamEntry { - pub fields: Vec<(String, String)>, -} - -#[derive(Serialize, Deserialize, Debug, Clone)] -pub struct ListValue { - pub elements: Vec, -} - -#[inline] -pub fn now_in_millis() -> u128 { - let start = SystemTime::now(); - let duration_since_epoch = start.duration_since(UNIX_EPOCH).unwrap(); - duration_since_epoch.as_millis() -} - -pub struct Storage { - db: Database, - crypto: Option, -} - -impl Storage { - pub fn new(path: impl AsRef, should_encrypt: bool, master_key: Option<&str>) -> Result { - let db = Database::create(path)?; - - // Create tables if they don't exist - let write_txn = db.begin_write()?; - { - let _ = write_txn.open_table(TYPES_TABLE)?; - let _ = write_txn.open_table(STRINGS_TABLE)?; - let _ = write_txn.open_table(HASHES_TABLE)?; - let _ = write_txn.open_table(LISTS_TABLE)?; - let _ = write_txn.open_table(STREAMS_META_TABLE)?; - let _ = write_txn.open_table(STREAMS_DATA_TABLE)?; - let _ = write_txn.open_table(ENCRYPTED_TABLE)?; - let _ = write_txn.open_table(EXPIRATION_TABLE)?; - } - write_txn.commit()?; - - // Check if database was previously encrypted - let read_txn = db.begin_read()?; - let encrypted_table = read_txn.open_table(ENCRYPTED_TABLE)?; - let was_encrypted = encrypted_table.get("encrypted")?.map(|v| v.value() == 1).unwrap_or(false); - drop(read_txn); - - let crypto = if should_encrypt || was_encrypted { - if let Some(key) = master_key { - Some(CryptoFactory::new(key.as_bytes())) - } else { - return Err(DBError("Encryption requested but no master key provided".to_string())); - } - } else { - None - }; - - // If we're enabling encryption for the first time, mark it - if should_encrypt && !was_encrypted { - let write_txn = db.begin_write()?; - { - let mut encrypted_table = write_txn.open_table(ENCRYPTED_TABLE)?; - encrypted_table.insert("encrypted", &1u8)?; - } - write_txn.commit()?; - } - - Ok(Storage { - db, - crypto, - }) - } - - pub fn is_encrypted(&self) -> bool { - self.crypto.is_some() - } - - // Helper methods for encryption - fn encrypt_if_needed(&self, data: &[u8]) -> Result, DBError> { - if let Some(crypto) = &self.crypto { - Ok(crypto.encrypt(data)) - } else { - Ok(data.to_vec()) - } - } - - fn decrypt_if_needed(&self, data: &[u8]) -> Result, DBError> { - if let Some(crypto) = &self.crypto { - Ok(crypto.decrypt(data)?) - } else { - Ok(data.to_vec()) - } - } -} \ No newline at end of file diff --git a/crates/libcrypto/Cargo.toml b/crates/libcrypto/Cargo.toml index dac73b4..7f7fec3 100644 --- a/crates/libcrypto/Cargo.toml +++ b/crates/libcrypto/Cargo.toml @@ -4,13 +4,7 @@ version = "0.1.0" edition = "2021" [dependencies] -chacha20poly1305 = "0.10" +chacha20poly1305 = { workspace = true } rand = { workspace = true } sha2 = { workspace = true } -thiserror = { workspace = true } - -# Add this dependency for the From for DBError -libdbstorage = { path = "../libdbstorage", optional = true } - -[features] -storage_compat = ["dep:libdbstorage"] \ No newline at end of file +thiserror = { workspace = true } \ No newline at end of file diff --git a/crates/libcrypto/src/lib.rs b/crates/libcrypto/src/lib.rs index b93cf3f..1e70314 100644 --- a/crates/libcrypto/src/lib.rs +++ b/crates/libcrypto/src/lib.rs @@ -1,14 +1,72 @@ -pub fn add(left: u64, right: u64) -> u64 { - left + right +// In crates/libcrypto/src/lib.rs +use chacha20poly1305::{ + aead::{Aead, KeyInit, OsRng}, + XChaCha20Poly1305, XNonce, +}; +use rand::RngCore; +use sha2::{Digest, Sha256}; +use thiserror::Error; + +const VERSION: u8 = 1; +const NONCE_LEN: usize = 24; +const TAG_LEN: usize = 16; + +#[derive(Error, Debug)] +pub enum CryptoError { + #[error("invalid format: data too short")] + Format, + #[error("unknown version: {0}")] + Version(u8), + #[error("decryption failed: wrong key or corrupted data")] + Decrypt, } -#[cfg(test)] -mod tests { - use super::*; +/// Super-simple factory: new(secret) + encrypt(bytes) + decrypt(bytes) +pub struct CryptoFactory { + key: chacha20poly1305::Key, +} - #[test] - fn it_works() { - let result = add(2, 2); - assert_eq!(result, 4); +impl CryptoFactory { + /// Accepts any secret bytes; turns them into a 32-byte key (SHA-256). + pub fn new>(secret: S) -> Self { + let mut h = Sha256::new(); + h.update(b"xchacha20poly1305-factory:v1"); // domain separation + h.update(secret.as_ref()); + let digest = h.finalize(); // 32 bytes + let key = chacha20poly1305::Key::from_slice(&digest).to_owned(); + Self { key } } -} + + /// Output layout: [version:1][nonce:24][ciphertext||tag] + pub fn encrypt(&self, plaintext: &[u8]) -> Vec { + let cipher = XChaCha20Poly1305::new(&self.key); + + let mut nonce_bytes = [0u8; NONCE_LEN]; + OsRng.fill_bytes(&mut nonce_bytes); + let nonce = XNonce::from_slice(&nonce_bytes); + + let mut out = Vec::with_capacity(1 + NONCE_LEN + plaintext.len() + TAG_LEN); + out.push(VERSION); + out.extend_from_slice(&nonce_bytes); + + let ct = cipher.encrypt(nonce, plaintext).expect("encrypt"); + out.extend_from_slice(&ct); + out + } + + pub fn decrypt(&self, blob: &[u8]) -> Result, CryptoError> { + if blob.len() < 1 + NONCE_LEN + TAG_LEN { + return Err(CryptoError::Format); + } + let ver = blob[0]; + if ver != VERSION { + return Err(CryptoError::Version(ver)); + } + + let nonce = XNonce::from_slice(&blob[1..1 + NONCE_LEN]); + let ct = &blob[1 + NONCE_LEN..]; + + let cipher = XChaCha20Poly1305::new(&self.key); + cipher.decrypt(nonce, ct).map_err(|_| CryptoError::Decrypt) + } +} \ No newline at end of file diff --git a/crates/libcryptoa/src/lib.rs b/crates/libcryptoa/src/lib.rs index b93cf3f..c0a0831 100644 --- a/crates/libcryptoa/src/lib.rs +++ b/crates/libcryptoa/src/lib.rs @@ -1,14 +1,100 @@ -pub fn add(left: u64, right: u64) -> u64 { - left + right +// In crates/libcryptoa/src/lib.rs +use std::str::FromStr; +use age::{Decryptor, Encryptor, x25519}; +use base64::{engine::general_purpose::STANDARD as B64, Engine as _}; +use ed25519_dalek::{Signature, Signer, SigningKey, Verifier, VerifyingKey}; +use secrecy::ExposeSecret; +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum AsymmetricCryptoError { + #[error("key parsing failed")] + ParseKey, + #[error("age crypto error: {0}")] + Age(String), + #[error("invalid utf-8 in plaintext")] + Utf8, + #[error("invalid signature length")] + SignatureLen, + #[error("signature verification failed")] + Verify, + #[error("base64 decoding failed: {0}")] + Base64(#[from] base64::DecodeError), + #[error("io error: {0}")] + Io(#[from] std::io::Error), } -#[cfg(test)] -mod tests { - use super::*; +fn parse_recipient(s: &str) -> Result { + x25519::Recipient::from_str(s).map_err(|_| AsymmetricCryptoError::ParseKey) +} - #[test] - fn it_works() { - let result = add(2, 2); - assert_eq!(result, 4); +fn parse_identity(s: &str) -> Result { + x25519::Identity::from_str(s).map_err(|_| AsymmetricCryptoError::ParseKey) +} + +fn parse_ed25519_signing_key(s: &str) -> Result { + let bytes = B64.decode(s)?; + let key_bytes: [u8; 32] = bytes.try_into().map_err(|_| AsymmetricCryptoError::ParseKey)?; + Ok(SigningKey::from_bytes(&key_bytes)) +} + +fn parse_ed25519_verifying_key(s: &str) -> Result { + let bytes = B64.decode(s)?; + let key_bytes: [u8; 32] = bytes.try_into().map_err(|_| AsymmetricCryptoError::ParseKey)?; + VerifyingKey::from_bytes(&key_bytes).map_err(|_| AsymmetricCryptoError::ParseKey) +} + +pub fn gen_enc_keypair() -> (String, String) { + let id = x25519::Identity::generate(); + let pk = id.to_public(); + (pk.to_string(), id.to_string().expose_secret().to_string()) +} + +pub fn gen_sign_keypair() -> (String, String) { + let signing_key = SigningKey::generate(&mut rand::rngs::OsRng); + let verifying_key = signing_key.verifying_key(); + (B64.encode(verifying_key.to_bytes()), B64.encode(signing_key.to_bytes())) +} + +pub fn encrypt_b64(recipient_str: &str, msg: &str) -> Result { + let recipient = parse_recipient(recipient_str)?; + let encryptor = Encryptor::with_recipients(vec![Box::new(recipient)]) + .ok_or_else(|| AsymmetricCryptoError::Age("Failed to create encryptor".into()))?; + + let mut encrypted = vec![]; + let mut writer = encryptor.wrap_output(&mut encrypted)?; + std::io::Write::write_all(&mut writer, msg.as_bytes())?; + writer.finish()?; + + Ok(B64.encode(encrypted)) +} + +pub fn decrypt_b64(identity_str: &str, ct_b64: &str) -> Result { + let identity = parse_identity(identity_str)?; + let ct = B64.decode(ct_b64)?; + + let decryptor = Decryptor::new(&ct[..]).map_err(|e| AsymmetricCryptoError::Age(e.to_string()))?; + + let mut decrypted = vec![]; + if let Decryptor::Recipients(d) = decryptor { + let mut reader = d.decrypt(std::iter::once(&identity as &dyn age::Identity)) + .map_err(|e| AsymmetricCryptoError::Age(e.to_string()))?; + std::io::Read::read_to_end(&mut reader, &mut decrypted)?; + String::from_utf8(decrypted).map_err(|_| AsymmetricCryptoError::Utf8) + } else { + Err(AsymmetricCryptoError::Age("Passphrase decryption not supported".into())) } } + +pub fn sign_b64(signing_secret_str: &str, msg: &str) -> Result { + let signing_key = parse_ed25519_signing_key(signing_secret_str)?; + let signature = signing_key.sign(msg.as_bytes()); + Ok(B64.encode(signature.to_bytes())) +} + +pub fn verify_b64(verify_pub_str: &str, msg: &str, sig_b64: &str) -> Result { + let verifying_key = parse_ed25519_verifying_key(verify_pub_str)?; + let sig_bytes = B64.decode(sig_b64)?; + let signature = Signature::from_slice(&sig_bytes).map_err(|_| AsymmetricCryptoError::SignatureLen)?; + Ok(verifying_key.verify(msg.as_bytes(), &signature).is_ok()) +} \ No newline at end of file diff --git a/crates/libdbstorage/Cargo.toml b/crates/libdbstorage/Cargo.toml index cfec9a4..3686d18 100644 --- a/crates/libdbstorage/Cargo.toml +++ b/crates/libdbstorage/Cargo.toml @@ -7,6 +7,7 @@ edition = "2021" redb = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } +thiserror = { workspace = true } -[dev-dependencies] -thiserror = { workspace = true } \ No newline at end of file +# Local Crate Dependencies +libcrypto = { path = "../libcrypto" } \ No newline at end of file diff --git a/crates/herodb/error.rs b/crates/libdbstorage/src/error.rs similarity index 93% rename from crates/herodb/error.rs rename to crates/libdbstorage/src/error.rs index 3037c70..f1f0049 100644 --- a/crates/herodb/error.rs +++ b/crates/libdbstorage/src/error.rs @@ -87,8 +87,3 @@ impl From for DBError { } } -impl From for DBError { - fn from(item: chacha20poly1305::Error) -> Self { - DBError(item.to_string()) - } -} diff --git a/crates/libdbstorage/src/lib.rs b/crates/libdbstorage/src/lib.rs index b93cf3f..315888d 100644 --- a/crates/libdbstorage/src/lib.rs +++ b/crates/libdbstorage/src/lib.rs @@ -1,14 +1,121 @@ -pub fn add(left: u64, right: u64) -> u64 { - left + right +// In crates/libdbstorage/src/lib.rs +use std::{ + path::Path, + time::{SystemTime, UNIX_EPOCH}, +}; + +use libcrypto::CryptoFactory; // Correct import +use redb::{Database, TableDefinition}; +use serde::{Deserialize, Serialize}; + +pub use crate::error::DBError; // Re-export for users of this crate + +// Declare modules +pub mod storage_basic; +pub mod storage_hset; +pub mod storage_lists; +pub mod storage_extra; + +// Table definitions for different Redis data types +const TYPES_TABLE: TableDefinition<&str, &str> = TableDefinition::new("types"); +const STRINGS_TABLE: TableDefinition<&str, &[u8]> = TableDefinition::new("strings"); +const HASHES_TABLE: TableDefinition<(&str, &str), &[u8]> = TableDefinition::new("hashes"); +const LISTS_TABLE: TableDefinition<&str, &[u8]> = TableDefinition::new("lists"); +const STREAMS_META_TABLE: TableDefinition<&str, &[u8]> = TableDefinition::new("streams_meta"); +const STREAMS_DATA_TABLE: TableDefinition<(&str, &str), &[u8]> = TableDefinition::new("streams_data"); +const ENCRYPTED_TABLE: TableDefinition<&str, u8> = TableDefinition::new("encrypted"); +const EXPIRATION_TABLE: TableDefinition<&str, u64> = TableDefinition::new("expiration"); + +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct StreamEntry { + pub fields: Vec<(String, String)>, } -#[cfg(test)] -mod tests { - use super::*; +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct ListValue { + pub elements: Vec, +} - #[test] - fn it_works() { - let result = add(2, 2); - assert_eq!(result, 4); +#[inline] +pub fn now_in_millis() -> u128 { + let start = SystemTime::now(); + let duration_since_epoch = start.duration_since(UNIX_EPOCH).unwrap(); + duration_since_epoch.as_millis() +} + +pub struct Storage { + db: Database, + crypto: Option, +} + +impl Storage { + pub fn new(path: impl AsRef, should_encrypt: bool, master_key: Option<&str>) -> Result { + let db = Database::create(path)?; + + // Create tables if they don't exist + let write_txn = db.begin_write()?; + { + let _ = write_txn.open_table(TYPES_TABLE)?; + let _ = write_txn.open_table(STRINGS_TABLE)?; + let _ = write_txn.open_table(HASHES_TABLE)?; + let _ = write_txn.open_table(LISTS_TABLE)?; + let _ = write_txn.open_table(STREAMS_META_TABLE)?; + let _ = write_txn.open_table(STREAMS_DATA_TABLE)?; + let _ = write_txn.open_table(ENCRYPTED_TABLE)?; + let _ = write_txn.open_table(EXPIRATION_TABLE)?; + } + write_txn.commit()?; + + // Check if database was previously encrypted + let read_txn = db.begin_read()?; + let encrypted_table = read_txn.open_table(ENCRYPTED_TABLE)?; + let was_encrypted = encrypted_table.get("encrypted")?.map(|v| v.value() == 1).unwrap_or(false); + drop(read_txn); + + let crypto = if should_encrypt || was_encrypted { + if let Some(key) = master_key { + Some(CryptoFactory::new(key.as_bytes())) + } else { + return Err(DBError("Encryption requested but no master key provided".to_string())); + } + } else { + None + }; + + // If we're enabling encryption for the first time, mark it + if should_encrypt && !was_encrypted { + let write_txn = db.begin_write()?; + { + let mut encrypted_table = write_txn.open_table(ENCRYPTED_TABLE)?; + encrypted_table.insert("encrypted", &1u8)?; + } + write_txn.commit()?; + } + + Ok(Storage { + db, + crypto, + }) } -} + + pub fn is_encrypted(&self) -> bool { + self.crypto.is_some() + } + + // Helper methods for encryption + fn encrypt_if_needed(&self, data: &[u8]) -> Result, DBError> { + if let Some(crypto) = &self.crypto { + Ok(crypto.encrypt(data)) + } else { + Ok(data.to_vec()) + } + } + + fn decrypt_if_needed(&self, data: &[u8]) -> Result, DBError> { + if let Some(crypto) = &self.crypto { + Ok(crypto.decrypt(data).map_err(|e| DBError(e.to_string()))?) + } else { + Ok(data.to_vec()) + } + } +} \ No newline at end of file diff --git a/crates/herodb/storage/storage_basic.rs b/crates/libdbstorage/src/storage/storage_basic.rs similarity index 100% rename from crates/herodb/storage/storage_basic.rs rename to crates/libdbstorage/src/storage/storage_basic.rs diff --git a/crates/herodb/storage/storage_extra.rs b/crates/libdbstorage/src/storage/storage_extra.rs similarity index 100% rename from crates/herodb/storage/storage_extra.rs rename to crates/libdbstorage/src/storage/storage_extra.rs diff --git a/crates/herodb/storage/storage_hset.rs b/crates/libdbstorage/src/storage/storage_hset.rs similarity index 100% rename from crates/herodb/storage/storage_hset.rs rename to crates/libdbstorage/src/storage/storage_hset.rs diff --git a/crates/herodb/storage/storage_lists.rs b/crates/libdbstorage/src/storage/storage_lists.rs similarity index 100% rename from crates/herodb/storage/storage_lists.rs rename to crates/libdbstorage/src/storage/storage_lists.rs diff --git a/crates/supervisorrpc/Cargo.toml b/crates/supervisorrpc/Cargo.toml index 4413902..bfeaf2d 100644 --- a/crates/supervisorrpc/Cargo.toml +++ b/crates/supervisorrpc/Cargo.toml @@ -1,6 +1,18 @@ [package] name = "supervisorrpc" version = "0.1.0" -edition = "2024" +edition = "2021" +[[bin]] +name = "supervisorrpc" +path = "src/main.rs" + [dependencies] +# Example dependencies for an RPC server +# axum = "0.7" +# jsonrpsee = { version = "0.22", features = ["server"] } +# openrpc-types = "0.7" + +tokio = { workspace = true } +redis = { version = "0.24", features = ["tokio-comp"] } +herocrypto = { path = "../herocrypto" } \ No newline at end of file diff --git a/crates/supervisorrpc/src/main.rs b/crates/supervisorrpc/src/main.rs index e7a11a9..b0bf1b9 100644 --- a/crates/supervisorrpc/src/main.rs +++ b/crates/supervisorrpc/src/main.rs @@ -1,3 +1,12 @@ -fn main() { - println!("Hello, world!"); -} +// To be implemented: +// 1. Define an OpenRPC schema for supervisor functions (e.g., server status, key rotation). +// 2. Implement an HTTP/TCP server (e.g., using Axum or jsonrpsee) that serves the schema +// and handles RPC calls. +// 3. Implement support for Unix domain sockets in addition to TCP. +// 4. Use the `herocrypto` or `redis-rs` crate to interact with the main `herodb` instance. + +#[tokio::main] +async fn main() { + println!("Supervisor RPC server starting... (not implemented)"); + // Server setup code will go here. +} \ No newline at end of file diff --git a/src/main.rs b/src/main.rs deleted file mode 100644 index e71fdf5..0000000 --- a/src/main.rs +++ /dev/null @@ -1 +0,0 @@ -fn main() {} \ No newline at end of file