use mtp::crypto::PublicKeyBundle; use opaque_ke::{ CipherSuite, CredentialFinalization, CredentialRequest, Identifiers, RegistrationRequest, RegistrationUpload, Ristretto255, ServerLogin, ServerLoginParameters, ServerRegistration, ServerSetup, TripleDh, }; use rand_core::OsRng; use sha2::{Digest, Sha256}; pub struct TensaminOpaque; impl CipherSuite for TensaminOpaque { type OprfCs = Ristretto255; type KeyExchange = TripleDh; type Ksf = argon2::Argon2<'static>; } pub type PasswordServerSetup = ServerSetup; #[derive(Debug, thiserror::Error)] pub enum PasswordAuthError { #[error("invalid OPAQUE exchange: {0}")] Opaque(String), #[error("invalid Contact public key: {0}")] ContactKey(String), #[error("OPAQUE context field is too large")] FieldTooLarge, } pub fn generate_server_setup() -> PasswordServerSetup { ServerSetup::new(&mut OsRng) } pub fn serialize_server_setup(setup: &PasswordServerSetup) -> Vec { setup.serialize().to_vec() } pub fn deserialize_server_setup(bytes: &[u8]) -> Result { ServerSetup::deserialize(bytes).map_err(|error| PasswordAuthError::Opaque(error.to_string())) } pub fn registration_start( setup: &PasswordServerSetup, request: &[u8], identifier: &[u8], ) -> Result, PasswordAuthError> { let message = RegistrationRequest::::deserialize(request) .map_err(|error| PasswordAuthError::Opaque(error.to_string()))?; let result = ServerRegistration::::start(setup, message, identifier) .map_err(|error| PasswordAuthError::Opaque(error.to_string()))?; Ok(result.message.serialize().to_vec()) } pub fn registration_finish(upload: &[u8]) -> Result, PasswordAuthError> { let message = RegistrationUpload::::deserialize(upload) .map_err(|error| PasswordAuthError::Opaque(error.to_string()))?; Ok(ServerRegistration::::finish(message) .serialize() .to_vec()) } pub fn server_identifier(iota_id: i64) -> Vec { let mut result = b"tensamin:iota-password:v1\0".to_vec(); result.extend_from_slice(&iota_id.to_be_bytes()); result } pub fn login_context( principal: &str, iota_id: i64, session_id: uuid::Uuid, contact_key: &PublicKeyBundle, ) -> Result, PasswordAuthError> { let mut result = b"tensamin:password-provisioning:v1\0".to_vec(); let principal_len = u32::try_from(principal.len()).map_err(|_| PasswordAuthError::FieldTooLarge)?; result.extend_from_slice(&principal_len.to_be_bytes()); result.extend_from_slice(principal.as_bytes()); result.extend_from_slice(&iota_id.to_be_bytes()); result.extend_from_slice(session_id.as_bytes()); let key = contact_key .try_as_bytes() .map_err(|error| PasswordAuthError::ContactKey(error.to_string()))?; result.extend_from_slice(&Sha256::digest(key)); Ok(result) } pub struct LoginStartResult { pub response: Vec, pub state: Vec, } pub fn login_start( setup: &PasswordServerSetup, record: Option<&[u8]>, request: &[u8], principal: &[u8], server_id: &[u8], context: &[u8], ) -> Result { let message = CredentialRequest::::deserialize(request) .map_err(|error| PasswordAuthError::Opaque(error.to_string()))?; let registration = record .map(ServerRegistration::::deserialize) .transpose() .map_err(|error| PasswordAuthError::Opaque(error.to_string()))?; let result = ServerLogin::::start( &mut OsRng, setup, registration, message, principal, ServerLoginParameters { context: Some(context), identifiers: Identifiers { client: Some(principal), server: Some(server_id), }, }, ) .map_err(|error| PasswordAuthError::Opaque(error.to_string()))?; Ok(LoginStartResult { response: result.message.serialize().to_vec(), state: result.state.serialize().to_vec(), }) } pub fn login_finish( serialized_state: &[u8], finalization: &[u8], principal: &[u8], server_id: &[u8], context: &[u8], ) -> Result<(), PasswordAuthError> { let state = ServerLogin::::deserialize(serialized_state) .map_err(|error| PasswordAuthError::Opaque(error.to_string()))?; let message = CredentialFinalization::::deserialize(finalization) .map_err(|error| PasswordAuthError::Opaque(error.to_string()))?; state .finish( message, ServerLoginParameters { context: Some(context), identifiers: Identifiers { client: Some(principal), server: Some(server_id), }, }, ) .map_err(|error| PasswordAuthError::Opaque(error.to_string()))?; Ok(()) } #[cfg(test)] mod tests { use super::*; use mtp::crypto::Keyring; use opaque_ke::{ ClientLogin, ClientLoginFinishParameters, ClientRegistration, ClientRegistrationFinishParameters, CredentialResponse, RegistrationResponse, }; use uuid::Uuid; #[test] fn registration_and_login_bind_context_identifiers_and_persisted_setup() { let setup = generate_server_setup(); let restored = deserialize_server_setup(&serialize_server_setup(&setup)).unwrap(); let principal = b"omega-key:example#7"; let server = server_identifier(11); let identifiers = Identifiers { client: Some(principal), server: Some(&server), }; let registration = ClientRegistration::::start(&mut OsRng, b"correct horse").unwrap(); let response = registration_start(&restored, ®istration.message.serialize(), principal).unwrap(); let upload = registration .state .finish( &mut OsRng, b"correct horse", RegistrationResponse::::deserialize(&response).unwrap(), ClientRegistrationFinishParameters::new(identifiers, None), ) .unwrap(); let record = registration_finish(&upload.message.serialize()).unwrap(); let key = Keyring::generate().public_key_bundle(); let context = login_context("omega-key:example#7", 11, Uuid::new_v4(), &key).unwrap(); let client = ClientLogin::::start(&mut OsRng, b"correct horse").unwrap(); let result = login_start( &restored, Some(&record), &client.message.serialize(), principal, &server, &context, ) .unwrap(); let ke2 = CredentialResponse::::deserialize(&result.response).unwrap(); let finish = client .state .finish( &mut OsRng, b"correct horse", ke2, ClientLoginFinishParameters::new(Some(&context), identifiers, None), ) .unwrap(); assert!( login_finish( &result.state, &finish.message.serialize(), principal, &server, &context ) .is_ok() ); let client = ClientLogin::::start(&mut OsRng, b"correct horse").unwrap(); let mismatched = login_start( &restored, Some(&record), &client.message.serialize(), principal, &server, b"wrong context", ) .unwrap(); assert!( client .state .finish( &mut OsRng, b"correct horse", CredentialResponse::::deserialize(&mismatched.response) .unwrap(), ClientLoginFinishParameters::new(Some(&context), identifiers, None) ) .is_err() ); let wrong_server = server_identifier(12); for (login_principal, login_server, password) in [ ( b"omega-key:example#8".as_slice(), server.as_slice(), b"correct horse".as_slice(), ), ( principal.as_slice(), wrong_server.as_slice(), b"correct horse".as_slice(), ), ( principal.as_slice(), server.as_slice(), b"wrong horse".as_slice(), ), ] { let client = ClientLogin::::start(&mut OsRng, password).unwrap(); let result = login_start( &restored, Some(&record), &client.message.serialize(), login_principal, login_server, &context, ) .unwrap(); assert!( client .state .finish( &mut OsRng, password, CredentialResponse::::deserialize(&result.response) .unwrap(), ClientLoginFinishParameters::new(Some(&context), identifiers, None) ) .is_err() ); } let client = ClientLogin::::start(&mut OsRng, b"correct horse").unwrap(); let dummy = login_start( &restored, None, &client.message.serialize(), principal, &server, &context, ) .unwrap(); assert!(CredentialResponse::::deserialize(&dummy.response).is_ok()); let unrelated = generate_server_setup(); let client = ClientLogin::::start(&mut OsRng, b"correct horse").unwrap(); let response = login_start( &unrelated, Some(&record), &client.message.serialize(), principal, &server, &context, ) .unwrap(); assert!( client .state .finish( &mut OsRng, b"correct horse", CredentialResponse::::deserialize(&response.response).unwrap(), ClientLoginFinishParameters::new(Some(&context), identifiers, None) ) .is_err() ); } #[test] fn login_context_binds_account_iota_session_and_contact_key() { let first = Keyring::generate().public_key_bundle(); let second = Keyring::generate().public_key_bundle(); let session = Uuid::new_v4(); let original = login_context("omega-key:example#7", 11, session, &first).unwrap(); for changed in [ login_context("omega-key:example#8", 11, session, &first).unwrap(), login_context("omega-key:example#7", 12, session, &first).unwrap(), login_context("omega-key:example#7", 11, Uuid::new_v4(), &first).unwrap(), login_context("omega-key:example#7", 11, session, &second).unwrap(), ] { assert_ne!(original, changed); } } }