use crate::{ client::{ email::{ parse_email_template, EmailValidationMessage, Sendable, DEFAULT_SIGNIN_EMAIL, DEFAULT_SIGNUP_EMAIL, }, store::{ AddressLens, AddressValidationLens, CredentialLens, IdentityLens, ImpersonatorLens, Storable, StoreError, }, }, util::{self, ErrorContext}, Address, AddressType, AddressValidation, AddressValidationId, AddressValidationMethod, Credential, CredentialId, CredentialType, Identity, IdentityId, Impersonator, Secd, SecdError, ADDRESSS_VALIDATION_CODE_SIZE, ADDRESS_VALIDATION_ALLOWS_ATTEMPTS, ADDRESS_VALIDATION_IDENTITY_SURJECTION, EMAIL_VALIDATION_DURATION, }; use email_address::EmailAddress; use log::warn; use rand::Rng; use std::str::FromStr; use time::{Duration, OffsetDateTime}; use tokio::join; use uuid::Uuid; impl Secd { pub async fn validate_email( &self, email_address: &str, identity_id: Option, ) -> Result { let email_address = EmailAddress::from_str(email_address)?; let mut email_template = self .cfg .email_signup_message .clone() .unwrap_or(DEFAULT_SIGNUP_EMAIL.into()); let mut address = Address { id: Uuid::new_v4(), t: AddressType::Email { email_address: Some(email_address.clone()), }, created_at: OffsetDateTime::now_utc(), }; if let Err(StoreError::IdempotentCheckAlreadyExists) = address.write(self.store.clone()).await { address = Address::find( self.store.clone(), &AddressLens { id: None, t: Some(&AddressType::Email { email_address: Some(email_address.clone()), }), }, ) .await? .into_iter() .next() .ok_or(SecdError::AddressValidationFailed)?; email_template = self .cfg .email_signin_message .clone() .unwrap_or(DEFAULT_SIGNIN_EMAIL.into()); } let secret = hex::encode(rand::thread_rng().gen::<[u8; 32]>()); let code: String = vec![0; ADDRESSS_VALIDATION_CODE_SIZE as usize] .into_iter() .map(|_| char::from_digit(rand::thread_rng().gen_range(0..=9), 10).unwrap()) .collect(); let mut validation = AddressValidation { id: Uuid::new_v4(), identity_id, address, method: AddressValidationMethod::Email, created_at: OffsetDateTime::now_utc(), expires_at: OffsetDateTime::now_utc() .checked_add(Duration::new(EMAIL_VALIDATION_DURATION, 0)) .ok_or(SecdError::Todo)?, revoked_at: None, validated_at: None, attempts: 0, hashed_token: util::hash(secret.as_bytes()), hashed_code: util::hash(code.as_bytes()), }; validation.write(self.store.clone()).await?; let msg = EmailValidationMessage { from_address: self .cfg .email_address_from .clone() .and_then(|s| s.parse().ok()) .unwrap_or("SecD ".parse().unwrap()), replyto_address: self .cfg .email_address_replyto .clone() .and_then(|s| s.parse().ok()) .unwrap_or("SecD ".parse().unwrap()), recipient: email_address.clone(), subject: "Login Request".into(), body: parse_email_template(&email_template, validation.id, Some(secret), Some(code))?, }; match msg.send(self.email_messenger.clone()).await { Ok(_) => { /* TODO: Write down the message*/ } Err(e) => { validation.revoked_at = Some(OffsetDateTime::now_utc()); validation.write(self.store.clone()).await?; return Err(SecdError::EmailMessengerError(e)); } } Ok(validation) } pub async fn validate_sms( &self, // phone_number: &PhoneNumber, ) -> Result { todo!() } pub async fn complete_address_validation( &self, validation_id: &AddressValidationId, plaintext_token: Option, plaintext_code: Option, ) -> Result { let mut validation = AddressValidation::find( self.store.clone(), &AddressValidationLens { id: Some(validation_id), }, ) .await? .into_iter() .next() .ok_or(SecdError::AddressValidationFailed)?; if validation.validated_at.is_some() { return Err(SecdError::AddressValidationExpiredOrConsumed); } validation.attempts += 1; if validation.attempts > ADDRESS_VALIDATION_ALLOWS_ATTEMPTS as i32 { warn!( "validation failed: Too many validation attempts were tried for validation {:?}", validation.id ); validation.write(self.store.clone()).await?; return Err(SecdError::AddressValidationExpiredOrConsumed); } let hashed_token = plaintext_token.map(|s| util::hash(s.as_bytes())); let hashed_code = plaintext_code.map(|c| util::hash(c.as_bytes())); let mut warn_msg = None; match (hashed_token, hashed_code) { (None, None) => { warn_msg = Some("neither token nor hash was provided during the address validation session exchange"); } (Some(t), None) => { if validation.hashed_token != t { warn_msg = Some("the provided token does not match the address validation token"); } } (None, Some(c)) => { if validation.hashed_code != c { warn_msg = Some("the provided code does not match the address validation code"); } } (Some(t), Some(c)) => { if validation.hashed_token != t || validation.hashed_code != c { warn_msg = Some("the provided token and code must both match the address validation token and code"); } } }; if let Some(msg) = warn_msg { warn!("validation failed: {}", msg); validation.write(self.store.clone()).await?; return Err(SecdError::AddressValidationSessionExchangeFailed); } let identity = Identity::find( self.store.clone(), &IdentityLens { id: None, address_type: Some(&validation.address.t), validated_address: Some(true), }, ) .await?; if !ADDRESS_VALIDATION_IDENTITY_SURJECTION && identity.len() > 1 { warn!("validation failed: identity validation surjection disallowed"); validation.write(self.store.clone()).await?; return Err(SecdError::TooManyIdentities); } let mut identity = identity.into_iter().next(); if identity.is_none() { let i = Identity { id: Uuid::new_v4(), address_validations: vec![], credentials: vec![], new_credentials: vec![], rules: vec![], metadata: None, created_at: OffsetDateTime::now_utc(), deleted_at: None, }; i.write(self.store.clone()).await?; identity = Some(i); } assert!(identity.is_some()); // If the validation was attached to another identity, unless surjection is allowed, it cannot be recorded. if !ADDRESS_VALIDATION_IDENTITY_SURJECTION && validation.identity_id.is_some() && identity.as_ref().map(|i| i.id) != validation.identity_id { warn!("validation failed: identity validation surjection is disallowed, but found existing identity for another account"); validation.write(self.store.clone()).await?; return Err(SecdError::TooManyIdentities); } validation.identity_id = identity.map(|i| i.id); validation.validated_at = Some(OffsetDateTime::now_utc()); validation.write(self.store.clone()).await?; 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, ) -> Result { 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, expires_at: Option, ) -> Result { let mut identity = match identity_id { Some(id) => Identity::find( self.store.clone(), &IdentityLens { id: Some(&id), address_type: None, validated_address: None, }, ) .await? .into_iter() .next() .ok_or(SecdError::IdentityNotFound)?, None => { let id = Identity { id: Uuid::new_v4(), address_validations: vec![], credentials: vec![], new_credentials: vec![], rules: vec![], metadata: None, created_at: OffsetDateTime::now_utc(), deleted_at: None, }; id.write(self.store.clone()).await?; id } }; let mut credential = match &Credential::find( self.store.clone(), &CredentialLens { id: None, identity_id: Some(identity.id), t: Some(&t), }, ) .await?[..] { [] => Credential { id: Uuid::new_v4(), identity_id: identity.id, t: t.clone(), created_at: OffsetDateTime::now_utc(), revoked_at: expires_at, deleted_at: None, }, _ => return Err(SecdError::CredentialAlreadyExists), }; credential.hash(&self.crypter)?; credential .write(self.store.clone()) .await .map_err(|err| match err { StoreError::IdempotentCheckAlreadyExists => SecdError::CredentialAlreadyExists, err => SecdError::StoreError(err), })?; 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, }); Ok(identity) } pub async fn validate_credential(&self, t: &CredentialType) -> Result { let mut retrieved = Credential::find( self.store.clone(), &CredentialLens { id: None, identity_id: None, t: Some(t), }, ) .await? .into_iter() .next() .ok_or(SecdError::InvalidCredential)?; 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.clone(); Ok(retrieved) } pub async fn get_identity( &self, i: Option, t: Option, ) -> Result { 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: Some(&identity_id), address_type: None, validated_address: None, }, ) .await?; assert!( i.len() <= 1, "The provided id refers to more than one identity. This is very _very_ bad." ); if i.is_empty() { Err(SecdError::IdentityNotFound) } else { Ok(i.swap_remove(0)) } } pub async fn update_identity_metadata( &self, i: IdentityId, md: String, ) -> Result { let mut identity = Identity::find( self.store.clone(), &IdentityLens { id: Some(&i), address_type: None, validated_address: None, }, ) .await? .into_iter() .next() .ok_or(SecdError::IdentityNotFound)?; identity.metadata = Some(md); identity.write(self.store.clone()).await?; Ok(identity) } 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(()) } pub async fn impersonate( &self, impersonator_id: &IdentityId, target_id: &IdentityId, ) -> Result { let impersonator_lens = IdentityLens { id: Some(&impersonator_id), address_type: None, validated_address: None, }; let target_lens = IdentityLens { id: Some(&target_id), address_type: None, validated_address: None, }; let (i, t) = join!( Identity::find(self.store.clone(), &impersonator_lens,), Identity::find(self.store.clone(), &target_lens,) ); let (i, t) = ( i.ctx("failed to retrieve impersonator identity")?, t.ctx("failed to retrieve target identity")?, ); if i.is_empty() || t.is_empty() { return Err(SecdError::IdentityNotFound) .ctx("failed to retrieve impersonator or target identity for impersonation"); } let existing_impersonation = Impersonator::find( self.store.clone(), &ImpersonatorLens { impersonator_id: Some(impersonator_id), target_id: Some(target_id), }, ) .await .ctx("failed to find existing impersonation")?; // TODO: We could expire the session chain, but I think we want to handle this more intelligently. // For now, just revoke the credential manually...pita I know... if !existing_impersonation.is_empty() { return Err(SecdError::ImpersonatorAlreadyExists) .ctx("Target already being impersonated by the provided impersonator identity"); } let new_identity = self .create_credential( Credential::new_session(*target_id)?.t, Some(*target_id), OffsetDateTime::now_utc().checked_add(Duration::minutes(30)), ) .await .ctx("failed to create new credential for target identity")?; let new_session = new_identity .new_credentials .iter() .next() .ok_or(SecdError::InvalidCredential) .ctx("failed to retrieve new session from newly created target credential")? .clone(); Impersonator { impersonator: i .into_iter() .next() .ok_or(SecdError::IdentityNotFound) .ctx("failed to find impersonator identity")?, target: new_identity, created_at: OffsetDateTime::now_utc(), } .write(self.store.clone()) .await .ctx("failed to write new impersonator")?; Ok(new_session) } }