diff --git a/.vscode/settings.json b/.vscode/settings.json index 3858614..39349ef 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -7,6 +7,7 @@ "Conn", "dotenv", "hmac", + "minisign", "oneshot", "openapi", "recv", diff --git a/Cargo.lock b/Cargo.lock index afaeb8a..acb6de4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -215,10 +215,12 @@ dependencies = [ "error-stack", "hmac", "ldap3", + "minisign", "once_cell", "rand", "rust-argon2", "serde", + "serde_json", "sha2", "sqlx", "strum", @@ -396,6 +398,16 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common", + "inout", +] + [[package]] name = "clap" version = "4.5.23" @@ -600,6 +612,12 @@ dependencies = [ "typenum", ] +[[package]] +name = "ct-codecs" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b916ba8ce9e4182696896f015e8a5ae6081b305f74690baa8465e35f5a142ea4" + [[package]] name = "der" version = "0.7.9" @@ -1264,6 +1282,15 @@ dependencies = [ "serde", ] +[[package]] +name = "inout" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0c10553d664a4d0bcff9f4215d0aac67a639cc68ef660840afe309b807bc9f5" +dependencies = [ + "generic-array", +] + [[package]] name = "is_terminal_polyfill" version = "1.70.1" @@ -1464,6 +1491,18 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" +[[package]] +name = "minisign" +version = "0.7.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26541387415a1e829df5d532aad019fb11bc723e2b5bc99edefa4cf5bfad0de7" +dependencies = [ + "ct-codecs", + "getrandom", + "rpassword", + "scrypt", +] + [[package]] name = "miniz_oxide" version = "0.8.2" @@ -1709,6 +1748,16 @@ version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3" +[[package]] +name = "pbkdf2" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2" +dependencies = [ + "digest", + "hmac", +] + [[package]] name = "pem" version = "3.0.4" @@ -1961,6 +2010,17 @@ dependencies = [ "serde_derive", ] +[[package]] +name = "rpassword" +version = "7.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80472be3c897911d0137b2d2b9055faf6eeac5b14e324073d83bc17b191d7e3f" +dependencies = [ + "libc", + "rtoolbox", + "windows-sys 0.48.0", +] + [[package]] name = "rsa" version = "0.9.7" @@ -1981,6 +2041,16 @@ dependencies = [ "zeroize", ] +[[package]] +name = "rtoolbox" +version = "0.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c247d24e63230cdb56463ae328478bd5eac8b8faa8c69461a77e8e323afac90e" +dependencies = [ + "libc", + "windows-sys 0.48.0", +] + [[package]] name = "rust-argon2" version = "2.1.0" @@ -2077,6 +2147,15 @@ version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" +[[package]] +name = "salsa20" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97a22f5af31f73a954c10289c93e8a50cc23d971e80ee446f1f6f7137a088213" +dependencies = [ + "cipher", +] + [[package]] name = "same-file" version = "1.0.6" @@ -2101,6 +2180,17 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "scrypt" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0516a385866c09368f0b5bcd1caff3366aace790fcd46e2bb032697bb172fd1f" +dependencies = [ + "pbkdf2", + "salsa20", + "sha2", +] + [[package]] name = "security-framework" version = "2.11.1" @@ -3053,8 +3143,9 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "utoipa" -version = "5.3.0" -source = "git+https://github.com/juhaku/utoipa#3ffad4bed73e5caeddc311bab70f810d1d772079" +version = "5.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "435c6f69ef38c9017b4b4eea965dfb91e71e53d869e896db40d1cf2441dd75c0" dependencies = [ "indexmap", "serde", @@ -3064,8 +3155,9 @@ dependencies = [ [[package]] name = "utoipa-axum" -version = "0.1.3" -source = "git+https://github.com/juhaku/utoipa#3ffad4bed73e5caeddc311bab70f810d1d772079" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff0d605008ed085986e1803fd5c81d18c0f8503b1e4bbb21ea75b3fc20dd1efb" dependencies = [ "axum", "paste", @@ -3076,8 +3168,9 @@ dependencies = [ [[package]] name = "utoipa-gen" -version = "5.3.0" -source = "git+https://github.com/juhaku/utoipa#3ffad4bed73e5caeddc311bab70f810d1d772079" +version = "5.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a77d306bc75294fd52f3e99b13ece67c02c1a2789190a6f31d32f736624326f7" dependencies = [ "proc-macro2", "quote", @@ -3087,8 +3180,9 @@ dependencies = [ [[package]] name = "utoipa-redoc" -version = "5.0.0" -source = "git+https://github.com/juhaku/utoipa#3ffad4bed73e5caeddc311bab70f810d1d772079" +version = "5.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33749b636458b2ed3e8ebc633febffb3e4ed7298d9f749e9b71cc25ea2f0eb9f" dependencies = [ "axum", "serde", @@ -3098,8 +3192,9 @@ dependencies = [ [[package]] name = "utoipa-scalar" -version = "0.2.0" -source = "git+https://github.com/juhaku/utoipa#3ffad4bed73e5caeddc311bab70f810d1d772079" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "088e93bf19f6bd06e0aacb02ca432b3c5a449c4aec2e4aa9fc333a667f2b2c55" dependencies = [ "axum", "serde", @@ -3109,8 +3204,9 @@ dependencies = [ [[package]] name = "utoipa-swagger-ui" -version = "8.1.0" -source = "git+https://github.com/juhaku/utoipa#3ffad4bed73e5caeddc311bab70f810d1d772079" +version = "8.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "040cad8bd8de63f3d028e08e5b39be49d68f8a646e99f4aea2e2d4d82c34b21f" dependencies = [ "axum", "base64 0.22.1", diff --git a/Cargo.toml b/Cargo.toml index a493c73..a721f0a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -26,6 +26,7 @@ config = "0.15.4" uuid = { version = "1.11.0", features = ["v4"] } sha2 = "0.10.8" hmac = "0.12.1" +minisign = "0.7.9" # axum-jwt-login = { path = "../axum-login-jwt" } axum-jwt-login = { version = "0.1.0", registry = "kellnr" } rust-argon2 = "2.1.0" @@ -38,27 +39,17 @@ windows-service = "0.7.0" axum = { version = "0.8.1", features = ["macros"] } strum = { version = "0.26", features = ["derive"] } -# utoipa = { version = "5.3.0", features = ["axum_extras"] } -utoipa = { git = "https://github.com/juhaku/utoipa", features = [ - "axum_extras", -] } -# utoipa-axum = "0.1.3" -utoipa-axum = { git = "https://github.com/juhaku/utoipa" } -# utoipa-swagger-ui = { version = "8.1.0", features = ["axum"] } -# utoipa-redoc = { version = "*", features = ["axum"] } -# utoipa-scalar = { version = "*", features = ["axum"] } -utoipa-swagger-ui = { git = "https://github.com/juhaku/utoipa", features = [ - "axum", -] } -utoipa-redoc = { git = "https://github.com/juhaku/utoipa", features = ["axum"] } -utoipa-scalar = { git = "https://github.com/juhaku/utoipa", features = [ - "axum", -] } +utoipa = { version = "5.3.1", features = ["axum_extras"] } +utoipa-axum = "0.1.4" +utoipa-swagger-ui = { version = "8.1.1", features = ["axum"] } +utoipa-redoc = { version = "*", features = ["axum"] } +utoipa-scalar = { version = "*", features = ["axum"] } ts-rs = { version = "10.1.0", features = ["chrono-impl"] } # Utilities # ======================================== serde = { version = "1.0.216", features = ["derive"] } +serde_json = "1.0.134" tokio = { version = "1.42.0", features = ["full"] } tokio-util = { version = "0.7.13", features = ["rt"] } once_cell = "1.20.2" diff --git a/src/api/description.rs b/src/api/description.rs index 28412e7..600fdbd 100644 --- a/src/api/description.rs +++ b/src/api/description.rs @@ -7,6 +7,7 @@ pub const AUTH_TAG: &str = "Authentication"; pub const USERS_TAG: &str = "Users"; pub const ORDER_TAG: &str = "order"; pub const API_KEY_TAG: &str = "API Keys"; +pub const SIGNATURE_TAG: &str = "Signature"; #[derive(OpenApi)] #[openapi( diff --git a/src/api/routes/mod.rs b/src/api/routes/mod.rs index e1675e8..89036f1 100644 --- a/src/api/routes/mod.rs +++ b/src/api/routes/mod.rs @@ -1,5 +1,6 @@ pub mod api_keys; pub mod auth; +mod signature; pub mod users; use api_keys::models::ApiKey as APIKey; @@ -27,8 +28,8 @@ macro_rules! login_required { macro_rules! permission_required { ($($perm:expr),+ $(,)?) => { axum_jwt_login::permission_required!( - User, - ApiBackend, + crate::api::routes::User, + crate::api::ApiBackend, crate::api::routes::APIKey, $($perm),+ ) @@ -42,6 +43,7 @@ pub fn create_routes(session: AuthBackendType) -> Router { .nest(API_BASE, auth::router()) .nest(API_BASE, users::router()) .nest(API_BASE, api_keys::router()) + .nest(API_BASE, signature::router()) // .nest( // "/api/order", // // order::router().route_layer(crate::login_required!(AuthenticationBackend)), diff --git a/src/api/routes/signature/mod.rs b/src/api/routes/signature/mod.rs new file mode 100644 index 0000000..5a95f7a --- /dev/null +++ b/src/api/routes/signature/mod.rs @@ -0,0 +1,275 @@ +use std::io::Cursor; + +use axum::{debug_handler, Json}; +use chrono::Utc; +use minisign::{KeyPair, PublicKeyBox, SecretKeyBox, SignatureBox}; +use models::{ + PublicKeyID, SignatureTest, SigningKeyRequest, SigningKeyStatus, VerificationRequest, +}; +use utoipa_axum::{router::OpenApiRouter, routes}; + +use crate::{ + api::{ + description::SIGNATURE_TAG, + routes::users::permissions::{Permission, PermissionDetail}, + }, + errors::ApiError, + permission_required, +}; + +use super::AuthBackendType; + +pub mod models; +pub mod sql; + +// expose the OpenAPI to parent module +pub fn router() -> OpenApiRouter { + let verify = OpenApiRouter::new().routes(routes!(verify)); + let write = OpenApiRouter::new() + .routes(routes!( + check_signing_key, + create_signing_key, + verify_signing_key + )) + .routes(routes!(sign)) + .route_layer(permission_required!(Permission::Write( + PermissionDetail::Signature + ))); + + OpenApiRouter::new().merge(write).merge(verify) +} + +#[debug_handler] +#[utoipa::path( + get, + path = "/signature/key", + summary = "Check for signing key", + description = "Check if the logged in user has configured a signing key.", + responses( + (status = OK, body = SigningKeyStatus, description = "Signature Status"), + ), + security( + ("user_auth" = ["write:signature",]), + ), + tag = SIGNATURE_TAG)] +pub async fn check_signing_key( + auth_session: AuthBackendType, +) -> Result, ApiError> { + let backend = auth_session.backend(); + let user = auth_session + .is_authenticated() + .ok_or(ApiError::AccessDenied)?; + + let private_key = sql::get_private_key(backend.pool(), &user.user_id).await?; + + Ok(Json(SigningKeyStatus { + configured: private_key.key.is_some(), + verified: None, + })) +} + +#[debug_handler] +#[utoipa::path( + post, + path = "/signature/key", + summary = "Create signing key", + description = "Create signing key for the currently logged in user.", + request_body(content = SigningKeyRequest, description = "Signing Key Request", content_type = "application/json"), + responses( + (status = OK, description = "Successfully created signing key"), + ), + security( + ("user_auth" = ["write:signature",]), + ), + tag = SIGNATURE_TAG)] +pub async fn create_signing_key( + auth_session: AuthBackendType, + Json(key_request): Json, +) -> Result<(), ApiError> { + let backend = auth_session.backend(); + let user = auth_session + .is_authenticated() + .ok_or(ApiError::AccessDenied)?; + + // create Key Pair + let KeyPair { pk, sk } = KeyPair::generate_encrypted_keypair(Some(key_request.secret))?; + let public_key = pk.to_box()?.to_string(); + let private_key = sk + .to_box(Some(&format!( + "{} {} - {}", + user.name, + user.surname, + Utc::now() + )))? // Optional comment about the key + .to_string(); + + // store keys in database + sql::update_private_key(backend.pool(), &user.user_id, private_key).await?; + sql::store_public_key(backend.pool(), &user.user_id, public_key).await?; + + Ok(()) +} + +#[debug_handler] +#[utoipa::path( + put, + path = "/signature/key", + summary = "Verify signing key", + description = "Verify signing key for the currently logged in user.", + request_body(content = SigningKeyRequest, description = "Signing Key Request", content_type = "application/json"), + responses( + (status = OK, body = SigningKeyStatus, description = "Signature Status"), + ), + security( + ("user_auth" = ["write:signature",]), + ), + tag = SIGNATURE_TAG)] +pub async fn verify_signing_key( + auth_session: AuthBackendType, + Json(key_request): Json, +) -> Result, ApiError> { + let backend = auth_session.backend(); + let user = auth_session + .is_authenticated() + .ok_or(ApiError::AccessDenied)?; + + // get private key + let status = match key_request + .to_private_key(backend.pool(), &user.user_id) + .await? + { + Some(_key) => SigningKeyStatus { + configured: true, + verified: Some(true), + }, + None => SigningKeyStatus { + configured: false, + verified: None, + }, + }; + + Ok(Json(status)) +} + +#[debug_handler] +#[utoipa::path( + post, + path = "/signature/sign", + summary = "Sign something with your signing key", + description = "Sign something as the currently logged in user.", + request_body(content = SigningKeyRequest, description = "Signing Key Request", content_type = "application/json"), + responses( + (status = OK, description = "Successfully signed"), + ), + security( + ("user_auth" = ["write:signature",]), + ), + tag = SIGNATURE_TAG)] +pub async fn sign( + auth_session: AuthBackendType, + Json(key_request): Json, +) -> Result, ApiError> { + let backend = auth_session.backend(); + let user = auth_session + .is_authenticated() + .ok_or(ApiError::AccessDenied)?; + + // get private key + let private_key = key_request + .to_private_key(backend.pool(), &user.user_id) + .await? + .ok_or(ApiError::AccessDenied)?; + + // get current public key + let public_key_id = sql::get_current_public_key(backend.pool(), &user.user_id) + .await? + .ok_or(ApiError::InternalError( + "No corresponding public key found".to_string(), + ))?; + + let data = SignatureTest { + value1: "blfjdljfa".to_string(), + value2: false, + value3: vec!["bladfa".to_string(), "noch bla".to_string()], + }; + + let data_reader = Cursor::new(data.calculate_hash().to_be_bytes()); + let signature_box = minisign::sign( + None, + &private_key, + data_reader, + None, + Some(&serde_json::to_string(&public_key_id)?), + )?; + + // We have a signature! Let's inspect it a little bit. + println!( + "Untrusted comment: [{}]", + signature_box.untrusted_comment().unwrap() + ); + println!( + "Trusted comment: [{}]", + signature_box.trusted_comment().unwrap() + ); + + // store signature + // Converting the signature box to a string in order to save it is easy. + let signature = signature_box.into_string(); + println!("BLA"); + println!("{signature}"); + + Ok(Json(VerificationRequest { + signature, + signed_from: "".to_string(), + })) +} + +#[debug_handler] +#[utoipa::path( + get, + path = "/signature/verify", + summary = "Verify a signature", + description = "Verify the signature.", + request_body(content = SigningKeyRequest, description = "Signing Key Request", content_type = "application/json"), + responses( + (status = OK, description = "Successfully signed"), + ), + tag = SIGNATURE_TAG)] +pub async fn verify( + auth_session: AuthBackendType, + Json(verification_request): Json, +) -> Result<(), ApiError> { + let backend = auth_session.backend(); + + // Now, let's verify the signature. + // Assuming we just loaded it into `signature_box_str`, get the box back. + let signature_box = SignatureBox::from_string(&verification_request.signature)?; + + // get public key id from signature + let public_key_id: PublicKeyID = serde_json::from_str(&signature_box.untrusted_comment()?)?; + + // Load the public key of the user that signed. + let public_key_str = sql::get_public_key_by_id(backend.pool(), public_key_id) + .await? + .key + .ok_or(ApiError::InternalError( + "No corresponding Public Key was found.".to_string(), + ))?; + let public_key_box = PublicKeyBox::from_string(&public_key_str)?; + let public_key = public_key_box.into_public_key()?; + + // And verify the data. + let data = SignatureTest { + value1: "blfjdljfa".to_string(), + value2: false, + value3: vec!["bladfa".to_string(), "noch bla".to_string()], + }; + let data_reader = Cursor::new(data.calculate_hash().to_be_bytes()); + let verified = minisign::verify(&public_key, &signature_box, data_reader, true, false, false); + match verified { + Ok(()) => println!("Success!"), + Err(_) => println!("Verification failed"), + }; + + Ok(()) +} diff --git a/src/api/routes/signature/models.rs b/src/api/routes/signature/models.rs new file mode 100644 index 0000000..a63ec95 --- /dev/null +++ b/src/api/routes/signature/models.rs @@ -0,0 +1,82 @@ +use std::{ + fmt::Display, + hash::{DefaultHasher, Hash, Hasher}, +}; + +use chrono::NaiveDateTime; +use minisign::{SecretKey, SecretKeyBox}; +use serde::{Deserialize, Serialize}; +use sqlx::PgPool; +use ts_rs::TS; +use utoipa::ToSchema; + +use crate::{api::routes::signature::sql, errors::ApiError}; + +#[derive(Debug, Clone, Serialize, Deserialize, TS, ToSchema)] +#[ts(export)] +pub struct SigningKeyStatus { + pub configured: bool, + pub verified: Option, +} + +pub struct KeyData { + pub key: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct PublicKeyID { + pub timestamp: NaiveDateTime, + pub user_id: String, +} + +#[derive(Debug, Clone, Deserialize, TS, ToSchema)] +#[ts(export)] +pub struct SigningKeyRequest { + pub secret: String, +} + +impl SigningKeyRequest { + pub async fn to_private_key( + self, + pool: &PgPool, + user_id: &str, + ) -> Result, ApiError> { + // try to get private key from database + let private_key = match sql::get_private_key(pool, user_id).await?.key { + Some(key) => key, + None => return Ok(None), + }; + + // create MinSign Box from key string + let private_key_box = SecretKeyBox::from_string(&private_key)?; + // and the box can be opened using the password to reveal the original secret key: + let secret_key = private_key_box.into_secret_key(Some(self.secret))?; + + Ok(Some(secret_key)) + } +} + +#[derive(Debug, Clone, Deserialize, Serialize, TS, ToSchema)] +#[ts(export)] +pub struct VerificationRequest { + pub signature: String, + pub signed_from: String, +} + +#[derive(Debug, Hash)] +pub struct SignatureTest { + pub value1: String, + pub value2: bool, + pub value3: Vec, +} + +impl SignatureTest +where + Self: Hash, +{ + pub fn calculate_hash(&self) -> u64 { + let mut s = DefaultHasher::new(); + self.hash(&mut s); + s.finish() + } +} diff --git a/src/api/routes/signature/sql.rs b/src/api/routes/signature/sql.rs new file mode 100644 index 0000000..e1ad838 --- /dev/null +++ b/src/api/routes/signature/sql.rs @@ -0,0 +1,93 @@ +use sqlx::PgPool; + +use crate::{ + api::routes::signature::models::{KeyData, PublicKeyID}, + errors::ApiError, +}; + +pub async fn get_private_key(pool: &PgPool, user_id: &str) -> Result { + Ok(sqlx::query_as!( + KeyData, + r#"SELECT + USERS."PrivateKey" as key + FROM + users + WHERE USERS."UserID" = $1"#, + user_id + ) + .fetch_one(pool) + .await?) +} + +pub async fn update_private_key( + pool: &PgPool, + user_id: &str, + private_key: String, +) -> Result<(), ApiError> { + sqlx::query!( + r#"UPDATE users SET + "PrivateKey" = $2 + WHERE "UserID" = $1"#, + user_id, + private_key + ) + .execute(pool) + .await?; + + Ok(()) +} + +pub async fn store_public_key( + pool: &PgPool, + user_id: &str, + public_key: String, +) -> Result<(), ApiError> { + sqlx::query!( + r#"INSERT INTO "PublicKeys" + ("UserID", "PublicKey") + VALUES ($1, $2)"#, + user_id, + public_key + ) + .execute(pool) + .await?; + + Ok(()) +} + +pub async fn get_current_public_key( + pool: &PgPool, + user_id: &str, +) -> Result, ApiError> { + Ok(sqlx::query_as!( + PublicKeyID, + r#"SELECT + "PublicKeys"."Timestamp" as timestamp, + "PublicKeys"."UserID" as user_id + FROM + "PublicKeys" + WHERE + "PublicKeys"."UserID" = $1 + ORDER BY timestamp DESC"#, + user_id, + ) + .fetch_optional(pool) + .await?) +} + +pub async fn get_public_key_by_id(pool: &PgPool, key_id: PublicKeyID) -> Result { + Ok(sqlx::query_as!( + KeyData, + r#"SELECT + "PublicKeys"."PublicKey" as key + FROM + "PublicKeys" + WHERE + "PublicKeys"."UserID" = $1 + AND "PublicKeys"."Timestamp" = $2"#, + key_id.user_id, + key_id.timestamp, + ) + .fetch_one(pool) + .await?) +} diff --git a/src/api/routes/users/permissions.rs b/src/api/routes/users/permissions.rs index dcaf5da..9012ed4 100644 --- a/src/api/routes/users/permissions.rs +++ b/src/api/routes/users/permissions.rs @@ -61,6 +61,7 @@ impl Display for Permission { pub enum PermissionDetail { Users, APIKeys, + Signature, #[default] None, } diff --git a/src/errors.rs b/src/errors.rs index cc8397b..7aa0666 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -6,6 +6,7 @@ use axum::{ Json, }; use error_stack::Context; +use minisign::PError; use serde::Serialize; use sha2::digest::InvalidLength; use tracing::error; @@ -110,6 +111,18 @@ impl From for ApiError { } } +impl From for ApiError { + fn from(value: PError) -> Self { + Self::InternalError(format!("MiniSign Error: {value}")) + } +} + +impl From for ApiError { + fn from(value: serde_json::Error) -> Self { + Self::InternalError(format!("Error on (de)serialization: {value}")) + } +} + impl Display for ApiError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let error_info = self.as_error_info().1; diff --git a/src/migrations/02_users.sql b/src/migrations/02_users.sql index cc01b3d..084e110 100644 --- a/src/migrations/02_users.sql +++ b/src/migrations/02_users.sql @@ -6,6 +6,7 @@ CREATE TABLE public.users "Surname" character varying(250) NOT NULL DEFAULT '', "Email" character varying(500) NOT NULL DEFAULT '', "Password" character varying(255) NOT NULL DEFAULT '', + "PrivateKey" text COLLATE, "CreationDate" timestamp without time zone NOT NULL DEFAULT NOW(), "LastChanged" timestamp without time zone NOT NULL DEFAULT NOW(), "StatusFlag" smallint NOT NULL, @@ -16,4 +17,7 @@ ALTER TABLE IF EXISTS public.users OWNER to postgres; COMMENT ON TABLE public.users - IS 'Table containing user information'; \ No newline at end of file + IS 'Table containing user information'; + +COMMENT ON COLUMN public.users."PrivateKey" + IS 'Private Key of the user with which the user can sign things'; \ No newline at end of file diff --git a/src/migrations/06_publc_keys.sql b/src/migrations/06_publc_keys.sql new file mode 100644 index 0000000..b18067f --- /dev/null +++ b/src/migrations/06_publc_keys.sql @@ -0,0 +1,19 @@ +CREATE TABLE IF NOT EXISTS public."PublicKeys" +( + "Timestamp" timestamp without time zone NOT NULL DEFAULT now(), + "UserID" character varying COLLATE pg_catalog."default" NOT NULL, + "PublicKey" text COLLATE pg_catalog."default" NOT NULL, + CONSTRAINT "PublicKeys_pkey" PRIMARY KEY ("Timestamp", "UserID"), + CONSTRAINT "PublicKeyUserID" FOREIGN KEY ("UserID") + REFERENCES public.users ("UserID") MATCH SIMPLE + ON UPDATE NO ACTION + ON DELETE NO ACTION +) + +TABLESPACE pg_default; + +ALTER TABLE IF EXISTS public."PublicKeys" + OWNER to postgres; + +COMMENT ON TABLE public."PublicKeys" + IS 'Public Key Storage for Signatures'; \ No newline at end of file