From ed34a5251f13bbded0aa15719887db4924b351eb Mon Sep 17 00:00:00 2001 From: benj Date: Mon, 22 May 2023 15:47:06 -0700 Subject: update credential API to include sessions This change updates the credential API to include sessions as just another credential type. It adds the ApiToken type and enables revocation of credentials. Updates were also made to the Identity API which now includes a list of new credentials added to an Identity. This change also migrates off the hacky ENV configuration paradigm and includes a new config.toml file specified by the SECD_CONFIG_PATH env var. No default is currently provided. Clippy updates and code cleanup. --- crates/secd/src/util/mod.rs | 275 +++++++++++++++++++++++++++++++++++++------- 1 file changed, 233 insertions(+), 42 deletions(-) (limited to 'crates/secd/src/util/mod.rs') 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 { let mut hasher = Sha256::new(); @@ -16,76 +18,265 @@ pub(crate) fn hash(i: &[u8]) -> Vec { hasher.finalize().to_vec() } +pub trait ErrorContext { + fn ctx(self, err: &str) -> Result; +} + +impl ErrorContext for Result { + fn ctx(self, err: &str) -> Result { + self.map_err(|e| { + log::error!("{} [{}]", err, e.to_string()); + e + }) + } +} + impl AddressType { pub fn get_value(&self) -> Option { 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 { - let token = (0..SESSION_SIZE_BYTES) +impl CredentialType { + pub fn new_api_token() -> Result { + let public = general_purpose::URL_SAFE_NO_PAD.encode( + (0..CREDENTIAL_PUBLIC_PART_BYTES) + .map(|_| rand::random::()) + .collect::>(), + ); + + let private = general_purpose::URL_SAFE_NO_PAD.encode( + (0..API_TOKEN_SIZE_BYTES) + .map(|_| rand::random::()) + .collect::>(), + ); + + Ok(CredentialType::ApiToken { public, private }) + } + + pub fn session_from_str(token: &str) -> Result { + 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 { + 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 { + 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 { + 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 { + let key = (0..CREDENTIAL_PUBLIC_PART_BYTES) + .map(|_| rand::random::()) + .collect::>(); + + let secret = (0..SESSION_SIZE_BYTES) .map(|_| rand::random::()) .collect::>(); + 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(()) } } -- cgit v1.2.3