diff --git a/.idea/trifid.iml b/.idea/trifid.iml index 1324adf..bef05f9 100644 --- a/.idea/trifid.iml +++ b/.idea/trifid.iml @@ -4,6 +4,7 @@ + diff --git a/Cargo.lock b/Cargo.lock index 93025dc..4d5d078 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2205,11 +2205,14 @@ dependencies = [ name = "trifid-api" version = "0.1.0" dependencies = [ + "aes-gcm", "base64 0.21.0", "chrono", "dotenvy", + "hex", "log", "paste", + "rand", "rocket", "serde", "sqlx", @@ -2221,6 +2224,10 @@ dependencies = [ "uuid", ] +[[package]] +name = "trifid-pki" +version = "0.1.0" + [[package]] name = "try-lock" version = "0.2.4" diff --git a/Cargo.toml b/Cargo.toml index 54ab1ef..1ad3cfa 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,6 @@ [workspace] members = [ "trifid-api", - "tfclient" + "tfclient", + "trifid-pki" ] \ No newline at end of file diff --git a/trifid-api/.cargo/config.toml b/trifid-api/.cargo/config.toml new file mode 100644 index 0000000..a7194eb --- /dev/null +++ b/trifid-api/.cargo/config.toml @@ -0,0 +1,2 @@ +[build] +rustc-wrapper = "sccache" \ No newline at end of file diff --git a/trifid-api/Cargo.toml b/trifid-api/Cargo.toml index 9f9ac6e..84d8cb0 100644 --- a/trifid-api/Cargo.toml +++ b/trifid-api/Cargo.toml @@ -19,4 +19,7 @@ totp-rs = { version = "4.2.0", features = ["qr", "otpauth", "gen_secret"]} uuid = { version = "1.3.0", features = ["v4", "fast-rng", "macro-diagnostics"]} url = { version = "2.3.1", features = ["serde"] } urlencoding = "2.1.2" -chrono = "0.4.23" \ No newline at end of file +chrono = "0.4.23" +aes-gcm = "0.10.1" +hex = "0.4.3" +rand = "0.8.5" \ No newline at end of file diff --git a/trifid-api/migrations/20230226020713_create_orgs_authorized_users.sql b/trifid-api/migrations/20230226020713_create_orgs_authorized_users.sql new file mode 100644 index 0000000..cf1b794 --- /dev/null +++ b/trifid-api/migrations/20230226020713_create_orgs_authorized_users.sql @@ -0,0 +1,7 @@ +CREATE TABLE organization_authorized_users ( + id SERIAL NOT NULL PRIMARY KEY, + user_id SERIAL NOT NULL REFERENCES users(id), + org_id SERIAL NOT NULL REFERENCES organizations(id) +); +CREATE INDEX idx_organization_authorized_users_user ON organization_authorized_users(user_id); +CREATE INDEX idx_organization_authorized_users_org ON organization_authorized_users(org_id); \ No newline at end of file diff --git a/trifid-api/src/crypto.rs b/trifid-api/src/crypto.rs new file mode 100644 index 0000000..193ddf0 --- /dev/null +++ b/trifid-api/src/crypto.rs @@ -0,0 +1,21 @@ +use std::error::Error; +use aes_gcm::{Aes256Gcm, KeyInit, Nonce}; +use aes_gcm::aead::{Aead, Payload}; +use crate::config::TFConfig; + +pub fn get_cipher_from_config(config: &TFConfig) -> Result> { + let key_slice = hex::decode(&config.data_key)?; + Ok(Aes256Gcm::new_from_slice(&key_slice)?) +} + +pub fn encrypt_with_nonce(plaintext: &[u8], nonce: [u8; 12], cipher: &Aes256Gcm) -> Result, aes_gcm::Error> { + let nonce = Nonce::from_slice(&nonce); + let ciphertext = cipher.encrypt(nonce, plaintext)?; + Ok(ciphertext) +} + +pub fn decrypt_with_nonce(ciphertext: &[u8], nonce: [u8; 12], cipher: &Aes256Gcm) -> Result, aes_gcm::Error> { + let nonce = Nonce::from_slice(&nonce); + let plaintext = cipher.decrypt(nonce, Payload::from(ciphertext))?; + Ok(plaintext) +} \ No newline at end of file diff --git a/trifid-api/src/main.rs b/trifid-api/src/main.rs index 2d7fa80..e7f143d 100644 --- a/trifid-api/src/main.rs +++ b/trifid-api/src/main.rs @@ -19,6 +19,8 @@ pub mod config; pub mod tokens; pub mod routes; pub mod auth; +pub mod crypto; +pub mod org; static MIGRATOR: Migrator = sqlx::migrate!(); @@ -118,7 +120,13 @@ async fn main() -> Result<(), Box> { crate::routes::v1::auth::check_session::options, crate::routes::v1::auth::check_session::options_auth, crate::routes::v2::whoami::whoami_request, - crate::routes::v2::whoami::options + crate::routes::v2::whoami::options, + + crate::routes::v1::organization::options, + crate::routes::v1::organization::orgidoptions, + crate::routes::v1::organization::orginfo_req, + crate::routes::v1::organization::orglist_req, + crate::routes::v1::organization::create_org ]) .register("/", catchers![ crate::routes::handler_400, diff --git a/trifid-api/src/org.rs b/trifid-api/src/org.rs new file mode 100644 index 0000000..de1dcb4 --- /dev/null +++ b/trifid-api/src/org.rs @@ -0,0 +1,50 @@ +use std::error::Error; +use rocket::form::validate::Contains; +use sqlx::PgPool; + +pub async fn get_org_by_owner_id(user: i32, db: &PgPool) -> Result, Box> { + Ok(match sqlx::query!("SELECT id FROM organizations WHERE owner = $1", user).fetch_optional(db).await? { + Some(r) => Some(r.id), + None => None + }) +} + +pub async fn get_orgs_by_assoc_id(user: i32, db: &PgPool) -> Result, Box> { + let res: Vec<_> = sqlx::query!("SELECT org_id FROM organization_authorized_users WHERE user_id = $1", user).fetch_all(db).await?; + + let mut ret = vec![]; + + for i in res { + ret.push(i.org_id); + } + + Ok(ret) +} + +pub async fn get_users_by_assoc_org(org: i32, db: &PgPool) -> Result, Box> { + let res: Vec<_> = sqlx::query!("SELECT user_id FROM organization_authorized_users WHERE org_id = $1", org).fetch_all(db).await?; + + let mut ret = vec![]; + + for i in res { + ret.push(i.org_id); + } + + Ok(ret) +} + +pub async fn get_associated_orgs(user: i32, db: &PgPool) -> Result, Box> { + let mut assoc_orgs = vec![]; + + if let Some(owned_org) = get_org_by_owner_id(user, db).await? { + assoc_orgs.push(owned_org); + } + + assoc_orgs.append(&mut get_orgs_by_assoc_id(user, db).await?); + + Ok(assoc_orgs) +} + +pub async fn user_has_org_assoc(user: i32, org: i32, db: &PgPool) -> Result> { + Ok(get_associated_orgs(user, db).await?.contains(org)) +} \ No newline at end of file diff --git a/trifid-api/src/routes/v1/mod.rs b/trifid-api/src/routes/v1/mod.rs index 6b8e56f..6ede35c 100644 --- a/trifid-api/src/routes/v1/mod.rs +++ b/trifid-api/src/routes/v1/mod.rs @@ -2,4 +2,5 @@ pub mod auth; pub mod signup; pub mod totp_authenticators; -pub mod verify_totp_authenticator; \ No newline at end of file +pub mod verify_totp_authenticator; +pub mod organization; \ No newline at end of file diff --git a/trifid-api/src/routes/v1/organization.rs b/trifid-api/src/routes/v1/organization.rs new file mode 100644 index 0000000..947e417 --- /dev/null +++ b/trifid-api/src/routes/v1/organization.rs @@ -0,0 +1,84 @@ +use std::slice::from_raw_parts_mut; +use rocket::{get, post, options, State}; +use rocket::http::{ContentType, Status}; +use rocket::serde::json::Json; +use serde::{Serialize, Deserialize}; +use sqlx::PgPool; +use crate::auth::TOTPAuthenticatedUserInfo; +use crate::config::TFConfig; +use crate::org::{get_associated_orgs, get_org_by_owner_id, get_users_by_assoc_org, user_has_org_assoc}; + +#[options("/v1/orgs")] +pub fn options() -> &'static str { + "" +} +#[options("/v1/org/<_id>")] +pub fn orgidoptions(_id: i32) -> &'static str { + "" +} + +#[derive(Serialize, Deserialize)] +#[serde(crate = "rocket::serde")] +pub struct OrglistStruct { + pub org_ids: Vec +} + + +#[get("/v1/orgs")] +pub async fn orglist_req(user: TOTPAuthenticatedUserInfo, db: &State) -> Result<(ContentType, Json), (Status, String)> { + // this endpoint lists the associated organizations this user has access to + let associated_orgs = match get_associated_orgs(user.user_id, db.inner()).await { + Ok(r) => r, + Err(e) => return Err((Status::InternalServerError, format!("{{\"errors\":[{{\"code\":\"{}\",\"message\":\"{} - {}\"}}]}}", "ERR_DB_QUERY_FAILED", "an error occurred while running the database query"))) + }; + + Ok((ContentType::JSON, Json(OrglistStruct { + org_ids: associated_orgs + }))) +} + +#[derive(Serialize, Deserialize)] +#[serde(crate = "rocket::serde")] +pub struct OrginfoStruct { + pub org_id: i32, + pub owner_id: i32, + pub ca_crt: String, + pub authorized_users: Vec +} + +#[get("/v1/org/")] +pub async fn orginfo_req(orgid: i32, user: TOTPAuthenticatedUserInfo, db: &State) -> Result<(ContentType, Json), (Status, String)> { + if !user_has_org_assoc(orgid, user.user_id, db.inner()).await.map_err(|e| (Status::InternalServerError, format!("{{\"errors\":[{{\"code\":\"{}\",\"message\":\"{} - {}\"}}]}}", "ERR_QL_QUERY_FAILED", "an error occurred while running the graphql query", e)))? { + return Err((Status::Unauthorized, format!("{{\"errors\":[{{\"code\":\"{}\",\"message\":\"{}\"}}]}}", "ERR_MISSING_ORG_AUTHORIZATION", "this user does not have permission to access this org"))); + } + + let org = sqlx::query!("SELECT id, owner, ca_crt FROM organizations WHERE id = $1", orgid).fetch_one(orgid).await.map_err(|e| (Status::InternalServerError, format!("{{\"errors\":[{{\"code\":\"{}\",\"message\":\"{} - {}\"}}]}}", "ERR_QL_QUERY_FAILED", "an error occurred while running the graphql query", e)))?; + let authorized_users = get_users_by_assoc_org(orgid, db.inner()).await.map_err(|e| (Status::InternalServerError, format!("{{\"errors\":[{{\"code\":\"{}\",\"message\":\"{} - {}\"}}]}}", "ERR_QL_QUERY_FAILED", "an error occurred while running the graphql query", e)))?; + + Ok((ContentType::JSON, Json( + OrginfoStruct { + org_id: orgid, + owner_id: org.owner, + ca_crt: org.ca_crt, + authorized_users, + } + ))) +} + +#[post("/v1/org")] +pub async fn create_org(user: TOTPAuthenticatedUserInfo, db: &State, config: &State) -> Result<(ContentType, Json), (Status, String)> { + if get_org_by_owner_id(user.user_id, db.inner()).await.map_err(|e| (Status::InternalServerError, format!("{{\"errors\":[{{\"code\":\"{}\",\"message\":\"{} - {}\"}}]}}", "ERR_QL_QUERY_FAILED", "an error occurred while running the graphql query", e)))?.is_some() { + return Err((Status::Conflict, format!("{{\"errors\":[{{\"code\":\"{}\",\"message\":\"{}\"}}]}}", "ERR_USER_OWNS_ORG", "a user can only own one organization at a time"))) + } + + // we need to generate a keypair and CA certificate for this new org + + Ok((ContentType::JSON, Json( + OrginfoStruct { + org_id: orgid, + owner_id: org.owner, + ca_crt: org.ca_crt, + authorized_users, + } + ))) +} \ No newline at end of file diff --git a/trifid-pki/Cargo.toml b/trifid-pki/Cargo.toml new file mode 100644 index 0000000..bfb2c5b --- /dev/null +++ b/trifid-pki/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "trifid-pki" +version = "0.1.0" +edition = "2021" +description = "A rust implementation of the Nebula PKI system" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] diff --git a/trifid-pki/src/lib.rs b/trifid-pki/src/lib.rs new file mode 100644 index 0000000..7d12d9a --- /dev/null +++ b/trifid-pki/src/lib.rs @@ -0,0 +1,14 @@ +pub fn add(left: usize, right: usize) -> usize { + left + right +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn it_works() { + let result = add(2, 2); + assert_eq!(result, 4); + } +}