diff options
25 files changed, 1029 insertions, 740 deletions
@@ -1,2 +1,3 @@ target/ -node_modules/
\ No newline at end of file +node_modules/ +config.*toml
\ No newline at end of file @@ -74,9 +74,9 @@ checksum = "224afbd727c3d6e4b90103ece64b8d1b67fbb1973b1046c2281eed3f3803f800" [[package]] name = "argon2" -version = "0.4.1" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db4ce4441f99dbd377ca8a8f57b698c44d0d6e712d8329b5040da5a64aa1ce73" +checksum = "95c2fcf79ad1932ac6269a738109997a83c227c09b75842ae564dc8ede6a861c" dependencies = [ "base64ct", "blake2", @@ -480,6 +480,25 @@ dependencies = [ ] [[package]] +name = "config" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d379af7f68bfc21714c6c7dea883544201741d2ce8274bb12fa54f89507f52a7" +dependencies = [ + "async-trait", + "json5", + "lazy_static", + "nom", + "pathdiff", + "ron", + "rust-ini", + "serde", + "serde_json", + "toml", + "yaml-rust", +] + +[[package]] name = "convert_case" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -704,6 +723,12 @@ dependencies = [ ] [[package]] +name = "dlv-list" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0688c2a7f92e427f44895cd63841bff7b29f8d7a1648b9e7e07a4a365b2e1257" + +[[package]] name = "dotenvy" version = "0.15.6" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1206,6 +1231,7 @@ version = "0.1.0" dependencies = [ "anyhow", "async-std", + "base64 0.21.0", "clap", "colored", "email_address", @@ -1219,6 +1245,7 @@ dependencies = [ "strum", "strum_macros", "thiserror", + "time", "tiny_http", "tokio", "toml", @@ -1349,6 +1376,17 @@ dependencies = [ ] [[package]] +name = "json5" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96b0db21af676c1ce64250b5f40f3ce2cf27e4e47cb91ed91eb6fe9350b430c1" +dependencies = [ + "pest", + "pest_derive", + "serde", +] + +[[package]] name = "kv-log-macro" version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1413,6 +1451,12 @@ dependencies = [ ] [[package]] +name = "linked-hash-map" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" + +[[package]] name = "linux-raw-sys" version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1604,6 +1648,16 @@ dependencies = [ ] [[package]] +name = "ordered-multimap" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccd746e37177e1711c20dd619a1620f34f5c8b569c53590a72dedd5344d8924a" +dependencies = [ + "dlv-list", + "hashbrown", +] + +[[package]] name = "os_str_bytes" version = "6.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1665,9 +1719,9 @@ dependencies = [ [[package]] name = "password-hash" -version = "0.4.2" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7676374caaee8a325c9e7a2ae557f216c5563a171d6997b0ef8a65af35147700" +checksum = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166" dependencies = [ "base64ct", "rand_core", @@ -1681,6 +1735,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d01a5bd0424d00070b0098dd17ebca6f961a959dead1dbcbbbc1d1cd8d3deeba" [[package]] +name = "pathdiff" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8835116a5c179084a830efb3adc117ab007512b535bc1a21c991d3b32a6b44dd" + +[[package]] name = "percent-encoding" version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1697,6 +1757,40 @@ dependencies = [ ] [[package]] +name = "pest_derive" +version = "2.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a81186863f3d0a27340815be8f2078dd8050b14cd71913db9fbda795e5f707d7" +dependencies = [ + "pest", + "pest_generator", +] + +[[package]] +name = "pest_generator" +version = "2.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75a1ef20bf3193c15ac345acb32e26b3dc3223aff4d77ae4fc5359567683796b" +dependencies = [ + "pest", + "pest_meta", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "pest_meta" +version = "2.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e3b284b1f13a20dc5ebc90aff59a51b8d7137c221131b52a7260c08cbc1cc80" +dependencies = [ + "once_cell", + "pest", + "sha2", +] + +[[package]] name = "petgraph" version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1982,6 +2076,27 @@ dependencies = [ ] [[package]] +name = "ron" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88073939a61e5b7680558e6be56b419e208420c2adb92be54921fa6b72283f1a" +dependencies = [ + "base64 0.13.1", + "bitflags", + "serde", +] + +[[package]] +name = "rust-ini" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6d5f2436026b4f6e79dc829837d467cc7e9a55ee40e750d716713540715a2df" +dependencies = [ + "cfg-if", + "ordered-multimap", +] + +[[package]] name = "rustc_version" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2045,7 +2160,9 @@ dependencies = [ "anyhow", "argon2", "async-trait", + "base64 0.21.0", "clap", + "config", "derive_more", "email_address", "glob", @@ -3095,3 +3212,12 @@ checksum = "80d0f4e272c85def139476380b12f9ac60926689dd2e01d4923222f40580869d" dependencies = [ "winapi", ] + +[[package]] +name = "yaml-rust" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85" +dependencies = [ + "linked-hash-map", +] diff --git a/crates/iam/Cargo.toml b/crates/iam/Cargo.toml index 966f341..2ee6fc6 100644 --- a/crates/iam/Cargo.toml +++ b/crates/iam/Cargo.toml @@ -6,6 +6,7 @@ edition = "2021" [dependencies] anyhow = "1.0" async-std = { version = "1.12.0", features = [ "attributes" ] } +base64 = "0.21.0" clap = { version = "4.0.29", features = ["derive"] } colored = "2.0.0" email_address = "0.2" @@ -18,6 +19,7 @@ serde = "1" serde_json = { version = "1.0", features = ["raw_value"] } strum = "0.24.1" strum_macros = "0.24" +time = { version = "0.3", features = [ "serde" ] } tiny_http = "0.12" tokio = { version = "1.23.0", features = ["full"] } toml = "0.5.9" diff --git a/crates/iam/src/api.rs b/crates/iam/src/api.rs index af175a7..c662e0c 100644 --- a/crates/iam/src/api.rs +++ b/crates/iam/src/api.rs @@ -97,6 +97,14 @@ pub enum Command { #[command(subcommand)] object: UpdateObject, }, + #[command( + about = "Validate an IAM credential, optionally returning a session from the validation", + long_about = "Validate\n\nCredentials which have been created for identities may be validated and optionally exchanged for sessions." + )] + Validate { + #[command(subcommand)] + object: ValidateObject, + }, } #[derive(Subcommand)] @@ -248,11 +256,15 @@ pub enum DevObject { #[derive(Subcommand)] pub enum CredentialMethod { - /// A + ApiToken { + #[arg(long)] + expires_at: Option<i64>, + }, + /// Unique username and passphrase credential. Each username may have at most one Passprhase credential. Passphrase { - /// B + /// The username associated with this credential username: String, - /// C + /// The secret passphrase for this credential passphrase: String, }, } @@ -274,60 +286,13 @@ pub enum ValidationMethod { #[derive(Subcommand)] pub enum GetObject { - ApiKey { - /// Public key associated with this api key set - public_key: String, - }, - Group { - /// Unique group name - name: String, - /// Unique group id - #[arg(long, short)] - id: Option<Uuid>, - }, Identity { /// The unique id corresponding to this identity. #[arg(long, short)] identity_id: Option<Uuid>, - /// Any session corresponding to this identity. - #[arg(long, short)] - session_token: Option<String>, - }, - Permission { - /// Unique permission name - name: String, - /// Unique permission id - #[arg(long, short)] - id: Option<Uuid>, - }, - Role { - /// Unique role name - name: String, - /// Unique role id - #[arg(long, short)] - id: Option<Uuid>, - }, - Session { - /// The plaintext token which uniquely identifies the session - secret: String, - }, - Service { - /// Unique service name - name: String, - /// Unique service id - #[arg(long, short)] - id: Option<Uuid>, - }, - ServiceAction { - /// Unique service action name - name: String, - /// Unique service action id - #[arg(long, short)] - id: Option<Uuid>, - }, - Validation { - /// Unique validation request id - id: Uuid, + /// The credential corresponding to this identity. + #[command(subcommand)] + credential: Option<ValidateObject>, }, } @@ -340,6 +305,33 @@ pub enum UpdateObject { #[arg(long, short)] metadata: Option<String>, }, + Credential { + /// Unique identifier for this credential. + /// Note: You can validate the credential to find it's id. + id: Uuid, + /// Whether to revoke this credential. Once revoked, the credential may no longer be used + /// and may not be un-revoked. + #[arg(long, short, action)] + revoke: bool, + }, +} + +#[derive(Subcommand)] +pub enum ValidateObject { + ApiToken { + /// Api token to validate + token: String, + }, + Passphrase { + /// The username associated with this credential + username: String, + /// The secret passphrase for this credential + passphrase: String, + }, + Session { + /// The secret token associated with this session. + token: String, + }, } #[derive(Serialize, Deserialize)] diff --git a/crates/iam/src/main.rs b/crates/iam/src/main.rs index c679af6..28f4e4c 100644 --- a/crates/iam/src/main.rs +++ b/crates/iam/src/main.rs @@ -2,19 +2,19 @@ mod api; mod command; mod util; +use crate::api::ValidationMethod; use anyhow::bail; -use api::{AdminAction, Args, CliError, Command, CreateObject, DevObject, GetObject, UpdateObject}; +use api::{ + AdminAction, Args, CliError, Command, CreateObject, DevObject, GetObject, UpdateObject, + ValidateObject, +}; + use clap::Parser; use command::dev_oauth2_listen; use env_logger::Env; -use secd::{ - auth::z, Credential, CredentialType, Secd, ENV_AUTH_STORE_CONN_STRING, ENV_SPICE_SECRET, - ENV_SPICE_SERVER, -}; -use util::{error_detail, Result}; -use uuid::Uuid; - -use crate::api::ValidationMethod; +use secd::{CredentialType, Secd}; +use time::OffsetDateTime; +use util::Result; const CONFIG_DIR_NAME: &str = "secdiam"; const CONFIG_PROFILE_FILE: &str = "profiles.toml"; @@ -49,15 +49,7 @@ async fn exec() -> Result<Option<String>> { rest @ _ => { // let cfg = util::read_config(args.profile).map_err(|_| CliError::InvalidProfile)?; - std::env::set_var( - ENV_AUTH_STORE_CONN_STRING, - // "sqlite:///home/benj/.config/secdiam/34wxC.sql?mode=rwc", - "postgresql://secduser:p4ssw0rd@localhost:5412/secd", - ); - std::env::set_var(ENV_SPICE_SECRET, "sup3rs3cr3tk3y"); - std::env::set_var(ENV_SPICE_SERVER, "http://[::1]:50051"); - - let secd = Secd::init(None) + let secd = Secd::init(None, None) .await .map_err(|e| CliError::SecdInitializationFailure(e.to_string()))?; @@ -70,6 +62,7 @@ async fn exec() -> Result<Option<String>> { unimplemented!() } Command::Update { object } => update(&secd, object).await?, + Command::Validate { object } => validate(&secd, object).await?, } } }) @@ -101,18 +94,27 @@ async fn create(secd: &Secd, cmd: CreateObject) -> Result<Option<String>> { method, identity_id, } => { - let t = match method { + let t = match &method { api::CredentialMethod::Passphrase { username, passphrase, } => CredentialType::Passphrase { - key: username, - value: passphrase, + key: username.clone(), + value: passphrase.clone(), }, + api::CredentialMethod::ApiToken { .. } => CredentialType::new_api_token()?, + }; + + let expires_at = match method { + api::CredentialMethod::ApiToken { expires_at, .. } => expires_at.map(|t| { + OffsetDateTime::from_unix_timestamp(t) + .expect("The provided value is an invalid unix timestamp") + }), + api::CredentialMethod::Passphrase { .. } => None, }; - let credential = secd.create_credential(t, identity_id).await?; - Some(serde_json::ser::to_string(&credential)?.to_string()) + let credential = secd.create_credential(t, identity_id, expires_at).await?; + Some(serde_json::ser::to_string_pretty(&credential)?.to_string()) } CreateObject::Validation { method, @@ -121,7 +123,7 @@ async fn create(secd: &Secd, cmd: CreateObject) -> Result<Option<String>> { ValidationMethod::Email { address } => { let validation = secd.validate_email(&address, identity_id).await?; - Some(serde_json::ser::to_string(&validation)?.to_string()) + Some(serde_json::ser::to_string_pretty(&validation)?.to_string()) } _ => unimplemented!(), }, @@ -136,7 +138,7 @@ async fn create(secd: &Secd, cmd: CreateObject) -> Result<Option<String>> { let session = secd .complete_address_validation(&validation_id, token, code) .await?; - Some(serde_json::ser::to_string(&session)?.to_string()) + Some(serde_json::ser::to_string_pretty(&session)?.to_string()) } }) } @@ -149,44 +151,31 @@ async fn dev(cmd: DevObject) -> Result<Option<String>> { async fn get(secd: &Secd, cmd: GetObject) -> Result<Option<String>> { Ok(match cmd { - GetObject::ApiKey { public_key } => { - println!("get object api key"); - None - } - GetObject::Group { name, id } => { - println!("get object group"); - None - } GetObject::Identity { identity_id, - session_token, - } => Some( - serde_json::ser::to_string(&secd.get_identity(identity_id, session_token).await?)? - .to_string(), - ), - - GetObject::Permission { name, id } => { - println!("get object permission"); - None - } - GetObject::Role { name, id } => { - println!("get object role"); - None - } - GetObject::Service { name, id } => { - println!("get object service"); - None - } - GetObject::ServiceAction { name, id } => { - println!("get object service action"); - None - } - GetObject::Session { secret } => { - Some(serde_json::ser::to_string(&secd.get_session(&secret).await?)?.to_string()) - } - GetObject::Validation { id } => { - println!("get object validation"); - None + + credential, + } => { + let t = credential.map(|cred| match cred { + ValidateObject::ApiToken { token } => { + CredentialType::api_token_from_str(&token).expect("failed to build api token") + } + ValidateObject::Passphrase { + username, + passphrase, + } => CredentialType::Passphrase { + key: username, + value: passphrase, + }, + ValidateObject::Session { token } => { + CredentialType::session_from_str(&token).expect("failed to build session") + } + }); + + Some( + serde_json::ser::to_string_pretty(&secd.get_identity(identity_id, t).await?)? + .to_string(), + ) } }) } @@ -202,5 +191,37 @@ async fn update(secd: &Secd, cmd: UpdateObject) -> Result<Option<String>> { Some(serde_json::to_string(&identity)?.to_string()) } + UpdateObject::Credential { id, revoke } => { + if revoke { + secd.revoke_credential(id).await?; + } + + Some("Ok".to_string()) + } }) } + +async fn validate(secd: &Secd, cmd: ValidateObject) -> Result<Option<String>> { + let credential = match cmd { + ValidateObject::ApiToken { token } => { + secd.validate_credential(CredentialType::api_token_from_str(&token)?) + .await? + } + ValidateObject::Passphrase { + username, + passphrase, + } => { + secd.validate_credential(CredentialType::Passphrase { + key: username, + value: passphrase, + }) + .await? + } + ValidateObject::Session { token } => { + secd.validate_credential(CredentialType::session_from_str(&token)?) + .await? + } + }; + + Ok(Some(serde_json::to_string_pretty(&credential)?.to_string())) +} diff --git a/crates/secd/Cargo.toml b/crates/secd/Cargo.toml index 94ae43c..2631934 100644 --- a/crates/secd/Cargo.toml +++ b/crates/secd/Cargo.toml @@ -6,10 +6,12 @@ edition = "2021" [dependencies] aes-gcm = "0.10.1" -argon2 = "0.4.1" +argon2 = "0.5.0" async-trait = "0.1.59" anyhow = "1.0" +base64 = "0.21.0" clap = { version = "4.0.29", features = ["derive"] } +config = "0.13.3" derive_more = "0.99" email_address = "0.2" hex = "0.4" @@ -25,7 +27,7 @@ reqwest = { version = "0.11.13", features = ["json"] } sendgrid = { version = "0.18", features = ["async"] } serde = "1" serde_json = { version = "1.0", features = ["raw_value"] } -serde_with = { version = "2.1", features = ["hex"] } +serde_with = { version = "2.1", features = ["base64", "hex"] } sha2 = "0.10.6" strum = "0.24.1" strum_macros = "0.24" diff --git a/crates/secd/build.rs b/crates/secd/build.rs index 8471105..e6f3362 100644 --- a/crates/secd/build.rs +++ b/crates/secd/build.rs @@ -8,7 +8,6 @@ fn main() { let proto_files: Vec<PathBuf> = glob("proto/**/*.proto") .unwrap() - .into_iter() .filter_map(Result::ok) .collect(); @@ -22,7 +21,7 @@ fn main() { tonic_build::configure() .server_mod_attribute("attrs", "#[cfg(feature = \"server\")]") .client_mod_attribute("attrs", "#[cfg(feature = \"client\")]") - .file_descriptor_set_path(&descriptor_path) + .file_descriptor_set_path(descriptor_path) .compile(&proto_files, &["proto"]) .unwrap(); } diff --git a/crates/secd/src/auth/n.rs b/crates/secd/src/auth/n.rs index 1f32fd6..dde6e7d 100644 --- a/crates/secd/src/auth/n.rs +++ b/crates/secd/src/auth/n.rs @@ -5,12 +5,12 @@ use crate::{ DEFAULT_SIGNUP_EMAIL, }, store::{ - AddressLens, AddressValidationLens, CredentialLens, IdentityLens, SessionLens, - Storable, StoreError, + AddressLens, AddressValidationLens, CredentialLens, IdentityLens, Storable, StoreError, }, }, - util, Address, AddressType, AddressValidation, AddressValidationId, AddressValidationMethod, - Credential, CredentialType, Identity, IdentityId, Secd, SecdError, Session, SessionToken, + util::{self, ErrorContext}, + Address, AddressType, AddressValidation, AddressValidationId, AddressValidationMethod, + Credential, CredentialId, CredentialType, Identity, IdentityId, Secd, SecdError, ADDRESSS_VALIDATION_CODE_SIZE, ADDRESS_VALIDATION_ALLOWS_ATTEMPTS, ADDRESS_VALIDATION_IDENTITY_SURJECTION, EMAIL_VALIDATION_DURATION, }; @@ -84,8 +84,8 @@ impl Secd { revoked_at: None, validated_at: None, attempts: 0, - hashed_token: util::hash(&secret.as_bytes()), - hashed_code: util::hash(&code.as_bytes()), + hashed_token: util::hash(secret.as_bytes()), + hashed_code: util::hash(code.as_bytes()), }; validation.write(self.store.clone()).await?; @@ -95,11 +95,13 @@ impl Secd { .cfg .email_address_from .clone() + .and_then(|s| s.parse().ok()) .unwrap_or("SecD <noreply@secd.com>".parse().unwrap()), replyto_address: self .cfg .email_address_replyto .clone() + .and_then(|s| s.parse().ok()) .unwrap_or("SecD <noreply@secd.com>".parse().unwrap()), recipient: email_address.clone(), subject: "Login Request".into(), @@ -129,7 +131,7 @@ impl Secd { validation_id: &AddressValidationId, plaintext_token: Option<String>, plaintext_code: Option<String>, - ) -> Result<Session, SecdError> { + ) -> Result<Credential, SecdError> { let mut validation = AddressValidation::find( self.store.clone(), &AddressValidationLens { @@ -193,7 +195,6 @@ impl Secd { id: None, address_type: Some(&validation.address.t), validated_address: Some(true), - session_token_hash: None, }, ) .await?; @@ -210,6 +211,7 @@ impl Secd { id: Uuid::new_v4(), address_validations: vec![], credentials: vec![], + new_credentials: vec![], rules: vec![], metadata: None, created_at: OffsetDateTime::now_utc(), @@ -235,30 +237,72 @@ impl Secd { validation.validated_at = Some(OffsetDateTime::now_utc()); validation.write(self.store.clone()).await?; - let session = Session::new(validation.identity_id.expect("unreachable d3ded289-72eb-4a42-a37d-f5c9c697cc61 [assert(identity.is_some()) prevents this]"))?; + let mut session = Credential::new_session(validation.identity_id.expect("unreachable d3ded289-72eb-4a42-a37d-f5c9c697cc61 [assert(identity.is_some()) prevents this]"))?; + let plaintext_type = session.t.clone(); + + session.hash(&self.crypter)?; session.write(self.store.clone()).await?; + session.t = plaintext_type; + Ok(session) } + pub async fn create_identity_with_credential( + &self, + t: CredentialType, + identity_id: IdentityId, + metadata: Option<String>, + ) -> Result<Identity, SecdError> { + let identity = Identity::find( + self.store.clone(), + &IdentityLens { + id: Some(&identity_id), + address_type: None, + validated_address: None, + }, + ) + .await?; + + if !identity.is_empty() { + log::error!("identity was found while creating a new identity with a credential"); + return Err(SecdError::IdentityAlreadyExists); + } + + Identity { + id: identity_id, + address_validations: vec![], + credentials: vec![], + new_credentials: vec![], + rules: vec![], + metadata, + created_at: OffsetDateTime::now_utc(), + deleted_at: None, + } + .write(self.store.clone()) + .await?; + + self.create_credential(t, Some(identity_id), None).await + } + pub async fn create_credential( &self, t: CredentialType, identity_id: Option<IdentityId>, + expires_at: Option<OffsetDateTime>, ) -> Result<Identity, SecdError> { - let identity = match identity_id { + let mut identity = match identity_id { Some(id) => Identity::find( self.store.clone(), &IdentityLens { id: Some(&id), address_type: None, validated_address: None, - session_token_hash: None, }, ) .await? .into_iter() - .nth(0) + .next() .ok_or(SecdError::IdentityNotFound)?, None => { @@ -266,6 +310,7 @@ impl Secd { id: Uuid::new_v4(), address_validations: vec![], credentials: vec![], + new_credentials: vec![], rules: vec![], metadata: None, created_at: OffsetDateTime::now_utc(), @@ -282,7 +327,6 @@ impl Secd { id: None, identity_id: Some(identity.id), t: Some(&t), - restrict_by_key: Some(false), }, ) .await?[..] @@ -290,9 +334,9 @@ impl Secd { [] => Credential { id: Uuid::new_v4(), identity_id: identity.id, - t, + t: t.clone(), created_at: OffsetDateTime::now_utc(), - revoked_at: None, + revoked_at: expires_at, deleted_at: None, }, _ => return Err(SecdError::CredentialAlreadyExists), @@ -307,57 +351,100 @@ impl Secd { err => SecdError::StoreError(err), })?; - Ok(identity) - } + identity.new_credentials.push(Credential { + id: credential.id, + identity_id: credential.identity_id, + t, + created_at: credential.created_at, + revoked_at: credential.revoked_at, + deleted_at: credential.deleted_at, + }); - pub async fn validate_credential( - &self, - // t: CredentialType, - // key: String, - // value: Option<String>, - ) -> Result<Session, SecdError> { - // Credential::find(store, lens) use key here as unique index - todo!() + Ok(identity) } - pub async fn get_session(&self, t: &SessionToken) -> Result<Session, SecdError> { - let token = hex::decode(t)?; - let mut session = Session::find( + pub async fn validate_credential(&self, t: CredentialType) -> Result<Credential, SecdError> { + let mut retrieved = Credential::find( self.store.clone(), - &SessionLens { - token_hash: Some(&util::hash(&token)), + &CredentialLens { + id: None, identity_id: None, + t: Some(&t), }, ) - .await?; - assert!(session.len() <= 1, "get session failed: multiple sessions found for a single token. This is very _very_ bad."); + .await? + .into_iter() + .next() + .ok_or(SecdError::InvalidCredential)?; - if session.is_empty() { - return Err(SecdError::InvalidSession); - } else { - let mut session = session.swap_remove(0); - session.token = token; - Ok(session) - } + match retrieved.revoked_at { + Some(t) if t <= OffsetDateTime::now_utc() => { + log::debug!("credential was revoked"); + Err(SecdError::InvalidCredential) + } + _ => Ok(()), + }?; + + match retrieved.deleted_at { + Some(t) if t <= OffsetDateTime::now_utc() => { + log::debug!("credential was deleted"); + Err(SecdError::InvalidCredential) + } + _ => Ok(()), + }?; + + retrieved.hash_compare(&t, &self.crypter)?; + + // Return the initially provided plaintext credential since it's valid + retrieved.t = t; + + Ok(retrieved) } pub async fn get_identity( &self, i: Option<IdentityId>, - t: Option<SessionToken>, + t: Option<CredentialType>, ) -> Result<Identity, SecdError> { - let token_hash = match t { - Some(tok) => Some(util::hash(&hex::decode(&tok)?)), - None => None, - }; + if i.is_none() && t.is_none() { + log::error!("get_identity expects that at least one of IdentityId or CredentialType is provided. None were found."); + return Err(SecdError::IdentityNotFound); + } + + let c = Credential::find( + self.store.clone(), + &CredentialLens { + id: None, + identity_id: i, + t: t.as_ref(), + }, + ) + .await?; + + assert!( + c.len() <= 1, + "The provided credential refers to more than one identity. This is very _very_ bad." + ); + let identity_id = c + .into_iter() + .next() + .ok_or(SecdError::InvalidCredential) + .ctx("No identities were found for the provided identity_id and credential_type")? + .identity_id; + + if i.is_some() && i != Some(identity_id) { + log::error!( + "The provided identity does not match the identity associated with this credential" + ); + return Err(SecdError::InvalidCredential); + } let mut i = Identity::find( self.store.clone(), &IdentityLens { - id: i.as_ref(), + id: Some(&identity_id), address_type: None, validated_address: None, - session_token_hash: token_hash, }, ) .await?; @@ -368,7 +455,7 @@ impl Secd { ); if i.is_empty() { - return Err(SecdError::IdentityNotFound); + Err(SecdError::IdentityNotFound) } else { Ok(i.swap_remove(0)) } @@ -385,12 +472,11 @@ impl Secd { id: Some(&i), address_type: None, validated_address: None, - session_token_hash: None, }, ) .await? .into_iter() - .nth(0) + .next() .ok_or(SecdError::IdentityNotFound)?; identity.metadata = Some(md); @@ -399,9 +485,22 @@ impl Secd { Ok(identity) } - pub async fn revoke_session(&self, session: &mut Session) -> Result<(), SecdError> { - session.revoked_at = Some(OffsetDateTime::now_utc()); - session.write(self.store.clone()).await?; + pub async fn revoke_credential(&self, credential_id: CredentialId) -> Result<(), SecdError> { + let mut credential = Credential::find( + self.store.clone(), + &CredentialLens { + id: Some(credential_id), + identity_id: None, + t: None, + }, + ) + .await? + .into_iter() + .next() + .ok_or(SecdError::InvalidCredential)?; + + credential.revoked_at = Some(OffsetDateTime::now_utc()); + credential.write(self.store.clone()).await?; Ok(()) } } diff --git a/crates/secd/src/auth/z/graph.rs b/crates/secd/src/auth/z/graph.rs index 9ca045b..f28c901 100644 --- a/crates/secd/src/auth/z/graph.rs +++ b/crates/secd/src/auth/z/graph.rs @@ -1,209 +1,207 @@ -use std::collections::{HashSet, VecDeque}; - -type NodeIdx = usize; -type EdgeIdx = usize; - -struct RelationGraph { - nodes: Vec<Node>, - edges: Vec<Edge>, -} - -#[derive(Hash, PartialEq, Eq)] -struct Node { - id: u64, - is_namespace: bool, - edge_list_head: Option<EdgeIdx>, -} - -struct Edge { - dst: NodeIdx, - op: Option<Operator>, - next_edge: Option<EdgeIdx>, -} - -enum Operator { - And, - Or, - Difference, - Complement, - AndAll, - OrAll, - DifferenceAll, - ComplementAll, -} - -impl RelationGraph { - pub fn new() -> Self { - // As I think about how to serialize this graph, when reading from the DB - // I should chunk up the namespaces so that I can read-on-demand when I - // encounter a namespace that has not yet been loaded, and in this way - // I don't need to deal with the entire graph... - // - // but that's a totally unnecessary fun optimization. - RelationGraph { - nodes: vec![], - edges: vec![], - } - } - - pub fn add_node(&mut self, id: u64, is_namespace: bool) -> NodeIdx { - let idx = self.nodes.len(); - self.nodes.push(Node { - id, - is_namespace, - edge_list_head: None, - }); - idx - } - - pub fn add_edge( - &mut self, - src: NodeIdx, - dst: NodeIdx, - op: Option<Operator>, - follow: Option<NodeIdx>, - ) { - let edge_idx = self.edges.len(); - let node = &mut self.nodes[src]; - // TODO: dupe check - self.edges.push(Edge { - dst, - op, - next_edge: node.edge_list_head, - }); - node.edge_list_head = Some(edge_idx); - } - - // doc:2#viewer@user:1 - pub fn find_leaves(&self, src: NodeIdx, filterIdx: Option<NodeIdx>) -> Vec<NodeIdx> { - let mut all_leaves = vec![]; - // let mut and_leaves = vec![]; - // let mut exc_leaves = vec![]; - - // the output should basically be leaves to look up - // for example - // the leaves of doc might be (user), <(organization, admin)>, <(role, foo)> - // and since (organization, admin) has leaves (user) as well - // if the user is in that final set, then they are valid - // and those leaves would be queried as relation tuples saved in the DB - - println!("HERE I AM"); - - let start = self.nodes.get(src); - let mut seen = HashSet::new(); - let mut q = VecDeque::new(); - - seen.insert(start); - q.push_back(start); - - // I need to build a chain of ops - // [AND, OR, OR, AND, OR, ...] - // at each stage, I need to apply the operator chain - while let Some(&Some(node)) = q.iter().next() { - if node.is_namespace { - all_leaves.push(node); - } - - // for edge in edge list - - println!("IN QUEUE\n"); - println!("node: {:?}\n", node.id); - q.pop_front(); - } - println!("DONE"); - - // task...starting at doc_viewer, return all leaf nodes - // to do this.... - // check all children - // if child is OR - // if child is leaf, return child - // else return all of these children - // if child is AND - // if child is leaf, return if also present in all other nodes - // else return all children which are also present in other nodes - // if child is Difference - // if child is leaf, do not include and remove if it part of the user set - // else remove all of these children - - // TOTAL pool of leafs - // Intersection leaves (i.e. ONLY leaves which MUST exist) - // exclusion leaves (i.e. leaves which CANNOT exist) - - // bfs build query... - // execute query - - // start at nodeIdx: doc_viewer - // expand until I find all leaf nodes. - // we are interested in leaf nodes with nodeIdx: user - // return all leaf nodeIdx that match the filter. If no filter provided, return all leaf idx. - - // TODO: optionally build the expansion path by pushing every intermediate step as the bfs is walked. - - // with each step of the bfs, perform the operator. - // e.g. if first step is doc_viewer -> user with operator OR then we have leaf user with operator chain [OR] - // the next step might be doc_viewer -> doc_editor with step OR and doc_editor -> user with step OR, so we have leaf user with chain [OR, OR] - // the next step might be doc_viewer -> doc_auditor with OR - // then doc_auditor -> doc_editor with OR - // then doc_editor -> doc_user with OR - // then doc_editor -> doc_owner - // then doc_auditor -> doc_owner with Difference - // - - todo!() - } -} - -#[cfg(test)] -mod test { - use super::*; - - #[test] - fn build_graph_test() { - let mut g = RelationGraph::new(); - let user = g.add_node(1, true); - let role = g.add_node(2, true); - let group = g.add_node(3, true); - let doc = g.add_node(4, true); - - let role_member = g.add_node(5, false); - - let group_member = g.add_node(6, false); - let group_admin = g.add_node(7, false); - - let doc_owner = g.add_node(8, false); - let doc_editor = g.add_node(9, false); - let doc_viewer = g.add_node(10, false); - let doc_auditor = g.add_node(11, false); - let doc_parent = g.add_node(12, false); - - g.add_edge(role, role_member, None, None); - g.add_edge(role_member, user, Some(Operator::Or), None); - g.add_edge(role_member, group_member, Some(Operator::Or), None); - - g.add_edge(group, group_member, None, None); - g.add_edge(group, group_admin, None, None); - - g.add_edge(group_member, user, Some(Operator::Or), None); - g.add_edge(group_member, group_admin, Some(Operator::Or), None); - g.add_edge(group_admin, user, Some(Operator::Or), None); - - g.add_edge(doc, doc_owner, None, None); - g.add_edge(doc, doc_editor, None, None); - g.add_edge(doc, doc_viewer, None, None); - g.add_edge(doc, doc_auditor, None, None); - g.add_edge(doc, doc_parent, None, None); - - g.add_edge(doc_owner, user, Some(Operator::Or), None); - g.add_edge(doc_editor, user, Some(Operator::Or), None); - g.add_edge(doc_editor, doc_owner, Some(Operator::Or), None); - g.add_edge(doc_viewer, user, Some(Operator::Or), None); - g.add_edge(doc_viewer, doc_editor, Some(Operator::Or), None); - g.add_edge(doc_viewer, doc_parent, Some(Operator::Or), Some(doc_viewer)); - g.add_edge(doc_viewer, user, Some(Operator::OrAll), None); - g.add_edge(doc_viewer, doc_auditor, Some(Operator::Or), None); - g.add_edge(doc_auditor, doc_editor, Some(Operator::Difference), None); - - g.find_leaves(doc_viewer, None); - assert_eq!(1, 2); - } -} +// use std::collections::{HashSet, VecDeque}; + +// type NodeIdx = usize; +// type EdgeIdx = usize; + +// struct RelationGraph { +// nodes: Vec<Node>, +// edges: Vec<Edge>, +// } + +// #[derive(Hash, PartialEq, Eq)] +// struct Node { +// id: u64, +// is_namespace: bool, +// edge_list_head: Option<EdgeIdx>, +// } + +// struct Edge { +// dst: NodeIdx, +// op: Option<Operator>, +// next_edge: Option<EdgeIdx>, +// } + +// enum Operator { +// And, +// Or, +// Difference, +// Complement, +// AndAll, +// OrAll, +// DifferenceAll, +// ComplementAll, +// } + +// impl RelationGraph { +// pub fn new() -> Self { +// // As I think about how to serialize this graph, when reading from the DB +// // I should chunk up the namespaces so that I can read-on-demand when I +// // encounter a namespace that has not yet been loaded, and in this way +// // I don't need to deal with the entire graph... +// // +// // but that's a totally unnecessary fun optimization. +// RelationGraph { +// nodes: vec![], +// edges: vec![], +// } +// } + +// pub fn add_node(&mut self, id: u64, is_namespace: bool) -> NodeIdx { +// let idx = self.nodes.len(); +// self.nodes.push(Node { +// id, +// is_namespace, +// edge_list_head: None, +// }); +// idx +// } + +// pub fn add_edge( +// &mut self, +// src: NodeIdx, +// dst: NodeIdx, +// op: Option<Operator>, +// follow: Option<NodeIdx>, +// ) { +// let edge_idx = self.edges.len(); +// let node = &mut self.nodes[src]; +// // TODO: dupe check +// self.edges.push(Edge { +// dst, +// op, +// next_edge: node.edge_list_head, +// }); +// node.edge_list_head = Some(edge_idx); +// } + +// // doc:2#viewer@user:1 +// pub fn find_leaves(&self, src: NodeIdx, filterIdx: Option<NodeIdx>) -> Vec<NodeIdx> { +// let mut all_leaves = vec![]; +// // let mut and_leaves = vec![]; +// // let mut exc_leaves = vec![]; + +// // the output should basically be leaves to look up +// // for example +// // the leaves of doc might be (user), <(organization, admin)>, <(role, foo)> +// // and since (organization, admin) has leaves (user) as well +// // if the user is in that final set, then they are valid +// // and those leaves would be queried as relation tuples saved in the DB + +// let start = self.nodes.get(src); +// let mut seen = HashSet::new(); +// let mut q = VecDeque::new(); + +// seen.insert(start); +// q.push_back(start); + +// // I need to build a chain of ops +// // [AND, OR, OR, AND, OR, ...] +// // at each stage, I need to apply the operator chain +// while let Some(&Some(node)) = q.iter().next() { +// if node.is_namespace { +// all_leaves.push(node); +// } + +// // for edge in edge list + +// println!("IN QUEUE\n"); +// println!("node: {:?}\n", node.id); +// q.pop_front(); +// } +// println!("DONE"); + +// // task...starting at doc_viewer, return all leaf nodes +// // to do this.... +// // check all children +// // if child is OR +// // if child is leaf, return child +// // else return all of these children +// // if child is AND +// // if child is leaf, return if also present in all other nodes +// // else return all children which are also present in other nodes +// // if child is Difference +// // if child is leaf, do not include and remove if it part of the user set +// // else remove all of these children + +// // TOTAL pool of leafs +// // Intersection leaves (i.e. ONLY leaves which MUST exist) +// // exclusion leaves (i.e. leaves which CANNOT exist) + +// // bfs build query... +// // execute query + +// // start at nodeIdx: doc_viewer +// // expand until I find all leaf nodes. +// // we are interested in leaf nodes with nodeIdx: user +// // return all leaf nodeIdx that match the filter. If no filter provided, return all leaf idx. + +// // TODO: optionally build the expansion path by pushing every intermediate step as the bfs is walked. + +// // with each step of the bfs, perform the operator. +// // e.g. if first step is doc_viewer -> user with operator OR then we have leaf user with operator chain [OR] +// // the next step might be doc_viewer -> doc_editor with step OR and doc_editor -> user with step OR, so we have leaf user with chain [OR, OR] +// // the next step might be doc_viewer -> doc_auditor with OR +// // then doc_auditor -> doc_editor with OR +// // then doc_editor -> doc_user with OR +// // then doc_editor -> doc_owner +// // then doc_auditor -> doc_owner with Difference +// // + +// todo!() +// } +// } + +// #[cfg(test)] +// mod test { +// use super::*; + +// #[test] +// fn build_graph_test() { +// let mut g = RelationGraph::new(); +// let user = g.add_node(1, true); +// let role = g.add_node(2, true); +// let group = g.add_node(3, true); +// let doc = g.add_node(4, true); + +// let role_member = g.add_node(5, false); + +// let group_member = g.add_node(6, false); +// let group_admin = g.add_node(7, false); + +// let doc_owner = g.add_node(8, false); +// let doc_editor = g.add_node(9, false); +// let doc_viewer = g.add_node(10, false); +// let doc_auditor = g.add_node(11, false); +// let doc_parent = g.add_node(12, false); + +// g.add_edge(role, role_member, None, None); +// g.add_edge(role_member, user, Some(Operator::Or), None); +// g.add_edge(role_member, group_member, Some(Operator::Or), None); + +// g.add_edge(group, group_member, None, None); +// g.add_edge(group, group_admin, None, None); + +// g.add_edge(group_member, user, Some(Operator::Or), None); +// g.add_edge(group_member, group_admin, Some(Operator::Or), None); +// g.add_edge(group_admin, user, Some(Operator::Or), None); + +// g.add_edge(doc, doc_owner, None, None); +// g.add_edge(doc, doc_editor, None, None); +// g.add_edge(doc, doc_viewer, None, None); +// g.add_edge(doc, doc_auditor, None, None); +// g.add_edge(doc, doc_parent, None, None); + +// g.add_edge(doc_owner, user, Some(Operator::Or), None); +// g.add_edge(doc_editor, user, Some(Operator::Or), None); +// g.add_edge(doc_editor, doc_owner, Some(Operator::Or), None); +// g.add_edge(doc_viewer, user, Some(Operator::Or), None); +// g.add_edge(doc_viewer, doc_editor, Some(Operator::Or), None); +// g.add_edge(doc_viewer, doc_parent, Some(Operator::Or), Some(doc_viewer)); +// g.add_edge(doc_viewer, user, Some(Operator::OrAll), None); +// g.add_edge(doc_viewer, doc_auditor, Some(Operator::Or), None); +// g.add_edge(doc_auditor, doc_editor, Some(Operator::Difference), None); + +// g.find_leaves(doc_viewer, None); +// assert_eq!(1, 2); +// } +// } diff --git a/crates/secd/src/auth/z/mod.rs b/crates/secd/src/auth/z/mod.rs index b364583..d663e65 100644 --- a/crates/secd/src/auth/z/mod.rs +++ b/crates/secd/src/auth/z/mod.rs @@ -1,3 +1,5 @@ +#![allow(dead_code)] // TODO: Remove when implemented +#![allow(unused_variables)] mod graph; use crate::{Authorization, Secd, SecdError}; @@ -50,7 +52,7 @@ impl Authorization for Secd { // they are "relationships" rather than what spice calls permissions spice .write_relationship( - &ts.into_iter() + &ts.iter() .map(|r| Relationship { subject: r.subject.clone(), object: r.object.clone(), diff --git a/crates/secd/src/client/email/mod.rs b/crates/secd/src/client/email/mod.rs index 7c7b233..9e591ba 100644 --- a/crates/secd/src/client/email/mod.rs +++ b/crates/secd/src/client/email/mod.rs @@ -42,7 +42,7 @@ pub enum MessengerType { pub(crate) struct LocalMailer {} impl LocalMailer { - pub fn new() -> Arc<dyn EmailMessenger + Send + Sync + 'static> { + pub fn new_ref() -> Arc<dyn EmailMessenger + Send + Sync + 'static> { warn!("You are using the local mailer, which will not work in production!"); Arc::new(LocalMailer {}) } @@ -59,7 +59,7 @@ pub(crate) struct Sendgrid { pub api_key: String, } impl Sendgrid { - pub fn new(api_key: String) -> Arc<dyn EmailMessenger + Send + Sync + 'static> { + pub fn new_ref(api_key: String) -> Arc<dyn EmailMessenger + Send + Sync + 'static> { Arc::new(Sendgrid { api_key }) } } @@ -89,7 +89,7 @@ impl Sendable for EmailValidationMessage { .subject(self.subject.clone()) .multipart(MultiPart::alternative_plain_html( "".to_string(), - String::from(self.body.clone()), + self.body.clone(), ))?; let mailer = lettre::SmtpTransport::unencrypted_localhost(); @@ -135,7 +135,7 @@ pub(crate) fn parse_email_template( validation_secret: Option<String>, validation_code: Option<String>, ) -> Result<String, EmailMessengerError> { - let mut t = template.clone().to_string(); + let mut t = String::from(template); // We do not allow substutions for a variety of reasons, but mainly security ones. // The only things we want to share are those which secd allows. In this case, that // means we only send an email with static content as provided by the filter, except @@ -143,8 +143,14 @@ pub(crate) fn parse_email_template( // present in the email. t = t.replace("{{secd::validation_id}}", &validation_id.to_string()); - validation_secret.map(|secret| t = t.replace("{{secd::validation_secret}}", &secret)); - validation_code.map(|code| t = t.replace("{{secd::validation_code}}", &code)); + + if let Some(secret) = validation_secret { + t = t.replace("{{secd::validation_secret}}", &secret); + } + + if let Some(code) = validation_code { + t = t.replace("{{secd::validation_code}}", &code); + } Ok(t) } diff --git a/crates/secd/src/client/spice/mod.rs b/crates/secd/src/client/spice/mod.rs index d3ca30d..67965d7 100644 --- a/crates/secd/src/client/spice/mod.rs +++ b/crates/secd/src/client/spice/mod.rs @@ -3,6 +3,7 @@ // favor of a light weight solution that leverages the Zanzibar API but disregards the // scaling part. +#[allow(clippy::module_inception)] pub mod spice { tonic::include_proto!("authzed.api.v1"); } @@ -10,7 +11,7 @@ pub mod spice { use spice::permissions_service_client::PermissionsServiceClient; use spice::schema_service_client::SchemaServiceClient; use spice::WriteSchemaRequest; -use std::env::var; +use std::matches; use tonic::metadata::MetadataValue; use tonic::transport::Channel; use tonic::{Request, Status}; @@ -19,7 +20,6 @@ use crate::auth::z::{self, Subject}; use crate::client::spice::spice::{ relationship_update, ObjectReference, Relationship, RelationshipUpdate, SubjectReference, }; -use crate::{ENV_SPICE_SECRET, ENV_SPICE_SERVER}; use self::spice::check_permission_response::Permissionship; use self::spice::{consistency, CheckPermissionRequest, Consistency, WriteRelationshipsRequest}; @@ -36,12 +36,7 @@ pub(crate) struct Spice { } impl Spice { - pub async fn new() -> Self { - let secret = - var(ENV_SPICE_SECRET).expect("initialization error: Failed to find SPICE_SECRET"); - let server = - var(ENV_SPICE_SERVER).expect("initialization error: Failed to find SPICE_SERVER"); - + pub async fn new(secret: String, server: String) -> Self { let channel = Channel::from_shared(server) .expect("invalid SPICE_SERVER uri") .connect() @@ -69,10 +64,10 @@ impl Spice { let response = client.check_permission(request).await?.into_inner(); - Ok(match Permissionship::from_i32(response.permissionship) { - Some(Permissionship::HasPermission) => true, - _ => false, - }) + Ok(matches!( + Permissionship::from_i32(response.permissionship), + Some(Permissionship::HasPermission) + )) } pub async fn write_relationship(&self, rs: &[z::Relationship]) -> Result<(), SpiceError> { @@ -83,7 +78,7 @@ impl Spice { let request = tonic::Request::new(WriteRelationshipsRequest { updates: rs - .into_iter() + .iter() .map(|t| RelationshipUpdate { operation: (relationship_update::Operation::Touch as i32), relationship: Some(Relationship { diff --git a/crates/secd/src/client/store/mod.rs b/crates/secd/src/client/store/mod.rs index 7bf01d5..6c42dba 100644 --- a/crates/secd/src/client/store/mod.rs +++ b/crates/secd/src/client/store/mod.rs @@ -6,8 +6,8 @@ use std::sync::Arc; use uuid::Uuid; use crate::{ - util, Address, AddressType, AddressValidation, Credential, CredentialId, CredentialType, - Identity, IdentityId, Session, + Address, AddressType, AddressValidation, Credential, CredentialId, CredentialType, Identity, + IdentityId, }; use self::sql_db::SqlClient; @@ -60,21 +60,13 @@ pub(crate) struct IdentityLens<'a> { pub id: Option<&'a Uuid>, pub address_type: Option<&'a AddressType>, pub validated_address: Option<bool>, - pub session_token_hash: Option<Vec<u8>>, } impl<'a> Lens for IdentityLens<'a> {} -pub(crate) struct SessionLens<'a> { - pub token_hash: Option<&'a Vec<u8>>, - pub identity_id: Option<&'a IdentityId>, -} -impl<'a> Lens for SessionLens<'a> {} - pub(crate) struct CredentialLens<'a> { pub id: Option<CredentialId>, pub identity_id: Option<IdentityId>, pub t: Option<&'a CredentialType>, - pub restrict_by_key: Option<bool>, } impl<'a> Lens for CredentialLens<'a> {} @@ -94,7 +86,7 @@ impl<'a> Storable<'a> for Address { store: Arc<dyn Store>, lens: &'a Self::Lens, ) -> Result<Vec<Self::Item>, StoreError> { - let typ = lens.t.map(|at| at.to_string().clone()); + let typ = lens.t.map(|at| at.to_string()); let typ = typ.as_deref(); let val = lens.t.and_then(|at| at.get_value()); @@ -151,54 +143,18 @@ impl<'a> Storable<'a> for Identity { Ok(match store.get_type() { StoreType::Postgres { c } => { - c.find_identity( - lens.id, - val, - lens.validated_address, - &lens.session_token_hash, - ) - .await? + c.find_identity(lens.id, val, lens.validated_address) + .await? } StoreType::Sqlite { c } => { - c.find_identity( - lens.id, - val, - lens.validated_address, - &lens.session_token_hash, - ) - .await? + c.find_identity(lens.id, val, lens.validated_address) + .await? } }) } } #[async_trait] -impl<'a> Storable<'a> for Session { - type Item = Session; - type Lens = SessionLens<'a>; - - async fn write(&self, store: Arc<dyn Store>) -> Result<(), StoreError> { - let token_hash = util::hash(&self.token); - match store.get_type() { - StoreType::Postgres { c } => c.write_session(self, &token_hash).await?, - StoreType::Sqlite { c } => c.write_session(self, &token_hash).await?, - } - Ok(()) - } - async fn find( - store: Arc<dyn Store>, - lens: &'a Self::Lens, - ) -> Result<Vec<Self::Item>, StoreError> { - let token = lens.token_hash.map(|t| t.clone()).unwrap_or(vec![]); - - Ok(match store.get_type() { - StoreType::Postgres { c } => c.find_session(token, lens.identity_id).await?, - StoreType::Sqlite { c } => c.find_session(token, lens.identity_id).await?, - }) - } -} - -#[async_trait] impl<'a> Storable<'a> for Credential { type Item = Credential; type Lens = CredentialLens<'a>; @@ -217,31 +173,9 @@ impl<'a> Storable<'a> for Credential { ) -> Result<Vec<Self::Item>, StoreError> { Ok(match store.get_type() { StoreType::Postgres { c } => { - c.find_credential( - lens.id, - lens.identity_id, - lens.t, - if let Some(true) = lens.restrict_by_key { - true - } else { - false - }, - ) - .await? - } - StoreType::Sqlite { c } => { - c.find_credential( - lens.id, - lens.identity_id, - lens.t, - if let Some(true) = lens.restrict_by_key { - true - } else { - false - }, - ) - .await? + c.find_credential(lens.id, lens.identity_id, lens.t).await? } + StoreType::Sqlite { c } => c.find_credential(lens.id, lens.identity_id, lens.t).await?, }) } } diff --git a/crates/secd/src/client/store/sql_db.rs b/crates/secd/src/client/store/sql_db.rs index 3e72fe8..7b3a68e 100644 --- a/crates/secd/src/client/store/sql_db.rs +++ b/crates/secd/src/client/store/sql_db.rs @@ -1,7 +1,7 @@ use super::{Store, StoreError, StoreType}; use crate::{ - Address, AddressType, AddressValidation, AddressValidationMethod, Credential, CredentialId, - CredentialType, Identity, IdentityId, Session, + util::ErrorContext, Address, AddressType, AddressValidation, AddressValidationMethod, + Credential, CredentialId, CredentialType, Identity, IdentityId, }; use email_address::EmailAddress; use lazy_static::lazy_static; @@ -26,8 +26,6 @@ const WRITE_CREDENTIAL: &str = "write_credential"; const FIND_CREDENTIAL: &str = "find_credential"; const WRITE_IDENTITY: &str = "write_identity"; const FIND_IDENTITY: &str = "find_identity"; -const WRITE_SESSION: &str = "write_session"; -const FIND_SESSION: &str = "find_session"; const ERR_MSG_MIGRATION_FAILED: &str = "Failed to apply secd migrations to a sql db. File a bug at https://www.github.com/branchcontrol/secdiam"; @@ -59,14 +57,6 @@ lazy_static! { include_str!("../../../store/sqlite/sql/find_identity.sql"), ), ( - WRITE_SESSION, - include_str!("../../../store/sqlite/sql/write_session.sql"), - ), - ( - FIND_SESSION, - include_str!("../../../store/sqlite/sql/find_session.sql"), - ), - ( WRITE_CREDENTIAL, include_str!("../../../store/sqlite/sql/write_credential.sql"), ), @@ -105,14 +95,6 @@ lazy_static! { include_str!("../../../store/pg/sql/find_identity.sql"), ), ( - WRITE_SESSION, - include_str!("../../../store/pg/sql/write_session.sql"), - ), - ( - FIND_SESSION, - include_str!("../../../store/pg/sql/find_session.sql"), - ), - ( WRITE_CREDENTIAL, include_str!("../../../store/pg/sql/write_credential.sql"), ), @@ -145,7 +127,7 @@ impl<T> SqlxResultExt<T> for Result<T, sqlx::Error> { return Err(StoreError::IdempotentCheckAlreadyExists); } } - self.map_err(|e| StoreError::SqlClientError(e)) + self.map_err(StoreError::SqlClientError) } } @@ -169,7 +151,7 @@ impl Store for PgClient { } impl PgClient { - pub async fn new(pool: sqlx::Pool<Postgres>) -> Arc<dyn Store + Send + Sync + 'static> { + pub async fn new_ref(pool: sqlx::Pool<Postgres>) -> Arc<dyn Store + Send + Sync + 'static> { sqlx::migrate!("store/pg/migrations", "secd") .run(&pool) .await @@ -196,7 +178,7 @@ impl Store for SqliteClient { } impl SqliteClient { - pub async fn new(pool: sqlx::Pool<Sqlite>) -> Arc<dyn Store + Send + Sync + 'static> { + pub async fn new_ref(pool: sqlx::Pool<Sqlite>) -> Arc<dyn Store + Send + Sync + 'static> { sqlx::migrate!("store/sqlite/migrations", "secd") .run(&pool) .await @@ -436,7 +418,6 @@ where id: Option<&Uuid>, address_value: Option<&str>, address_is_validated: Option<bool>, - session_token_hash: &Option<Vec<u8>>, ) -> Result<Vec<Identity>, StoreError> { let sqls = get_sqls(&self.sqls_root, FIND_IDENTITY); let rs = sqlx::query_as::< @@ -452,7 +433,6 @@ where .bind(id) .bind(address_value) .bind(address_is_validated) - .bind(session_token_hash) .fetch_all(&self.pool) .await .extend_err()?; @@ -462,7 +442,8 @@ where res.push(Identity { id, address_validations: vec![], - credentials: vec![], + credentials: self.find_credential(None, Some(id), None).await?, + new_credentials: vec![], rules: vec![], metadata, created_at, @@ -473,57 +454,12 @@ where Ok(res) } - pub async fn write_session(&self, s: &Session, token_hash: &[u8]) -> Result<(), StoreError> { - let sqls = get_sqls(&self.sqls_root, WRITE_SESSION); - sqlx::query(&sqls[0]) - .bind(s.identity_id) - .bind(token_hash) - .bind(s.created_at) - .bind(s.expired_at) - .bind(s.revoked_at) - .execute(&self.pool) - .await - .extend_err()?; - - Ok(()) - } - - pub async fn find_session( - &self, - token: Vec<u8>, - identity_id: Option<&Uuid>, - ) -> Result<Vec<Session>, StoreError> { - let sqls = get_sqls(&self.sqls_root, FIND_SESSION); - let rs = - sqlx::query_as::<_, (Uuid, OffsetDateTime, OffsetDateTime, Option<OffsetDateTime>)>( - &sqls[0], - ) - .bind(token) - .bind(identity_id) - .bind(OffsetDateTime::now_utc()) - .bind(OffsetDateTime::now_utc()) - .fetch_all(&self.pool) - .await - .extend_err()?; - - let mut res = vec![]; - for (identity_id, created_at, expired_at, revoked_at) in rs.into_iter() { - res.push(Session { - identity_id, - token: vec![], - created_at, - expired_at, - revoked_at, - }); - } - Ok(res) - } - pub async fn write_credential(&self, c: &Credential) -> Result<(), StoreError> { let sqls = get_sqls(&self.sqls_root, WRITE_CREDENTIAL); let partial_key = match &c.t { - crate::CredentialType::Passphrase { key, value: _ } => Some(key.clone()), - _ => None, + CredentialType::Passphrase { key, .. } => Some(key.clone()), + CredentialType::ApiToken { public, .. } => Some(public.clone()), + CredentialType::Session { key, .. } => Some(key.clone()), }; sqlx::query(&sqls[0]) @@ -545,17 +481,13 @@ where id: Option<Uuid>, identity_id: Option<Uuid>, t: Option<&CredentialType>, - restrict_by_key: bool, ) -> Result<Vec<Credential>, StoreError> { let sqls = get_sqls(&self.sqls_root, FIND_CREDENTIAL); - let key = restrict_by_key - .then(|| { - t.map(|i| match i { - CredentialType::Passphrase { key, value: _ } => key.clone(), - _ => todo!(), - }) - }) - .flatten(); + let key = t.map(|i| match i { + CredentialType::Passphrase { key, .. } => key.clone(), + CredentialType::ApiToken { public, .. } => public.clone(), + CredentialType::Session { key, .. } => key.clone(), + }); let rs = sqlx::query_as::< _, @@ -578,7 +510,9 @@ where let mut res = vec![]; for (id, identity_id, data, created_at, revoked_at, deleted_at) in rs.into_iter() { - let t: CredentialType = serde_json::from_str(&data)?; + let t: CredentialType = + serde_json::from_str(&data).ctx("error while deserializing credential_type")?; + res.push(Credential { id, identity_id, 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, diff --git a/crates/secd/src/util/crypter.rs b/crates/secd/src/util/crypter.rs index 1717377..e5ec796 100644 --- a/crates/secd/src/util/crypter.rs +++ b/crates/secd/src/util/crypter.rs @@ -3,8 +3,8 @@ use aes_gcm::{ Aes256Gcm, Nonce, }; use argon2::{ - password_hash::{self, SaltString}, - Argon2, PasswordHasher, + password_hash::{Salt, SaltString}, + Argon2, PasswordHash, PasswordHasher, }; use derive_more::Display; use rand::Rng; @@ -13,10 +13,10 @@ use thiserror::Error; #[derive(Debug, Display, Error)] pub enum CrypterError { - EncryptError(String), - DecryptError(String), - DecodeError(String), - HashError(String), + Encrypt(String), + Decrypt(String), + Decode(String), + Hash(String), } pub struct Crypter { @@ -37,8 +37,8 @@ impl Crypter { let rbs = rand::thread_rng().gen::<[u8; 12]>(); let iv = Nonce::from_slice(&rbs); let crypt = cipher - .encrypt(&iv, data) - .map_err(|e| CrypterError::EncryptError(e.to_string()))?; + .encrypt(iv, data) + .map_err(|e| CrypterError::Encrypt(e.to_string()))?; let mut msg = iv.to_vec(); msg.extend_from_slice(&crypt); @@ -53,19 +53,36 @@ impl Crypter { let iv = Nonce::from_slice(&data[0..=11]); let data = &data[12..]; - Ok(cipher - .decrypt(&iv, data) - .map_err(|e| CrypterError::DecryptError(e.to_string()))?) + cipher + .decrypt(iv, data) + .map_err(|e| CrypterError::Decrypt(e.to_string())) } - pub fn hash(&self, data: &[u8]) -> Result<String, CrypterError> { - let salt = SaltString::generate(&mut OsRng); + /// Hash data. + /// If a candidate is provided, then use that candidate's salt, passed as a full phc string. + pub fn hash(&self, data: &[u8], phc_candidate: Option<&str>) -> Result<String, CrypterError> { + let salt = match phc_candidate { + None => SaltString::generate(&mut OsRng).as_str().to_owned(), + Some(phc) => PasswordHash::new(phc) + .map_err(|e| CrypterError::Hash(e.to_string()))? + .salt.expect("unreachable fbacabb8-082a-423a-abff-06a961dc5828 [no salt found means a forced error since all resources, even those of high entropy, are salted]").as_str().to_owned(), + }; + let hasher = Argon2::default(); Ok(hasher - .hash_password(data, &salt) - .map_err(|e| CrypterError::HashError(e.to_string()))? + .hash_password( + data, + Salt::from_b64(&salt).map_err(|e| CrypterError::Hash(e.to_string()))?, + ) + .map_err(|e| CrypterError::Hash(e.to_string()))? .to_string()) } + + pub fn weak_hash(&self, data: &[u8]) -> Result<String, CrypterError> { + let mut hasher = Sha256::new(); + hasher.update(data); + Ok(hex::encode(hasher.finalize())) + } } #[cfg(test)] diff --git a/crates/secd/src/util/from.rs b/crates/secd/src/util/from.rs index ec5b62d..7401468 100644 --- a/crates/secd/src/util/from.rs +++ b/crates/secd/src/util/from.rs @@ -3,7 +3,7 @@ use crate::AuthStore; impl From<Option<String>> for AuthStore { fn from(s: Option<String>) -> Self { let conn = s.clone().unwrap_or("sqlite::memory:".into()); - match conn.split(":").next() { + match conn.split(':').next() { Some("postgresql") | Some("postgres") => AuthStore::Postgres { conn }, Some("sqlite") => AuthStore::Sqlite { conn }, _ => panic!( diff --git a/crates/secd/src/util/mod.rs b/crates/secd/src/util/mod.rs index 8676f26..fb984d1 100644 --- a/crates/secd/src/util/mod.rs +++ b/crates/secd/src/util/mod.rs @@ -3,12 +3,14 @@ pub(crate) mod from; use self::crypter::{Crypter, CrypterError}; use crate::{ - AddressType, Credential, CredentialType, IdentityId, SecdError, Session, SESSION_DURATION, - SESSION_SIZE_BYTES, + AddressType, Credential, CredentialType, IdentityId, SecdError, API_TOKEN_SIZE_BYTES, + CREDENTIAL_PUBLIC_PART_BYTES, SESSION_DURATION, SESSION_SIZE_BYTES, }; +use base64::{engine::general_purpose, Engine as _}; use sha2::{Digest, Sha256}; use std::str::from_utf8; use time::OffsetDateTime; +use uuid::Uuid; pub(crate) fn hash(i: &[u8]) -> Vec<u8> { let mut hasher = Sha256::new(); @@ -16,76 +18,265 @@ pub(crate) fn hash(i: &[u8]) -> Vec<u8> { hasher.finalize().to_vec() } +pub trait ErrorContext<T, E: std::error::Error> { + fn ctx(self, err: &str) -> Result<T, E>; +} + +impl<T, E: std::error::Error> ErrorContext<T, E> for Result<T, E> { + fn ctx(self, err: &str) -> Result<T, E> { + self.map_err(|e| { + log::error!("{} [{}]", err, e.to_string()); + e + }) + } +} + impl AddressType { pub fn get_value(&self) -> Option<String> { match &self { - AddressType::Email { email_address } => { - email_address.as_ref().map(|a| a.to_string().clone()) - } + AddressType::Email { email_address } => email_address.as_ref().map(|a| a.to_string()), AddressType::Sms { phone_number } => phone_number.as_ref().cloned(), } } } -impl Session { - pub(crate) fn new(identity_id: IdentityId) -> Result<Self, SecdError> { - let token = (0..SESSION_SIZE_BYTES) +impl CredentialType { + pub fn new_api_token() -> Result<Self, SecdError> { + let public = general_purpose::URL_SAFE_NO_PAD.encode( + (0..CREDENTIAL_PUBLIC_PART_BYTES) + .map(|_| rand::random::<u8>()) + .collect::<Vec<u8>>(), + ); + + let private = general_purpose::URL_SAFE_NO_PAD.encode( + (0..API_TOKEN_SIZE_BYTES) + .map(|_| rand::random::<u8>()) + .collect::<Vec<u8>>(), + ); + + Ok(CredentialType::ApiToken { public, private }) + } + + pub fn session_from_str(token: &str) -> Result<Self, SecdError> { + let decoded = general_purpose::URL_SAFE_NO_PAD + .decode(token) + .map_err(|e| SecdError::DecodeError(e.to_string()))?; + + let key = + general_purpose::URL_SAFE_NO_PAD.encode(&decoded[0..CREDENTIAL_PUBLIC_PART_BYTES]); + let secret = + general_purpose::URL_SAFE_NO_PAD.encode(&decoded[CREDENTIAL_PUBLIC_PART_BYTES..]); + + Ok(CredentialType::Session { key, secret }) + } + + pub fn api_token_from_str(token: &str) -> Result<Self, SecdError> { + let decoded = general_purpose::URL_SAFE_NO_PAD + .decode(token) + .map_err(|e| SecdError::DecodeError(e.to_string()))?; + + let public = + general_purpose::URL_SAFE_NO_PAD.encode(&decoded[0..CREDENTIAL_PUBLIC_PART_BYTES]); + let private = + general_purpose::URL_SAFE_NO_PAD.encode(&decoded[CREDENTIAL_PUBLIC_PART_BYTES..]); + + Ok(CredentialType::ApiToken { public, private }) + } + + pub fn session_token(&self) -> Result<String, SecdError> { + match self { + CredentialType::Session { key, secret } => { + let key_bytes = general_purpose::URL_SAFE_NO_PAD + .decode(key) + .map_err(|e| SecdError::DecodeError(e.to_string()))?; + let secret_bytes = general_purpose::URL_SAFE_NO_PAD + .decode(secret) + .map_err(|e| SecdError::DecodeError(e.to_string()))?; + + let mut input = key_bytes; + input.extend(secret_bytes); + + Ok(general_purpose::URL_SAFE_NO_PAD.encode(input)) + } + _ => Err(SecdError::InvalidCredential), + } + .ctx("the credential type is not a session") + } + + pub fn api_token(&self) -> Result<String, SecdError> { + match self { + CredentialType::ApiToken { public, private } => { + let public_bytes = general_purpose::URL_SAFE_NO_PAD + .decode(public) + .map_err(|e| SecdError::DecodeError(e.to_string()))?; + let private_bytes = general_purpose::URL_SAFE_NO_PAD + .decode(private) + .map_err(|e| SecdError::DecodeError(e.to_string()))?; + + let mut input = public_bytes; + input.extend(private_bytes); + + Ok(general_purpose::URL_SAFE_NO_PAD.encode(input)) + } + _ => Err(SecdError::InvalidCredential), + } + .ctx("the credential type is not an api token") + } +} + +impl Credential { + pub fn new_session(identity_id: IdentityId) -> Result<Self, SecdError> { + let key = (0..CREDENTIAL_PUBLIC_PART_BYTES) + .map(|_| rand::random::<u8>()) + .collect::<Vec<u8>>(); + + let secret = (0..SESSION_SIZE_BYTES) .map(|_| rand::random::<u8>()) .collect::<Vec<u8>>(); + let now = OffsetDateTime::now_utc(); - Ok(Session { + Ok(Credential { + id: Uuid::new_v4(), identity_id, - token, + t: CredentialType::Session { + key: general_purpose::URL_SAFE_NO_PAD.encode(key), + secret: general_purpose::URL_SAFE_NO_PAD.encode(secret), + }, created_at: now, - expired_at: now - .checked_add(time::Duration::new(SESSION_DURATION, 0)) - .ok_or(SecdError::Todo)?, - revoked_at: None, + revoked_at: Some( + now.checked_add(time::Duration::new(SESSION_DURATION, 0)) + .ok_or(SecdError::Todo)?, + ), + deleted_at: None, }) } -} -impl Credential { - pub(crate) fn encrypt(&mut self, crypter: &Crypter) -> Result<(), SecdError> { - Ok(match self.t { - CredentialType::Passphrase { - key: _, - ref mut value, + pub fn encrypt(&mut self, crypter: &Crypter) -> Result<(), SecdError> { + match self.t { + CredentialType::ApiToken { + ref mut private, .. } => { + *private = hex::encode(crypter.encrypt(private.as_bytes())?); + } + CredentialType::Passphrase { ref mut value, .. } => { *value = hex::encode(crypter.encrypt(value.as_bytes())?); } - _ => {} - }) + CredentialType::Session { ref mut secret, .. } => { + *secret = hex::encode(crypter.encrypt(secret.as_bytes())?); + } + }; + Ok(()) } - pub(crate) fn decrypt(&mut self, crypter: &Crypter) -> Result<(), SecdError> { - Ok(match self.t { - CredentialType::Passphrase { - key: _, - ref mut value, + pub fn decrypt(&mut self, crypter: &Crypter) -> Result<(), SecdError> { + match self.t { + CredentialType::ApiToken { + ref mut private, .. } => { - *value = from_utf8( + *private = from_utf8( &crypter.decrypt( - &hex::decode(value.clone()) - .map_err(|e| CrypterError::DecodeError(e.to_string()))?, + &hex::decode(private.clone()) + .map_err(|e| CrypterError::Decode(e.to_string()))?, )?, ) - .map_err(|e| CrypterError::DecodeError(e.to_string()))? + .map_err(|e| CrypterError::Decode(e.to_string()))? .to_string() } - _ => {} - }) + CredentialType::Passphrase { ref mut value, .. } => { + *value = from_utf8(&crypter.decrypt( + &hex::decode(value.clone()).map_err(|e| CrypterError::Decode(e.to_string()))?, + )?) + .map_err(|e| CrypterError::Decode(e.to_string()))? + .to_string() + } + CredentialType::Session { ref mut secret, .. } => { + *secret = from_utf8( + &crypter.decrypt( + &hex::decode(secret.clone()) + .map_err(|e| CrypterError::Decode(e.to_string()))?, + )?, + ) + .map_err(|e| CrypterError::Decode(e.to_string()))? + .to_string() + } + }; + Ok(()) } - pub(crate) fn hash(&mut self, crypter: &Crypter) -> Result<(), SecdError> { - Ok(match self.t { - CredentialType::Passphrase { - key: _, - ref mut value, + pub fn hash(&mut self, crypter: &Crypter) -> Result<(), SecdError> { + match self.t { + CredentialType::ApiToken { + ref mut private, .. } => { - *value = crypter.hash(value.as_bytes())?; + *private = crypter.weak_hash(private.as_bytes())?; } - _ => {} - }) + CredentialType::Passphrase { ref mut value, .. } => { + *value = crypter.hash(value.as_bytes(), None)?; + } + CredentialType::Session { ref mut secret, .. } => { + *secret = crypter.weak_hash(secret.as_bytes())?; + } + }; + Ok(()) + } + + pub fn hash_compare( + &mut self, + plaintext: &CredentialType, + crypter: &Crypter, + ) -> Result<(), SecdError> { + match (&self.t, plaintext) { + ( + CredentialType::ApiToken { + public: current_public, + private: current_private, + }, + CredentialType::ApiToken { + public: plaintext_public, + private: plaintext_private, + }, + ) => { + let plaintext_hash = crypter.weak_hash(plaintext_private.as_bytes())?; + if plaintext_public != current_public || &plaintext_hash != current_private { + return Err(SecdError::InvalidCredential); + } + } + ( + CredentialType::Passphrase { + key: current_key, + value: current_value, + }, + CredentialType::Passphrase { + key: plaintext_key, + value: plaintext_value, + }, + ) => { + let plaintext_hash = + crypter.hash(plaintext_value.as_bytes(), Some(current_value))?; + if plaintext_key != current_key || &plaintext_hash != current_value { + return Err(SecdError::InvalidCredential); + } + } + ( + CredentialType::Session { + key: current_key, + secret: current_secret, + }, + CredentialType::Session { + key: plaintext_key, + secret: plaintext_secret, + }, + ) => { + let plaintext_hash = crypter.weak_hash(plaintext_secret.as_bytes())?; + if plaintext_key != current_key || &plaintext_hash != current_secret { + return Err(SecdError::InvalidCredential); + } + } + _ => panic!( + "unreachable 78cfff7c-5493-42c5-add7-044241b3d713 [different credential types]" + ), + } + + Ok(()) } } diff --git a/crates/secd/store/pg/migrations/20221222002434_bootstrap.sql b/crates/secd/store/pg/migrations/20221222002434_bootstrap.sql index 0cf3fa0..0fd423e 100644 --- a/crates/secd/store/pg/migrations/20221222002434_bootstrap.sql +++ b/crates/secd/store/pg/migrations/20221222002434_bootstrap.sql @@ -36,11 +36,10 @@ create table if not exists secd.credential ( , created_at timestamptz not null , revoked_at timestamptz , deleted_at timestamptz + , unique(partial_key) ); -create unique index if not exists credential_passphrase_type_key_ix -on secd.credential (partial_key) -where type = 'Passphrase'; +create unique index if not exists credential_partial_key_type_key_ix on secd.credential (partial_key); create table if not exists secd.address ( address_id bigserial primary key diff --git a/crates/secd/store/pg/sql/find_credential.sql b/crates/secd/store/pg/sql/find_credential.sql index e30c0ea..1736500 100644 --- a/crates/secd/store/pg/sql/find_credential.sql +++ b/crates/secd/store/pg/sql/find_credential.sql @@ -9,4 +9,4 @@ join secd.identity i using (identity_id) where (($1::uuid is null) or (c.credential_public_id = $1)) and (($2::uuid is null) or (i.identity_public_id = $2)) and (($3::text is null) or (c.type = $3)) -and (($3::text is null or $4::text is null) or (c.data->$3->>'key' = $4)) +and (($3::text is null or $4::text is null) or (c.partial_key = $4)) diff --git a/crates/secd/store/pg/sql/find_identity.sql b/crates/secd/store/pg/sql/find_identity.sql index 37105cb..41c8518 100644 --- a/crates/secd/store/pg/sql/find_identity.sql +++ b/crates/secd/store/pg/sql/find_identity.sql @@ -7,9 +7,7 @@ select distinct from secd.identity i left join secd.address_validation av using (identity_id) left join secd.address a using (address_id) -left join secd.session s using (identity_id) where (($1::uuid is null) or (i.identity_public_id = $1)) and (($2::text is null) or (a.value = $2)) and (($3::bool is null) or (($3::bool is true) and (av.validated_at is not null))) -and (($4::bytea is null) or (s.token_hash = $4)) and i.deleted_at is null; diff --git a/crates/secd/store/pg/sql/find_session.sql b/crates/secd/store/pg/sql/find_session.sql deleted file mode 100644 index ca58480..0000000 --- a/crates/secd/store/pg/sql/find_session.sql +++ /dev/null @@ -1,11 +0,0 @@ -select distinct - i.identity_public_id - , s.created_at - , s.expired_at - , s.revoked_at -from secd.session s -join secd.identity i using (identity_id) -where (($1::bytea is null) or (s.token_hash = $1)) -and (($2::uuid is null) or (i.identity_public_id = $2)) -and (($3::timestamptz is null) or (s.expired_at > $3)) -and ((revoked_at is null) or ($4::timestamptz is null) or (s.revoked_at > $4)); diff --git a/crates/secd/store/pg/sql/write_credential.sql b/crates/secd/store/pg/sql/write_credential.sql index 17e03a2..ecaf523 100644 --- a/crates/secd/store/pg/sql/write_credential.sql +++ b/crates/secd/store/pg/sql/write_credential.sql @@ -16,4 +16,6 @@ insert into secd.credential ( , $6 , $7 , $8 -); +) on conflict (partial_key) do update + set revoked_at = excluded.revoked_at + , deleted_at = excluded.deleted_at; diff --git a/crates/secd/store/pg/sql/write_identity.sql b/crates/secd/store/pg/sql/write_identity.sql index 4b2745b..e86d2f5 100644 --- a/crates/secd/store/pg/sql/write_identity.sql +++ b/crates/secd/store/pg/sql/write_identity.sql @@ -9,4 +9,5 @@ insert into secd.identity ( ) on conflict (identity_public_id) do update set data = excluded.data , updated_at = excluded.updated_at - , deleted_at = excluded.deleted_at; + , deleted_at = excluded.deleted_at +returning (xmax = 0); diff --git a/crates/secd/store/pg/sql/write_session.sql b/crates/secd/store/pg/sql/write_session.sql deleted file mode 100644 index aa9c0a1..0000000 --- a/crates/secd/store/pg/sql/write_session.sql +++ /dev/null @@ -1,11 +0,0 @@ -insert into secd.session ( - identity_id - , token_hash - , created_at - , expired_at - , revoked_at -) values ( - (select identity_id from secd.identity where identity_public_id = $1) - , $2, $3, $4, $5 -) on conflict (token_hash) do update - set revoked_at = excluded.revoked_at; |
