diff options
Diffstat (limited to '')
| -rw-r--r-- | crates/secd/src/lib.rs | 170 |
1 files changed, 81 insertions, 89 deletions
diff --git a/crates/secd/src/lib.rs b/crates/secd/src/lib.rs index 54759a5..eb5d33d 100644 --- a/crates/secd/src/lib.rs +++ b/crates/secd/src/lib.rs @@ -12,37 +12,26 @@ use client::{ Store, StoreError, }, }; +use config::Config; use email_address::EmailAddress; -use lettre::message::Mailbox; use log::{error, info, warn}; use serde::{Deserialize, Serialize}; -use serde_with::serde_as; -use std::{ - env::{set_var, var}, - str::FromStr, - sync::Arc, +use serde_with::{ + base64::{Base64, UrlSafe}, + formats::Unpadded, + serde_as, }; +use std::{env::var, fs::read_to_string, str::FromStr, sync::Arc}; use strum_macros::{Display, EnumString, EnumVariantNames}; use time::OffsetDateTime; use util::crypter::{Crypter, CrypterError}; use uuid::Uuid; -pub const ENV_AUTH_STORE_CONN_STRING: &str = "SECD_AUTH_STORE_CONN_STRING"; -pub const ENV_CRYPTER_SECRET_KEY: &str = "SECD_CRYPTER_SECRET_KEY"; -pub const ENV_EMAIL_ADDRESS_FROM: &str = "SECD_EMAIL_ADDRESS_FROM"; -pub const ENV_EMAIL_ADDRESS_REPLYTO: &str = "SECD_EMAIL_ADDRESS_REPLYTO"; -pub const ENV_EMAIL_MESSENGER: &str = "SECD_EMAIL_MESSENGER"; -pub const ENV_EMAIL_MESSENGER_CLIENT_ID: &str = "SECD_EMAIL_MESSENGER_CLIENT_ID"; -pub const ENV_EMAIL_MESSENGER_CLIENT_SECRET: &str = "SECD_EMAIL_MESSENGER_CLIENT_SECRET"; -pub const ENV_EMAIL_SENDGRID_API_KEY: &str = "SECD_EMAIL_SENDGRID_API_KEY"; -pub const ENV_EMAIL_SIGNIN_MESSAGE: &str = "SECD_EMAIL_SIGNIN_MESSAGE"; -pub const ENV_EMAIL_SIGNUP_MESSAGE: &str = "SECD_EMAIL_SIGNUP_MESSAGE"; -pub const ENV_SPICE_SECRET: &str = "SECD_SPICE_SECRET"; -pub const ENV_SPICE_SERVER: &str = "SECD_SPICE_SERVER"; - const ADDRESSS_VALIDATION_CODE_SIZE: u8 = 6; const ADDRESS_VALIDATION_ALLOWS_ATTEMPTS: u8 = 5; const ADDRESS_VALIDATION_IDENTITY_SURJECTION: bool = true; +const API_TOKEN_SIZE_BYTES: usize = 64; +const CREDENTIAL_PUBLIC_PART_BYTES: usize = 16; const CRYPTER_SECRET_KEY_DEFAULT: &str = "sup3rs3cr3t"; const EMAIL_VALIDATION_DURATION: i64 = 60 /* seconds*/ * 15 /* minutes */; const SESSION_DURATION: i64 = 60 /* seconds*/ * 60 /* minutes */ * 24 /* hours */ * 360 /* days */; @@ -55,7 +44,6 @@ pub type IdentityId = Uuid; pub type MotifId = Uuid; pub type PhoneNumber = String; pub type RefId = Uuid; -pub type SessionToken = String; #[derive(Debug, derive_more::Display, thiserror::Error)] pub enum SecdError { @@ -67,11 +55,14 @@ pub enum SecdError { AddressValidationExpiredOrConsumed, CredentialAlreadyExists, + InvalidCredential, + CredentialIsNotApiToken, CrypterError(#[from] CrypterError), TooManyIdentities, IdentityNotFound, + IdentityAlreadyExists, EmailMessengerError(#[from] EmailMessengerError), InvalidEmaillAddress(#[from] email_address::Error), @@ -79,10 +70,13 @@ pub enum SecdError { FailedToProvideSessionIdentity(String), InvalidSession, + ParseAssetFileError(String), + StoreError(#[from] StoreError), StoreInitFailure(String), FailedToDecodeInput(#[from] hex::FromHexError), + DecodeError(String), AuthorizationNotSupported(String), SpiceClient(#[from] SpiceError), @@ -97,11 +91,20 @@ pub struct Secd { cfg: Cfg, } +#[derive(Debug, Deserialize)] struct Cfg { - email_address_from: Option<Mailbox>, - email_address_replyto: Option<Mailbox>, - email_signup_message: Option<String>, + auth_store_conn: String, + crypter_secret_key: Option<String>, + email_address_from: Option<String>, + email_address_replyto: Option<String>, + email_messenger: Option<String>, + email_sendgrid_api_key: Option<String>, + email_signin_message_asset_loc: Option<String>, + email_signup_message_asset_loc: Option<String>, email_signin_message: Option<String>, + email_signup_message: Option<String>, + spice_secret: String, + spice_server: String, } #[async_trait] @@ -191,9 +194,9 @@ pub struct AddressValidation { #[serde(with = "time::serde::timestamp::option")] pub validated_at: Option<OffsetDateTime>, pub attempts: i32, - #[serde_as(as = "serde_with::hex::Hex")] + #[serde_as(as = "Base64<UrlSafe, Unpadded>")] hashed_token: Vec<u8>, - #[serde_as(as = "serde_with::hex::Hex")] + #[serde_as(as = "Base64<UrlSafe, Unpadded>")] hashed_code: Vec<u8>, } @@ -225,38 +228,19 @@ pub struct Credential { } #[serde_as] -#[derive(Debug, Display, Serialize, Deserialize, EnumString)] +#[derive(Clone, Debug, Display, Serialize, Deserialize, EnumString)] pub enum CredentialType { + ApiToken { public: String, private: String }, Passphrase { key: String, value: String }, - Oidc { value: String }, - OneTimeCodes { codes: Vec<String> }, - // Totp { - // #[serde_as(as = "DisplayFromStr")] - // url: Url, - // code: String, - // }, - WebAuthn { value: String }, -} - -struct SecuredCredential { - pub id: CredentialId, - pub identity_id: IdentityId, - pub t: CredentialType, - pub created_at: OffsetDateTime, - pub revoked_at: Option<OffsetDateTime>, - pub deleted_at: Option<OffsetDateTime>, -} - -enum SecuredCredentialType { - Passphrase { key: String, value: String }, - Oidc { value: String }, - OneTimeCodes { codes: Vec<String> }, + Session { key: String, secret: String }, + // Oidc { key: String, value: String }, + // OneTimeCodes { key: String, codes: Vec<String> }, // Totp { // #[serde_as(as = "DisplayFromStr")] // url: Url, // code: String, // }, - WebAuthn { value: String }, + // WebAuthn { value: String }, } #[serde_with::skip_serializing_none] @@ -265,6 +249,8 @@ pub struct Identity { pub id: IdentityId, pub address_validations: Vec<AddressValidation>, pub credentials: Vec<Credential>, + #[serde(skip_serializing_if = "Vec::is_empty")] + pub new_credentials: Vec<Credential>, pub rules: Vec<String>, // TODO: rules for (e.g. mfa reqs) pub metadata: Option<String>, #[serde(with = "time::serde::timestamp")] @@ -273,20 +259,22 @@ pub struct Identity { pub deleted_at: Option<OffsetDateTime>, } -#[serde_as] -#[serde_with::skip_serializing_none] -#[derive(Debug, Serialize)] -pub struct Session { - pub identity_id: IdentityId, - #[serde_as(as = "serde_with::hex::Hex")] - #[serde(skip_serializing_if = "Vec::is_empty")] - pub token: Vec<u8>, - #[serde(with = "time::serde::timestamp")] - pub created_at: OffsetDateTime, - #[serde(with = "time::serde::timestamp")] - pub expired_at: OffsetDateTime, - #[serde(with = "time::serde::timestamp::option")] - pub revoked_at: Option<OffsetDateTime>, +impl Cfg { + fn resolve(&mut self) -> Result<(), SecdError> { + if let Some(path) = &self.email_signin_message_asset_loc { + self.email_signin_message = Some(read_to_string(path).map_err(|err| { + SecdError::ParseAssetFileError(format!("Email Sign In Asset [{}]: {}", path, err)) + })?); + } + + if let Some(path) = &self.email_signup_message_asset_loc { + self.email_signup_message = Some(read_to_string(path).map_err(|err| { + SecdError::ParseAssetFileError(format!("Email Sign In Asset [{}]: {}", path, err)) + })?); + } + + Ok(()) + } } impl Secd { @@ -294,16 +282,28 @@ impl Secd { /// /// Initialize SecD with the specified configuration, established the necessary /// constraints, persistance stores, and options. - pub async fn init(z_schema: Option<&str>) -> Result<Self, SecdError> { - let auth_store = AuthStore::from(var(ENV_AUTH_STORE_CONN_STRING).ok()); + pub async fn init(cfg_path: Option<&str>, z_schema: Option<&str>) -> Result<Self, SecdError> { + let mut cfg: Cfg = Config::builder() + .add_source(config::File::with_name(cfg_path.unwrap_or( + &var("SECD_CONFIG_PATH").expect("coud not read SECD_CONFIG_PATH from environment"), + ))) + .build() + .unwrap() + .try_deserialize() + .expect("failed to retrieve secd config.toml"); + + cfg.resolve()?; + + let auth_store = AuthStore::from(Some(cfg.auth_store_conn.clone())); let email_messenger = AuthEmailMessenger::from_str( - &var(ENV_EMAIL_MESSENGER).unwrap_or(AuthEmailMessenger::Local.to_string()), + &cfg.email_messenger + .clone() + .unwrap_or(AuthEmailMessenger::Local.to_string()), ) .expect("unreachable f4ad0f48-0812-427f-b477-0f9c67bb69c5"); - let crypter_secret_key = var(ENV_CRYPTER_SECRET_KEY).unwrap_or_else(|_| { + let crypter_secret_key = cfg.crypter_secret_key.clone().unwrap_or_else(|| { warn!( - "NO CRYPTER KEY PROVIDED, USING DEFAULT KEY. DO NOT USE THIS KEY IN PRODUCTION. PROVIDE A UNIQUE SECRET KEY BY SETTING THE ENVIORNMENT VARIABLE {}. THE DEFAULT KEY IS: {}", - ENV_CRYPTER_SECRET_KEY, + "NO CRYPTER KEY PROVIDED, USING DEFAULT KEY. DO NOT USE THIS KEY IN PRODUCTION. PROVIDE A UNIQUE SECRET KEY BY SETTING THE CONFIGURATION KEY. THE DEFAULT KEY IS: {}", CRYPTER_SECRET_KEY_DEFAULT, ); CRYPTER_SECRET_KEY_DEFAULT.to_string() @@ -312,17 +312,6 @@ impl Secd { info!("starting client with auth_store: {:?}", auth_store); info!("starting client with email_messenger: {:?}", auth_store); - let cfg = Cfg { - email_address_from: var(ENV_EMAIL_ADDRESS_FROM) - .ok() - .and_then(|s| s.parse().ok()), - email_address_replyto: var(ENV_EMAIL_ADDRESS_REPLYTO) - .ok() - .and_then(|s| s.parse().ok()), - email_signup_message: var(ENV_EMAIL_SIGNUP_MESSAGE).ok(), - email_signin_message: var(ENV_EMAIL_SIGNIN_MESSAGE).ok(), - }; - let store = match auth_store { AuthStore::Sqlite { conn } => { if z_schema.is_some() { @@ -331,7 +320,7 @@ impl Secd { )); } - SqliteClient::new( + SqliteClient::new_ref( sqlx::sqlite::SqlitePoolOptions::new() .connect(&conn) .await @@ -342,7 +331,7 @@ impl Secd { .await } AuthStore::Postgres { conn } => { - PgClient::new( + PgClient::new_ref( sqlx::postgres::PgPoolOptions::new() .connect(&conn) .await @@ -352,7 +341,7 @@ impl Secd { ) .await } - rest @ _ => { + rest => { error!( "requested an AuthStore which has not yet been implemented: {:?}", rest @@ -362,19 +351,22 @@ impl Secd { }; let email_sender = match email_messenger { - AuthEmailMessenger::Local => LocalMailer::new(), - AuthEmailMessenger::Sendgrid => Sendgrid::new( - var(ENV_EMAIL_SENDGRID_API_KEY).expect("No SENDGRID_API_KEY provided"), + AuthEmailMessenger::Local => LocalMailer::new_ref(), + AuthEmailMessenger::Sendgrid => Sendgrid::new_ref( + cfg.email_sendgrid_api_key + .clone() + .expect("No SENDGRID_API_KEY provided"), ), _ => unimplemented!(), }; let spice = match z_schema { Some(schema) => { - let c: Arc<Spice> = Arc::new(Spice::new().await); + let c: Arc<Spice> = + Arc::new(Spice::new(cfg.spice_secret.clone(), cfg.spice_server.clone()).await); c.write_schema(schema) .await - .expect("failed to write authorization schema".into()); + .unwrap_or_else(|_| panic!("{}", "failed to write authorization schema")); Some(c) } None => None, |
