pub(crate) mod crypter; pub(crate) mod from; use self::crypter::{Crypter, CrypterError}; use crate::{ 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(); hasher.update(i); 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()), AddressType::Sms { phone_number } => phone_number.as_ref().cloned(), } } } 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 .get(0..CREDENTIAL_PUBLIC_PART_BYTES) .ok_or(SecdError::CredentialIsNotApiToken) .ctx("insufficent number of bytes to find credential's public part")?, ); let private = general_purpose::URL_SAFE_NO_PAD.encode( &decoded .get(CREDENTIAL_PUBLIC_PART_BYTES..) .ok_or(SecdError::CredentialIsNotApiToken) .ctx("insufficent number of bytes to find credential's secret part")?, ); 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(Credential { id: Uuid::new_v4(), identity_id, t: CredentialType::Session { key: general_purpose::URL_SAFE_NO_PAD.encode(key), secret: general_purpose::URL_SAFE_NO_PAD.encode(secret), }, created_at: now, revoked_at: Some( now.checked_add(time::Duration::new(SESSION_DURATION, 0)) .ok_or(SecdError::Todo)?, ), deleted_at: None, }) } 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 fn decrypt(&mut self, crypter: &Crypter) -> Result<(), SecdError> { match self.t { CredentialType::ApiToken { ref mut private, .. } => { *private = from_utf8( &crypter.decrypt( &hex::decode(private.clone()) .map_err(|e| CrypterError::Decode(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 fn hash(&mut self, crypter: &Crypter) -> Result<(), SecdError> { match self.t { CredentialType::ApiToken { ref mut private, .. } => { *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(()) } } #[cfg(test)] mod test { use uuid::Uuid; use super::*; #[test] fn test_credential_encrypt() { let c = Crypter::new("AMAZING_KEY".as_bytes()); let plaintext_secret = "super_password".to_string(); let mut credential = Credential { id: Uuid::new_v4(), identity_id: Uuid::new_v4(), t: CredentialType::Passphrase { key: "super_user".into(), value: plaintext_secret.clone(), }, created_at: OffsetDateTime::now_utc(), revoked_at: None, deleted_at: None, }; credential.encrypt(&c).unwrap(); match &credential.t { CredentialType::Passphrase { key: _, value } => { assert_ne!(plaintext_secret.clone(), value.clone()) } _ => {} }; credential.decrypt(&c).unwrap(); match &credential.t { CredentialType::Passphrase { key: _, value } => { assert_eq!(plaintext_secret.clone(), value.clone()) } _ => {} }; } }