From 8c37fc1181e00deb004fccf9d8ad1bb5b559c7d1 Mon Sep 17 00:00:00 2001 From: Philippe Loctaux Date: Sun, 12 Mar 2023 18:45:55 +0100 Subject: [PATCH] database: added keys migration, get/insert, insert keys at launch if none are present --- .../migrations/20230312153840_keys.down.sql | 1 + .../migrations/20230312153840_keys.up.sql | 8 + crates/database/queries/keys/get_all.sql | 8 + .../database/queries/keys/get_all_revoked.sql | 9 + .../database/queries/keys/get_all_valid.sql | 9 + .../database/queries/keys/get_most_recent.sql | 10 + crates/database/queries/keys/insert.sql | 2 + crates/database/sqlx-data.json | 178 ++++++++++++++++++ crates/database/src/tables.rs | 2 + crates/database/src/tables/keys.rs | 66 +++++++ crates/ezidam/src/database.rs | 80 +++++++- crates/jwt/Cargo.toml | 2 + crates/jwt/src/database.rs | 76 ++++++++ crates/jwt/src/error.rs | 3 + crates/jwt/src/lib.rs | 1 + 15 files changed, 453 insertions(+), 2 deletions(-) create mode 100644 crates/database/migrations/20230312153840_keys.down.sql create mode 100644 crates/database/migrations/20230312153840_keys.up.sql create mode 100644 crates/database/queries/keys/get_all.sql create mode 100644 crates/database/queries/keys/get_all_revoked.sql create mode 100644 crates/database/queries/keys/get_all_valid.sql create mode 100644 crates/database/queries/keys/get_most_recent.sql create mode 100644 crates/database/queries/keys/insert.sql create mode 100644 crates/database/src/tables/keys.rs create mode 100644 crates/jwt/src/database.rs diff --git a/crates/database/migrations/20230312153840_keys.down.sql b/crates/database/migrations/20230312153840_keys.down.sql new file mode 100644 index 0000000..067ec35 --- /dev/null +++ b/crates/database/migrations/20230312153840_keys.down.sql @@ -0,0 +1 @@ +drop table if exists keys; diff --git a/crates/database/migrations/20230312153840_keys.up.sql b/crates/database/migrations/20230312153840_keys.up.sql new file mode 100644 index 0000000..8900993 --- /dev/null +++ b/crates/database/migrations/20230312153840_keys.up.sql @@ -0,0 +1,8 @@ +create table if not exists keys +( + id TEXT not null primary key, + created_at TEXT not null default CURRENT_TIMESTAMP, + revoked_at TEXT, + private_der BLOB not null, + public_der BLOB not null +); diff --git a/crates/database/queries/keys/get_all.sql b/crates/database/queries/keys/get_all.sql new file mode 100644 index 0000000..9cbb506 --- /dev/null +++ b/crates/database/queries/keys/get_all.sql @@ -0,0 +1,8 @@ +select id, + created_at as "created_at: DateTime", + revoked_at as "revoked_at: DateTime", + private_der, + public_der + +from keys +order by created_at desc diff --git a/crates/database/queries/keys/get_all_revoked.sql b/crates/database/queries/keys/get_all_revoked.sql new file mode 100644 index 0000000..95d08d3 --- /dev/null +++ b/crates/database/queries/keys/get_all_revoked.sql @@ -0,0 +1,9 @@ +select id, + created_at as "created_at: DateTime", + revoked_at as "revoked_at: DateTime", + private_der, + public_der + +from keys +where revoked_at is not null +order by created_at desc diff --git a/crates/database/queries/keys/get_all_valid.sql b/crates/database/queries/keys/get_all_valid.sql new file mode 100644 index 0000000..ac49af8 --- /dev/null +++ b/crates/database/queries/keys/get_all_valid.sql @@ -0,0 +1,9 @@ +select id, + created_at as "created_at: DateTime", + revoked_at as "revoked_at: DateTime", + private_der, + public_der + +from keys +where revoked_at is null +order by created_at desc diff --git a/crates/database/queries/keys/get_most_recent.sql b/crates/database/queries/keys/get_most_recent.sql new file mode 100644 index 0000000..a806881 --- /dev/null +++ b/crates/database/queries/keys/get_most_recent.sql @@ -0,0 +1,10 @@ +select id, + created_at as "created_at: DateTime", + revoked_at as "revoked_at: DateTime", + private_der, + public_der + +from keys +where revoked_at is null +order by created_at desc +limit 1 diff --git a/crates/database/queries/keys/insert.sql b/crates/database/queries/keys/insert.sql new file mode 100644 index 0000000..c244fea --- /dev/null +++ b/crates/database/queries/keys/insert.sql @@ -0,0 +1,2 @@ +insert into keys (id, private_der, public_der) +values (?, ?, ?) diff --git a/crates/database/sqlx-data.json b/crates/database/sqlx-data.json index e0fb04f..b196860 100644 --- a/crates/database/sqlx-data.json +++ b/crates/database/sqlx-data.json @@ -30,6 +30,90 @@ }, "query": "insert into users (id, is_admin, username, password)\nvalues (?, ?, ?, ?)\n" }, + "56a9c0dff010858189a95087d014c7d0ce930da5d841b9d788a9c0e84b580bc6": { + "describe": { + "columns": [ + { + "name": "id", + "ordinal": 0, + "type_info": "Text" + }, + { + "name": "created_at: DateTime", + "ordinal": 1, + "type_info": "Text" + }, + { + "name": "revoked_at: DateTime", + "ordinal": 2, + "type_info": "Text" + }, + { + "name": "private_der", + "ordinal": 3, + "type_info": "Blob" + }, + { + "name": "public_der", + "ordinal": 4, + "type_info": "Blob" + } + ], + "nullable": [ + false, + false, + true, + false, + false + ], + "parameters": { + "Right": 0 + } + }, + "query": "select id,\n created_at as \"created_at: DateTime\",\n revoked_at as \"revoked_at: DateTime\",\n private_der,\n public_der\n\nfrom keys\norder by created_at desc\n" + }, + "5f946348ad62389fab3c97a1563d1592cbc5180abbba6d5abd44326bf0862669": { + "describe": { + "columns": [ + { + "name": "id", + "ordinal": 0, + "type_info": "Text" + }, + { + "name": "created_at: DateTime", + "ordinal": 1, + "type_info": "Text" + }, + { + "name": "revoked_at: DateTime", + "ordinal": 2, + "type_info": "Text" + }, + { + "name": "private_der", + "ordinal": 3, + "type_info": "Blob" + }, + { + "name": "public_der", + "ordinal": 4, + "type_info": "Blob" + } + ], + "nullable": [ + false, + false, + true, + false, + false + ], + "parameters": { + "Right": 0 + } + }, + "query": "select id,\n created_at as \"created_at: DateTime\",\n revoked_at as \"revoked_at: DateTime\",\n private_der,\n public_der\n\nfrom keys\nwhere revoked_at is not null\norder by created_at desc\n" + }, "62c75412f673f6a293b0d188d79c50676ec21cf94e2e50e18f9279c91e6b85c8": { "describe": { "columns": [], @@ -166,6 +250,48 @@ }, "query": "select id,\n created_at as \"created_at: DateTime\",\n updated_at as \"updated_at: DateTime\",\n is_admin as \"is_admin: bool\",\n username,\n name,\n email,\n password,\n password_recover,\n paper_key,\n is_archived as \"is_archived: bool\"\nfrom users\n\nwhere email is (?)\n" }, + "6e1431ff2b4f589daaa7b221c1bc2a08ee378949fb27988531210ee75fc88298": { + "describe": { + "columns": [ + { + "name": "id", + "ordinal": 0, + "type_info": "Text" + }, + { + "name": "created_at: DateTime", + "ordinal": 1, + "type_info": "Text" + }, + { + "name": "revoked_at: DateTime", + "ordinal": 2, + "type_info": "Text" + }, + { + "name": "private_der", + "ordinal": 3, + "type_info": "Blob" + }, + { + "name": "public_der", + "ordinal": 4, + "type_info": "Blob" + } + ], + "nullable": [ + false, + false, + true, + false, + false + ], + "parameters": { + "Right": 0 + } + }, + "query": "select id,\n created_at as \"created_at: DateTime\",\n revoked_at as \"revoked_at: DateTime\",\n private_der,\n public_der\n\nfrom keys\nwhere revoked_at is null\norder by created_at desc\nlimit 1\n" + }, "87906834faa6f185aee0e4d893b9754908b1c173e9dce383663d723891a89cd1": { "describe": { "columns": [], @@ -342,6 +468,48 @@ }, "query": "select u.id,\n u.created_at as \"created_at: DateTime\",\n u.updated_at as \"updated_at: DateTime\",\n u.is_admin as \"is_admin: bool\",\n u.username,\n u.name,\n u.email,\n u.password,\n u.password_recover,\n u.paper_key,\n u.is_archived as \"is_archived: bool\"\nfrom users u\n\n inner join settings s on u.id = s.first_admin\n\nwhere u.is_admin is 1\n and u.is_archived is 0\n and u.id is s.first_admin\n\nlimit 1" }, + "d166553746afb2d3eaa1ddcb9986b7b9723258f4051bce8287038e3dd1ac928a": { + "describe": { + "columns": [ + { + "name": "id", + "ordinal": 0, + "type_info": "Text" + }, + { + "name": "created_at: DateTime", + "ordinal": 1, + "type_info": "Text" + }, + { + "name": "revoked_at: DateTime", + "ordinal": 2, + "type_info": "Text" + }, + { + "name": "private_der", + "ordinal": 3, + "type_info": "Blob" + }, + { + "name": "public_der", + "ordinal": 4, + "type_info": "Blob" + } + ], + "nullable": [ + false, + false, + true, + false, + false + ], + "parameters": { + "Right": 0 + } + }, + "query": "select id,\n created_at as \"created_at: DateTime\",\n revoked_at as \"revoked_at: DateTime\",\n private_der,\n public_der\n\nfrom keys\nwhere revoked_at is null\norder by created_at desc\n" + }, "f4edf4567542eaead2e0db14b0d4197c5d3c1bc02da1897b571bf63bfcb4526a": { "describe": { "columns": [ @@ -419,5 +587,15 @@ } }, "query": "select id,\n created_at as \"created_at: DateTime\",\n updated_at as \"updated_at: DateTime\",\n is_admin as \"is_admin: bool\",\n username,\n name,\n email,\n password,\n password_recover,\n paper_key,\n is_archived as \"is_archived: bool\"\nfrom users\n\nwhere id is (?)\n" + }, + "f705411720bd037562f7e3622832262ac4c0a8fc0921fbd934d2b98146d3f413": { + "describe": { + "columns": [], + "nullable": [], + "parameters": { + "Right": 3 + } + }, + "query": "insert into keys (id, private_der, public_der)\nvalues (?, ?, ?)\n" } } \ No newline at end of file diff --git a/crates/database/src/tables.rs b/crates/database/src/tables.rs index 8458c40..dcfaaf0 100644 --- a/crates/database/src/tables.rs +++ b/crates/database/src/tables.rs @@ -1,5 +1,7 @@ +mod keys; mod settings; mod users; +pub use keys::Keys; pub use settings::Settings; pub use users::Users; diff --git a/crates/database/src/tables/keys.rs b/crates/database/src/tables/keys.rs new file mode 100644 index 0000000..fe664fa --- /dev/null +++ b/crates/database/src/tables/keys.rs @@ -0,0 +1,66 @@ +use crate::error::{handle_error, Error}; +use sqlx::sqlite::SqliteQueryResult; +use sqlx::types::chrono::{DateTime, Utc}; +use sqlx::{FromRow, SqliteExecutor}; + +#[derive(FromRow)] +pub struct Keys { + pub id: String, + pub created_at: DateTime, + pub revoked_at: Option>, + pub private_der: Vec, + pub public_der: Vec, +} + +impl Keys { + pub async fn insert( + conn: impl SqliteExecutor<'_>, + id: &str, + private_der: &[u8], + public_der: &[u8], + ) -> Result, Error> { + let query: SqliteQueryResult = + sqlx::query_file!("queries/keys/insert.sql", id, private_der, public_der) + .execute(conn) + .await + .map_err(handle_error)?; + + Ok((query.rows_affected() == 1).then_some(())) + } + + pub async fn get_most_recent(conn: impl SqliteExecutor<'_>) -> Result, Error> { + sqlx::query_file_as!(Self, "queries/keys/get_most_recent.sql") + .fetch_optional(conn) + .await + .map_err(handle_error) + } + + pub async fn get_all( + conn: impl SqliteExecutor<'_>, + filter_get_revoked: Option, + ) -> Result, Error> { + match filter_get_revoked { + Some(true) => { + // Get all revoked keys + sqlx::query_file_as!(Self, "queries/keys/get_all_revoked.sql") + .fetch_all(conn) + .await + .map_err(handle_error) + } + Some(false) => { + // Get all valid keys + sqlx::query_file_as!(Self, "queries/keys/get_all_valid.sql") + .fetch_all(conn) + .await + .map_err(handle_error) + } + None => { + // Get all keys + sqlx::query_file_as!(Self, "queries/keys/get_all.sql") + .fetch_all(conn) + .await + .map_err(handle_error) + } + } + } +} diff --git a/crates/ezidam/src/database.rs b/crates/ezidam/src/database.rs index b2d8272..e15eac8 100644 --- a/crates/ezidam/src/database.rs +++ b/crates/ezidam/src/database.rs @@ -1,5 +1,6 @@ use database_pool::run_migrations; use rocket::fairing::AdHoc; +use rocket::tokio::task; use rocket::{error, fairing, info, Build, Rocket}; use rocket_db_pools::{sqlx, Database as RocketDatabase}; use settings::Settings; @@ -42,12 +43,87 @@ impl Database { } else { info!("Found existing settings in database"); } - Ok(rocket) } Err(e) => { error!("Failed to interact with settings: {}", e); - Err(rocket) + return Err(rocket); } } + + // Make sure at least one key is available on startup + match jwt::database::Key::get_most_recent(&db.0).await { + Ok(most_recent) => match most_recent { + Some(most_recent) => { + info!( + "Most recent key: {}\t{}", + most_recent.key_id(), + most_recent.created_at() + ); + } + None => { + info!("No valid keys are present. Starting generation..."); + + // Generate key id + let key_id = match task::spawn_blocking(id::KeyID::default).await { + Ok(key_id) => { + info!("Generated KeyID {}", key_id); + key_id + } + Err(e) => { + error!("Failed to run KeyID generation: {}", e); + return Err(rocket); + } + }; + + // Generate keys + info!("Starting key generation. This should not be long."); + let key_id_for_generation = key_id.clone(); + let new_keys = match task::spawn_blocking(move || { + jwt::generate(&key_id_for_generation) + }) + .await + { + Ok(res) => match res { + Ok(keys) => { + info!("Generated public and private key! Starting to save in database."); + keys + } + Err(e) => { + error!("Failed to generate keys: {}", e); + return Err(rocket); + } + }, + Err(e) => { + error!("Failed to run key generation: {}", e); + return Err(rocket); + } + }; + + // Insert keys in database + match jwt::database::save_new_keys(&db.0, &key_id, &new_keys.0, &new_keys.1) + .await + { + Ok(Some(())) => { + info!("Saved keys with id {}", key_id); + } + Ok(None) => { + error!("Keys got generated, but they were not saved in database"); + return Err(rocket); + } + Err(e) => { + error!("Failed to save keys in database: {}", e); + return Err(rocket); + } + } + } + }, + Err(e) => { + error!("Failed to interact with keys: {}", e); + return Err(rocket); + } + } + + info!("Ready to launch!"); + Ok(rocket) } } diff --git a/crates/jwt/Cargo.toml b/crates/jwt/Cargo.toml index 1a2b7d4..3dd7357 100644 --- a/crates/jwt/Cargo.toml +++ b/crates/jwt/Cargo.toml @@ -10,6 +10,8 @@ serde = { workspace = true } serde_json = { workspace = true } rand = "0.8.5" rsa = "0.7.2" +chrono = { workspace = true } # local crates id = { path = "../id" } +database = { path = "../database" } diff --git a/crates/jwt/src/database.rs b/crates/jwt/src/database.rs new file mode 100644 index 0000000..c678964 --- /dev/null +++ b/crates/jwt/src/database.rs @@ -0,0 +1,76 @@ +use crate::{Error, PrivateKey, PublicKey}; +use chrono::{DateTime, Utc}; +use database::sqlx::SqliteExecutor; +use database::Keys as DatabaseKeys; +use id::KeyID; + +pub async fn save_new_keys( + conn: impl SqliteExecutor<'_>, + id: &KeyID, + private: &PrivateKey, + public: &PublicKey, +) -> Result, Error> { + Ok(DatabaseKeys::insert( + conn, + &id.0, + private.to_der()?.as_slice(), + public.to_der()?.as_slice(), + ) + .await?) +} + +#[derive(Debug)] +pub struct Key { + id: KeyID, + created_at: DateTime, + revoked_at: Option>, + private_der: Vec, + public_der: Vec, +} + +impl Key { + pub fn key_id(&self) -> &KeyID { + &self.id + } + + pub fn created_at(&self) -> DateTime { + self.created_at + } + + pub fn private_der(&self) -> &[u8] { + &self.private_der + } + + pub fn public_der(&self) -> &[u8] { + &self.public_der + } +} + +impl From for Key { + fn from(db: DatabaseKeys) -> Self { + Self { + id: KeyID(db.id), + created_at: db.created_at, + revoked_at: db.revoked_at, + private_der: db.private_der, + public_der: db.public_der, + } + } +} + +impl Key { + pub async fn get_most_recent(conn: impl SqliteExecutor<'_>) -> Result, Error> { + Ok(DatabaseKeys::get_most_recent(conn).await?.map(Self::from)) + } + + pub async fn get_all( + conn: impl SqliteExecutor<'_>, + filter_get_revoked: Option, + ) -> Result, Error> { + Ok(DatabaseKeys::get_all(conn, filter_get_revoked) + .await? + .into_iter() + .map(Self::from) + .collect::>()) + } +} diff --git a/crates/jwt/src/error.rs b/crates/jwt/src/error.rs index ef1bf5f..32a2aea 100644 --- a/crates/jwt/src/error.rs +++ b/crates/jwt/src/error.rs @@ -3,6 +3,9 @@ // the rest #[derive(Debug)] pub enum Error { + #[error("Database: {0}")] + Database(#[from] database::Error), + #[error("Failed to generate key: `{0}`")] Generation(#[from] rsa::errors::Error), diff --git a/crates/jwt/src/lib.rs b/crates/jwt/src/lib.rs index b6241fa..6d9dd9c 100644 --- a/crates/jwt/src/lib.rs +++ b/crates/jwt/src/lib.rs @@ -1,5 +1,6 @@ extern crate core; +pub mod database; mod error; mod jwk; mod key;