diff --git a/.gitignore b/.gitignore index 0201000..83ea716 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,7 @@ devenv.local.yaml # pre-commit .pre-commit-config.yaml + +# Environment Variables +.env* +!.env.example diff --git a/Cargo.lock b/Cargo.lock index 066dac3..e314e68 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -32,6 +32,15 @@ dependencies = [ "memchr", ] +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + [[package]] name = "anstream" version = "1.0.0" @@ -309,6 +318,16 @@ dependencies = [ "cipher 0.4.4", ] +[[package]] +name = "cc" +version = "1.2.67" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e17dd265a7d0f31ef544e1b20e03add05d3b45b491b633b10d67145d2acc1a38" +dependencies = [ + "find-msvc-tools", + "shlex", +] + [[package]] name = "cfg-if" version = "1.0.4" @@ -339,6 +358,19 @@ dependencies = [ "poly1305", ] +[[package]] +name = "chrono" +version = "0.4.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327" +dependencies = [ + "iana-time-zone", + "js-sys", + "num-traits", + "wasm-bindgen", + "windows-link", +] + [[package]] name = "cipher" version = "0.4.4" @@ -404,11 +436,17 @@ checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" name = "cli" version = "1.0.0" dependencies = [ + "chrono", "clap", "colored", + "crypto", + "diesel", + "dotenvy", "git-version", + "hex", "keyring", "tokio", + "uuid", ] [[package]] @@ -519,6 +557,83 @@ dependencies = [ "cmov", ] +[[package]] +name = "darling" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cdf337090841a411e2a7f3deb9187445851f91b309c0c0a29e05f74a00a48c0" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1247195ecd7e3c85f83c8d2a366e4210d588e802133e1e355180a9870b517ea4" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn", +] + +[[package]] +name = "darling_macro" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81" +dependencies = [ + "darling_core", + "quote", + "syn", +] + +[[package]] +name = "deranged" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" + +[[package]] +name = "diesel" +version = "2.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e54d1f576cd3a3460f212a4615fd12ce1b6303c095b79a44449ffbe627753dc1" +dependencies = [ + "chrono", + "diesel_derives", + "downcast-rs", + "libsqlite3-sys", + "sqlite-wasm-rs", + "time", +] + +[[package]] +name = "diesel_derives" +version = "2.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1817b7f4279b947fc4cafddec12b0e5f8727141706561ce3ac94a60bddd1cf5" +dependencies = [ + "diesel_table_macro_syntax", + "dsl_auto_type", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "diesel_table_macro_syntax" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe2444076b48641147115697648dc743c2c00b61adade0f01ce67133c7babe8c" +dependencies = [ + "syn", +] + [[package]] name = "digest" version = "0.10.7" @@ -530,6 +645,38 @@ dependencies = [ "subtle", ] +[[package]] +name = "dotenvy" +version = "0.15.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" + +[[package]] +name = "downcast-rs" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "117240f60069e65410b3ae1bb213295bd828f707b5bec6596a1afc8793ce0cbc" + +[[package]] +name = "dsl_auto_type" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd122633e4bef06db27737f21d3738fb89c8f6d5360d6d9d7635dda142a7757e" +dependencies = [ + "darling", + "either", + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "either" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e" + [[package]] name = "endi" version = "1.1.1" @@ -600,6 +747,24 @@ version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + [[package]] name = "futures-core" version = "0.3.32" @@ -708,6 +873,15 @@ dependencies = [ "syn", ] +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +dependencies = [ + "foldhash", +] + [[package]] name = "hashbrown" version = "0.17.1" @@ -759,6 +933,36 @@ dependencies = [ "typenum", ] +[[package]] +name = "iana-time-zone" +version = "0.1.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + [[package]] name = "indexmap" version = "2.14.0" @@ -766,7 +970,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" dependencies = [ "equivalent", - "hashbrown", + "hashbrown 0.17.1", ] [[package]] @@ -832,6 +1036,16 @@ version = "0.2.186" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" +[[package]] +name = "libsqlite3-sys" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f111c8c41e7c61a49cd34e44c7619462967221a6443b0ec299e0ac30cfb9b1" +dependencies = [ + "pkg-config", + "vcpkg", +] + [[package]] name = "linux-raw-sys" version = "0.12.1" @@ -912,6 +1126,12 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-conv" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" + [[package]] name = "num-integer" version = "0.1.46" @@ -1019,6 +1239,12 @@ dependencies = [ "futures-io", ] +[[package]] +name = "pkg-config" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" + [[package]] name = "polling" version = "3.11.0" @@ -1043,6 +1269,12 @@ dependencies = [ "universal-hash", ] +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + [[package]] name = "proc-macro-crate" version = "3.5.0" @@ -1131,6 +1363,16 @@ version = "0.8.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" +[[package]] +name = "rsqlite-vfs" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c51c9ae4df8a7fba42103df5c621fa3c37eccf3a3c650879e90fc48b11cc192c" +dependencies = [ + "hashbrown 0.16.1", + "thiserror", +] + [[package]] name = "rustix" version = "1.1.4" @@ -1250,6 +1492,12 @@ dependencies = [ "digest", ] +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + [[package]] name = "signal-hook-registry" version = "1.4.8" @@ -1282,6 +1530,18 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "sqlite-wasm-rs" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc3efc0da82635d7e1ced0053bbbfa8c7ab9645d0bf36ceb4f7127bb85315d75" +dependencies = [ + "cc", + "js-sys", + "rsqlite-vfs", + "wasm-bindgen", +] + [[package]] name = "strsim" version = "0.11.1" @@ -1318,6 +1578,56 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "time" +version = "0.3.53" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18dfaaeddcb932337b5e7866ee7d0ce9b76d2fd092997146f187ec09b4558a50" +dependencies = [ + "deranged", + "num-conv", + "powerfmt", + "serde_core", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109" + +[[package]] +name = "time-macros" +version = "0.2.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c431b87111666e491a90baa837f914fb45cd5dc3c268591b0220ff5057f2085f" +dependencies = [ + "num-conv", + "time-core", +] + [[package]] name = "tokio" version = "1.52.3" @@ -1457,6 +1767,12 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + [[package]] name = "version_check" version = "0.9.5" @@ -1514,6 +1830,41 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "windows-link" version = "0.2.1" @@ -1533,6 +1884,24 @@ dependencies = [ "zeroize", ] +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + [[package]] name = "windows-sys" version = "0.61.2" diff --git a/crates/cli/Cargo.toml b/crates/cli/Cargo.toml index 5c743bb..2e6f247 100644 --- a/crates/cli/Cargo.toml +++ b/crates/cli/Cargo.toml @@ -7,8 +7,18 @@ description = "An opensource secrets management and distribution platform" license = "GLP-3.0" [dependencies] -tokio = { version = "1.52.3", features = ["full"] } +diesel = { version = "2.2.0", features = [ + "sqlite", + "returning_clauses_for_sqlite_3_35", + "chrono", +] } clap = { version = "4.6.1", features = ["derive", "cargo"] } +tokio = { version = "1.52.3", features = ["full"] } +crypto = { path = "../crypto" } +git-version = "0.3" keyring = "4.1.4" colored = "3.1.1" -git-version = "0.3" +chrono = "0.4.45" +dotenvy = "0.15" +uuid = "1.23.5" +hex = "0.4" diff --git a/crates/cli/diesel.toml b/crates/cli/diesel.toml new file mode 100644 index 0000000..bb1d1f7 --- /dev/null +++ b/crates/cli/diesel.toml @@ -0,0 +1,9 @@ +# For documentation on how to configure this file, +# see https://diesel.rs/guides/configuring-diesel-cli + +[print_schema] +file = "src/db/schema.rs" +custom_type_derives = ["diesel::query_builder::QueryId", "Clone"] + +[migrations_directory] +dir = "migrations" diff --git a/crates/cli/harbor.db b/crates/cli/harbor.db new file mode 100644 index 0000000..7552bbd Binary files /dev/null and b/crates/cli/harbor.db differ diff --git a/crates/cli/migrations/.diesel_lock b/crates/cli/migrations/.diesel_lock new file mode 100644 index 0000000..e69de29 diff --git a/crates/cli/migrations/.keep b/crates/cli/migrations/.keep new file mode 100644 index 0000000..e69de29 diff --git a/crates/cli/migrations/2026-07-13-221551-0000_create_tables/down.sql b/crates/cli/migrations/2026-07-13-221551-0000_create_tables/down.sql new file mode 100644 index 0000000..863dae2 --- /dev/null +++ b/crates/cli/migrations/2026-07-13-221551-0000_create_tables/down.sql @@ -0,0 +1,2 @@ +DROP TABLE IF EXISTS secrets; +DROP TABLE IF EXISTS projects; diff --git a/crates/cli/migrations/2026-07-13-221551-0000_create_tables/up.sql b/crates/cli/migrations/2026-07-13-221551-0000_create_tables/up.sql new file mode 100644 index 0000000..52c8d0a --- /dev/null +++ b/crates/cli/migrations/2026-07-13-221551-0000_create_tables/up.sql @@ -0,0 +1,19 @@ +CREATE TABLE projects ( + id TEXT PRIMARY KEY, + name TEXT NOT NULL, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +CREATE TABLE secrets ( + id TEXT PRIMARY KEY, + name TEXT NOT NULL, + project_id TEXT NOT NULL, + config TEXT NOT NULL, + secret BLOB NOT NULL, + nonce BLOB NOT NULL, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + + FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE, + + UNIQUE (project_id, config, name) +); diff --git a/crates/cli/src/db/mod.rs b/crates/cli/src/db/mod.rs new file mode 100644 index 0000000..07465e5 --- /dev/null +++ b/crates/cli/src/db/mod.rs @@ -0,0 +1,14 @@ +use diesel::prelude::*; +use dotenvy::dotenv; +use std::env; + +pub mod models; +pub mod schema; + +pub fn establish_connection() -> SqliteConnection { + dotenv().ok(); + + let database_url = env::var("DATABASE_URL").expect("DATABASE_URL must be set"); + SqliteConnection::establish(&database_url) + .unwrap_or_else(|_| panic!("Error connecting to {}", database_url)) +} diff --git a/crates/cli/src/db/models.rs b/crates/cli/src/db/models.rs new file mode 100644 index 0000000..acd9e6b --- /dev/null +++ b/crates/cli/src/db/models.rs @@ -0,0 +1,25 @@ +use crate::db::schema::{projects, secrets}; +use chrono::NaiveDateTime; +use diesel::prelude::*; + +#[derive(Debug, Queryable, Selectable)] +#[diesel(table_name = projects)] +#[diesel(check_for_backend(diesel::sqlite::Sqlite))] +pub struct Project { + pub id: String, + pub name: String, + pub created_at: NaiveDateTime, +} + +#[derive(Debug, Queryable, Selectable)] +#[diesel(table_name = secrets)] +#[diesel(check_for_backend(diesel::sqlite::Sqlite))] +pub struct Secret { + pub id: String, + pub name: String, + pub project_id: String, + pub config: String, + pub secret: Vec, + pub nonce: Vec, + pub created_at: NaiveDateTime, +} diff --git a/crates/cli/src/db/schema.rs b/crates/cli/src/db/schema.rs new file mode 100644 index 0000000..2a17d94 --- /dev/null +++ b/crates/cli/src/db/schema.rs @@ -0,0 +1,25 @@ +// @generated automatically by Diesel CLI. + +diesel::table! { + projects (id) { + id -> Text, + name -> Text, + created_at -> Timestamp, + } +} + +diesel::table! { + secrets (id) { + id -> Text, + name -> Text, + project_id -> Text, + config -> Text, + secret -> Binary, + nonce -> Binary, + created_at -> Timestamp, + } +} + +diesel::joinable!(secrets -> projects (project_id)); + +diesel::allow_tables_to_appear_in_same_query!(projects, secrets,); diff --git a/crates/cli/src/lib.rs b/crates/cli/src/lib.rs index 06a68a8..545624f 100644 --- a/crates/cli/src/lib.rs +++ b/crates/cli/src/lib.rs @@ -1,4 +1,7 @@ use clap::{Args, Parser, Subcommand}; +use keyring::Entry; +use std::error::Error; +mod db; #[derive(Parser)] #[clap( @@ -54,3 +57,39 @@ pub enum ConfigCommands { Create { project: String, name: String }, Delete { project: String, name: String }, } + +pub fn gen_or_get_key() -> Result> { + let entry = Entry::new("harbor", "encryption-key")?; + + match entry.get_password() { + Ok(hex_string) => { + let key_bytes = hex::decode(hex_string)?; + + if key_bytes.len() != 32 { + return Err("Retrieved key length is invalid (must be 32 bytes)".into()); + } + + Ok(key_bytes + .as_slice() + .try_into() + .map_err(|_| "Invalid key length")?) + } + + Err(err) => { + if is_key_not_found(&err) { + let key = crypto::helper::gen_key(); + + let hex_string = hex::encode(key.as_slice()); + entry.set_password(&hex_string)?; + + Ok(key) + } else { + Err(Box::new(err)) + } + } + } +} + +fn is_key_not_found(err: &keyring::Error) -> bool { + matches!(err, keyring::Error::NoEntry) +} diff --git a/crates/crypto/src/lib.rs b/crates/crypto/src/lib.rs index dd1cfc6..79394fe 100644 --- a/crates/crypto/src/lib.rs +++ b/crates/crypto/src/lib.rs @@ -1,11 +1,14 @@ use chacha20poly1305::{ - ChaCha20Poly1305, Key, Nonce, + ChaCha20Poly1305, aead::{Aead, Generate, KeyInit}, }; type Result = std::result::Result>; -mod helper { +pub type Key = chacha20poly1305::Key; +pub type Nonce = chacha20poly1305::Nonce; + +pub mod helper { use super::{Generate, Key, Nonce}; pub fn gen_nonce() -> Nonce { Nonce::generate() diff --git a/devenv.nix b/devenv.nix index d15304e..3a8a605 100644 --- a/devenv.nix +++ b/devenv.nix @@ -9,6 +9,7 @@ prettier nixfmt clippy + sqlite ]; languages.rust = { @@ -23,6 +24,8 @@ ]; }; + dotenv.enable = true; + enterTest = '' cargo test '';